UNPKG

@atomiqlabs/chain-evm

Version:

EVM specific base implementation

780 lines (677 loc) 38.3 kB
import { BigIntBufferUtils, BitcoinRpc, BtcTx, RelaySynchronizer, SpvVaultContract, SpvVaultTokenData, SpvWithdrawalState, SpvWithdrawalStateType, SpvWithdrawalTransactionData, TransactionConfirmationOptions } from "@atomiqlabs/base"; import {Buffer} from "buffer"; import {EVMTx} from "../chain/modules/EVMTransactions"; import {EVMContractBase} from "../contract/EVMContractBase"; import {EVMSigner} from "../wallet/EVMSigner"; import {SpvVaultContractAbi} from "./SpvVaultContractAbi"; import {SpvVaultManager, SpvVaultParametersStructOutput} from "./SpvVaultContractTypechain"; import {EVMBtcRelay} from "../btcrelay/EVMBtcRelay"; import {getLogger} from "../../utils/Utils"; import {EVMChainInterface} from "../chain/EVMChainInterface"; import {AbiCoder, getAddress, hexlify, keccak256, TransactionRequest, ZeroAddress, ZeroHash} from "ethers"; import {EVMAddresses} from "../chain/modules/EVMAddresses"; import {EVMSpvVaultData, getVaultParamsCommitment, getVaultUtxoFromState} from "./EVMSpvVaultData"; import {EVMSpvWithdrawalData} from "./EVMSpvWithdrawalData"; import {EVMFees} from "../chain/modules/EVMFees"; import {EVMBtcStoredHeader} from "../btcrelay/headers/EVMBtcStoredHeader"; import {TypedEventLog} from "../typechain/common"; import {PromiseLruCache} from "promise-cache-ts"; function decodeUtxo(utxo: string): {txHash: string, vout: bigint} { const [txId, vout] = utxo.split(":"); return { txHash: "0x"+Buffer.from(txId, "hex").reverse().toString("hex"), vout: BigInt(vout) } } export function packOwnerAndVaultId(owner: string, vaultId: bigint): string { if(owner.length!==42) throw new Error("Invalid owner address"); return owner.toLowerCase() + BigIntBufferUtils.toBuffer(vaultId, "be", 12).toString("hex"); } export function unpackOwnerAndVaultId(data: string): [string, bigint] { return [getAddress(data.substring(0, 42)), BigInt("0x"+data.substring(42, 66))]; } export class EVMSpvVaultContract<ChainId extends string> extends EVMContractBase<SpvVaultManager> implements SpvVaultContract< EVMTx, EVMSigner, ChainId, EVMSpvVaultData, EVMSpvWithdrawalData > { public static readonly GasCosts = { DEPOSIT_BASE: 15_000 + 21_000, DEPOSIT_ERC20: 40_000, OPEN: 80_000 + 21_000, CLAIM_BASE: 85_000 + 21_000, CLAIM_NATIVE_TRANSFER: 85_000, CLAIM_ERC20_TRANSFER: 40_000, CLAIM_EXECUTION_SCHEDULE: 30_000, FRONT_BASE: 75_000 + 21_000, FRONT_NATIVE_TRANSFER: 85_000, FRONT_ERC20_TRANSFER: 40_000, FRONT_EXECUTION_SCHEDULE: 30_000 }; readonly chainId: ChainId; readonly btcRelay: EVMBtcRelay<any>; readonly bitcoinRpc: BitcoinRpc<any>; readonly claimTimeout: number = 180; readonly logger = getLogger("EVMSpvVaultContract: "); constructor( chainInterface: EVMChainInterface<ChainId>, btcRelay: EVMBtcRelay<any>, bitcoinRpc: BitcoinRpc<any>, contractAddress: string, contractDeploymentHeight?: number ) { super(chainInterface, contractAddress, SpvVaultContractAbi, contractDeploymentHeight); this.btcRelay = btcRelay; this.bitcoinRpc = bitcoinRpc; } //Transactions protected async Open(signer: string, vault: EVMSpvVaultData, feeRate: string): Promise<TransactionRequest> { const {txHash, vout} = decodeUtxo(vault.getUtxo()); const tokens = vault.getTokenData(); if(tokens.length!==2) throw new Error("Must specify exactly 2 tokens for vault!"); const tx = await this.contract.open.populateTransaction(vault.vaultId, vault.getVaultParamsStruct(), txHash, vout); tx.from = signer; EVMFees.applyFeeRate(tx, EVMSpvVaultContract.GasCosts.OPEN, feeRate); return tx; } protected async Deposit(signer: string, vault: EVMSpvVaultData, rawAmounts: bigint[], feeRate: string): Promise<TransactionRequest> { let totalGas = EVMSpvVaultContract.GasCosts.DEPOSIT_BASE; let value = 0n; if(vault.token0.token.toLowerCase()===this.Chain.getNativeCurrencyAddress().toLowerCase()) { value += rawAmounts[0] * vault.token0.multiplier; } else { if(rawAmounts[0] > 0n) totalGas += EVMSpvVaultContract.GasCosts.DEPOSIT_ERC20; } if(vault.token1.token.toLowerCase()===this.Chain.getNativeCurrencyAddress().toLowerCase()) { value += (rawAmounts[1] ?? 0n) * vault.token1.multiplier; } else { if(rawAmounts[1]!=null && rawAmounts[1] > 0n && vault.token0.token.toLowerCase()!==vault.token1.token.toLowerCase()) totalGas += EVMSpvVaultContract.GasCosts.DEPOSIT_ERC20; } const tx = await this.contract.deposit.populateTransaction( vault.owner, vault.vaultId, vault.getVaultParamsStruct(), rawAmounts[0], rawAmounts[1] ?? 0n, { value } ); tx.from = signer; EVMFees.applyFeeRate(tx, totalGas, feeRate); return tx; } protected async Front( signer: string, vault: EVMSpvVaultData, data: EVMSpvWithdrawalData, withdrawalSequence: number, feeRate: string ): Promise<TransactionRequest> { let value = 0n; const frontingAmount = data.getFrontingAmount(); if(vault.token0.token.toLowerCase()===this.Chain.getNativeCurrencyAddress().toLowerCase()) value += frontingAmount[0] * vault.token0.multiplier; if(vault.token1.token.toLowerCase()===this.Chain.getNativeCurrencyAddress().toLowerCase()) value += (frontingAmount[1] ?? 0n) * vault.token1.multiplier; const tx = await this.contract.front.populateTransaction( vault.owner, vault.vaultId, vault.getVaultParamsStruct(), withdrawalSequence, data.getTxHash(), data.serializeToStruct(), { value } ); tx.from = signer; EVMFees.applyFeeRate(tx, this.getFrontGas(signer, vault, data), feeRate); return tx; } protected async Claim( signer: string, vault: EVMSpvVaultData, data: EVMSpvWithdrawalData, blockheader: EVMBtcStoredHeader, merkle: Buffer[], position: number, feeRate: string ): Promise<TransactionRequest> { const tx = await this.contract.claim.populateTransaction( vault.owner, vault.vaultId, vault.getVaultParamsStruct(), "0x"+data.btcTx.hex, blockheader.serializeToStruct(), merkle, position ) tx.from = signer; EVMFees.applyFeeRate(tx, this.getClaimGas(signer, vault, data), feeRate); return tx; } async checkWithdrawalTx(tx: SpvWithdrawalTransactionData): Promise<void> { const result = await this.contract.parseBitcoinTx(Buffer.from(tx.btcTx.hex, "hex")); if(result==null) throw new Error("Failed to parse transaction!"); } createVaultData( owner: string, vaultId: bigint, utxo: string, confirmations: number, tokenData: SpvVaultTokenData[] ): Promise<EVMSpvVaultData> { if(tokenData.length!==2) throw new Error("Must specify 2 tokens in tokenData!"); const vaultParams = { btcRelayContract: this.btcRelay.contractAddress, token0: tokenData[0].token, token1: tokenData[1].token, token0Multiplier: tokenData[0].multiplier, token1Multiplier: tokenData[1].multiplier, confirmations: BigInt(confirmations) }; const spvVaultParametersCommitment = keccak256(AbiCoder.defaultAbiCoder().encode( ["address", "address", "address", "uint192", "uint192", "uint256"], [vaultParams.btcRelayContract, vaultParams.token0, vaultParams.token1, vaultParams.token0Multiplier, vaultParams.token1Multiplier, vaultParams.confirmations] )); return Promise.resolve(new EVMSpvVaultData(owner, vaultId, { spvVaultParametersCommitment, utxoTxHash: ZeroHash, utxoVout: 0n, openBlockheight: 0n, withdrawCount: 0n, depositCount: 0n, token0Amount: 0n, token1Amount: 0n }, vaultParams, utxo)); } //Getters async getFronterAddress(owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData): Promise<string | null> { const frontingAddress = await this.contract.getFronterById(owner, vaultId, "0x"+withdrawal.getFrontingId()); if(frontingAddress===ZeroAddress) return null; return frontingAddress; } async getFronterAddresses(withdrawals: {owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData}[]): Promise<{[btcTxId: string]: string | null}> { const result: { [btcTxId: string]: string | null } = {}; let promises: Promise<void>[] = []; //TODO: We can upgrade this to use multicall for(let {owner, vaultId, withdrawal} of withdrawals) { promises.push(this.getFronterAddress(owner, vaultId, withdrawal).then(val => { result[withdrawal.getTxId()] = val; })); if(promises.length>=this.Chain.config.maxParallelCalls) { await Promise.all(promises); promises = []; } } await Promise.all(promises); return result; } private vaultParamsCache: PromiseLruCache<string, SpvVaultParametersStructOutput> = new PromiseLruCache<string, SpvVaultParametersStructOutput>(5000); async getVaultData(owner: string, vaultId: bigint): Promise<EVMSpvVaultData> { const vaultState = await this.contract.getVault(owner, vaultId); const vaultParams = await this.vaultParamsCache.getOrComputeAsync(vaultState.spvVaultParametersCommitment, async () => { const blockheight = Number(vaultState.openBlockheight); const events = await this.Events.getContractBlockEvents( ["Opened"], [ "0x"+owner.substring(2).padStart(64, "0"), hexlify(BigIntBufferUtils.toBuffer(vaultId, "be", 32)) ], blockheight ); const foundEvent = events.find( event => getVaultParamsCommitment(event.args.params)===vaultState.spvVaultParametersCommitment ); if(foundEvent==null) throw new Error("Valid open event not found!"); return foundEvent.args.params; }); if(vaultParams.btcRelayContract.toLowerCase()!==this.btcRelay.contractAddress.toLowerCase()) return null; return new EVMSpvVaultData(owner, vaultId, vaultState, vaultParams); } async getMultipleVaultData(vaults: {owner: string, vaultId: bigint}[]): Promise<{[owner: string]: {[vaultId: string]: EVMSpvVaultData}}> { const result: {[owner: string]: {[vaultId: string]: EVMSpvVaultData}} = {}; let promises: Promise<void>[] = []; //TODO: We can upgrade this to use multicall for(let {owner, vaultId} of vaults) { promises.push(this.getVaultData(owner, vaultId).then(val => { result[owner] ??= {}; result[owner][vaultId.toString(10)] = val; })); if(promises.length>=this.Chain.config.maxParallelCalls) { await Promise.all(promises); promises = []; } } await Promise.all(promises); return result; } async getVaultLatestUtxo(owner: string, vaultId: bigint): Promise<string | null> { const vaultState = await this.contract.getVault(owner, vaultId); const utxo = getVaultUtxoFromState(vaultState); if(utxo==="0000000000000000000000000000000000000000000000000000000000000000:0") return null; return utxo; } async getVaultLatestUtxos(vaults: {owner: string, vaultId: bigint}[]): Promise<{[owner: string]: {[vaultId: string]: string | null}}> { const result: {[owner: string]: {[vaultId: string]: string | null}} = {}; let promises: Promise<void>[] = []; //TODO: We can upgrade this to use multicall for(let {owner, vaultId} of vaults) { promises.push(this.getVaultLatestUtxo(owner, vaultId).then(val => { result[owner] ??= {}; result[owner][vaultId.toString(10)] = val; })); if(promises.length>=this.Chain.config.maxParallelCalls) { await Promise.all(promises); promises = []; } } await Promise.all(promises); return result; } async getAllVaults(owner?: string): Promise<EVMSpvVaultData[]> { const openedVaults = new Map<string, SpvVaultParametersStructOutput>(); await this.Events.findInContractEventsForward( ["Opened", "Closed"], owner==null ? null : [ "0x"+owner.substring(2).padStart(64, "0") ], (event) => { const vaultIdentifier = event.args.owner+":"+event.args.vaultId.toString(10); if(event.eventName==="Opened") { const _event = event as TypedEventLog<SpvVaultManager["filters"]["Opened"]>; openedVaults.set(vaultIdentifier, _event.args.params); } else { openedVaults.delete(vaultIdentifier); } return null; } ); const vaults: EVMSpvVaultData[] = []; for(let [identifier, vaultParams] of openedVaults.entries()) { const [owner, vaultIdStr] = identifier.split(":"); const vaultState = await this.contract.getVault(owner, BigInt(vaultIdStr)); if(vaultState.spvVaultParametersCommitment === getVaultParamsCommitment(vaultParams)) { vaults.push(new EVMSpvVaultData(owner, BigInt(vaultIdStr), vaultState, vaultParams)) } } return vaults; } private parseWithdrawalEvent(event: TypedEventLog<SpvVaultManager["filters"][keyof SpvVaultManager["filters"]]>): SpvWithdrawalState | null { switch(event.eventName) { case "Fronted": const frontedEvent = event as TypedEventLog<SpvVaultManager["filters"]["Fronted"]>; const [ownerFront, vaultIdFront] = unpackOwnerAndVaultId(frontedEvent.args.ownerAndVaultId); return { type: SpvWithdrawalStateType.FRONTED, txId: event.transactionHash, owner: ownerFront, vaultId: vaultIdFront, recipient: frontedEvent.args.recipient, fronter: frontedEvent.args.caller }; case "Claimed": const claimedEvent = event as TypedEventLog<SpvVaultManager["filters"]["Claimed"]>; const [ownerClaim, vaultIdClaim] = unpackOwnerAndVaultId(claimedEvent.args.ownerAndVaultId); return { type: SpvWithdrawalStateType.CLAIMED, txId: event.transactionHash, owner: ownerClaim, vaultId: vaultIdClaim, recipient: claimedEvent.args.recipient, claimer: claimedEvent.args.caller, fronter: claimedEvent.args.frontingAddress }; case "Closed": const closedEvent = event as TypedEventLog<SpvVaultManager["filters"]["Closed"]>; return { type: SpvWithdrawalStateType.CLOSED, txId: event.transactionHash, owner: closedEvent.args.owner, vaultId: closedEvent.args.vaultId, error: closedEvent.args.error }; default: return null; } } async getWithdrawalState(withdrawalTx: EVMSpvWithdrawalData, scStartHeight?: number): Promise<SpvWithdrawalState> { const txHash = Buffer.from(withdrawalTx.getTxId(), "hex").reverse(); const events: ["Fronted", "Claimed", "Closed"] = ["Fronted", "Claimed", "Closed"]; const keys = [null, null, hexlify(txHash)]; let result: SpvWithdrawalState; if(scStartHeight==null) { result = await this.Events.findInContractEvents( events, keys, async (event) => { const result = this.parseWithdrawalEvent(event); if(result!=null) return result; } ); } else { result = await this.Events.findInContractEventsForward( events, keys, async (event) => { const result = this.parseWithdrawalEvent(event); if(result==null) return; if(result.type===SpvWithdrawalStateType.FRONTED) { //Check if still fronted const fronterAddress = await this.getFronterAddress(result.owner, result.vaultId, withdrawalTx); //Not fronted now, there should be a claim/close event after the front event, continue if(fronterAddress==null) return; } return result; }, scStartHeight ); } result ??= { type: SpvWithdrawalStateType.NOT_FOUND }; return result; } async getWithdrawalStates(withdrawalTxs: {withdrawal: EVMSpvWithdrawalData, scStartBlockheight?: number}[]): Promise<{[btcTxId: string]: SpvWithdrawalState}> { const result: {[btcTxId: string]: SpvWithdrawalState} = {}; const events: ["Fronted", "Claimed", "Closed"] = ["Fronted", "Claimed", "Closed"]; for(let i=0;i<withdrawalTxs.length;i+=this.Chain.config.maxLogTopics) { const checkWithdrawalTxs = withdrawalTxs.slice(i, i+this.Chain.config.maxLogTopics); const checkWithdrawalTxsMap = new Map(checkWithdrawalTxs.map(val => [val.withdrawal.getTxId() as string, val.withdrawal])); let scStartHeight = null; for(let val of checkWithdrawalTxs) { if(val.scStartBlockheight==null) { scStartHeight = null; break; } scStartHeight = Math.min(scStartHeight ?? Infinity, val.scStartBlockheight); } const keys = [null, null, checkWithdrawalTxs.map(withdrawal => hexlify(Buffer.from(withdrawal.withdrawal.getTxId(), "hex").reverse()))]; if(scStartHeight==null) { await this.Events.findInContractEvents( events, keys, async (event) => { const _event = event as TypedEventLog<SpvVaultManager["filters"]["Fronted" | "Claimed" | "Closed"]>; const btcTxId = Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex"); if(!checkWithdrawalTxsMap.has(btcTxId)) { this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`) return null; } const eventResult = this.parseWithdrawalEvent(event); if(eventResult==null) return null; checkWithdrawalTxsMap.delete(btcTxId); result[btcTxId] = eventResult; if(checkWithdrawalTxsMap.size===0) return true; //All processed } ); } else { await this.Events.findInContractEventsForward( events, keys, async (event) => { const _event = event as TypedEventLog<SpvVaultManager["filters"]["Fronted" | "Claimed" | "Closed"]>; const btcTxId = Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex"); const withdrawalTx = checkWithdrawalTxsMap.get(btcTxId); if(withdrawalTx==null) { this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`) return; } const eventResult = this.parseWithdrawalEvent(event); if(eventResult==null) return; if(eventResult.type===SpvWithdrawalStateType.FRONTED) { //Check if still fronted const fronterAddress = await this.getFronterAddress(eventResult.owner, eventResult.vaultId, withdrawalTx); //Not fronted now, so there should be a claim/close event after the front event, continue if(fronterAddress==null) return; //Fronted still, so this should be the latest current state } checkWithdrawalTxsMap.delete(btcTxId); result[btcTxId] = eventResult; if(checkWithdrawalTxsMap.size===0) return true; //All processed }, scStartHeight ); } } for(let val of withdrawalTxs) { result[val.withdrawal.getTxId()] ??= { type: SpvWithdrawalStateType.NOT_FOUND }; } return result; } getWithdrawalData(btcTx: BtcTx): Promise<EVMSpvWithdrawalData> { return Promise.resolve(new EVMSpvWithdrawalData(btcTx)); } //OP_RETURN data encoding/decoding fromOpReturnData(data: Buffer): { recipient: string; rawAmounts: bigint[]; executionHash: string } { return EVMSpvVaultContract.fromOpReturnData(data); } static fromOpReturnData(data: Buffer): { recipient: string; rawAmounts: bigint[]; executionHash: string } { let rawAmount0: bigint = 0n; let rawAmount1: bigint = 0n; let executionHash: string = null; if(data.length===28) { rawAmount0 = data.readBigInt64BE(20).valueOf(); } else if(data.length===36) { rawAmount0 = data.readBigInt64BE(20).valueOf(); rawAmount1 = data.readBigInt64BE(28).valueOf(); } else if(data.length===60) { rawAmount0 = data.readBigInt64BE(20).valueOf(); executionHash = data.slice(28, 60).toString("hex"); } else if(data.length===68) { rawAmount0 = data.readBigInt64BE(20).valueOf(); rawAmount1 = data.readBigInt64BE(28).valueOf(); executionHash = data.slice(36, 68).toString("hex"); } else { throw new Error("Invalid OP_RETURN data length!"); } const recipient = "0x"+data.slice(0, 20).toString("hex"); if(!EVMAddresses.isValidAddress(recipient)) throw new Error("Invalid recipient specified"); return {executionHash, rawAmounts: [rawAmount0, rawAmount1], recipient: getAddress(recipient)}; } toOpReturnData(recipient: string, rawAmounts: bigint[], executionHash?: string): Buffer { return EVMSpvVaultContract.toOpReturnData(recipient, rawAmounts, executionHash); } static toOpReturnData(recipient: string, rawAmounts: bigint[], executionHash?: string): Buffer { if(!EVMAddresses.isValidAddress(recipient)) throw new Error("Invalid recipient specified"); if(rawAmounts.length < 1) throw new Error("At least 1 amount needs to be specified"); if(rawAmounts.length > 2) throw new Error("At most 2 amounts need to be specified"); rawAmounts.forEach(val => { if(val < 0n) throw new Error("Negative raw amount specified"); if(val >= 2n**64n) throw new Error("Raw amount overflow"); }); if(executionHash!=null) { if(Buffer.from(executionHash, "hex").length !== 32) throw new Error("Invalid execution hash"); } const recipientBuffer = Buffer.from(recipient.substring(2).padStart(40, "0"), "hex"); const amount0Buffer = BigIntBufferUtils.toBuffer(rawAmounts[0], "be", 8); const amount1Buffer = rawAmounts[1]==null || rawAmounts[1]===0n ? Buffer.alloc(0) : BigIntBufferUtils.toBuffer(rawAmounts[1], "be", 8); const executionHashBuffer = executionHash==null ? Buffer.alloc(0) : Buffer.from(executionHash, "hex"); return Buffer.concat([ recipientBuffer, amount0Buffer, amount1Buffer, executionHashBuffer ]); } //Actions async claim(signer: EVMSigner, vault: EVMSpvVaultData, txs: {tx: EVMSpvWithdrawalData, storedHeader?: EVMBtcStoredHeader}[], synchronizer?: RelaySynchronizer<any, any, any>, initAta?: boolean, txOptions?: TransactionConfirmationOptions): Promise<string> { const result = await this.txsClaim(signer.getAddress(), vault, txs, synchronizer, initAta, txOptions?.feeRate); const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return signature; } async deposit(signer: EVMSigner, vault: EVMSpvVaultData, rawAmounts: bigint[], txOptions?: TransactionConfirmationOptions): Promise<string> { const result = await this.txsDeposit(signer.getAddress(), vault, rawAmounts, txOptions?.feeRate); const txHashes = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return txHashes[txHashes.length - 1]; } async frontLiquidity(signer: EVMSigner, vault: EVMSpvVaultData, realWithdrawalTx: EVMSpvWithdrawalData, withdrawSequence: number, txOptions?: TransactionConfirmationOptions): Promise<string> { const result = await this.txsFrontLiquidity(signer.getAddress(), vault, realWithdrawalTx, withdrawSequence, txOptions?.feeRate); const txHashes = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return txHashes[txHashes.length - 1]; } async open(signer: EVMSigner, vault: EVMSpvVaultData, txOptions?: TransactionConfirmationOptions): Promise<string> { const result = await this.txsOpen(signer.getAddress(), vault, txOptions?.feeRate); const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return signature; } //Transactions async txsClaim( signer: string, vault: EVMSpvVaultData, txs: { tx: EVMSpvWithdrawalData, storedHeader?: EVMBtcStoredHeader }[], synchronizer?: RelaySynchronizer<any, any, any>, initAta?: boolean, feeRate?: string ): Promise<EVMTx[]> { if(!vault.isOpened()) throw new Error("Cannot claim from a closed vault!"); feeRate ??= await this.Chain.Fees.getFeeRate(); const txsWithMerkleProofs: { tx: EVMSpvWithdrawalData, reversedTxId: Buffer, pos: number, blockheight: number, merkle: Buffer[], storedHeader?: EVMBtcStoredHeader }[] = []; for(let tx of txs) { const merkleProof = await this.bitcoinRpc.getMerkleProof(tx.tx.btcTx.txid, tx.tx.btcTx.blockhash); this.logger.debug("txsClaim(): merkle proof computed: ", merkleProof); txsWithMerkleProofs.push({ ...merkleProof, ...tx }); } const evmTxs: EVMTx[] = []; const storedHeaders: {[blockhash: string]: EVMBtcStoredHeader} = await EVMBtcRelay.getCommitedHeadersAndSynchronize( signer, this.btcRelay, txsWithMerkleProofs.filter(tx => tx.storedHeader==null).map(tx => { return { blockhash: tx.tx.btcTx.blockhash, blockheight: tx.blockheight, requiredConfirmations: vault.getConfirmations() } }), evmTxs, synchronizer, feeRate ); if(storedHeaders==null) throw new Error("Cannot fetch committed header!"); for(let tx of txsWithMerkleProofs) { evmTxs.push(await this.Claim(signer, vault, tx.tx, tx.storedHeader ?? storedHeaders[tx.tx.btcTx.blockhash], tx.merkle, tx.pos, feeRate)); } this.logger.debug("txsClaim(): "+evmTxs.length+" claim TXs created claiming "+txs.length+" txs, owner: "+vault.getOwner()+ " vaultId: "+vault.getVaultId().toString(10)); return evmTxs; } async txsDeposit(signer: string, vault: EVMSpvVaultData, rawAmounts: bigint[], feeRate?: string): Promise<EVMTx[]> { if(!vault.isOpened()) throw new Error("Cannot deposit to a closed vault!"); feeRate ??= await this.Chain.Fees.getFeeRate(); const txs: EVMTx[] = []; let realAmount0: bigint = 0n; let realAmount1: bigint = 0n; //Approve first const requiredApprovals: {[address: string]: bigint} = {}; if(rawAmounts[0]!=null && rawAmounts[0]!==0n) { if(vault.token0.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) { realAmount0 = rawAmounts[0] * vault.token0.multiplier; requiredApprovals[vault.token0.token.toLowerCase()] = realAmount0; } } if(rawAmounts[1]!=null && rawAmounts[1]!==0n) { if(vault.token1.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) { realAmount1 = rawAmounts[1] * vault.token1.multiplier; requiredApprovals[vault.token1.token.toLowerCase()] ??= 0n; requiredApprovals[vault.token1.token.toLowerCase()] += realAmount1; } } const requiredApprovalTxns = await Promise.all( Object.keys(requiredApprovals).map(token => this.Chain.Tokens.checkAndGetApproveTx(signer, token, requiredApprovals[token], this.contractAddress, feeRate)) ); requiredApprovalTxns.forEach(tx => tx!=null && txs.push(tx)); txs.push(await this.Deposit(signer, vault, rawAmounts, feeRate)); this.logger.debug("txsDeposit(): deposit TX created,"+ " token0: "+vault.token0.token+" rawAmount0: "+rawAmounts[0].toString(10)+" amount0: "+realAmount0.toString(10)+ " token1: "+vault.token1.token+" rawAmount1: "+(rawAmounts[1] ?? 0n).toString(10)+" amount1: "+realAmount1.toString(10)); return txs; } async txsFrontLiquidity(signer: string, vault: EVMSpvVaultData, realWithdrawalTx: EVMSpvWithdrawalData, withdrawSequence: number, feeRate?: string): Promise<EVMTx[]> { if(!vault.isOpened()) throw new Error("Cannot front on a closed vault!"); feeRate ??= await this.Chain.Fees.getFeeRate(); const txs: EVMTx[] = []; let realAmount0 = 0n; let realAmount1 = 0n; //Approve first const rawAmounts = realWithdrawalTx.getFrontingAmount(); //Approve first const requiredApprovals: {[address: string]: bigint} = {}; if(rawAmounts[0]!=null && rawAmounts[0]!==0n) { if(vault.token0.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) { realAmount0 = rawAmounts[0] * vault.token0.multiplier; requiredApprovals[vault.token0.token.toLowerCase()] = realAmount0; } } if(rawAmounts[1]!=null && rawAmounts[1]!==0n) { if(vault.token1.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) { realAmount1 = rawAmounts[1] * vault.token1.multiplier; requiredApprovals[vault.token1.token.toLowerCase()] ??= 0n; requiredApprovals[vault.token1.token.toLowerCase()] += realAmount1; } } const requiredApprovalTxns = await Promise.all( Object.keys(requiredApprovals).map(token => this.Chain.Tokens.checkAndGetApproveTx(signer, token, requiredApprovals[token], this.contractAddress, feeRate)) ); requiredApprovalTxns.forEach(tx => tx!=null && txs.push(tx)); txs.push(await this.Front(signer, vault, realWithdrawalTx, withdrawSequence, feeRate)); this.logger.debug("txsFrontLiquidity(): front TX created,"+ " token0: "+vault.token0.token+" rawAmount0: "+rawAmounts[0].toString(10)+" amount0: "+realAmount0.toString(10)+ " token1: "+vault.token1.token+" rawAmount1: "+(rawAmounts[1] ?? 0n).toString(10)+" amount1: "+realAmount1.toString(10)); return txs; } async txsOpen(signer: string, vault: EVMSpvVaultData, feeRate?: string): Promise<EVMTx[]> { if(vault.isOpened()) throw new Error("Cannot open an already opened vault!"); feeRate ??= await this.Chain.Fees.getFeeRate(); const tx = await this.Open(signer, vault, feeRate); this.logger.debug("txsOpen(): open TX created, owner: "+vault.getOwner()+ " vaultId: "+vault.getVaultId().toString(10)); return [tx]; } getClaimGas(signer: string, vault: EVMSpvVaultData, data?: EVMSpvWithdrawalData): number { let totalGas = EVMSpvVaultContract.GasCosts.CLAIM_BASE; if (data==null || (data.rawAmounts[0] != null && data.rawAmounts[0] > 0n)) { const transferFee = vault==null || vault.token0.token.toLowerCase() === this.Chain.getNativeCurrencyAddress() ? EVMSpvVaultContract.GasCosts.CLAIM_NATIVE_TRANSFER : EVMSpvVaultContract.GasCosts.CLAIM_ERC20_TRANSFER; totalGas += transferFee; if (data==null || data.frontingFeeRate > 0n) totalGas += transferFee; //Also needs to pay out to fronter if (data==null || (data.callerFeeRate > 0n && !data.isRecipient(signer))) totalGas += transferFee; //Also needs to pay out to caller } if (data==null || (data.rawAmounts[1] != null && data.rawAmounts[1] > 0n)) { const transferFee = vault==null || vault.token1.token.toLowerCase() === this.Chain.getNativeCurrencyAddress() ? EVMSpvVaultContract.GasCosts.CLAIM_NATIVE_TRANSFER : EVMSpvVaultContract.GasCosts.CLAIM_ERC20_TRANSFER; totalGas += transferFee; if (data==null || data.frontingFeeRate > 0n) totalGas += transferFee; //Also needs to pay out to fronter if (data==null || (data.callerFeeRate > 0n && !data.isRecipient(signer))) totalGas += transferFee; //Also needs to pay out to caller } if (data==null || (data.executionHash != null && data.executionHash !== ZeroHash)) totalGas += EVMSpvVaultContract.GasCosts.CLAIM_EXECUTION_SCHEDULE; return totalGas; } getFrontGas(signer: string, vault: EVMSpvVaultData, data?: EVMSpvWithdrawalData): number { let totalGas = EVMSpvVaultContract.GasCosts.FRONT_BASE; if (data==null || (data.rawAmounts[0] != null && data.rawAmounts[0] > 0n)) { totalGas += vault==null || vault.token0.token.toLowerCase() === this.Chain.getNativeCurrencyAddress() ? EVMSpvVaultContract.GasCosts.FRONT_NATIVE_TRANSFER : EVMSpvVaultContract.GasCosts.FRONT_ERC20_TRANSFER; } if (data==null || (data.rawAmounts[1] != null && data.rawAmounts[1] > 0n)) { totalGas += vault==null || vault.token1.token.toLowerCase() === this.Chain.getNativeCurrencyAddress() ? EVMSpvVaultContract.GasCosts.FRONT_NATIVE_TRANSFER : EVMSpvVaultContract.GasCosts.FRONT_ERC20_TRANSFER; } if (data==null || (data.executionHash != null && data.executionHash !== ZeroHash)) totalGas += EVMSpvVaultContract.GasCosts.FRONT_EXECUTION_SCHEDULE; return totalGas; } async getClaimFee(signer: string, vault: EVMSpvVaultData, withdrawalData: EVMSpvWithdrawalData, feeRate?: string): Promise<bigint> { feeRate ??= await this.Chain.Fees.getFeeRate(); return EVMFees.getGasFee(this.getClaimGas(signer, vault, withdrawalData), feeRate); } async getFrontFee(signer: string, vault?: EVMSpvVaultData, withdrawalData?: EVMSpvWithdrawalData, feeRate?: string): Promise<bigint> { vault ??= EVMSpvVaultData.randomVault(); feeRate ??= await this.Chain.Fees.getFeeRate(); let totalFee = EVMFees.getGasFee(this.getFrontGas(signer, vault, withdrawalData), feeRate); if(withdrawalData==null || (withdrawalData.rawAmounts[0]!=null && withdrawalData.rawAmounts[0]>0n)) { if(vault.token0.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) { totalFee += await this.Chain.Tokens.getApproveFee(feeRate); } } if(withdrawalData==null || (withdrawalData.rawAmounts[1]!=null && withdrawalData.rawAmounts[1]>0n)) { if(vault.token1.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) { if(vault.token1.token.toLowerCase()!==vault.token0.token.toLowerCase() || withdrawalData==null || withdrawalData.rawAmounts[0]==null || withdrawalData.rawAmounts[0]===0n) { totalFee += await this.Chain.Tokens.getApproveFee(feeRate); } } } return totalFee; } }