@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
1,106 lines (1,020 loc) • 131 kB
text/typescript
import {ISwapPrice} from "../prices/abstract/ISwapPrice";
import {
BitcoinNetwork, BitcoinRpc, BitcoinRpcWithAddressIndex, BtcBlock,
BtcRelay,
ChainData,
ChainSwapType,
ChainType, LightningNetworkApi,
Messenger,
RelaySynchronizer, SpvWithdrawalClaimedState, SpvWithdrawalFrontedState, SwapCommitState, SwapContract, SwapData
} from "@atomiqlabs/base";
import {
ToBTCLNOptions,
ToBTCLNWrapper
} from "../swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper";
import {ToBTCOptions, ToBTCWrapper} from "../swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper";
import {FromBTCLNOptions, FromBTCLNWrapper} from "../swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper";
import {FromBTCOptions, FromBTCWrapper} from "../swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper";
import {IntermediaryDiscovery, MultichainSwapBounds, SwapBounds} from "../intermediaries/IntermediaryDiscovery";
import {decode as bolt11Decode} from "@atomiqlabs/bolt11";
import {ISwap} from "../swaps/ISwap";
import {IntermediaryError} from "../errors/IntermediaryError";
import {SwapType} from "../enums/SwapType";
import {FromBTCLNSwap} from "../swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap";
import {FromBTCSwap} from "../swaps/escrow_swaps/frombtc/onchain/FromBTCSwap";
import {ToBTCLNSwap} from "../swaps/escrow_swaps/tobtc/ln/ToBTCLNSwap";
import {ToBTCSwap} from "../swaps/escrow_swaps/tobtc/onchain/ToBTCSwap";
import {LnForGasWrapper} from "../swaps/trusted/ln/LnForGasWrapper";
import {LnForGasSwap} from "../swaps/trusted/ln/LnForGasSwap";
import {EventEmitter} from "events";
import {Intermediary} from "../intermediaries/Intermediary";
import {ISwapWrapper, WrapperCtorTokens} from "../swaps/ISwapWrapper";
import {bigIntCompare, bigIntMax, bigIntMin, fromDecimal, objectMap, randomBytes} from "../utils/Utils";
import {OutOfBoundsError} from "../errors/RequestError";
import {SwapperWithChain} from "./SwapperWithChain";
import {OnchainForGasSwap} from "../swaps/trusted/onchain/OnchainForGasSwap";
import {OnchainForGasWrapper} from "../swaps/trusted/onchain/OnchainForGasWrapper";
import {BTC_NETWORK, NETWORK, TEST_NETWORK} from "@scure/btc-signer/utils";
import {IUnifiedStorage, QueryParams} from "../storage/IUnifiedStorage";
import {
UnifiedSwapStorage,
UnifiedSwapStorageCompositeIndexes,
UnifiedSwapStorageIndexes
} from "../storage/UnifiedSwapStorage";
import {UnifiedSwapEventListener} from "../events/UnifiedSwapEventListener";
import {IToBTCSwap} from "../swaps/escrow_swaps/tobtc/IToBTCSwap";
import {SpvFromBTCOptions, SpvFromBTCWrapper} from "../swaps/spv_swaps/SpvFromBTCWrapper";
import {SpvFromBTCSwap} from "../swaps/spv_swaps/SpvFromBTCSwap";
import {SwapperUtils} from "./SwapperUtils";
import {FromBTCLNAutoOptions, FromBTCLNAutoWrapper} from "../swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper";
import {FromBTCLNAutoSwap} from "../swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap";
import {UserError} from "../errors/UserError";
import {SwapAmountType} from "../enums/SwapAmountType";
import {IClaimableSwap} from "../swaps/IClaimableSwap";
import {correctClock} from "../utils/AutomaticClockDriftCorrection";
import {isSwapType, SwapProtocolInfo, SwapTypeMapping} from "../utils/SwapUtils";
import {IndexedDBUnifiedStorage} from "../storage-browser/IndexedDBUnifiedStorage";
import {TokenAmount, toTokenAmount} from "../types/TokenAmount";
import {BitcoinTokens, BtcToken, isBtcToken, isSCToken, SCToken, Token} from "../types/Token";
import {AmountData} from "../types/AmountData";
import {getLogger} from "../utils/Logger";
import {isLNURLWithdraw, LNURLWithdraw} from "../types/lnurl/LNURLWithdraw";
import {isLNURLPay, LNURLPay} from "../types/lnurl/LNURLPay";
import {tryWithRetries} from "../utils/RetryUtils";
import {NotNever} from "../utils/TypeUtils";
import {IEscrowSwap} from "../swaps/escrow_swaps/IEscrowSwap";
import {LightningInvoiceCreateService, isLightningInvoiceCreateService} from "../types/wallets/LightningInvoiceCreateService";
import {SwapSide} from "../enums/SwapSide";
import {IntermediaryAPI} from "../intermediaries/apis/IntermediaryAPI";
import {BitcoinWalletUtxo, BitcoinWalletUtxoBase, IBitcoinWallet} from "../bitcoin/wallet/IBitcoinWallet";
import {MinimalBitcoinWalletInterface} from "../types/wallets/MinimalBitcoinWalletInterface";
import {toBitcoinWallet} from "../utils/BitcoinWalletUtils";
import {getSignedKeyBasedAuthHandler} from "../intermediaries/auth/SignedKeyBasedAuth";
/**
* Configuration options for the Swapper
* @category Core
*/
export type SwapperOptions = {
/**
* Manual override for the intermediary (LP) URLs for the SDK to use, by default these are fetched automatically
* from the registry
*/
intermediaryUrl?: string | string[],
/**
* Registry URL for where to look for active intermediary (LP) endpoint URLs
*/
registryUrl?: string,
/**
* Bitcoin network to use for the swaps,
*/
bitcoinNetwork?: BitcoinNetwork,
/**
* Timeout (in milliseconds) for HTTP GET requests done by the SDK
*/
getRequestTimeout?: number,
/**
* Timeout (in milliseconds) for HTTP POST requests done by the SDK
*/
postRequestTimeout?: number,
/**
* Additional parameters to be sent to the intermediaries (LPs), when requesting quotes from them
*/
defaultAdditionalParameters?: {[key: string]: any},
/**
* Optional name prefix to use when creating a swap storage, you can use this to create separate storage
* instances that don't overlap.
*/
storagePrefix?: string,
/**
* Sets the default intermediary (LP) to use for the trusted gas swaps, if not set the SDK uses a default one
*/
defaultTrustedIntermediaryUrl?: string,
/**
* A function callback to retrieve a specific named storage container for swap persistency. If not present, the
* default IndexedDB storage adapter is used. When you use the SDK in non-browser based environments you need to
* provide this callback such that the SDK is able to use a custom storage adapter.
*
* @param storageName Name of the container to retrieve
*/
swapStorage?: (storageName: string) => IUnifiedStorage<UnifiedSwapStorageIndexes, UnifiedSwapStorageCompositeIndexes>,
/**
* By setting this flag, the swapper doesn't schedule automatic tick timers. To make sure the swap states are
* properly updated (e.g. the expired swaps properly move to the expired state), you should call the
* {@link Swapper._syncSwaps} function periodically. This flag should be set when you run an environment that
* doesn't support long-running timers - e.g. serverless environments like Azure Function Apps or AWS Lambda
*/
noTimers?: boolean,
/**
* By setting this flag, the swapper doesn't subscribe to on-chain events. To make sure the swap states are
* properly updated you should either call the {@link Swapper._syncSwaps} function periodically, or use the
* {@link Swapper._pollChainEvents} function to manually poll for on-chain events. This flag should be set
* when you run an environment that doesn't support long-running timers and websocket connections - e.g.
* serverless environments like Azure Function Apps or AWS Lambda
*/
noEvents?: boolean,
/**
* By setting this flag, the swap objects will not be cached in the SDK and instead will always be loaded from
* the persistent storage. By default, the SDK uses a `WeakRef` mapping of swaps, to ensure that when the same
* swap is loaded concurrently, it returns the same object reference to both, making the changes on the object
* atomic. This flag should be set to `true` when running in an environment where multiple instances of the SDK
* access the same swap database - e.g. serverless environments like Azure Function Apps or AWS Lambda
*/
noSwapCache?: boolean,
/**
* Skip checking past swaps when the swapper is initiated with {@link Swapper.init}, you can call the
* {@link Swapper._syncSwaps} function later, to check the swaps. By default, the SDK checks the state
* of all the known swaps during init.
*/
dontCheckPastSwaps?: boolean,
/**
* Skip fetching the LPs when the swapper is initiated with {@link Swapper.init}, this means the list of available
* tokens and swap limits won't be available immediately. LPs will be fetched automatically later, when a swap
* is requested
*/
dontFetchLPs?: boolean,
/**
* Defaults to `true`, this means every swap regardless of it being initiated (i.e. when `commit()`, `execute()` or
* `waitTillPayment` is called) is saved to the persistent storage. This is a reasonable default for when you
* want to only create a swap, and then later on retrieve it with the `swapper.getSwapById()` function.
*
* Setting this to `false` means the SDK only saves and persists swaps that are considered initiated, i.e. when
* `commit()`, `execute()` or `waitTillPayment()` is called (or their respective txs... prefixed variations). This
* might save calls to the persistent storage for swaps that are never initiated. This is useful in e.g.
* frontend implementations where the frontend holds the swap object reference until it is initiated anyway, not
* necessitating the saving of the swap data to the persistent storage until it is actually initiated.
*/
saveUninitializedSwaps?: boolean,
/**
* Automatically checks system time on initialize, if the system time drifts too far from the actual time
* (as checked from multiple server sources) it adjusts the `Date.now()` function to return proper actual time.
*/
automaticClockDriftCorrection?: boolean,
/**
* Used in centralized API deployments to allow higher rate limits from LPs
*/
signedKeyBasedAuth?: {
certificate: string,
privateKey: string
},
/**
* If you set the option to `true` the chains for which the RPC is unresponsive are skipped and not initialized
* letting the swapper continue with only the available chains with responsive RPCs
*/
gracefullyHandleChainErrors?: boolean,
};
/**
* Type representing multiple blockchain configurations
* @category Core
*/
export type MultiChain = {
[chainIdentifier in string]: ChainType;
};
type ChainSpecificData<T extends ChainType> = {
wrappers: {
[SwapType.TO_BTCLN]: ToBTCLNWrapper<T>,
[SwapType.TO_BTC]: ToBTCWrapper<T>,
[SwapType.FROM_BTCLN]: FromBTCLNWrapper<T>,
[SwapType.FROM_BTC]: FromBTCWrapper<T>,
[SwapType.TRUSTED_FROM_BTCLN]: LnForGasWrapper<T>,
[SwapType.TRUSTED_FROM_BTC]: OnchainForGasWrapper<T>,
[SwapType.SPV_VAULT_FROM_BTC]: SpvFromBTCWrapper<T>,
[SwapType.FROM_BTCLN_AUTO]: FromBTCLNAutoWrapper<T>
}
chainEvents: T["Events"],
chainInterface: T["ChainInterface"],
unifiedChainEvents: UnifiedSwapEventListener<T>,
unifiedSwapStorage: UnifiedSwapStorage<T>,
reviver: (val: any) => ISwap<T>,
defaultVersion: string,
versionedContracts: {
[contractVersion: string]: {
swapContract: T["Contract"],
spvVaultContract: T["SpvVaultContract"],
btcRelay: BtcRelay<any, T["TX"], BtcBlock, T["Signer"]>,
synchronizer: RelaySynchronizer<any, T["TX"], BtcBlock>,
}
}
};
type MultiChainData<T extends MultiChain> = {
[chainIdentifier in keyof T]: ChainSpecificData<T[chainIdentifier]>
};
type CtorMultiChainData<T extends MultiChain> = {
[chainIdentifier in keyof T]: ChainData<T[chainIdentifier]>
};
type SwapperCtorTokens<T extends MultiChain = MultiChain> = {
ticker: string,
name: string,
chains: {[chainId in ChainIds<T>]?: {
address: string,
decimals: number,
displayDecimals?: number
}}
}[];
/**
* Type extracting chain identifiers from a MultiChain type
* @category Core
*/
export type ChainIds<T extends MultiChain> = keyof T & string;
/**
* Type helper to check if a chain supports a specific swap type
* @category Core
*/
export type SupportsSwapType<
C extends ChainType,
Type extends SwapType
> = Type extends SwapType.SPV_VAULT_FROM_BTC ?
NotNever<C["SpvVaultContract"]> :
Type extends (SwapType.TRUSTED_FROM_BTCLN | SwapType.TRUSTED_FROM_BTC) ?
true :
Type extends SwapType.FROM_BTCLN_AUTO ? (C["Contract"]["supportsInitWithoutClaimer"] extends true ? true : false) :
NotNever<C["Contract"]>;
/**
* Core orchestrator for all atomiq swap operations
*
* @category Core
*/
export class Swapper<T extends MultiChain> extends EventEmitter<{
lpsRemoved: [Intermediary[]],
lpsAdded: [Intermediary[]],
swapState: [ISwap],
swapLimitsChanged: []
}> {
private readonly logger = getLogger(this.constructor.name+": ");
private readonly swapStateListener: (swap: ISwap) => void;
private defaultTrustedIntermediary?: Intermediary;
private readonly bitcoinNetwork: BitcoinNetwork;
private readonly options: SwapperOptions;
/**
* Data propagation layer used for broadcasting messages to watchtowers
*/
private readonly messenger: Messenger;
/**
* A dictionary of smart chains used by the SDK
* @internal
*/
readonly _chains: MultiChainData<T>;
/**
* Bitcoin RPC for fetching bitcoin chain data
* @internal
*/
readonly _bitcoinRpc: BitcoinRpcWithAddressIndex<any>;
/**
* Bitcoin network specification
* @internal
*/
readonly _btcNetwork: BTC_NETWORK;
/**
* Token data indexed by chain identifier and token addresses
* @internal
*/
readonly _tokens: {
[chainId: string]: {
[tokenAddress: string]: SCToken
}
};
/**
* Token data indexed by chain identifier and token tickers
* @internal
*/
readonly _tokensByTicker: {
[chainId: string]: {
[tokenTicker: string]: SCToken
}
};
/**
* Pricing API used by the SDK
*/
readonly prices: ISwapPrice<T>;
/**
* API for contacting LPs
*/
readonly lpApi: IntermediaryAPI;
/**
* Intermediary discovery instance
*/
readonly intermediaryDiscovery: IntermediaryDiscovery;
/**
* Miscellaneous utility functions
*/
readonly Utils: SwapperUtils<T>;
/**
* @internal
*/
constructor(
bitcoinRpc: BitcoinRpcWithAddressIndex<any>,
lightningApi: LightningNetworkApi,
bitcoinSynchronizer: (btcRelay: BtcRelay<any, any, any>) => RelaySynchronizer<any, any, any>,
chainsData: CtorMultiChainData<T>,
pricing: ISwapPrice<T>,
tokens: SCToken[],
messenger: Messenger,
options?: SwapperOptions
) {
super();
const storagePrefix = options?.storagePrefix ?? "atomiq-";
options ??= {};
options.saveUninitializedSwaps ??= true;
options.bitcoinNetwork = options.bitcoinNetwork==null ? BitcoinNetwork.TESTNET : options.bitcoinNetwork;
const swapStorage = options.swapStorage ??= (name: string) => new IndexedDBUnifiedStorage(name);
this.options = options;
this.bitcoinNetwork = options.bitcoinNetwork;
this._btcNetwork = options.bitcoinNetwork===BitcoinNetwork.MAINNET ? NETWORK :
(options.bitcoinNetwork===BitcoinNetwork.TESTNET || options.bitcoinNetwork===BitcoinNetwork.TESTNET4) ? TEST_NETWORK : {
bech32: 'bcrt',
pubKeyHash: 111,
scriptHash: 196,
wif: 239
};
this.Utils = new SwapperUtils(this);
this.prices = pricing;
this._bitcoinRpc = bitcoinRpc;
this.messenger = messenger;
this._tokens = {};
this._tokensByTicker = {};
for(let tokenData of tokens) {
const chainId = tokenData.chainId;
this._tokens[chainId] ??= {};
this._tokensByTicker[chainId] ??= {};
this._tokens[chainId][tokenData.address] = this._tokensByTicker[chainId][tokenData.ticker] = tokenData;
}
const lpApi = new IntermediaryAPI(
this.options.signedKeyBasedAuth!=null
? getSignedKeyBasedAuthHandler(this.options.signedKeyBasedAuth.certificate, this.options.signedKeyBasedAuth.privateKey)
: undefined
);
this.lpApi = lpApi;
this.swapStateListener = (swap: ISwap) => {
this.emit("swapState", swap);
};
this._chains = objectMap<CtorMultiChainData<T>, MultiChainData<T>>(chainsData, <InputKey extends keyof CtorMultiChainData<T>>(chainData: CtorMultiChainData<T>[InputKey], key: string) => {
let {
chainInterface, chainEvents, chainId,
btcRelay,
swapContract, swapDataConstructor,
spvVaultContract, spvVaultWithdrawalDataConstructor, spvVaultDataConstructor,
defaultVersion, versions
} = chainData;
defaultVersion ??= "v1";
if(versions==null) {
versions = {
[defaultVersion]: {
btcRelay,
swapContract,
swapDataConstructor,
spvVaultContract,
spvVaultDataConstructor,
spvVaultWithdrawalDataConstructor
}
}
}
const versionedContracts = objectMap(versions, (value, key) => {
return {
swapContract: value.swapContract,
spvVaultContract: value.spvVaultContract,
btcRelay: value.btcRelay,
synchronizer: bitcoinSynchronizer(value.btcRelay)
};
});
const storageHandler = swapStorage(storagePrefix + chainId);
const unifiedSwapStorage = new UnifiedSwapStorage<T[InputKey]>(storageHandler, this.options.noSwapCache);
const unifiedChainEvents = new UnifiedSwapEventListener<T[InputKey]>(unifiedSwapStorage, chainEvents);
const wrappers: any = {};
wrappers[SwapType.TO_BTCLN] = new ToBTCLNWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
pricing,
this._tokens[chainId],
versions,
lpApi,
{
getRequestTimeout: this.options.getRequestTimeout,
postRequestTimeout: this.options.postRequestTimeout,
saveUninitializedSwaps: this.options.saveUninitializedSwaps,
}
);
wrappers[SwapType.TO_BTC] = new ToBTCWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
pricing,
this._tokens[chainId],
versions,
this._bitcoinRpc,
lpApi,
{
getRequestTimeout: this.options.getRequestTimeout,
postRequestTimeout: this.options.postRequestTimeout,
saveUninitializedSwaps: this.options.saveUninitializedSwaps,
bitcoinNetwork: this._btcNetwork
}
);
wrappers[SwapType.FROM_BTCLN] = new FromBTCLNWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
pricing,
this._tokens[chainId],
versions,
lightningApi,
lpApi,
{
getRequestTimeout: this.options.getRequestTimeout,
postRequestTimeout: this.options.postRequestTimeout,
saveUninitializedSwaps: this.options.saveUninitializedSwaps,
unsafeSkipLnNodeCheck: this.bitcoinNetwork===BitcoinNetwork.TESTNET4 || this.bitcoinNetwork===BitcoinNetwork.REGTEST
}
);
wrappers[SwapType.FROM_BTC] = new FromBTCWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
pricing,
this._tokens[chainId],
versions,
versionedContracts,
this._bitcoinRpc,
lpApi,
{
getRequestTimeout: this.options.getRequestTimeout,
postRequestTimeout: this.options.postRequestTimeout,
saveUninitializedSwaps: this.options.saveUninitializedSwaps,
bitcoinNetwork: this._btcNetwork
}
);
wrappers[SwapType.TRUSTED_FROM_BTCLN] = new LnForGasWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
pricing,
this._tokens[chainId],
lpApi,
{
getRequestTimeout: this.options.getRequestTimeout,
postRequestTimeout: this.options.postRequestTimeout,
saveUninitializedSwaps: this.options.saveUninitializedSwaps,
}
);
wrappers[SwapType.TRUSTED_FROM_BTC] = new OnchainForGasWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
pricing,
this._tokens[chainId],
bitcoinRpc,
lpApi,
{
getRequestTimeout: this.options.getRequestTimeout,
postRequestTimeout: this.options.postRequestTimeout,
saveUninitializedSwaps: this.options.saveUninitializedSwaps,
bitcoinNetwork: this._btcNetwork
}
);
// This is gated on the default version of the contracts
if(spvVaultContract!=null) {
wrappers[SwapType.SPV_VAULT_FROM_BTC] = new SpvFromBTCWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
pricing,
this._tokens[chainId],
versions,
versionedContracts,
bitcoinRpc,
lpApi,
{
getRequestTimeout: this.options.getRequestTimeout,
postRequestTimeout: this.options.postRequestTimeout,
saveUninitializedSwaps: this.options.saveUninitializedSwaps,
bitcoinNetwork: this._btcNetwork
}
);
}
// This is gated on the default version of the contracts
if(swapContract.supportsInitWithoutClaimer) {
wrappers[SwapType.FROM_BTCLN_AUTO] = new FromBTCLNAutoWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
pricing,
this._tokens[chainId],
versions,
lightningApi,
this.messenger,
lpApi,
{
getRequestTimeout: this.options.getRequestTimeout,
postRequestTimeout: this.options.postRequestTimeout,
saveUninitializedSwaps: this.options.saveUninitializedSwaps,
unsafeSkipLnNodeCheck: this.bitcoinNetwork===BitcoinNetwork.TESTNET4 || this.bitcoinNetwork===BitcoinNetwork.REGTEST
}
);
}
Object.keys(wrappers).forEach(key => wrappers[key].events.on("swapState", this.swapStateListener));
const reviver = (val: any) => {
const wrapper: ISwapWrapper<any, any> = wrappers[val.type];
if(wrapper==null) return null;
return new wrapper._swapDeserializer(wrapper, val);
};
return {
chainEvents,
chainInterface,
wrappers,
unifiedChainEvents,
unifiedSwapStorage,
defaultVersion,
reviver,
versionedContracts
}
});
const contracts = objectMap(chainsData, (data) => data.versions ?? {[data.defaultVersion ?? "v1"]: {swapContract: data.swapContract, spvVaultContract: data.spvVaultContract}});
if(options.intermediaryUrl!=null) {
this.intermediaryDiscovery = new IntermediaryDiscovery(contracts, lpApi, options.registryUrl, Array.isArray(options.intermediaryUrl) ? options.intermediaryUrl : [options.intermediaryUrl], options.getRequestTimeout);
} else {
this.intermediaryDiscovery = new IntermediaryDiscovery(contracts, lpApi, options.registryUrl, undefined, options.getRequestTimeout);
}
this.intermediaryDiscovery.on("removed", (intermediaries: Intermediary[]) => {
this.emit("lpsRemoved", intermediaries);
});
this.intermediaryDiscovery.on("added", (intermediaries: Intermediary[]) => {
this.emit("lpsAdded", intermediaries);
});
}
private async _init(): Promise<void> {
this.logger.debug("init(): Initializing swapper");
const abortController = new AbortController();
const promises: Promise<void>[] = [];
let automaticClockDriftCorrectionPromise: Promise<void> | undefined = undefined;
if(this.options.automaticClockDriftCorrection) {
promises.push(automaticClockDriftCorrectionPromise = tryWithRetries(correctClock, undefined, undefined, abortController.signal).catch((err) => {
abortController.abort(err);
}));
}
this.logger.debug("init(): Initializing intermediary discovery");
if(!this.options.dontFetchLPs) promises.push(this.intermediaryDiscovery.init(abortController.signal).catch(err => {
if(abortController.signal.aborted) return;
this.logger.error("init(): Failed to fetch intermediaries/LPs: ", err);
}));
if(this.options.defaultTrustedIntermediaryUrl!=null) {
promises.push(
this.intermediaryDiscovery.getIntermediary(this.options.defaultTrustedIntermediaryUrl, abortController.signal)
.then(val => {
if(val==null) throw new Error("Cannot get trusted LP");
this.defaultTrustedIntermediary = val;
})
.catch(err => {
if(abortController.signal.aborted) return;
this.logger.error("init(): Failed to contact trusted LP url: ", err);
})
);
}
if(automaticClockDriftCorrectionPromise!=null) {
//We should await the promises here before checking the swaps
await automaticClockDriftCorrectionPromise;
}
const chainPromises = [];
for(let chainIdentifier in this._chains) {
chainPromises.push((async() => {
const {
chainInterface,
versionedContracts,
unifiedChainEvents,
unifiedSwapStorage,
wrappers,
reviver
} = this._chains[chainIdentifier];
try {
const _chainInterface: any = chainInterface;
if(_chainInterface.verifyNetwork!=null) {
await _chainInterface.verifyNetwork(this.bitcoinNetwork);
}
for(let contractVersion in versionedContracts) {
await versionedContracts[contractVersion].swapContract.start();
this.logger.debug("init(): Intialized swap contract: "+chainIdentifier+` version: ${contractVersion}`);
}
await unifiedSwapStorage.init();
if(unifiedSwapStorage.storage instanceof IndexedDBUnifiedStorage) {
//Try to migrate the data here
const storagePrefix = chainIdentifier==="SOLANA" ?
"SOLv4-"+this.bitcoinNetwork+"-Swaps-" :
"atomiqsdk-"+this.bitcoinNetwork+chainIdentifier+"-Swaps-";
await unifiedSwapStorage.storage.tryMigrate(
[
[storagePrefix+"FromBTC", SwapType.FROM_BTC],
[storagePrefix+"FromBTCLN", SwapType.FROM_BTCLN],
[storagePrefix+"ToBTC", SwapType.TO_BTC],
[storagePrefix+"ToBTCLN", SwapType.TO_BTCLN]
],
(obj: any) => {
const swap = reviver(obj);
if(swap._randomNonce==null) {
const oldIdentifierHash = swap.getId();
swap._randomNonce = randomBytes(16).toString("hex");
const newIdentifierHash = swap.getId();
this.logger.info("init(): Found older swap version without randomNonce, replacing, old hash: "+oldIdentifierHash+
" new hash: "+newIdentifierHash);
}
return swap;
}
)
}
await unifiedChainEvents.start(this.options.noEvents);
this.logger.debug("init(): Initialized events: "+chainIdentifier);
} catch (e) {
if(!this.options.gracefullyHandleChainErrors) throw e;
this.logger.error(`init(): Failed to initialize ${chainIdentifier} (skipped): `, e);
delete this._chains[chainIdentifier];
return;
}
for(let key in wrappers) {
// this.logger.debug("init(): Initializing "+SwapType[key]+": "+chainIdentifier);
await wrappers[key as unknown as SwapType].init(this.options.noTimers, this.options.dontCheckPastSwaps);
}
})());
}
await Promise.all(chainPromises);
await Promise.all(promises);
this.logger.debug("init(): Initializing messenger");
await this.messenger.init();
}
private initPromise?: Promise<void>;
private initialized: boolean = false;
/**
* Initializes the swap storage and loads existing swaps, needs to be called before any other action
*/
async init(): Promise<void> {
if(this.initialized) return;
if(this.initPromise!=null) {
await this.initPromise;
return;
}
try {
const promise = this._init();
this.initPromise = promise;
await promise;
delete this.initPromise;
this.initialized = true;
} catch (e) {
delete this.initPromise;
throw e;
}
}
/**
* Whether the SDK is initialized (after {@link init} is called)
*/
isInitialized(): boolean {
return this.initialized;
}
/**
* Stops listening for onchain events and closes this Swapper instance
*/
async stop() {
if(this.initPromise) await this.initPromise;
for(let chainIdentifier in this._chains) {
const {
wrappers,
unifiedChainEvents
} = this._chains[chainIdentifier];
for(let key in wrappers) {
const wrapper = wrappers[key as unknown as SwapType];
wrapper.events.removeListener("swapState", this.swapStateListener);
await wrapper.stop();
}
await unifiedChainEvents.stop();
await this.messenger.stop();
}
this.initialized = false;
}
/**
* Creates swap & handles intermediary, quote selection
*
* @param chainIdentifier
* @param create Callback to create the
* @param amountData Amount data as passed to the function
* @param swapType Swap type of the execution
* @param maxWaitTimeMS Maximum waiting time after the first intermediary returns the quote
* @private
* @throws {Error} when no intermediary was found
* @throws {Error} if the chain with the provided identifier cannot be found
*/
private async createSwap<ChainIdentifier extends ChainIds<T>, S extends ISwap<T[ChainIdentifier]>>(
chainIdentifier: ChainIdentifier,
create: (candidates: Intermediary[], abortSignal: AbortSignal, chain: ChainSpecificData<T[ChainIdentifier]>) => Promise<{
quote: Promise<S>,
intermediary: Intermediary
}[]>,
amountData: { amount?: bigint, token: string, exactIn: boolean },
swapType: SwapType,
maxWaitTimeMS: number = 2000
): Promise<S> {
if(!this.initialized) throw new Error("Swapper not initialized, init first with swapper.init()!");
if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
let candidates: Intermediary[];
const inBtc: boolean = swapType===SwapType.TO_BTCLN || swapType===SwapType.TO_BTC ? !amountData.exactIn : amountData.exactIn;
if(!inBtc || amountData.amount==null) {
//Get candidates not based on the amount
candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token);
} else {
candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token, amountData.amount);
}
let swapLimitsChanged = false;
if(candidates.length===0) {
this.logger.warn("createSwap(): No valid intermediary found to execute the swap with, reloading intermediary database...");
await this.intermediaryDiscovery.reloadIntermediaries();
swapLimitsChanged = true;
if(!inBtc || amountData.amount==null) {
//Get candidates not based on the amount
candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token);
} else {
candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token, amountData.amount);
if(candidates.length===0) {
const min = this.intermediaryDiscovery.getSwapMinimum(chainIdentifier, swapType, amountData.token);
const max = this.intermediaryDiscovery.getSwapMaximum(chainIdentifier, swapType, amountData.token);
if(min!=null && max!=null) {
if(amountData.amount < BigInt(min)) throw new OutOfBoundsError("Swap amount too low! Try swapping a higher amount.", 200, BigInt(min), BigInt(max));
if(amountData.amount > BigInt(max)) throw new OutOfBoundsError("Swap amount too high! Try swapping a lower amount.", 200, BigInt(min), BigInt(max));
}
}
}
if(candidates.length===0) throw new Error("No intermediary found for the requested pair and amount! You can try swapping different pair or higher/lower amount.");
}
const abortController = new AbortController();
this.logger.debug("createSwap() Swap candidates: ", candidates.map(lp => lp.url).join());
const quotePromises: {quote: Promise<S>, intermediary: Intermediary}[] = await create(candidates, abortController.signal, this._chains[chainIdentifier]);
const promiseAll = new Promise<{
quote: S,
intermediary: Intermediary
}[]>((resolve, reject) => {
let min: bigint;
let max: bigint;
let error: Error;
let numResolved = 0;
let quotes: {
quote: S,
intermediary: Intermediary
}[] = [];
let timeout: NodeJS.Timeout;
quotePromises.forEach(data => {
data.quote.then(quote => {
if(numResolved===0) {
timeout = setTimeout(() => {
abortController.abort(new Error("Timed out waiting for quote!"));
resolve(quotes);
}, maxWaitTimeMS);
}
numResolved++;
quotes.push({
quote,
intermediary: data.intermediary
});
if(numResolved===quotePromises.length) {
clearTimeout(timeout);
resolve(quotes);
return;
}
}).catch(e => {
numResolved++;
if(e instanceof IntermediaryError) {
//Blacklist that node
this.intermediaryDiscovery.removeIntermediary(data.intermediary);
swapLimitsChanged = true;
} else if(e instanceof OutOfBoundsError) {
if(min==null || max==null) {
min = e.min;
max = e.max;
} else {
min = bigIntMin(min, e.min);
max = bigIntMax(max, e.max);
}
data.intermediary.swapBounds[swapType] ??= {};
data.intermediary.swapBounds[swapType]![chainIdentifier] ??= {};
const tokenBoundsData = (data.intermediary.swapBounds[swapType]![chainIdentifier]![amountData.token] ??= {input: {}, output: {}});
if(amountData.exactIn) {
tokenBoundsData.input = {min: e.min, max: e.max};
} else {
tokenBoundsData.output = {min: e.min, max: e.max};
}
swapLimitsChanged = true;
}
this.logger.warn("createSwap(): Intermediary "+data.intermediary.url+" error: ", e);
error = e;
if(numResolved===quotePromises.length) {
if(timeout!=null) clearTimeout(timeout);
if(quotes.length>0) {
resolve(quotes);
return;
}
if(min!=null && max!=null) {
let msg = "Swap amount too high or too low! Try swapping a different amount.";
if(amountData.amount!=null) {
if(min > amountData.amount) msg = "Swap amount too low! Try swapping a higher amount.";
if(max < amountData.amount) msg = "Swap amount too high! Try swapping a lower amount.";
}
reject(new OutOfBoundsError(msg, 400, min, max));
return;
}
reject(error);
}
});
});
});
try {
const quotes = await promiseAll;
//TODO: Intermediary's reputation is not taken into account!
quotes.sort((a, b) => {
if(amountData.exactIn) {
//Compare outputs
return bigIntCompare(b.quote.getOutput().rawAmount!, a.quote.getOutput().rawAmount!);
} else {
//Compare inputs
return bigIntCompare(a.quote.getInput().rawAmount!, b.quote.getInput().rawAmount!);
}
});
this.logger.debug("createSwap(): Sorted quotes, best price to worst: ", quotes);
if(swapLimitsChanged) this.emit("swapLimitsChanged");
const quote = quotes[0].quote;
await quote._save();
return quote;
} catch (e) {
if(swapLimitsChanged) this.emit("swapLimitsChanged");
throw e;
}
}
/**
* Creates Smart chain -> Bitcoin ({@link SwapType.TO_BTC}) swap
*
* @param chainIdentifier Chain identifier string of the source smart chain
* @param signer Signer's address on the source chain
* @param tokenAddress Token address to pay with
* @param address Recipient's bitcoin address
* @param amount Amount to send in token based units (if `exactIn=true`) or receive in satoshis (if `exactIn=false`)
* @param exactIn Whether to use exact in instead of exact out
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param options Additional options for the swap
*/
createToBTCSwap<ChainIdentifier extends ChainIds<T>>(
chainIdentifier: ChainIdentifier,
signer: string,
tokenAddress: string,
address: string,
amount: bigint,
exactIn: boolean = false,
additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
options?: ToBTCOptions
): Promise<ToBTCSwap<T[ChainIdentifier]>> {
if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
if(address.startsWith("bitcoin:")) {
address = address.substring(8).split("?")[0];
}
if(!this.Utils.isValidBitcoinAddress(address)) throw new Error("Invalid bitcoin address");
if(!this._chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address");
signer = this._chains[chainIdentifier].chainInterface.normalizeAddress(signer);
const amountData = {
amount,
token: tokenAddress,
exactIn
};
return this.createSwap(
chainIdentifier as ChainIdentifier,
(candidates: Intermediary[], abortSignal, chain) => Promise.resolve(chain.wrappers[SwapType.TO_BTC].create(
signer,
address,
amountData,
candidates,
options,
additionalParams,
abortSignal
)),
amountData,
SwapType.TO_BTC
);
}
/**
* Creates Smart chain -> Bitcoin Lightning ({@link SwapType.TO_BTCLN}) swap
*
* @param chainIdentifier Chain identifier string of the source smart chain
* @param signer Signer's address on the source chain
* @param tokenAddress Token address to pay with
* @param paymentRequest BOLT11 lightning network invoice to be paid (needs to have a fixed amount), and the swap
* amount is taken from this fixed amount, hence only exact output swaps are supported
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param options Additional options for the swap
*/
async createToBTCLNSwap<ChainIdentifier extends ChainIds<T>>(
chainIdentifier: ChainIdentifier,
signer: string,
tokenAddress: string,
paymentRequest: string,
additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
options?: ToBTCLNOptions
): Promise<ToBTCLNSwap<T[ChainIdentifier]>> {
if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
if(paymentRequest.startsWith("lightning:")) paymentRequest = paymentRequest.substring(10);
if(!this.Utils.isValidLightningInvoice(paymentRequest)) throw new Error("Invalid lightning network invoice");
if(!this._chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address");
signer = this._chains[chainIdentifier].chainInterface.normalizeAddress(signer);
const parsedPR = bolt11Decode(paymentRequest);
if(parsedPR.millisatoshis==null) throw new Error("Invalid lightning network invoice, no msat value field!");
const amountData = {
amount: (BigInt(parsedPR.millisatoshis) + 999n) / 1000n,
token: tokenAddress,
exactIn: false as const
};
return this.createSwap(
chainIdentifier as ChainIdentifier,
(candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.TO_BTCLN].create(
signer,
paymentRequest,
amountData,
candidates,
options,
additionalParams,
abortSignal
),
amountData,
SwapType.TO_BTCLN
);
}
/**
* Creates Smart chain -> Bitcoin Lightning ({@link SwapType.TO_BTCLN}) swap via LNURL-pay link
*
* @param chainIdentifier Chain identifier string of the source smart chain
* @param signer Signer's address on the source chain
* @param tokenAddress Token address to pay with
* @param lnurlPay LNURL-pay link to use for the payment
* @param amount Amount to send in token based units (if `exactIn=true`) or receive in satoshis (if `exactIn=false`)
* @param exactIn Whether to do an exact in swap instead of exact out
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param options Additional options for the swap
*/
async createToBTCLNSwapViaLNURL<ChainIdentifier extends ChainIds<T>>(
chainIdentifier: ChainIdentifier,
signer: string,
tokenAddress: string,
lnurlPay: string | LNURLPay,
amount: bigint,
exactIn: boolean = false,
additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
options?: ToBTCLNOptions & {comment?: string}
): Promise<ToBTCLNSwap<T[ChainIdentifier]>> {
if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
if(typeof(lnurlPay)==="string" && !this.Utils.isValidLNURL(lnurlPay)) throw new Error("Invalid LNURL-pay link");
if(!this._chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address");
signer = this._chains[chainIdentifier].chainInterface.normalizeAddress(signer);
const amountData = {
amount,
token: tokenAddress,
exactIn
};
return this.createSwap(
chainIdentifier as ChainIdentifier,
(candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.TO_BTCLN].createViaLNURL(
signer,
typeof(lnurlPay)==="string" ? (lnurlPay.startsWith("lightning:") ? lnurlPay.substring(10): lnurlPay) : lnurlPay.params,
amountData,
candidates,
options,
additionalParams,
abortSignal
),
amountData,
SwapType.TO_BTCLN
);
}
/**
* Creates Smart chain -> Bitcoin Lightning ({@link SwapType.TO_BTCLN}) swap via {@link LightningInvoiceCreateService}
*
* @param chainIdentifier Chain identifier string of the source smart chain
* @param signer Signer's address on the source chain
* @param tokenAddress Token address to pay with
* @param service Invoice create service object which facilitates the creation of fixed amount LN invoices
* @param amount Amount to send in token based units (if `exactIn=true`) or receive in satoshis (if `exactIn=false`)
* @param exactIn Whether to do an exact in swap instead