@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
285 lines (261 loc) • 11.2 kB
text/typescript
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>;
}