UNPKG

@atomiqlabs/sdk

Version:

atomiq labs SDK for cross-chain swaps between smart chains and bitcoin

285 lines (261 loc) 11.2 kB
import {ISwapWrapper, ISwapWrapperOptions, SwapTypeDefinition, WrapperCtorTokens} from "../ISwapWrapper"; import { ChainType, ClaimEvent, InitializeEvent, RefundEvent, SignatureData, SwapCommitState, SwapEvent } from "@atomiqlabs/base"; import {ISwap} from "../ISwap"; import {UnifiedSwapStorage} from "../../storage/UnifiedSwapStorage"; import {UnifiedSwapEventListener} from "../../events/UnifiedSwapEventListener"; import {ISwapPrice} from "../../prices/abstract/ISwapPrice"; import {EventEmitter} from "events"; import {SwapType} from "../../enums/SwapType"; import {IEscrowSwap} from "./IEscrowSwap"; import {Intermediary} from "../../intermediaries/Intermediary"; import {IntermediaryAPI} from "../../intermediaries/apis/IntermediaryAPI"; export type IEscrowSwapDefinition<T extends ChainType, W extends IEscrowSwapWrapper<T, any>, S extends IEscrowSwap<T>> = SwapTypeDefinition<T, W, S>; /** * Base class for wrappers of escrow-based swaps (i.e. swaps utilizing PrTLC and HTLC primitives) * * @category Swaps/Abstract */ export abstract class IEscrowSwapWrapper< T extends ChainType, D extends IEscrowSwapDefinition<T, IEscrowSwapWrapper<T, D>, IEscrowSwap<T, D>>, O extends ISwapWrapperOptions = ISwapWrapperOptions > extends ISwapWrapper<T, D, O> { readonly abstract TYPE: SwapType; /** * @internal */ readonly _contract: (version?: string) => T["Contract"] = (version?: string) => { const _version = version ?? "v1"; const data = this._versionedContracts[_version]; if(data==null) throw new Error(`Invalid contract version ${_version} requested`); return data.swapContract; }; /** * @internal */ readonly _swapDataDeserializer: (version?: string) => new (data: any) => T["Data"] = (version?: string) => { const _version = version ?? "v1"; const data = this._versionedContracts[_version]; if(data==null) throw new Error(`Invalid contract version ${_version} requested`); return data.swapDataConstructor; }; //TODO: Properly populate in constructor readonly _versionedContracts: { [version: string]: { swapContract: T["Contract"], swapDataConstructor: new (data: any) => T["Data"] } } = {}; constructor( chainIdentifier: string, unifiedStorage: UnifiedSwapStorage<T>, unifiedChainEvents: UnifiedSwapEventListener<T>, chain: T["ChainInterface"], prices: ISwapPrice, tokens: WrapperCtorTokens, lpApi: IntermediaryAPI, options: O, versionedContracts: { [version: string]: { swapContract: T["Contract"], swapDataConstructor: new (data: any) => T["Data"] } }, events?: EventEmitter<{swapState: [ISwap]}> ) { super(chainIdentifier, unifiedStorage, unifiedChainEvents, chain, prices, tokens, lpApi, options, events); this._versionedContracts = versionedContracts; } /** * Pre-fetches signature verification data from the server's pre-sent promise, doesn't throw, instead returns null * * @param signDataPrefetch Promise that resolves when we receive "signDataPrefetch" from the LP in streaming mode * @param contractVersion * @returns Pre-fetched signature verification data or null if failed * * @internal */ protected preFetchSignData(signDataPrefetch: Promise<any | null>, contractVersion: string): Promise<T["PreFetchVerification"] | undefined> { if(this._contract(contractVersion).preFetchForInitSignatureVerification==null) { // Catch promise rejections, should they happen signDataPrefetch.catch(() => {}); return Promise.resolve(undefined); } return signDataPrefetch.then(obj => { if(obj==null) return undefined; return this._contract(contractVersion).preFetchForInitSignatureVerification!(obj); }).catch(e => { this.logger.error("preFetchSignData(): Error: ", e); }); } /** * Verifies swap initialization signature returned by the intermediary * * @param initiator A smart chain account initiating the swap * @param data Parsed swap data from the intermediary * @param signature Response of the intermediary * @param feeRatePromise Pre-fetched fee rate promise * @param preFetchSignatureVerificationData Pre-fetched signature verification data * @param contractVersion * @param abortSignal * @returns Swap initialization signature expiry * @throws {SignatureVerificationError} when swap init signature is invalid * * @internal */ protected async verifyReturnedSignature( initiator: string, data: T["Data"], signature: SignatureData, feeRatePromise: Promise<any>, preFetchSignatureVerificationData: Promise<any>, contractVersion: string, abortSignal?: AbortSignal, ): Promise<number> { const [feeRate, preFetchedSignatureData] = await Promise.all([feeRatePromise, preFetchSignatureVerificationData]); await this._contract(contractVersion).isValidInitAuthorization(initiator, data, signature, feeRate, preFetchedSignatureData); return await this._contract(contractVersion).getInitAuthorizationExpiry(data, signature, preFetchedSignatureData); } /** * Processes InitializeEvent for a given swap * @param swap * @param event * @returns Whether the swap was updated/changed * * @internal */ protected abstract processEventInitialize(swap: D["Swap"], event: InitializeEvent<T["Data"]>): Promise<boolean>; /** * Processes ClaimEvent for a given swap * @param swap * @param event * @returns Whether the swap was updated/changed * * @internal */ protected abstract processEventClaim(swap: D["Swap"], event: ClaimEvent<T["Data"]>): Promise<boolean>; /** * Processes RefundEvent for a given swap * @param swap * @param event * @returns Whether the swap was updated/changed * * @internal */ protected abstract processEventRefund(swap: D["Swap"], event: RefundEvent<T["Data"]>): Promise<boolean>; /** * Processes a single SC on-chain event * @param event * @param swap * * @internal */ protected async processEvent(event: SwapEvent<T["Data"]>, swap: D["Swap"]): Promise<void> { if(swap==null) return; let swapChanged: boolean = false; if(event instanceof InitializeEvent) { swapChanged = await this.processEventInitialize(swap, event); if(event.meta?.txId!=null && swap._commitTxId!==event.meta.txId) { swap._commitTxId = event.meta.txId; swapChanged ||= true; } } if(event instanceof ClaimEvent) { swapChanged = await this.processEventClaim(swap, event); if(event.meta?.txId!=null && swap._claimTxId!==event.meta.txId) { swap._claimTxId = event.meta.txId; swapChanged ||= true; } } if(event instanceof RefundEvent) { swapChanged = await this.processEventRefund(swap, event); if(event.meta?.txId!=null && swap._refundTxId!==event.meta.txId) { swap._refundTxId = event.meta.txId; swapChanged ||= true; } } this.logger.info("processEvents(): "+event.constructor.name+" processed for "+swap.getId()+" swap: ", swap); if(swapChanged) { await swap._saveAndEmit(); } } /** * @inheritDoc * @internal */ protected async _checkPastSwaps(pastSwaps: D["Swap"][]): Promise<{ changedSwaps: D["Swap"][]; removeSwaps: D["Swap"][] }> { const changedSwaps: D["Swap"][] = []; const removeSwaps: D["Swap"][] = []; const swapExpiredStatus: {[id: string]: boolean} = {}; const checkStatusSwaps: {[contractVersion: string]: (D["Swap"] & {_data: T["Data"]})[]} = {}; for(let pastSwap of pastSwaps) { if(pastSwap._shouldFetchExpiryStatus()) { //Check expiry swapExpiredStatus[pastSwap.getId()] = await pastSwap._verifyQuoteDefinitelyExpired(); } if(pastSwap._shouldFetchOnchainState()) { //Add to swaps for which status should be checked if(pastSwap._data!=null) (checkStatusSwaps[pastSwap._contractVersion ?? "v1"] ??= []).push(pastSwap as (D["Swap"] & {_data: T["Data"]})); } } for(let version in checkStatusSwaps) { if(this._versionedContracts[version]==null) { this.logger.warn(`_checkPastSwaps(): No contract was found for ${this.chainIdentifier} version ${version}! Skipping these swaps!`); continue; } const _checkStatusSwap = checkStatusSwaps[version]; const swapStatuses = await this._contract(version).getCommitStatuses( _checkStatusSwap.map(val => ({ signer: val._getInitiator(), swapData: val._data })) ); for(let pastSwap of _checkStatusSwap) { const escrowHash = pastSwap.getEscrowHash(); const shouldSave = await pastSwap._sync( false, swapExpiredStatus[pastSwap.getId()], escrowHash==null ? undefined : swapStatuses[escrowHash] ); if(shouldSave) { if(pastSwap.isQuoteExpired()) { removeSwaps.push(pastSwap); } else { changedSwaps.push(pastSwap); } } } } return { changedSwaps, removeSwaps }; } /** * Recovers a swap from smart chain on-chain data, please note that not all values for the recovered * swaps might be populated, as some data is purely off-chain and can never be recovered purely * from on-chain data. * * @param init Swap escrow initialization transaction and swap data * @param state Current on-chain state of the swap * @param lp Intermediary (LP) used as a counterparty for the swap */ public abstract recoverFromSwapDataAndState( init: { data: T["Data"], getInitTxId: () => Promise<string>, getTxBlock: () => Promise<{blockTime: number, blockHeight: number}> }, state: SwapCommitState, contractVersion: string, lp?: Intermediary ): Promise<D["Swap"] | null>; }