UNPKG

@atomiqlabs/sdk

Version:

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

308 lines (277 loc) 13.3 kB
import { ChainData, BitcoinNetwork, ChainType, StorageObject, IStorageManager, Messenger, ChainInitializer, BtcRelay } from "@atomiqlabs/base"; import {SmartChainAssets, SmartChainAssetTickers} from "../SmartChainAssets"; import {NostrMessenger} from "@atomiqlabs/messenger-nostr"; import {Swapper, SwapperOptions} from "./Swapper"; import {CustomPriceProvider} from "../prices/providers/CustomPriceProvider"; import {BitcoinTokens, BtcToken, SCToken, Token} from "../types/Token"; import {SwapType} from "../enums/SwapType"; import {SwapTypeMapping} from "../utils/SwapUtils"; import {RedundantSwapPrice, RedundantSwapPriceAssets} from "../prices/RedundantSwapPrice"; import {LocalStorageManager} from "../storage-browser/LocalStorageManager"; import {SingleSwapPrice} from "../prices/SingleSwapPrice"; import {CustomPriceFunction} from "../types/CustomPriceFunction"; import {MempoolApi, MempoolBitcoinRpc, MempoolBtcRelaySynchronizer} from "@atomiqlabs/btc-mempool"; //Helper types /** * Token definitions for a specific chain */ type TypedChainTokens<T extends ChainInitializer<any, any, any>> = { [val in keyof T["tokens"]]: SCToken<T["chainId"]> }; /** * Token resolver for a specific chain */ type TypedChainTokenResolver<T extends ChainInitializer<any, any, any>> = { getToken: (address: string) => SCToken<T["chainId"]> }; type ChainTypeDict<T extends ChainInitializer<any, any, any>> = {[K in T["chainId"]]: T["chainType"]}; type ToMultichain<T extends readonly ChainInitializer<any, any, any>[]> = (T extends readonly [infer First extends ChainInitializer<any, any, any>, ...infer Rest extends ChainInitializer<any, any, any>[]] ? ChainTypeDict<First> & ToMultichain<Rest> : {}); type TokensDict<T extends ChainInitializer<any, any, any>> = { [K in T["chainId"]]: TypedChainTokens<T> }; type GetAllTokens<T extends readonly ChainInitializer<any, any, any>[]> = (T extends readonly [infer First extends ChainInitializer<any, any, any>, ...infer Rest extends ChainInitializer<any, any, any>[]] ? TokensDict<First> & GetAllTokens<Rest> : unknown); type TokenResolverDict<T extends ChainInitializer<any, any, any>> = { [K in T["chainId"]]: TypedChainTokenResolver<T> }; type OptionsDict<T extends ChainInitializer<any, any, any>> = {[K in T["chainId"]]: T["options"]}; type GetAllOptions<T extends readonly ChainInitializer<any, any, any>[]> = (T extends readonly [infer First extends ChainInitializer<any, any, any>, ...infer Rest extends ChainInitializer<any, any, any>[]] ? OptionsDict<First> & GetAllOptions<Rest> : unknown); //Exported types /** * Configuration options for creating a Swapper instance * * @category Core */ export type TypedSwapperOptions<T extends readonly ChainInitializer<any, any, any>[]> = { /** * A dictionary with chain-specific settings, like RPC URLs and others */ chains: GetAllOptions<T>, /** * A function callback to retrieve a specific named storage container for key-value persistency. If not present, the * default browser Local 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 */ chainStorageCtor?: <T extends StorageObject>(storageName: string) => IStorageManager<T>, /** * Maximum allowed pricing difference between intermediary (LP) returned rate and current market rate. In * parts-per-million, e.g. 2% is written as `20000`. */ pricingFeeDifferencePPM?: bigint, /** * Optional pricing function to use instead of default pricing APIs. See {@link CustomPriceFunction} for details. */ getPriceFn?: CustomPriceFunction, /** * Mempool API instance or URLs to use, if not provided the defaults will be used for the specified bitcoin network */ mempoolApi?: MempoolApi | MempoolBitcoinRpc | string | string[], /** * Data propagation messenger instance to use for broadcasting data to watchtowers. By default, a Nostr-based * messenger is used. */ messenger?: Messenger, } & SwapperOptions; /** * Token resolvers for all chains, resolve tokens based on their address * * @category Core */ export type TypedTokenResolvers<T extends readonly ChainInitializer<any, any, any>[]> = (T extends readonly [infer First extends ChainInitializer<any, any, any>, ...infer Rest extends ChainInitializer<any, any, any>[]] ? TokenResolverDict<First> & TypedTokenResolvers<Rest> : unknown); /** * All tokens including Bitcoin tokens * * @category Core */ export type TypedTokens<T extends readonly ChainInitializer<any, ChainType, any>[]> = GetAllTokens<T> & { BITCOIN: { BTC: BtcToken<false>, BTCLN: BtcToken<true> } }; /** * Type alias for a Swapper instance with typed chain support * * @category Core */ export type TypedSwapper<T extends readonly ChainInitializer<any, ChainType, any>[]> = Swapper<ToMultichain<T>>; /** * Type alias for a specific swap type * * @category Core */ export type TypedSwap< T extends ChainInitializer<any, ChainType, any>, S extends SwapType > = SwapTypeMapping<T["chainType"]>[S]; const registries: {[key in BitcoinNetwork]?: string} = { [BitcoinNetwork.MAINNET]: "https://api.github.com/repos/adambor/SolLightning-registry/contents/registry-mainnet.json?ref=main", [BitcoinNetwork.TESTNET]: "https://api.github.com/repos/adambor/SolLightning-registry/contents/registry.json?ref=main", [BitcoinNetwork.TESTNET4]: "https://api.github.com/repos/adambor/SolLightning-registry/contents/registry-testnet4.json?ref=main" } const trustedIntermediaries: {[key in BitcoinNetwork]?: string} = { [BitcoinNetwork.MAINNET]: "https://node3.gethopa.com:34100", [BitcoinNetwork.TESTNET]: "https://node3.gethopa.com:24100" } const mempoolUrls: {[key in BitcoinNetwork]?: string[]} = { [BitcoinNetwork.MAINNET]: [ "https://mempool.space/api/", // "https://mempool.holdings/api/", "https://mempool.fra.mempool.space/api/", "https://mempool.va1.mempool.space/api/", "https://mempool.tk7.mempool.space/api/" ], [BitcoinNetwork.TESTNET]: [ "https://mempool.space/testnet/api/", // "https://mempool.holdings/testnet/api/", "https://mempool.fra.mempool.space/testnet/api/", "https://mempool.va1.mempool.space/testnet/api/", "https://mempool.tk7.mempool.space/testnet/api/" ], [BitcoinNetwork.TESTNET4]: [ "https://mempool.space/testnet4/api/", // "https://mempool.holdings/testnet4/api/", "https://mempool.fra.mempool.space/testnet4/api/", "https://mempool.va1.mempool.space/testnet4/api/", "https://mempool.tk7.mempool.space/testnet4/api/" ] } const nostrUrls: string[] = [ "wss://relay.damus.io", "wss://nostr.einundzwanzig.space", "wss://relay01.lnfi.network/", "wss://relay.puresignal.news/", "wss://relay.fountain.fm/", "wss://sendit.nosflare.com/" ]; /** * Factory class for creating and initializing Swapper instances with typed chain support * * @category Core */ export class SwapperFactory<T extends readonly ChainInitializer<any, ChainType, any>[]> { /** * All available tokens for the atomiq SDK */ Tokens: TypedTokens<T> = { BITCOIN: BitcoinTokens } as any; /** * Token resolvers for various smart chains supported by the SDK, allow fetching tokens based on their addresses */ TokenResolver: TypedTokenResolvers<T> = {} as any; private smartChainTokens: SCToken[] = []; constructor(readonly initializers: T) { this.initializers = initializers; initializers.forEach(initializer => { const addressMap: {[tokenAddress: string]: SCToken} = {}; const tokens = (this.Tokens[initializer.chainId as keyof GetAllTokens<T>] = {} as any); for(let ticker in initializer.tokens) { const assetData = initializer.tokens[ticker] as any; const token: SCToken = { chain: "SC", chainId: initializer.chainId, ticker, name: SmartChainAssets[ticker as SmartChainAssetTickers]?.name ?? ticker, decimals: assetData.decimals, displayDecimals: assetData.displayDecimals, address: assetData.address, equals: (other: Token) => other.chainId===initializer.chainId && other.ticker===ticker && other.address===assetData.address, toString: () => `${initializer.chainId}-${ticker}` }; this.smartChainTokens.push(token); tokens[ticker] = addressMap[assetData.address] = token; } this.TokenResolver[initializer.chainId as keyof TypedTokenResolvers<T>] = { getToken: (address: string) => addressMap[address] } as any; }); } /** * Returns a new swapper instance with the passed options. * * The swapper returned here is not yet initialized, be sure to call {@link Swapper.init}, before * calling any other functions in the swapper instance. * * @param options Options for customizing the swapper instance */ newSwapper(options: TypedSwapperOptions<T>): TypedSwapper<T> { options.bitcoinNetwork ??= BitcoinNetwork.MAINNET; options.storagePrefix ??= "atomiqsdk-"+options.bitcoinNetwork+"-"; options.messenger ??= new NostrMessenger(options.bitcoinNetwork, nostrUrls); options.defaultTrustedIntermediaryUrl ??= trustedIntermediaries[options.bitcoinNetwork]; options.registryUrl ??= registries[options.bitcoinNetwork]; let bitcoinRpc: MempoolBitcoinRpc; if(options.mempoolApi!=null) { bitcoinRpc = options.mempoolApi instanceof MempoolBitcoinRpc ? options.mempoolApi : new MempoolBitcoinRpc(options.mempoolApi, options.bitcoinNetwork); } else { const urls = mempoolUrls[options.bitcoinNetwork]; if(urls==null) throw new Error(`No pre-configured urls for ${BitcoinNetwork[options.bitcoinNetwork]} network were found, please explicitly pass mempoolApi parameter!`); bitcoinRpc = new MempoolBitcoinRpc(urls, options.bitcoinNetwork); } const pricingAssets: (RedundantSwapPriceAssets<ToMultichain<T>>[number] & {ticker: string, name: string})[] = []; Object.keys(SmartChainAssets).forEach((ticker) => { const chains: any = {}; for(let {tokens, chainId} of this.initializers) { if(tokens[ticker]!=null) chains[chainId] = tokens[ticker]; } const assetData = SmartChainAssets[ticker as SmartChainAssetTickers]; pricingAssets.push({ ...assetData.pricing, chains, ticker, name: assetData.name }) }); options.chainStorageCtor ??= <T extends StorageObject>(name: string) => new LocalStorageManager<T>(name); const chains: {[key in T[number]["chainId"]]: ChainData<any>} = {} as any; for(let {initializer, chainId} of this.initializers) { const chainOptions = options.chains[chainId as keyof GetAllOptions<T>]; if(chainOptions==null) continue; chains[chainId as T[number]["chainId"]] = initializer(chainOptions, bitcoinRpc, options.bitcoinNetwork, options.chainStorageCtor) as any; } const swapPricing = options.getPriceFn!=null ? new SingleSwapPrice(options.pricingFeeDifferencePPM ?? 10000n, new CustomPriceProvider(pricingAssets.map(val => { return { coinId: val.ticker, chains: val.chains } }), options.getPriceFn)) : RedundantSwapPrice.createFromTokenMap<ToMultichain<T>>(options.pricingFeeDifferencePPM ?? 10000n, pricingAssets); return new Swapper<ToMultichain<T>>( bitcoinRpc, bitcoinRpc, (btcRelay: BtcRelay<any, any, any>) => new MempoolBtcRelaySynchronizer(btcRelay, bitcoinRpc), chains as any, swapPricing, this.smartChainTokens, options.messenger, options ); } /** * Returns a new and already initialized swapper instance with the passed options. There is no need * to call {@link Swapper.init} anymore. * * @param options Options for customizing the swapper instance */ async newSwapperInitialized(options: TypedSwapperOptions<T>): Promise<TypedSwapper<T>> { const swapper = this.newSwapper(options); await swapper.init(); return swapper; } }