UNPKG

@atomiqlabs/chain-evm

Version:

EVM specific base implementation

591 lines (520 loc) 23.1 kB
import { BigIntBufferUtils, ChainSwapType, IntermediaryReputationType, RelaySynchronizer, SignatureData, SwapCommitState, SwapCommitStateType, SwapContract, TransactionConfirmationOptions } from "@atomiqlabs/base"; import {Buffer} from "buffer"; import {TimelockRefundHandler} from "./handlers/refund/TimelockRefundHandler"; import {claimHandlersList, IClaimHandler} from "./handlers/claim/ClaimHandlers" import {IHandler} from "./handlers/IHandler"; import {sha256} from "@noble/hashes/sha2"; import { EVMContractBase } from "../contract/EVMContractBase"; import {EscrowManager} from "./EscrowManagerTypechain"; import { EVMSwapData } from "./EVMSwapData"; import {EVMTx} from "../chain/modules/EVMTransactions"; import { EVMSigner } from "../wallet/EVMSigner"; import {EVMChainInterface} from "../chain/EVMChainInterface"; import { EVMBtcRelay } from "../btcrelay/EVMBtcRelay"; import {EscrowManagerAbi} from "./EscrowManagerAbi"; import {hexlify} from "ethers"; import {EVMBtcStoredHeader} from "../btcrelay/headers/EVMBtcStoredHeader"; import {EVMLpVault} from "./modules/EVMLpVault"; import {EVMPreFetchVerification, EVMSwapInit} from "./modules/EVMSwapInit"; import { EVMSwapRefund } from "./modules/EVMSwapRefund"; import {EVMSwapClaim} from "./modules/EVMSwapClaim"; const ESCROW_STATE_COMMITTED = 1; const ESCROW_STATE_CLAIMED = 2; const ESCROW_STATE_REFUNDED = 3; export class EVMSwapContract<ChainId extends string = string> extends EVMContractBase<EscrowManager> implements SwapContract< EVMSwapData, EVMTx, never, EVMPreFetchVerification, EVMSigner, ChainId > { //////////////////////// //// Constants readonly chainId: ChainId; //////////////////////// //// Timeouts readonly claimWithSecretTimeout: number = 180; readonly claimWithTxDataTimeout: number = 180; readonly refundTimeout: number = 180; readonly claimGracePeriod: number = 10*60; readonly refundGracePeriod: number = 10*60; readonly authGracePeriod: number = 30; //////////////////////// //// Services readonly Init: EVMSwapInit; readonly Refund: EVMSwapRefund; readonly Claim: EVMSwapClaim; readonly LpVault: EVMLpVault; //////////////////////// //// Handlers readonly claimHandlersByAddress: {[address: string]: IClaimHandler<any, any>} = {}; readonly claimHandlersBySwapType: {[type in ChainSwapType]?: IClaimHandler<any, any>} = {}; readonly refundHandlersByAddress: {[address: string]: IHandler<any, any>} = {}; readonly timelockRefundHandler: IHandler<any, any>; readonly btcRelay: EVMBtcRelay<any>; constructor( chainInterface: EVMChainInterface<ChainId>, btcRelay: EVMBtcRelay<any>, contractAddress: string, handlerAddresses: { refund: { timelock: string }, claim: { [type in ChainSwapType]: string } } ) { super(chainInterface, contractAddress, EscrowManagerAbi); this.chainId = chainInterface.chainId; this.Init = new EVMSwapInit(chainInterface, this); this.Refund = new EVMSwapRefund(chainInterface, this); this.Claim = new EVMSwapClaim(chainInterface, this); this.LpVault = new EVMLpVault(chainInterface, this); this.btcRelay = btcRelay; 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(handlerAddresses.refund.timelock); this.refundHandlersByAddress[this.timelockRefundHandler.address.toLowerCase()] = this.timelockRefundHandler; } async start(): Promise<void> { } //////////////////////////////////////////// //// Signatures preFetchForInitSignatureVerification(): Promise<EVMPreFetchVerification> { return this.Init.preFetchForInitSignatureVerification(); } getInitSignature(signer: EVMSigner, swapData: EVMSwapData, authorizationTimeout: number, preFetchedBlockData?: never, feeRate?: string): Promise<SignatureData> { return this.Init.signSwapInitialization(signer, swapData, authorizationTimeout); } isValidInitAuthorization(sender: string, swapData: EVMSwapData, {timeout, prefix, signature}, feeRate?: string, preFetchedData?: EVMPreFetchVerification): Promise<Buffer> { return this.Init.isSignatureValid(sender, swapData, timeout, prefix, signature, preFetchedData); } getInitAuthorizationExpiry(swapData: EVMSwapData, {timeout, prefix, signature}, preFetchedData?: EVMPreFetchVerification): Promise<number> { return this.Init.getSignatureExpiry(timeout); } isInitAuthorizationExpired(swapData: EVMSwapData, {timeout, prefix, signature}): Promise<boolean> { return this.Init.isSignatureExpired(timeout); } getRefundSignature(signer: EVMSigner, swapData: EVMSwapData, authorizationTimeout: number): Promise<SignatureData> { return this.Refund.signSwapRefund(signer, swapData, authorizationTimeout); } isValidRefundAuthorization(swapData: EVMSwapData, {timeout, prefix, signature}): Promise<Buffer> { return this.Refund.isSignatureValid(swapData, timeout, prefix, signature); } getDataSignature(signer: EVMSigner, data: Buffer): Promise<string> { return this.Chain.Signatures.getDataSignature(signer, data); } isValidDataSignature(data: Buffer, signature: string, publicKey: string): Promise<boolean> { 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: string, data: EVMSwapData): Promise<boolean> { 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: EVMSwapData): Promise<boolean> { 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: string, data: EVMSwapData): Promise<boolean> { let currentTimestamp: bigint = 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: string, data: EVMSwapData): Promise<boolean> { //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: string, confirmations: number) { return Buffer.from(this.claimHandlersBySwapType[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: Buffer, amount: bigint, confirmations: number, nonce?: bigint): Buffer { let result: string; if(nonce==null || nonce === 0n) { result = this.claimHandlersBySwapType[ChainSwapType.CHAIN].getCommitment({ output: outputScript, amount, confirmations, btcRelay: this.btcRelay }); } else { result = this.claimHandlersBySwapType[ChainSwapType.CHAIN_NONCED].getCommitment({ output: outputScript, amount, nonce, confirmations, btcRelay: this.btcRelay }); } return 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: Buffer): Buffer { return Buffer.from(this.claimHandlersBySwapType[ChainSwapType.HTLC].getCommitment(paymentHash).slice(2), "hex"); } getExtraData(outputScript: Buffer, amount: bigint, confirmations: number, nonce?: bigint): Buffer { if(nonce==null) nonce = 0n; const txoHash = Buffer.from(sha256(Buffer.concat([ BigIntBufferUtils.toBuffer(amount, "le", 8), outputScript ]))); return Buffer.concat([ txoHash, BigIntBufferUtils.toBuffer(nonce, "be", 8), 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: string, data: EVMSwapData): Promise<SwapCommitState> { 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: SwapCommitStateType.REFUNDABLE}; return {type: SwapCommitStateType.COMMITED}; case ESCROW_STATE_CLAIMED: return { type: 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) ? SwapCommitStateType.EXPIRED : 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: string): Promise<EVMSwapData> { //TODO: Noop return null; } //////////////////////////////////////////// //// Swap data initializer createSwapData( type: ChainSwapType, offerer: string, claimer: string, token: string, amount: bigint, paymentHash: string, sequence: bigint, expiry: bigint, payIn: boolean, payOut: boolean, securityDeposit: bigint, claimerBounty: bigint, depositToken: string = this.Chain.Tokens.getNativeCurrencyAddress() ): Promise<EVMSwapData> { return Promise.resolve(new 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, hexlify(BigIntBufferUtils.toBuffer(expiry, "be", 32)), amount, depositToken, securityDeposit, claimerBounty, type, null )); } //////////////////////////////////////////// //// Utils async getBalance(signer: string, tokenAddress: string, inContract?: boolean): Promise<bigint> { if(inContract) return await this.getIntermediaryBalance(signer, tokenAddress); return await this.Chain.getBalance(signer, tokenAddress); } getIntermediaryData(address: string, token: string): Promise<{ balance: bigint, reputation: IntermediaryReputationType }> { return this.LpVault.getIntermediaryData(address, token); } getIntermediaryReputation(address: string, token: string): Promise<IntermediaryReputationType> { return this.LpVault.getIntermediaryReputation(address, token); } getIntermediaryBalance(address: string, token: string): Promise<bigint> { return this.LpVault.getIntermediaryBalance(address, token); } //////////////////////////////////////////// //// Transaction initializers async txsClaimWithSecret( signer: string | EVMSigner, swapData: EVMSwapData, secret: string, checkExpiry?: boolean, initAta?: boolean, feeRate?: string, skipAtaCheck?: boolean ): Promise<EVMTx[]> { return this.Claim.txsClaimWithSecret(typeof(signer)==="string" ? signer : signer.getAddress(), swapData, secret, checkExpiry, feeRate) } async txsClaimWithTxData( signer: string | EVMSigner, swapData: EVMSwapData, tx: { blockhash: string, confirmations: number, txid: string, hex: string, height: number }, requiredConfirmations: number, vout: number, commitedHeader?: EVMBtcStoredHeader, synchronizer?: RelaySynchronizer<EVMBtcStoredHeader, EVMTx, any>, initAta?: boolean, feeRate?: string ): Promise<EVMTx[] | null> { return this.Claim.txsClaimWithTxData( typeof(signer)==="string" ? signer : signer.getAddress(), swapData, tx, requiredConfirmations, vout, commitedHeader, synchronizer, feeRate ); } txsRefund(signer: string, swapData: EVMSwapData, check?: boolean, initAta?: boolean, feeRate?: string): Promise<EVMTx[]> { return this.Refund.txsRefund(signer, swapData, check, feeRate); } txsRefundWithAuthorization(signer: string, swapData: EVMSwapData, {timeout, prefix, signature}, check?: boolean, initAta?: boolean, feeRate?: string): Promise<EVMTx[]> { return this.Refund.txsRefundWithAuthorization(signer, swapData, timeout, prefix,signature, check, feeRate); } txsInit(signer: string, swapData: EVMSwapData, {timeout, prefix, signature}, skipChecks?: boolean, feeRate?: string): Promise<EVMTx[]> { return this.Init.txsInit(signer, swapData, timeout, prefix, signature, skipChecks, feeRate); } txsWithdraw(signer: string, token: string, amount: bigint, feeRate?: string): Promise<EVMTx[]> { return this.LpVault.txsWithdraw(signer, token, amount, feeRate); } txsDeposit(signer: string, token: string, amount: bigint, feeRate?: string): Promise<EVMTx[]> { return this.LpVault.txsDeposit(signer, token, amount, feeRate); } //////////////////////////////////////////// //// Executors async claimWithSecret( signer: EVMSigner, swapData: EVMSwapData, secret: string, checkExpiry?: boolean, initAta?: boolean, txOptions?: TransactionConfirmationOptions ): Promise<string> { 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: EVMSigner, swapData: EVMSwapData, tx: { blockhash: string, confirmations: number, txid: string, hex: string, height: number }, requiredConfirmations: number, vout: number, commitedHeader?: EVMBtcStoredHeader, synchronizer?: RelaySynchronizer<EVMBtcStoredHeader, EVMTx, any>, initAta?: boolean, txOptions?: TransactionConfirmationOptions ): Promise<string> { 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: EVMSigner, swapData: EVMSwapData, check?: boolean, initAta?: boolean, txOptions?: TransactionConfirmationOptions ): Promise<string> { 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: EVMSigner, swapData: EVMSwapData, signature: SignatureData, check?: boolean, initAta?: boolean, txOptions?: TransactionConfirmationOptions ): Promise<string> { 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: EVMSigner, swapData: EVMSwapData, signature: SignatureData, skipChecks?: boolean, txOptions?: TransactionConfirmationOptions ): Promise<string> { 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: EVMSigner, token: string, amount: bigint, txOptions?: TransactionConfirmationOptions ): Promise<string> { 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: EVMSigner, token: string, amount: bigint, txOptions?: TransactionConfirmationOptions ): Promise<string> { 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?: string, claimer?: string, token?: string, paymentHash?: string): Promise<string> { return this.Chain.Fees.getFeeRate(); } getInitFeeRate(offerer?: string, claimer?: string, token?: string, paymentHash?: string): Promise<string> { return this.Chain.Fees.getFeeRate(); } getRefundFeeRate(swapData: EVMSwapData): Promise<string> { return this.Chain.Fees.getFeeRate(); } getClaimFeeRate(signer: string, swapData: EVMSwapData): Promise<string> { return this.Chain.Fees.getFeeRate(); } getClaimFee(signer: string, swapData: EVMSwapData, feeRate?: string): Promise<bigint> { return this.Claim.getClaimFee(swapData, feeRate); } /** * Get the estimated solana fee of the commit transaction */ getCommitFee(swapData: EVMSwapData, feeRate?: string): Promise<bigint> { return this.Init.getInitFee(swapData, feeRate); } /** * Get the estimated solana transaction fee of the refund transaction */ getRefundFee(swapData: EVMSwapData, feeRate?: string): Promise<bigint> { return this.Refund.getRefundFee(swapData, feeRate); } }