UNPKG

@xlink-network/xlink-sdk

Version:
297 lines (277 loc) 8.47 kB
import { BigNumber } from "./BigNumber" import { concat, last, reduce } from "./arrayHelpers" import { checkNever, OneOrMore } from "./typeHelpers" import { TransferProphet, TransferProphet_Fee_Fixed, TransferProphet_Fee_Rate, TransferProphetAggregated, } from "./types/TransferProphet" export interface TransferProphetAppliedResult { fees: ( | (TransferProphet_Fee_Rate & { amount: BigNumber }) | TransferProphet_Fee_Fixed )[] netAmount: BigNumber } export const applyTransferProphets = ( transferProphets: OneOrMore<TransferProphet>, amount: BigNumber, ): OneOrMore<TransferProphetAppliedResult> => { return reduce( (acc, transferProphet) => concat(acc, [applyTransferProphet(transferProphet, last(acc).netAmount)]), [{ fees: [], netAmount: amount }], transferProphets, ) } export const applyTransferProphet = ( transferProphet: TransferProphet, amount: BigNumber, ): TransferProphetAppliedResult => { const fees: TransferProphetAppliedResult["fees"] = [] let totalFeeAmount = BigNumber.ZERO for (const f of transferProphet.fees) { let feeAmount = BigNumber.ZERO if (f.type === "rate") { if (f.token !== transferProphet.bridgeToken) { throw new Error( `[XLinkSDK#applyTransferProphet] transferProphet.bridgeToken (${transferProphet.bridgeToken}) does not match rateFee.token (${f.token}), which is not expected`, ) } feeAmount = BigNumber.max([ f.minimumAmount, BigNumber.mul(f.rate, amount), ]) fees.push({ ...f, amount: feeAmount }) } else if (f.type === "fixed") { if (f.token === transferProphet.bridgeToken) { feeAmount = f.amount } fees.push(f) } else { checkNever(f) } totalFeeAmount = BigNumber.add(totalFeeAmount, feeAmount) } const netAmount = BigNumber.max([ BigNumber.ZERO, BigNumber.minus(amount, totalFeeAmount), ]) return { fees, netAmount } } /** * @example * composeTransferProphets( * [ * // tokenA : chain1 -> chain2 * transferProphetOf(tokenAChain1, tokenAChain2), * // tokenB : chain2 -> chain3 * transferProphetOf(tokenBChain2, tokenBChain3), * // tokenC : chain3 -> chain4 * transferProphetOf(tokenCChain3, tokenCChain4), * ], * [ * // tokenA -> tokenB : chain2 * exchangeRateOf(tokenAChain2, tokenBChain2), * // tokenB -> tokenC : chain3 * exchangeRateOf(tokenBChain3, tokenCChain3), * ], * ) */ export const composeTransferProphets = ( transferProphets: readonly TransferProphet[], exchangeRates: readonly BigNumber[], ): TransferProphetAggregated<TransferProphet[]> => { const cumulativeExchangeRates = calcCumulativeExchangeRates(exchangeRates) return reduce( (res, transferProphet, idx) => ({ ...composeTransferProphet2( res, transferProphet, cumulativeExchangeRates[idx], ), transferProphets: [...res.transferProphets, transferProphet], }), composeTransferProphet2( transferProphets[0], transferProphets[1], cumulativeExchangeRates[0], ) as TransferProphetAggregated<TransferProphet[]>, transferProphets.slice(2), ) } /** * @example * calcCumulativeExchangeRates([ * exchangeRateOf(tokenA, tokenB), * exchangeRateOf(tokenB, tokenC), * exchangeRateOf(tokenC, tokenD), * // ... * ]) * * // => * * [ * exchangeRateOf(tokenA, tokenB), * exchangeRateOf(tokenA, tokenC), * exchangeRateOf(tokenA, tokenD), * // ... * ] */ const calcCumulativeExchangeRates = ( exchangeRates: readonly BigNumber[], ): readonly BigNumber[] => { return BigNumber.cumulativeMul(BigNumber.ONE, exchangeRates).slice(1) } export const composeTransferProphet2 = ( transferProphet1: TransferProphet, transferProphet2: TransferProphet, exchangeRate: BigNumber, ): TransferProphetAggregated<[TransferProphet, TransferProphet]> => { if (exchangeRate == null) { throw new Error( "[XLinkSDK#composeTransferProphet2] exchangeRate is required", ) } const bridgeTokenMinFeeAmount = BigNumber.sum([ ...transferProphet1.fees.flatMap(f => f.type === "rate" && f.token === transferProphet1.bridgeToken ? [f.minimumAmount] : [], ), ...transferProphet2.fees.flatMap(f => f.type === "rate" && f.token === transferProphet2.bridgeToken ? [ /** * convert to the denomination of the first step token */ BigNumber.div(f.minimumAmount, exchangeRate), ] : [], ), ]) /** * flatFeeRate = 1 - (amount1 * (1-feeRate1) * (1-feeRate2) * (1-feeRateN...) / amount1) * flatFeeRate = 1 - (1-feeRate1) * (1-feeRate2) * (1-feeRateN...) */ const step1FlatFeeRate = BigNumber.minus( BigNumber.ONE, last( BigNumber.cumulativeMul( BigNumber.ONE, transferProphet1.fees.flatMap(f => f.type === "rate" && f.token === transferProphet1.bridgeToken ? [BigNumber.minus(1, f.rate)] : [], ), ), ), ) /** * step2Amount = step1Amount * (1-feeRate1) * (1-feeRate2) * (1-feeRateN)... * exchangeRate * step2Amount = step1Amount * (1-flatFeeRate) * exchangeRate */ const step1ToStep2Rate = BigNumber.mul( BigNumber.minus(1, step1FlatFeeRate), exchangeRate, ) let minBridgeAmount: BigNumber | null = null if ( BigNumber.isZero(bridgeTokenMinFeeAmount) /* min fee amount not set */ && transferProphet1.minBridgeAmount == null && transferProphet2.minBridgeAmount == null ) { minBridgeAmount = null } else { minBridgeAmount = BigNumber.max([ bridgeTokenMinFeeAmount, transferProphet1.minBridgeAmount ?? 0, transferProphet2.minBridgeAmount == null ? 0 : BigNumber.div(transferProphet2.minBridgeAmount, step1ToStep2Rate), ]) } let maxBridgeAmount: BigNumber | null = null if ( transferProphet1.maxBridgeAmount == null && transferProphet2.maxBridgeAmount == null ) { maxBridgeAmount = null } else if ( transferProphet1.maxBridgeAmount != null && transferProphet2.maxBridgeAmount != null ) { maxBridgeAmount = BigNumber.min([ transferProphet1.maxBridgeAmount, BigNumber.div(transferProphet2.maxBridgeAmount, step1ToStep2Rate), ]) } else { maxBridgeAmount = transferProphet1.maxBridgeAmount ?? transferProphet2.maxBridgeAmount } return { isPaused: transferProphet1.isPaused || transferProphet2.isPaused, bridgeToken: transferProphet1.bridgeToken, minBridgeAmount, maxBridgeAmount, fees: [ ...transferProphet1.fees, ...transferProphet2.fees.map(fee => { if (fee.type === "fixed") { if (fee.token !== transferProphet2.bridgeToken) return fee return { type: "fixed", token: transferProphet1.bridgeToken, amount: BigNumber.div(fee.amount, step1ToStep2Rate), } satisfies TransferProphet_Fee_Fixed } if (fee.type === "rate") { if (fee.token !== transferProphet2.bridgeToken) return fee return { type: "rate", token: transferProphet1.bridgeToken, /** * feeAmount = step2Amount * fee.rate * feeAmount = (step1Amount * (1 - step1FlatFeeRate) * exchangeRate) * fee.rate * step1Amount * newFeeRate = (step1Amount * (1 - step1FlatFeeRate) * exchangeRate) * fee.rate * newFeeRate = (1 - step1FlatFeeRate) * exchangeRate * fee.rate * newFeeRate = step1ToStep2Rate * fee.rate */ rate: BigNumber.mul(step1ToStep2Rate, fee.rate), minimumAmount: BigNumber.div(fee.minimumAmount, step1ToStep2Rate), } satisfies TransferProphet_Fee_Rate } checkNever(fee) return fee }), ], transferProphets: [transferProphet1, transferProphet2], } } export const composeRates2 = ( rate1: BigNumber, rate2: BigNumber, ): BigNumber => { /** * n = bridge amount * rate = ((n * r1) + * (n * (1 - r1)) * r2) * / n * | * V * = (n * r1 / n) + * (n * (1 - r1) * r2 / n) * | * V * = r1 + (1 - r1) * r2 */ // prettier-ignore return BigNumber.sum([ rate1, BigNumber.mul( BigNumber.minus(1, rate1), rate2, ), ]) }