UNPKG

@atomiqlabs/chain-evm

Version:

EVM specific base implementation

374 lines (373 loc) 18 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EVMSwapContract = void 0; const base_1 = require("@atomiqlabs/base"); const buffer_1 = require("buffer"); const TimelockRefundHandler_1 = require("./handlers/refund/TimelockRefundHandler"); const ClaimHandlers_1 = require("./handlers/claim/ClaimHandlers"); const sha2_1 = require("@noble/hashes/sha2"); const EVMContractBase_1 = require("../contract/EVMContractBase"); const EVMSwapData_1 = require("./EVMSwapData"); const EscrowManagerAbi_1 = require("./EscrowManagerAbi"); const ethers_1 = require("ethers"); const EVMLpVault_1 = require("./modules/EVMLpVault"); const EVMSwapInit_1 = require("./modules/EVMSwapInit"); const EVMSwapRefund_1 = require("./modules/EVMSwapRefund"); const EVMSwapClaim_1 = require("./modules/EVMSwapClaim"); const ESCROW_STATE_COMMITTED = 1; const ESCROW_STATE_CLAIMED = 2; const ESCROW_STATE_REFUNDED = 3; class EVMSwapContract extends EVMContractBase_1.EVMContractBase { constructor(chainInterface, btcRelay, contractAddress, handlerAddresses) { super(chainInterface, contractAddress, EscrowManagerAbi_1.EscrowManagerAbi); //////////////////////// //// Timeouts this.claimWithSecretTimeout = 180; this.claimWithTxDataTimeout = 180; this.refundTimeout = 180; this.claimGracePeriod = 10 * 60; this.refundGracePeriod = 10 * 60; this.authGracePeriod = 30; //////////////////////// //// Handlers this.claimHandlersByAddress = {}; this.claimHandlersBySwapType = {}; this.refundHandlersByAddress = {}; this.chainId = chainInterface.chainId; this.Init = new EVMSwapInit_1.EVMSwapInit(chainInterface, this); this.Refund = new EVMSwapRefund_1.EVMSwapRefund(chainInterface, this); this.Claim = new EVMSwapClaim_1.EVMSwapClaim(chainInterface, this); this.LpVault = new EVMLpVault_1.EVMLpVault(chainInterface, this); this.btcRelay = btcRelay; ClaimHandlers_1.claimHandlersList.forEach(handlerCtor => { const handler = new handlerCtor(handlerAddresses.claim[handlerCtor.type]); this.claimHandlersByAddress[handler.address.toLowerCase()] = handler; this.claimHandlersBySwapType[handlerCtor.type] = handler; }); this.timelockRefundHandler = new TimelockRefundHandler_1.TimelockRefundHandler(handlerAddresses.refund.timelock); this.refundHandlersByAddress[this.timelockRefundHandler.address.toLowerCase()] = this.timelockRefundHandler; } async start() { } //////////////////////////////////////////// //// Signatures preFetchForInitSignatureVerification() { return this.Init.preFetchForInitSignatureVerification(); } getInitSignature(signer, swapData, authorizationTimeout, preFetchedBlockData, feeRate) { return this.Init.signSwapInitialization(signer, swapData, authorizationTimeout); } isValidInitAuthorization(sender, swapData, { timeout, prefix, signature }, feeRate, preFetchedData) { return this.Init.isSignatureValid(sender, swapData, timeout, prefix, signature, preFetchedData); } getInitAuthorizationExpiry(swapData, { timeout, prefix, signature }, preFetchedData) { return this.Init.getSignatureExpiry(timeout); } isInitAuthorizationExpired(swapData, { timeout, prefix, signature }) { return this.Init.isSignatureExpired(timeout); } getRefundSignature(signer, swapData, authorizationTimeout) { return this.Refund.signSwapRefund(signer, swapData, authorizationTimeout); } isValidRefundAuthorization(swapData, { timeout, prefix, signature }) { return this.Refund.isSignatureValid(swapData, timeout, prefix, signature); } getDataSignature(signer, data) { return this.Chain.Signatures.getDataSignature(signer, data); } isValidDataSignature(data, signature, publicKey) { return this.Chain.Signatures.isValidDataSignature(data, signature, publicKey); } //////////////////////////////////////////// //// Swap data utils /** * Checks whether the claim is claimable by us, that means not expired, we are claimer & the swap is commited * * @param signer * @param data */ async isClaimable(signer, data) { if (!data.isClaimer(signer)) return false; if (await this.isExpired(signer, data)) return false; return await this.isCommited(data); } /** * Checks whether a swap is commited, i.e. the swap still exists on-chain and was not claimed nor refunded * * @param swapData */ async isCommited(swapData) { const data = await this.contract.getHashState("0x" + swapData.getEscrowHash()); return Number(data.state) === ESCROW_STATE_COMMITTED; } /** * Checks whether the swap is expired, takes into consideration possible on-chain time skew, therefore for claimer * the swap expires a bit sooner than it should've & for the offerer it expires a bit later * * @param signer * @param data */ isExpired(signer, data) { let currentTimestamp = BigInt(Math.floor(Date.now() / 1000)); if (data.isClaimer(signer)) currentTimestamp = currentTimestamp + BigInt(this.claimGracePeriod); if (data.isOfferer(signer)) currentTimestamp = currentTimestamp - BigInt(this.refundGracePeriod); return Promise.resolve(data.getExpiry() < currentTimestamp); } /** * Checks if the swap is refundable by us, checks if we are offerer, if the swap is already expired & if the swap * is still commited * * @param signer * @param data */ async isRequestRefundable(signer, data) { //Swap can only be refunded by the offerer if (!data.isOfferer(signer)) return false; if (!(await this.isExpired(signer, data))) return false; return await this.isCommited(data); } getHashForTxId(txId, confirmations) { return buffer_1.Buffer.from(this.claimHandlersBySwapType[base_1.ChainSwapType.CHAIN_TXID].getCommitment({ txId, confirmations, btcRelay: this.btcRelay }).slice(2), "hex"); } /** * Get the swap payment hash to be used for an on-chain swap, uses poseidon hash of the value * * @param outputScript output script required to claim the swap * @param amount sats sent required to claim the swap * @param confirmations * @param nonce swap nonce uniquely identifying the transaction to prevent replay attacks */ getHashForOnchain(outputScript, amount, confirmations, nonce) { let result; if (nonce == null || nonce === 0n) { result = this.claimHandlersBySwapType[base_1.ChainSwapType.CHAIN].getCommitment({ output: outputScript, amount, confirmations, btcRelay: this.btcRelay }); } else { result = this.claimHandlersBySwapType[base_1.ChainSwapType.CHAIN_NONCED].getCommitment({ output: outputScript, amount, nonce, confirmations, btcRelay: this.btcRelay }); } return buffer_1.Buffer.from(result.slice(2), "hex"); } /** * Get the swap payment hash to be used for a lightning htlc swap, uses poseidon hash of the sha256 hash of the preimage * * @param paymentHash payment hash of the HTLC */ getHashForHtlc(paymentHash) { return buffer_1.Buffer.from(this.claimHandlersBySwapType[base_1.ChainSwapType.HTLC].getCommitment(paymentHash).slice(2), "hex"); } getExtraData(outputScript, amount, confirmations, nonce) { if (nonce == null) nonce = 0n; const txoHash = buffer_1.Buffer.from((0, sha2_1.sha256)(buffer_1.Buffer.concat([ base_1.BigIntBufferUtils.toBuffer(amount, "le", 8), outputScript ]))); return buffer_1.Buffer.concat([ txoHash, base_1.BigIntBufferUtils.toBuffer(nonce, "be", 8), base_1.BigIntBufferUtils.toBuffer(BigInt(confirmations), "be", 2) ]); } //////////////////////////////////////////// //// Swap data getters /** * Gets the status of the specific swap, this also checks if we are offerer/claimer & checks for expiry (to see * if swap is refundable) * * @param signer * @param data */ async getCommitStatus(signer, data) { const escrowHash = data.getEscrowHash(); const stateData = await this.contract.getHashState("0x" + escrowHash); const state = Number(stateData.state); const blockHeight = Number(stateData.finishBlockheight); switch (state) { case ESCROW_STATE_COMMITTED: if (data.isOfferer(signer) && await this.isExpired(signer, data)) return { type: base_1.SwapCommitStateType.REFUNDABLE }; return { type: base_1.SwapCommitStateType.COMMITED }; case ESCROW_STATE_CLAIMED: return { type: base_1.SwapCommitStateType.PAID, getTxBlock: async () => { return { blockTime: await this.Chain.Blocks.getBlockTime(blockHeight), blockHeight: blockHeight }; }, getClaimTxId: async () => { const events = await this.Events.getContractBlockEvents(["Claim"], [null, null, "0x" + escrowHash], blockHeight, blockHeight); return events.length === 0 ? null : events[0].transactionHash; } }; default: return { type: await this.isExpired(signer, data) ? base_1.SwapCommitStateType.EXPIRED : base_1.SwapCommitStateType.NOT_COMMITED, getTxBlock: async () => { return { blockTime: await this.Chain.Blocks.getBlockTime(blockHeight), blockHeight: blockHeight }; }, getRefundTxId: async () => { const events = await this.Events.getContractBlockEvents(["Refund"], [null, null, "0x" + escrowHash], blockHeight, blockHeight); return events.length === 0 ? null : events[0].transactionHash; } }; } } /** * Returns the data committed for a specific payment hash, or null if no data is currently commited for * the specific swap * * @param paymentHashHex */ async getCommitedData(paymentHashHex) { //TODO: Noop return null; } //////////////////////////////////////////// //// Swap data initializer createSwapData(type, offerer, claimer, token, amount, paymentHash, sequence, expiry, payIn, payOut, securityDeposit, claimerBounty, depositToken = this.Chain.Tokens.getNativeCurrencyAddress()) { return Promise.resolve(new EVMSwapData_1.EVMSwapData(offerer, claimer, token, this.timelockRefundHandler.address, this.claimHandlersBySwapType?.[type]?.address, payOut, payIn, payIn, //For now track reputation for all payIn swaps sequence, "0x" + paymentHash, (0, ethers_1.hexlify)(base_1.BigIntBufferUtils.toBuffer(expiry, "be", 32)), amount, depositToken, securityDeposit, claimerBounty, type, null)); } //////////////////////////////////////////// //// Utils async getBalance(signer, tokenAddress, inContract) { if (inContract) return await this.getIntermediaryBalance(signer, tokenAddress); return await this.Chain.getBalance(signer, tokenAddress); } getIntermediaryData(address, token) { return this.LpVault.getIntermediaryData(address, token); } getIntermediaryReputation(address, token) { return this.LpVault.getIntermediaryReputation(address, token); } getIntermediaryBalance(address, token) { return this.LpVault.getIntermediaryBalance(address, token); } //////////////////////////////////////////// //// Transaction initializers async txsClaimWithSecret(signer, swapData, secret, checkExpiry, initAta, feeRate, skipAtaCheck) { return this.Claim.txsClaimWithSecret(typeof (signer) === "string" ? signer : signer.getAddress(), swapData, secret, checkExpiry, feeRate); } async txsClaimWithTxData(signer, swapData, tx, requiredConfirmations, vout, commitedHeader, synchronizer, initAta, feeRate) { return this.Claim.txsClaimWithTxData(typeof (signer) === "string" ? signer : signer.getAddress(), swapData, tx, requiredConfirmations, vout, commitedHeader, synchronizer, feeRate); } txsRefund(signer, swapData, check, initAta, feeRate) { return this.Refund.txsRefund(signer, swapData, check, feeRate); } txsRefundWithAuthorization(signer, swapData, { timeout, prefix, signature }, check, initAta, feeRate) { return this.Refund.txsRefundWithAuthorization(signer, swapData, timeout, prefix, signature, check, feeRate); } txsInit(signer, swapData, { timeout, prefix, signature }, skipChecks, feeRate) { return this.Init.txsInit(signer, swapData, timeout, prefix, signature, skipChecks, feeRate); } txsWithdraw(signer, token, amount, feeRate) { return this.LpVault.txsWithdraw(signer, token, amount, feeRate); } txsDeposit(signer, token, amount, feeRate) { return this.LpVault.txsDeposit(signer, token, amount, feeRate); } //////////////////////////////////////////// //// Executors async claimWithSecret(signer, swapData, secret, checkExpiry, initAta, txOptions) { const result = await this.Claim.txsClaimWithSecret(signer.getAddress(), swapData, secret, checkExpiry, txOptions?.feeRate); const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return signature; } async claimWithTxData(signer, swapData, tx, requiredConfirmations, vout, commitedHeader, synchronizer, initAta, txOptions) { const txs = await this.Claim.txsClaimWithTxData(signer.getAddress(), swapData, tx, requiredConfirmations, vout, commitedHeader, synchronizer, txOptions?.feeRate); if (txs === null) throw new Error("Btc relay not synchronized to required blockheight!"); const txHashes = await this.Chain.sendAndConfirm(signer, txs, txOptions?.waitForConfirmation, txOptions?.abortSignal); return txHashes[txHashes.length - 1]; } async refund(signer, swapData, check, initAta, txOptions) { let result = await this.txsRefund(signer.getAddress(), swapData, check, initAta, txOptions?.feeRate); const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return signature; } async refundWithAuthorization(signer, swapData, signature, check, initAta, txOptions) { let result = await this.txsRefundWithAuthorization(signer.getAddress(), swapData, signature, check, initAta, txOptions?.feeRate); const [txSignature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return txSignature; } async init(signer, swapData, signature, skipChecks, txOptions) { if (swapData.isPayIn()) { if (!swapData.isOfferer(signer.getAddress()) && !swapData.isOfferer(signer.getAddress())) throw new Error("Invalid signer provided!"); } else { if (!swapData.isClaimer(signer.getAddress()) && !swapData.isOfferer(signer.getAddress())) throw new Error("Invalid signer provided!"); } let result = await this.txsInit(signer.getAddress(), swapData, signature, skipChecks, txOptions?.feeRate); const txHashes = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return txHashes[txHashes.length - 1]; } async withdraw(signer, token, amount, txOptions) { const txs = await this.LpVault.txsWithdraw(signer.getAddress(), token, amount, txOptions?.feeRate); const [txId] = await this.Chain.sendAndConfirm(signer, txs, txOptions?.waitForConfirmation, txOptions?.abortSignal, false); return txId; } async deposit(signer, token, amount, txOptions) { const txs = await this.LpVault.txsDeposit(signer.getAddress(), token, amount, txOptions?.feeRate); const [txId] = await this.Chain.sendAndConfirm(signer, txs, txOptions?.waitForConfirmation, txOptions?.abortSignal, false); return txId; } //////////////////////////////////////////// //// Fees getInitPayInFeeRate(offerer, claimer, token, paymentHash) { return this.Chain.Fees.getFeeRate(); } getInitFeeRate(offerer, claimer, token, paymentHash) { return this.Chain.Fees.getFeeRate(); } getRefundFeeRate(swapData) { return this.Chain.Fees.getFeeRate(); } getClaimFeeRate(signer, swapData) { return this.Chain.Fees.getFeeRate(); } getClaimFee(signer, swapData, feeRate) { return this.Claim.getClaimFee(swapData, feeRate); } /** * Get the estimated solana fee of the commit transaction */ getCommitFee(swapData, feeRate) { return this.Init.getInitFee(swapData, feeRate); } /** * Get the estimated solana transaction fee of the refund transaction */ getRefundFee(swapData, feeRate) { return this.Refund.getRefundFee(swapData, feeRate); } } exports.EVMSwapContract = EVMSwapContract;