From 46e9f0b428e7ddbf5a1fbf22f4b692eb9ae6c652 Mon Sep 17 00:00:00 2001 From: Xiaohui Zhao Date: Fri, 16 Sep 2022 16:18:36 +0800 Subject: [PATCH] feature: simple route (#9) * get all pairs * update * getAllLPCoinResourcesWithAdmin * [WIP]bestTradeExactIn * add swap payload * add price impact * update route readme --- README.md | 87 ++++---- src/modules/RouteModule.ts | 424 +++++++++++++++++++++++++++++++++++++ src/modules/SwapModule.ts | 120 +++++++++-- src/sdk.ts | 7 + src/test/main.test.ts | 16 +- src/test/route.test.ts | 88 ++++++++ src/utils/contract.ts | 8 + src/utils/hex.ts | 9 + src/utils/is.ts | 5 + 9 files changed, 706 insertions(+), 58 deletions(-) create mode 100644 src/modules/RouteModule.ts create mode 100644 src/test/route.test.ts diff --git a/README.md b/README.md index 7ee0021..564769b 100644 --- a/README.md +++ b/README.md @@ -145,35 +145,29 @@ const sdk = new SDK({ const BTC = '0x16fe2df00ea7dde4a63409201f7f4e536bde7bb7335526a35d05111e68aa322c::TestCoinsV1::BTC' const aptosAmount = 1e6 - const output = await sdk.swap.swapRates({ + const trades = await sdk.route.getRouteSwapExactCoinForCoin({ fromCoin: APTOS, toCoin: BTC, - amount: aptosAmount, - fixedCoin: 'from', // fixed input coin - slippage: 0.05, // 5% + amount: aptosAmount.toString(), }); - + if (trades.length == 0) throw("No route error") + const bestTrade = trades[0] /** - output type: + bestTrade type: { - amount: Decimal - amountWithSlippage: Decimal + coinPairList: LiquidityPoolResource[] + amountList: string[] + coinTypeList: string[] priceImpact: Decimal - coinFromDivCoinTo: Decimal - coinToDivCoinFrom: Decimal } */ - const txPayload = sdk.swap.swapPayload({ - fromCoin: APTOS, - toCoin: BTC, - fromAmount: aptosAmount, - toAmount: output.amount, - fixedCoin: 'from', // fixed input coin - toAddress: '0xA11ce', // receive `toCoin` address. In the most case, should be the same as sender address - slippage: 0.05, // 5% - deadline: 20, // 20 minutes - }) + const output = sdk.route.swapExactCoinForCoinPayload( + trade: bestTrade, + toAddress: SenderAddress, // receive `toCoin` address. In the most case, should be the same as sender address + slippage: 0.05, // 5% + deadline: 20, // 20 minutes + ) /** output type: tx payload @@ -181,7 +175,6 @@ const sdk = new SDK({ })() ``` - ### Swap (exact out) rate calculation and tx payload. Swap coin to exact coin mode ```typescript (async () => { @@ -189,35 +182,29 @@ const sdk = new SDK({ const BTC = '0x16fe2df00ea7dde4a63409201f7f4e536bde7bb7335526a35d05111e68aa322c::TestCoinsV1::BTC' const btcAmount = 1e6 - const output = await sdk.swap.swapRates({ + const trades = await sdk.route.getRouteSwapCoinForExactCoin({ fromCoin: APTOS, toCoin: BTC, - amount: btcAmount, - fixedCoin: 'to', // fixed output coin - slippage: 0.05, // 5% - }) - + amount: btcAmount.toString(), + }); + if (trades.length == 0) throw("No route error") + const bestTrade = trades[0] /** - output type: + bestTrade type: { - amount: Decimal - amountWithSlippage: Decimal + coinPairList: LiquidityPoolResource[] + amountList: string[] + coinTypeList: string[] priceImpact: Decimal - coinFromDivCoinTo: Decimal - coinToDivCoinFrom: Decimal } */ - const txPayload = sdk.swap.swapPayload({ - fromCoin: APTOS, - toCoin: BTC, - fromAmount: output.amount, - toAmount: btcAmount, - fixedCoin: 'to', // fixed output coin - toAddress: '0xA11ce', // receive `toCoin` address. In the most case, should be the same as sender address + const output = sdk.route.swapCoinForExactCoinPayload( + trade: bestTrade, + toAddress: SenderAddress, // receive `toCoin` address. In the most case, should be the same as sender address slippage: 0.05, // 5% deadline: 20, // 20 minutes - }) + ) /** output type: tx payload @@ -225,6 +212,26 @@ const sdk = new SDK({ })() ``` +### Get all LPCoin by address +```typescript +(async () => { + const queryAddress = '0xA11ce' + const output = await sdk.swap.getAllLPCoinResourcesByAddress({ + address: queryAddress, + }) + + /** + output type: + [{ + coinX: AptosResourceType + coinY: AptosResourceType + lpCoin: AptosResourceType + value: string + }] + */ +})() +``` + ### Get LPCoin amount ```typescript (async () => { diff --git a/src/modules/RouteModule.ts b/src/modules/RouteModule.ts new file mode 100644 index 0000000..5cbd133 --- /dev/null +++ b/src/modules/RouteModule.ts @@ -0,0 +1,424 @@ +import { SDK } from '../sdk' +import { IModule } from '../interfaces/IModule' +import { + AptosResourceType, + Payload, +} from '../types/aptos' +import { d } from '../utils/number' +import { + getCoinInWithFees, + getCoinOutWithFees, + LiquidityPoolResource, + withSlippage, +} from './SwapModule' +import { composeSwapPoolData, composeType } from '../utils/contract' +import { + SwapPoolData, +} from '../types/swap' +import Decimal from 'decimal.js' + +type Trade = { + coinPairList: LiquidityPoolResource[] // coin pair info with reserve amount + amountList: string[] // coin amount, from `fromCoin` to `toCoin` + coinTypeList: string[] // coin type, from `fromCoin` to `toCoin` + priceImpact: Decimal // price impact of this trade +} + +export type SwapCoinParams = { + fromCoin: AptosResourceType + toCoin: AptosResourceType + amount: string +} + +export class RouteModule implements IModule { + protected _sdk: SDK + + get sdk() { + return this._sdk + } + + constructor(sdk: SDK) { + this._sdk = sdk + } + + /** + * FromExactCoinToCoin + * @param pairList all pair list from `getAllLPCoinResourcesWithAdmin()` + * @param coinTypeOutOrigin out coin type + * @param maxNumResults top result nums + * @param maxHops remaining hops + * @param currentPairs current path pairs + * @param currentAmounts current path amounts + * @param nextCoinType next coin type + * @param nextAmountIn next coin amount in + * @param fee swap fee + * @param bestTrades saved trade results + * @returns bestTrades + */ + async bestTradeExactIn( + pairList: LiquidityPoolResource[], + coinTypeInOrigin: AptosResourceType, + coinTypeOutOrigin: AptosResourceType, + maxNumResults: number, + maxHops: number, + currentPairs: LiquidityPoolResource[], + currentAmounts: string[], + nextCoinType: AptosResourceType, + nextAmountIn: string, + fee: string, + bestTrades: Trade[], + ): Promise { + for (let i = 0; i < pairList.length; i++) { + const pair = pairList[i] + if (!pair) continue + if (!(pair.coinX == nextCoinType) && !(pair.coinY == nextCoinType)) continue + if (pair.coinXReserve === '0' || pair.coinYReserve === '0') continue + const coinTypeOut = (pair.coinX == nextCoinType) + ? pair.coinY + : pair.coinX + const [reserveIn, reserveOut] = (pair.coinX == nextCoinType) + ? [pair.coinXReserve, pair.coinYReserve] + : [pair.coinYReserve, pair.coinXReserve] + const coinOut = getCoinOutWithFees(d(nextAmountIn), d(reserveIn), d(reserveOut), d(fee)) + if (coinOut.lessThan(d(0) || coinOut.greaterThan(d(reserveOut)))) continue + // we have arrived at the output token, so this is the final trade of one of the paths + if (coinTypeOut == coinTypeOutOrigin) { + const coinPairList = [...currentPairs, pair] + const amountList = [...currentAmounts, coinOut.toString()] + const coinTypeList = getCoinTypeList(coinTypeInOrigin, coinPairList) + const priceImpact = getPriceImpact(coinTypeInOrigin, coinPairList, amountList) + const newTrade = { + coinPairList, + amountList, + coinTypeList, + priceImpact, + } + sortedInsert( + bestTrades, + newTrade, + maxNumResults, + tradeComparator, + ) + } else if (maxHops > 1 && pairList.length > 1) { + const pairListExcludingThisPair = pairList.slice(0, i).concat(pairList.slice(i + 1, pairList.length)) + + this.bestTradeExactIn( + pairListExcludingThisPair, + coinTypeInOrigin, + coinTypeOutOrigin, + maxNumResults, + maxHops - 1, + [...currentPairs, pair], + [...currentAmounts, coinOut.toString()], + coinTypeOut, + coinOut.toString(), + fee, + bestTrades, + ) + } + } + + return bestTrades + } + + + /** + * FromCoinToExactCoin + * @param pairList all pair list from `getAllLPCoinResourcesWithAdmin()` + * @param coinTypeInOrigin in coin type + * @param maxNumResults top result nums + * @param maxHops remaining hops + * @param currentPairs current path pairs + * @param currentAmounts current path amounts + * @param nextCoinType next coin type + * @param nextAmountOut next coin amount out + * @param fee swap fee + * @param bestTrades saved trade results + * @returns bestTrades + */ + async bestTradeExactOut( + pairList: LiquidityPoolResource[], + coinTypeInOrigin: AptosResourceType, + coinTypeOutOrigin: AptosResourceType, + maxNumResults: number, + maxHops: number, + currentPairs: LiquidityPoolResource[], + currentAmounts: string[], + nextCoinType: AptosResourceType, + nextAmountOut: string, + fee: string, + bestTrades: Trade[], + ): Promise { + for (let i = 0; i < pairList.length; i++) { + const pair = pairList[i] + if (!pair) continue + if (!(pair.coinX == nextCoinType) && !(pair.coinY == nextCoinType)) continue + if (pair.coinXReserve === '0' || pair.coinYReserve === '0') continue + const coinTypeIn = (pair.coinX == nextCoinType) + ? pair.coinY + : pair.coinX + const [reserveIn, reserveOut] = (pair.coinX == nextCoinType) + ? [pair.coinYReserve, pair.coinXReserve] + : [pair.coinXReserve, pair.coinYReserve] + const coinIn = getCoinInWithFees(d(nextAmountOut), d(reserveOut), d(reserveIn), d(fee)) + if (coinIn.lessThan(d(0) || coinIn.greaterThan(d(reserveIn)))) continue + // we have arrived at the output token, so this is the final trade of one of the paths + if (coinTypeIn == coinTypeInOrigin) { + const coinPairList = [pair, ...currentPairs] + const amountList = [coinIn.toString(), ...currentAmounts] + const coinTypeList = getCoinTypeList(coinTypeInOrigin, coinPairList) + const priceImpact = getPriceImpact(coinTypeInOrigin, coinPairList, amountList) + const newTrade = { + coinPairList, + amountList, + coinTypeList, + priceImpact, + } + sortedInsert( + bestTrades, + newTrade, + maxNumResults, + tradeComparator, + ) + } else if (maxHops > 1 && pairList.length > 1) { + const pairListExcludingThisPair = pairList.slice(0, i).concat(pairList.slice(i + 1, pairList.length)) + + this.bestTradeExactOut( + pairListExcludingThisPair, + coinTypeInOrigin, + coinTypeOutOrigin, + maxNumResults, + maxHops - 1, + [pair, ...currentPairs], + [coinIn.toString(), ...currentAmounts], + coinTypeIn, + coinIn.toString(), + fee, + bestTrades, + ) + } + } + + return bestTrades + } + + async getRouteSwapExactCoinForCoin(params: SwapCoinParams): Promise { + const { modules } = this.sdk.networkOptions + const task1 = this._sdk.swap.getAllLPCoinResourcesWithAdmin() + const swapPoolDataType = composeSwapPoolData(modules.DeployerAddress) + const task2 = this.sdk.resources.fetchAccountResource( + modules.DeployerAddress, + swapPoolDataType + ) + const [pairList, swapPoolData] = await Promise.all([task1, task2]) + if (!swapPoolData) { + throw new Error(`swapPoolData (${swapPoolDataType}) not found`) + } + + const fee = swapPoolData.data.swap_fee + const bestTrades = this.bestTradeExactIn( + pairList, + params.fromCoin, + params.toCoin, + 3, + 3, + [], + [params.amount], + params.fromCoin, + params.amount, + fee, + [], + ) + return bestTrades + } + + async getRouteSwapCoinForExactCoin(params: SwapCoinParams): Promise { + const { modules } = this.sdk.networkOptions + const task1 = this._sdk.swap.getAllLPCoinResourcesWithAdmin() + const swapPoolDataType = composeSwapPoolData(modules.DeployerAddress) + const task2 = this.sdk.resources.fetchAccountResource( + modules.DeployerAddress, + swapPoolDataType + ) + const [pairList, swapPoolData] = await Promise.all([task1, task2]) + if (!swapPoolData) { + throw new Error(`swapPoolData (${swapPoolDataType}) not found`) + } + + const fee = swapPoolData.data.swap_fee + const bestTrades = this.bestTradeExactOut( + pairList, + params.fromCoin, + params.toCoin, + 3, + 3, + [], + [params.amount], + params.toCoin, + params.amount, + fee, + [], + ) + return bestTrades + } + + swapExactCoinForCoinPayload( + trade: Trade, + toAddress: AptosResourceType, + slippage: number, + deadline: number, + ): Payload { + if (trade.coinPairList.length > 3 || trade.coinPairList.length < 1) { + throw new Error(`Invalid coin pair length (${trade.coinPairList.length}) value`) + } + const { modules } = this.sdk.networkOptions + + let functionEntryName = '' + if (trade.coinPairList.length == 1) { + functionEntryName = 'swap_exact_coins_for_coins_entry' + } else if (trade.coinPairList.length == 2) { + functionEntryName = 'swap_exact_coins_for_coins_2_pair_entry' + } else if (trade.coinPairList.length == 3) { + functionEntryName = 'swap_exact_coins_for_coins_3_pair_entry' + } + + const functionName = composeType( + modules.Scripts, + functionEntryName + ) + + const typeArguments = trade.coinTypeList + + const fromAmount = trade.amountList[0] + const toAmount = withSlippage(d(trade.amountList[trade.amountList.length - 1]), d(slippage), 'minus') + + const deadlineArgs = Math.floor(Date.now() / 1000) + deadline * 60 + + const args = [modules.ResourceAccountAddress, fromAmount, d(toAmount).toString(), toAddress, d(deadlineArgs).toString()] + + return { + type: 'entry_function_payload', + function: functionName, + typeArguments: typeArguments, + arguments: args, + } + } + + swapCoinForExactCoinPayload( + trade: Trade, + toAddress: AptosResourceType, + slippage: number, + deadline: number, + ): Payload { + if (trade.coinPairList.length > 3 || trade.coinPairList.length < 1) { + throw new Error(`Invalid coin pair length (${trade.coinPairList.length}) value`) + } + const { modules } = this.sdk.networkOptions + + let functionEntryName = '' + if (trade.coinPairList.length == 1) { + functionEntryName = 'swap_coins_for_exact_coins_entry' + } else if (trade.coinPairList.length == 2) { + functionEntryName = 'swap_coins_for_exact_coins_2_pair_entry' + } else if (trade.coinPairList.length == 3) { + functionEntryName = 'swap_coins_for_exact_coins_3_pair_entry' + } + + const functionName = composeType( + modules.Scripts, + functionEntryName + ) + + const typeArguments = trade.coinTypeList + + const toAmount = trade.amountList[trade.amountList.length - 1] + const fromAmount = withSlippage(d(trade.amountList[0]), d(slippage), 'plus') + + const deadlineArgs = Math.floor(Date.now() / 1000) + deadline * 60 + + const args = [modules.ResourceAccountAddress, toAmount, d(fromAmount).toString(), toAddress, d(deadlineArgs).toString()] + + return { + type: 'entry_function_payload', + function: functionName, + typeArguments: typeArguments, + arguments: args, + } + } +} + +function sortedInsert(items: T[], add: T, maxSize: number, comparator: (a: T, b: T) => number) { + let index + for (index = 0; index < items.length; index++) { + const comp = comparator(items[index], add) + if (comp >= 0) { + break + } else if (comp == -1) { + continue + } + } + items.splice(index, 0, add) + if (items.length > maxSize) { + items.pop() + } +} + +function tradeComparator(trade1: Trade, trade2: Trade): number { + const trade1In = d(trade1.amountList[0]) + const trade2In = d(trade2.amountList[0]) + const trade1Out = d(trade1.amountList[trade1.amountList.length - 1]) + const trade2Out = d(trade2.amountList[trade2.amountList.length - 1]) + if (trade1In.eq(trade2In)) { + if (trade1Out.eq(trade2Out)) { + return trade1.amountList.length - trade2.amountList.length + } + if (trade1Out.lessThan(trade2Out)) { + return 1 + } else { + return -1 + } + } else { + if (trade1In.lessThan(trade2In)) { + return -1 + } else { + return 1 + } + } +} + +function getCoinTypeList(coinInType: AptosResourceType, coinPairList: LiquidityPoolResource[]): AptosResourceType[] { + const coinTypeList = [coinInType] + let currentCoinType = coinInType + for (let i = 0; i < coinPairList.length; i++) { + const coinPair = coinPairList[i] + if (!coinPair) continue + if (coinPair.coinX == currentCoinType) { + currentCoinType = coinPair.coinY + coinTypeList.push(coinPair.coinY) + } else { + currentCoinType = coinPair.coinX + coinTypeList.push(coinPair.coinX) + } + } + return coinTypeList +} + +// calculated as: abs(realAmountOut - noImpactAmountOut) / noImpactAmountOut +function getPriceImpact(coinInType: AptosResourceType, coinPairList: LiquidityPoolResource[], amountList: string[]): Decimal { + const realAmountOut = d(amountList[amountList.length - 1]) + let noImpactAmountOut = d(amountList[0]) + let currentCoinType = coinInType + for (let i = 0; i < coinPairList.length; i++) { + const coinPair = coinPairList[i] + if (!coinPair) continue + if (coinPair.coinX == currentCoinType) { + currentCoinType = coinPair.coinY + noImpactAmountOut = noImpactAmountOut.mul(d(coinPair.coinYReserve)).div(d(coinPair.coinXReserve)) + } else { + currentCoinType = coinPair.coinX + noImpactAmountOut = noImpactAmountOut.mul(d(coinPair.coinXReserve)).div(d(coinPair.coinYReserve)) + } + } + const priceImpact = realAmountOut.sub(noImpactAmountOut).div(noImpactAmountOut) + return priceImpact.abs() +} diff --git a/src/modules/SwapModule.ts b/src/modules/SwapModule.ts index 03d8275..ac8eee3 100644 --- a/src/modules/SwapModule.ts +++ b/src/modules/SwapModule.ts @@ -21,9 +21,13 @@ import { composeLPCoinType, composeSwapPoolData, composeCoinStore, + composePairInfo, + composeLiquidityPool, } from '../utils/contract' import { d } from '../utils/number' import Decimal from 'decimal.js' +import { hexToString } from '../utils/hex' +import { notEmpty } from '../utils/is' export type AddLiquidityParams = { coinX: AptosResourceType @@ -103,12 +107,47 @@ export type LPCoinResource = { value: string } | null +export type LiquidityPoolResource = { + coinX: AptosResourceType + coinY: AptosResourceType + coinXReserve: string + coinYReserve: string +} | null + export type LPCoinParams = { address: address coinX: AptosResourceType coinY: AptosResourceType } +export type PairListResource = [{ + coin_x: { + account_address: string + module_name: string + struct_name: string + } + coin_y: { + account_address: string + module_name: string + struct_name: string + } + lp_coin: { + account_address: string + module_name: string + struct_name: string + } +}] + +export type CoinPair = { + coinX: AptosResourceType + coinY: AptosResourceType +} + +export type PairInfoResource = { + pair_created_event: AptosResourceType + pair_list: PairListResource +} + export class SwapModule implements IModule { protected _sdk: SDK @@ -262,7 +301,7 @@ export class SwapModule implements IModule { const { modules } = this.sdk.networkOptions const isSorted = isSortedSymbols(params.fromCoin, params.toCoin) const lpType = composeLP(modules.DeployerAddress, params.fromCoin, params.toCoin) - const SwapPoolDataType = composeSwapPoolData(modules.DeployerAddress) + const swapPoolDataType = composeSwapPoolData(modules.DeployerAddress) const task1 = this.sdk.resources.fetchAccountResource( modules.ResourceAccountAddress, @@ -271,17 +310,17 @@ export class SwapModule implements IModule { const task2 = this.sdk.resources.fetchAccountResource( modules.DeployerAddress, - SwapPoolDataType + swapPoolDataType ) - const [lp, SwapPoolData] = await Promise.all([task1, task2]) + const [lp, swapPoolData] = await Promise.all([task1, task2]) if (!lp) { throw new Error(`LiquidityPool (${lpType}) not found`) } - if (!SwapPoolData) { - throw new Error(`SwapPoolData (${SwapPoolDataType}) not found`) + if (!swapPoolData) { + throw new Error(`SwapPoolData (${swapPoolDataType}) not found`) } const coinXReserve = lp.data.coin_x_reserve @@ -291,7 +330,7 @@ export class SwapModule implements IModule { ? [d(coinXReserve), d(coinYReserve)] : [d(coinYReserve), d(coinXReserve)] - const fee = SwapPoolData.data.swap_fee + const fee = swapPoolData.data.swap_fee const outputCoins = isSorted @@ -342,18 +381,18 @@ export class SwapModule implements IModule { params.toCoin, ] - const fromAmount = + const frontAmount = params.fixedCoin === 'from' ? params.fromAmount - : withSlippage(d(params.fromAmount), d(params.slippage), 'minus') - const toAmount = + : params.toAmount + const backAmount = params.fixedCoin === 'to' - ? params.toAmount - : withSlippage(d(params.toAmount), d(params.slippage), 'plus') + ? withSlippage(d(params.fromAmount), d(params.slippage), 'plus') + : withSlippage(d(params.toAmount), d(params.slippage), 'minus') const deadline = Math.floor(Date.now() / 1000) + params.deadline * 60 - const args = [modules.ResourceAccountAddress, d(fromAmount).toString(), d(toAmount).toString(), params.toAddress, d(deadline).toString()] + const args = [modules.ResourceAccountAddress, d(frontAmount).toString(), d(backAmount).toString(), params.toAddress, d(deadline).toString()] return { type: 'entry_function_payload', @@ -372,7 +411,7 @@ export class SwapModule implements IModule { return coinInfo } - async getAllLPCoinResources(address: address): Promise { + async getAllLPCoinResourcesByAddress(address: address): Promise { const { modules } = this.sdk.networkOptions const resources = await this.sdk.resources.fetchAccountResources( address @@ -392,7 +431,7 @@ export class SwapModule implements IModule { lpCoin: regexResult[1], value: resource.data.coin.value, } - }).filter(v => v != null) + }).filter(notEmpty) if (!filteredResource) { throw new Error(`filteredResource (${filteredResource}) not found`) } @@ -420,9 +459,58 @@ export class SwapModule implements IModule { value: lpCoinStore.data.coin.value, } } + + async getAllPairs(): Promise { + const { modules } = this.sdk.networkOptions + const pairInfoType = composePairInfo(modules.ResourceAccountAddress) + const pairInfo = await this.sdk.resources.fetchAccountResource( + modules.ResourceAccountAddress, + pairInfoType, + ) + + if (!pairInfo) { + throw new Error(`PairInfo (${pairInfoType}) not found`) + } + + const pairList = pairInfo.data.pair_list + const ret = pairList.map(v => { + return { + coinX: `${v.coin_x.account_address}::${hexToString(v.coin_x.module_name)}::${hexToString(v.coin_x.struct_name)}`, + coinY: `${v.coin_y.account_address}::${hexToString(v.coin_y.module_name)}::${hexToString(v.coin_y.struct_name)}`, + } + }) + return ret + } + + async getAllLPCoinResourcesWithAdmin(): Promise { + const { modules } = this.sdk.networkOptions + const resources = await this.sdk.resources.fetchAccountResources( + modules.ResourceAccountAddress + ) + if (!resources) { + throw new Error(`resources (${resources}) not found`) + } + const lpCoinType = composeLiquidityPool(modules.DeployerAddress) + const regexStr = `${lpCoinType}<(.+?), ?(.+?), ?(.+)>` + const filteredResource = resources.map(resource => { + const regex = new RegExp(regexStr, 'g') + const regexResult = regex.exec(resource.type) + if (!regexResult) return null + return { + coinX: regexResult[1], + coinY: regexResult[2], + coinXReserve: resource.data.coin_x_reserve, + coinYReserve: resource.data.coin_y_reserve, + } + }).filter(notEmpty) + if (!filteredResource) { + throw new Error(`filteredResource (${filteredResource}) not found`) + } + return filteredResource + } } -function getCoinOutWithFees( +export function getCoinOutWithFees( coinInVal: Decimal.Instance, reserveInSize: Decimal.Instance, reserveOutSize: Decimal.Instance, @@ -436,7 +524,7 @@ function getCoinOutWithFees( return coinInAfterFees.mul(reserveOutSize).div(newReservesInSize).toDP(0) } -function getCoinInWithFees( +export function getCoinInWithFees( coinOutVal: Decimal.Instance, reserveOutSize: Decimal.Instance, reserveInSize: Decimal.Instance, diff --git a/src/sdk.ts b/src/sdk.ts index 66b2056..4547e82 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -1,5 +1,6 @@ import { AptosClient } from 'aptos' import { SwapModule } from './modules/SwapModule' +import { RouteModule } from './modules/RouteModule' import { ResourcesModule } from './modules/ResourcesModule' import { AptosResourceType } from './types/aptos' @@ -20,6 +21,7 @@ export type SdkOptions = { export class SDK { protected _client: AptosClient protected _swap: SwapModule + protected _route: RouteModule protected _resources: ResourcesModule protected _networkOptions: SdkOptions['networkOptions'] @@ -27,6 +29,10 @@ export class SDK { return this._swap } + get route() { + return this._route + } + get resources() { return this._resources } @@ -43,6 +49,7 @@ export class SDK { this._networkOptions = options.networkOptions this._client = new AptosClient(options.nodeUrl) this._swap = new SwapModule(this) + this._route = new RouteModule(this) this._resources = new ResourcesModule(this) } } diff --git a/src/test/main.test.ts b/src/test/main.test.ts index 6df9aa7..08c5e98 100644 --- a/src/test/main.test.ts +++ b/src/test/main.test.ts @@ -40,9 +40,21 @@ describe('Swap Module', () => { }, }) - test('getAllLPCoinResources', async () => { + test('getAllPairs', async () => { + const output = await sdk.swap.getAllPairs() + console.log(output) + expect(1).toBe(1) + }) + + test('getAllLPCoinResourcesByAddress', async () => { const address = '0x16fe2df00ea7dde4a63409201f7f4e536bde7bb7335526a35d05111e68aa322c' - const output = await sdk.swap.getAllLPCoinResources(address) + const output = await sdk.swap.getAllLPCoinResourcesByAddress(address) + console.log(output) + expect(1).toBe(1) + }) + + test('getAllLPCoinResourcesWithAdmin', async () => { + const output = await sdk.swap.getAllLPCoinResourcesWithAdmin() console.log(output) expect(1).toBe(1) }) diff --git a/src/test/route.test.ts b/src/test/route.test.ts new file mode 100644 index 0000000..2c0a2cd --- /dev/null +++ b/src/test/route.test.ts @@ -0,0 +1,88 @@ +import SDK from '../main' + +const CoinsMapping: { [key: string]: string } = { + APTOS: '0x1::aptos_coin::AptosCoin', + BTC: '0x16fe2df00ea7dde4a63409201f7f4e536bde7bb7335526a35d05111e68aa322c::TestCoinsV1::BTC', +} +const SenderAddress = '0xa1ice' + +describe('Route Module', () => { + const sdk = new SDK({ + nodeUrl: 'https://fullnode.devnet.aptoslabs.com', + networkOptions: { + nativeCoin: '0x1::aptos_coin::AptosCoin', + modules: { + Scripts: '0xe73ee18380b91e37906a728540d2c8ac7848231a26b99ee5631351b3543d7cf2::AnimeSwapPoolV1', + CoinInfo: '0x1::coin::CoinInfo', + CoinStore: '0x1::coin::CoinStore', + DeployerAddress: '0xe73ee18380b91e37906a728540d2c8ac7848231a26b99ee5631351b3543d7cf2', + ResourceAccountAddress: '0xe73ee18380b91e37906a728540d2c8ac7848231a26b99ee5631351b3543d7cf2', + }, + }, + }) + + test('getRouteSwapExactCoinForCoin (no route)', async () => { + const trades = await sdk.route.getRouteSwapExactCoinForCoin({ + fromCoin: CoinsMapping.APTOS, + toCoin: CoinsMapping.BTC, + amount: 1e20.toString(), + }) + console.log(trades) + expect(trades.length).toBeGreaterThanOrEqual(1) + expect(trades[0].priceImpact.toNumber()).toBeGreaterThan(0.99) + }) + + test('getRouteSwapExactCoinForCoin', async () => { + const trades = await sdk.route.getRouteSwapExactCoinForCoin({ + fromCoin: CoinsMapping.APTOS, + toCoin: CoinsMapping.BTC, + amount: '100000', + }) + console.log(trades) + expect(1).toBe(1) + }) + + test('swapExactCoinToCoinPayload', async () => { + const trades = await sdk.route.getRouteSwapExactCoinForCoin({ + fromCoin: CoinsMapping.APTOS, + toCoin: CoinsMapping.BTC, + amount: '100000', + }) + expect(trades.length).toBeGreaterThanOrEqual(1) + const output = sdk.route.swapExactCoinForCoinPayload( + trades[0], + SenderAddress, + 0.05, + 20, + ) + console.log(output) + expect(1).toBe(1) + }) + + test('getRouteSwapCoinForExactCoin', async () => { + const trades = await sdk.route.getRouteSwapCoinForExactCoin({ + fromCoin: CoinsMapping.APTOS, + toCoin: CoinsMapping.BTC, + amount: '100000', + }) + console.log(trades) + expect(1).toBe(1) + }) + + test('swapCoinForExactCoinPayload', async () => { + const trades = await sdk.route.getRouteSwapCoinForExactCoin({ + fromCoin: CoinsMapping.APTOS, + toCoin: CoinsMapping.BTC, + amount: '100000', + }) + expect(trades.length).toBeGreaterThanOrEqual(1) + const output = sdk.route.swapCoinForExactCoinPayload( + trades[0], + SenderAddress, + 0.05, + 20, + ) + console.log(output) + expect(1).toBe(1) + }) +}) diff --git a/src/utils/contract.ts b/src/utils/contract.ts index ab006d3..457617b 100644 --- a/src/utils/contract.ts +++ b/src/utils/contract.ts @@ -92,10 +92,18 @@ export function composeSwapPoolData(address: string) { return composeType(address, 'AnimeSwapPoolV1', 'AdminData') } +export function composePairInfo(address: string) { + return composeType(address, 'AnimeSwapPoolV1', 'PairInfo') +} + export function composeCoinStore(coinStore: string, lpCoinType: string) { return `${coinStore}<${lpCoinType}>` } +export function composeLiquidityPool(address: string) { + return composeType(address, 'AnimeSwapPoolV1', 'LiquidityPool') +} + export function extractAddressFromType(type: string) { return type.split('::')[0] } diff --git a/src/utils/hex.ts b/src/utils/hex.ts index 840d245..ccff1b6 100644 --- a/src/utils/hex.ts +++ b/src/utils/hex.ts @@ -61,3 +61,12 @@ export function toBuffer(v: any): Buffer { export function bufferToHex(buffer: Buffer): string { return addHexPrefix(toBuffer(buffer).toString('hex')) } + +export function hexToString(str: string) { + // remove additional 0x prefix + if (str.startsWith('0x')) { + str = str.substring(2) + } + const buf = Buffer.from(str, 'hex') + return buf.toString('utf8') +} diff --git a/src/utils/is.ts b/src/utils/is.ts index d50fb51..54bc1e4 100644 --- a/src/utils/is.ts +++ b/src/utils/is.ts @@ -7,3 +7,8 @@ export function isAxiosError(e: any): e is AxiosError { } return e } + +export function notEmpty(value: TValue | null | undefined): value is TValue { + if (value === null || value === undefined) return false + return true +}