@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
281 lines (258 loc) • 9.99 kB
text/typescript
import {SwapType} from "../enums/SwapType";
import {SwapHandlerInfoType} from "./IntermediaryDiscovery";
import {ChainSwapType, LNNodeLiquidity, SwapContract} from "@atomiqlabs/base";
import {tryWithRetries} from "../utils/RetryUtils";
/**
* Services offered by an intermediary
*
* @category LPs
*/
export type ServicesType = {
[key in SwapType]?: SwapHandlerInfoType
};
/**
* Reputation data for an intermediary on a single chain
*
* @category LPs
*/
export type SingleChainReputationType = {
[token: string]: {
[key in ChainSwapType]: {
successVolume: bigint,
successCount: bigint,
failVolume: bigint,
failCount: bigint,
coopCloseVolume: bigint,
coopCloseCount: bigint,
}
}
};
/**
* Smart chain liquidity data
*
* @category LPs
*/
export type SCLiquidity = {
[token: string]: bigint
};
/**
* Represents an intermediary (liquidity provider)
*
* @category LPs
*/
export class Intermediary {
/**
* Base URL where the intermediary is listening for HTTP requests
*/
readonly url: string;
/**
* Addresses of the intermediary on smart chains, used for checking the provided signatures
*/
readonly addresses: {[chainIdentifier: string]: string};
/**
* Contract versions of the intermediary on smart chains
*/
readonly contractVersions: {[chainIdentifier: string]: string};
/**
* Swap protocol services offered by the intermediary
*/
readonly services: ServicesType;
/**
* Input/output swap bounds for various swap protocols offered by the intermediary
*/
readonly swapBounds: {
[swapType in SwapType]?: {
[chainIdentifier: string]: {
[tokenAddress: string]: {
input: {
min?: bigint,
max?: bigint
},
output: {
min?: bigint,
max?: bigint
}
}
}
}
}
/**
* Reputation of the intermediary on different smart chains, this is only fetched
* on-demand when creating a swap where reputation is checked
*/
reputation: { [chainIdentifier: string]: SingleChainReputationType } = {};
/**
* Liquidity of the intermediary across different smart chains, this is only fetched
* on-demand when creating a swap where intermediary's liquidity is checked
*/
liquidity: { [chainIdentifier: string]: SCLiquidity } = {};
/**
* Data about a lightning network node used by this intermediary, if it offers lightning
* network swaps, this is only fetched on-demand when creating a Bitcoin Lightning -> Smart chain
* swap through the intermediary (which necessitates checking intermediary's channel capacities)
*/
lnData?: LNNodeLiquidity;
constructor(
url: string,
addresses: {[chainIdentifier: string]: string},
services: ServicesType,
reputation: { [chainIdentifier: string]: SingleChainReputationType } = {},
contractVersions: {[chainIdentifier: string]: string} = {},
) {
this.url = url;
this.addresses = addresses;
this.services = services;
this.reputation = reputation;
this.contractVersions = contractVersions;
this.swapBounds = {};
for(let _swapType in this.services) {
const swapType: SwapType = parseInt(_swapType);
const serviceInfo = this.services[swapType]!;
const btcBounds = {min: BigInt(serviceInfo.min), max: BigInt(serviceInfo.max)};
const isSend = swapType===SwapType.TO_BTC || swapType===SwapType.TO_BTCLN;
this.swapBounds[swapType] = {};
for(let chainIdentifier in serviceInfo.chainTokens) {
this.swapBounds[swapType]![chainIdentifier] = {};
for(let tokenAddress of serviceInfo.chainTokens[chainIdentifier]) {
this.swapBounds[swapType]![chainIdentifier][tokenAddress] = {
input: isSend ? {} : btcBounds,
output: !isSend ? {} : btcBounds,
};
}
}
}
}
/**
* Returns the input/output swap limit for given swap type, chain and token
*
* @param swapType Swap protocol service to check
* @param chainId Chain identifier of the smart chain to check
* @param tokenAddress Address of the token to check
*/
getSwapLimits(swapType: SwapType, chainId: string, tokenAddress: string): {input: {min?: bigint, max?: bigint}, output: {min?: bigint, max?: bigint}} | undefined {
return this.swapBounds[swapType]?.[chainId]?.[tokenAddress];
}
/**
* Returns tokens supported by the intermediary, optionally constrained to the specific swap types
*
* @param chainIdentifier Chain identifier of the smart chain to check
* @param swapTypesArr An array of swap type services to check
* @private
*/
private getSupportedTokens(chainIdentifier: string, swapTypesArr: SwapType[] = [
SwapType.TO_BTC,
SwapType.TO_BTCLN,
SwapType.FROM_BTC,
SwapType.FROM_BTCLN,
SwapType.SPV_VAULT_FROM_BTC,
SwapType.FROM_BTCLN_AUTO
]): Set<string> {
const swapTypes = new Set(swapTypesArr);
let tokens: Set<string> = new Set<string>();
swapTypes.forEach((swapType) => {
const supportedTokens = this.services[swapType]?.chainTokens?.[chainIdentifier];
if(supportedTokens!=null) supportedTokens.forEach(token => tokens.add(token));
});
return tokens;
}
/**
* Fetches, returns and saves the reputation of the intermediary, either for all or just for a single token
*
* @param chainIdentifier Chain identifier of the chain on which to fetch the reputation
* @param swapContract Swap contract for the requested smart chain
* @param tokens An optional array of tokens to fetch the data for (by default it uses all tokens supported
* by the intermediary)
* @param abortSignal
*/
async getReputation(
chainIdentifier: string,
swapContract: SwapContract<any>,
tokens?: string[],
abortSignal?: AbortSignal
): Promise<SingleChainReputationType> {
const checkReputationTokens: Set<string> = tokens==null ?
this.getSupportedTokens(chainIdentifier, [SwapType.TO_BTC, SwapType.TO_BTCLN]) :
new Set<string>(tokens);
const promises: Promise<void>[] = [];
const reputation: SingleChainReputationType = {};
for(let token of checkReputationTokens) {
promises.push(
tryWithRetries(() =>
swapContract.getIntermediaryReputation(this.getAddress(chainIdentifier), token),
undefined, undefined, abortSignal
).then(result => {
if(result!=null) reputation[token] = result;
})
);
}
await Promise.all(promises);
this.reputation ??= {};
this.reputation[chainIdentifier] ??= {};
for(let key in reputation) {
this.reputation[chainIdentifier][key] = reputation[key];
}
return reputation;
}
/**
* Fetches, returns and saves the liquidity of the intermediary for a specific token
*
* @param chainIdentifier Chain identifier of the chain on which to fetch the reputation
* @param swapContract Swap contract for the requested smart chain
* @param token Token address of the token to fetch the liquidity for
* @param abortSignal
*/
async getLiquidity(
chainIdentifier: string,
swapContract: SwapContract<any>,
token: string,
abortSignal?: AbortSignal
): Promise<bigint> {
const result = await tryWithRetries(
() => swapContract.getBalance(this.getAddress(chainIdentifier), token, true),
undefined, undefined, abortSignal
);
this.liquidity ??= {};
this.liquidity[chainIdentifier] ??= {};
this.liquidity[chainIdentifier][token] = result;
return result;
}
/**
* Checks whether the intermediary supports swaps of any tokens on the smart chain
*
* @param chainIdentifier Chain identifier of the smart chain
*/
supportsChain(chainIdentifier: string): boolean {
if(this.addresses[chainIdentifier]==null) return false;
return this.getSupportedTokens(chainIdentifier).size!==0;
}
/**
* Returns intermediary's address on a given smart chain
*
* @param chainIdentifier Chain identifier of the smart chain
*/
getAddress(chainIdentifier: string) {
return this.addresses[chainIdentifier];
}
/**
* Returns the contract version used by the intermediary for a given chain
*
* @param chainIdentifier
*/
getContractVersion(chainIdentifier: string): string {
return this.contractVersions[chainIdentifier] ?? "v1";
}
/**
* Returns the range of contract versions used by the LPs
*
* @param chainIdentifier
* @param lps
*/
static getContractVersionsForLps(chainIdentifier: string, lps: Intermediary[]): string[] {
const versions: string[] = [];
lps.forEach((lp) => {
const lpVersion = lp.getContractVersion(chainIdentifier);
if(!versions.includes(lpVersion)) versions.push(lpVersion);
});
return versions;
}
}