diff --git a/examples/example.ts b/examples/example.ts index 478ed15..fe80136 100644 --- a/examples/example.ts +++ b/examples/example.ts @@ -28,7 +28,11 @@ import { NetworkId, OrderV2, PoolV1, + StableOrder, + StableswapCalculation, + StableswapConstant, } from "../src"; +import { Stableswap } from "../src/stableswap"; import { Slippage } from "../src/utils/slippage.internal"; const MIN: Asset = { @@ -70,7 +74,6 @@ async function main(): Promise { .signWithPrivateKey("") .complete(); const txId = await signedTx.submit(); - // eslint-disable-next-line no-console console.info(`Transaction submitted successfully: ${txId}`); } @@ -845,6 +848,298 @@ async function _cancelV2TxExample( }); } +async function _swapStableExample( + lucid: Lucid, + blockfrostAdapter: BlockfrostAdapter, + address: Address, + availableUtxos: UTxO[] +): Promise { + const lpAsset = Asset.fromString( + "d16339238c9e1fb4d034b6a48facb2f97794a9cdb7bc049dd7c49f54646a65642d697573642d76312e342d6c70" + ); + const config = StableswapConstant.getConfigByLpAsset( + lpAsset, + NetworkId.TESTNET + ); + + const pool = await blockfrostAdapter.getStablePoolByLpAsset(lpAsset); + + invariant(pool, `Can not find pool by lp asset ${Asset.toString(lpAsset)}`); + + const swapAmount = 1_000n; + + // This pool has 2 assets in its config. They are [tDJED, tiUSD]. + // Index-0 Asset is tDJED. Index-1 Asset is tiUSD. + // This order swaps 1_000n tDJED to ... tiUSD. + const amountOut = StableswapCalculation.calculateSwapAmount({ + inIndex: 0, + outIndex: 1, + amountIn: swapAmount, + amp: pool.amp, + multiples: config.multiples, + datumBalances: pool.datum.balances, + fee: config.fee, + adminFee: config.adminFee, + feeDenominator: config.feeDenominator, + }); + + return new Stableswap(lucid).createBulkOrdersTx({ + sender: address, + availableUtxos: availableUtxos, + options: [ + { + lpAsset: lpAsset, + type: StableOrder.StepType.SWAP, + assetInAmount: swapAmount, + assetInIndex: 0n, + assetOutIndex: 1n, + minimumAssetOut: amountOut, + }, + ], + }); +} + +async function _depositStableExample( + lucid: Lucid, + blockfrostAdapter: BlockfrostAdapter, + address: Address, + availableUtxos: UTxO[] +): Promise { + const lpAsset = Asset.fromString( + "d16339238c9e1fb4d034b6a48facb2f97794a9cdb7bc049dd7c49f54646a65642d697573642d76312e342d6c70" + ); + const config = StableswapConstant.getConfigByLpAsset( + lpAsset, + NetworkId.TESTNET + ); + + const pool = await blockfrostAdapter.getStablePoolByLpAsset(lpAsset); + + invariant(pool, `Can not find pool by lp asset ${Asset.toString(lpAsset)}`); + + // This pool has 2 assets in its config. They are [tDJED, tiUSD]. + // This order deposits 100_000n tDJED and 1_000n tiUSD into the pool. + const amountIns = [100_000n, 1_000n]; + + const lpAmount = StableswapCalculation.calculateDeposit({ + amountIns: amountIns, + totalLiquidity: pool.totalLiquidity, + amp: pool.amp, + multiples: config.multiples, + datumBalances: pool.datum.balances, + fee: config.fee, + adminFee: config.adminFee, + feeDenominator: config.feeDenominator, + }); + + return new Stableswap(lucid).createBulkOrdersTx({ + sender: address, + availableUtxos: availableUtxos, + options: [ + { + lpAsset: lpAsset, + type: StableOrder.StepType.DEPOSIT, + assetsAmount: [ + [Asset.fromString(pool.assets[0]), 100_000n], + [Asset.fromString(pool.assets[1]), 1_000n], + ], + minimumLPReceived: lpAmount, + totalLiquidity: pool.totalLiquidity, + }, + ], + }); +} + +async function _withdrawStableExample( + lucid: Lucid, + blockfrostAdapter: BlockfrostAdapter, + address: Address, + availableUtxos: UTxO[] +): Promise { + const lpAsset = Asset.fromString( + "d16339238c9e1fb4d034b6a48facb2f97794a9cdb7bc049dd7c49f54646a65642d697573642d76312e342d6c70" + ); + const config = StableswapConstant.getConfigByLpAsset( + lpAsset, + NetworkId.TESTNET + ); + + const pool = await blockfrostAdapter.getStablePoolByLpAsset(lpAsset); + + invariant(pool, `Can not find pool by lp asset ${Asset.toString(lpAsset)}`); + + const lpAmount = 10_000n; + + const amountOuts = StableswapCalculation.calculateWithdraw({ + withdrawalLPAmount: lpAmount, + multiples: config.multiples, + datumBalances: pool.datum.balances, + totalLiquidity: pool.totalLiquidity, + }); + + return new Stableswap(lucid).createBulkOrdersTx({ + sender: address, + availableUtxos: availableUtxos, + options: [ + { + lpAsset: lpAsset, + type: StableOrder.StepType.WITHDRAW, + lpAmount: lpAmount, + minimumAmounts: amountOuts, + }, + ], + }); +} + +async function _withdrawImbalanceStableExample( + lucid: Lucid, + blockfrostAdapter: BlockfrostAdapter, + address: Address, + availableUtxos: UTxO[] +): Promise { + const lpAsset = Asset.fromString( + "d16339238c9e1fb4d034b6a48facb2f97794a9cdb7bc049dd7c49f54646a65642d697573642d76312e342d6c70" + ); + const config = StableswapConstant.getConfigByLpAsset( + lpAsset, + NetworkId.TESTNET + ); + + const pool = await blockfrostAdapter.getStablePoolByLpAsset(lpAsset); + + invariant(pool, `Can not find pool by lp asset ${Asset.toString(lpAsset)}`); + + const withdrawAmounts = [1234n, 5678n]; + + // This pool has 2 assets in its config. They are [tDJED, tiUSD]. + // This order withdraws exactly 1234n tDJED and 5678n tiUSD from the pool. + const lpAmount = StableswapCalculation.calculateWithdrawImbalance({ + withdrawAmounts: withdrawAmounts, + totalLiquidity: pool.totalLiquidity, + amp: pool.amp, + multiples: config.multiples, + datumBalances: pool.datum.balances, + fee: config.fee, + adminFee: config.adminFee, + feeDenominator: config.feeDenominator, + }); + + return new Stableswap(lucid).createBulkOrdersTx({ + sender: address, + availableUtxos: availableUtxos, + options: [ + { + lpAsset: lpAsset, + type: StableOrder.StepType.WITHDRAW_IMBALANCE, + lpAmount: lpAmount, + withdrawAmounts: withdrawAmounts, + }, + ], + }); +} + +async function _zapOutStableExample( + lucid: Lucid, + blockfrostAdapter: BlockfrostAdapter, + address: Address, + availableUtxos: UTxO[] +): Promise { + const lpAsset = Asset.fromString( + "d16339238c9e1fb4d034b6a48facb2f97794a9cdb7bc049dd7c49f54646a65642d697573642d76312e342d6c70" + ); + const config = StableswapConstant.getConfigByLpAsset( + lpAsset, + NetworkId.TESTNET + ); + + const pool = await blockfrostAdapter.getStablePoolByLpAsset(lpAsset); + + invariant(pool, `Can not find pool by lp asset ${Asset.toString(lpAsset)}`); + + // This pool has 2 assets in its config. They are [tDJED, tiUSD]. + // This order withdraws xxx tiUSD by 12345 Lp Assets from the pool. + const lpAmount = 12345n; + const outIndex = 0; + const amountOut = StableswapCalculation.calculateZapOut({ + amountLpIn: lpAmount, + outIndex: outIndex, + totalLiquidity: pool.totalLiquidity, + amp: pool.amp, + multiples: config.multiples, + datumBalances: pool.datum.balances, + fee: config.fee, + adminFee: config.adminFee, + feeDenominator: config.feeDenominator, + }); + + return new Stableswap(lucid).createBulkOrdersTx({ + sender: address, + availableUtxos: availableUtxos, + options: [ + { + lpAsset: lpAsset, + type: StableOrder.StepType.ZAP_OUT, + lpAmount: lpAmount, + assetOutIndex: BigInt(outIndex), + minimumAssetOut: amountOut, + }, + ], + }); +} + +async function _bulkOrderStableExample( + lucid: Lucid, + address: Address, + availableUtxos: UTxO[] +): Promise { + const lpAsset = Asset.fromString( + "d16339238c9e1fb4d034b6a48facb2f97794a9cdb7bc049dd7c49f54646a65642d697573642d76312e342d6c70" + ); + const lpAmount = 12345n; + const outIndex = 0; + + return new Stableswap(lucid).createBulkOrdersTx({ + sender: address, + availableUtxos: availableUtxos, + options: [ + { + lpAsset: lpAsset, + type: StableOrder.StepType.ZAP_OUT, + lpAmount: lpAmount, + assetOutIndex: BigInt(outIndex), + minimumAssetOut: 1n, + }, + { + lpAsset: lpAsset, + type: StableOrder.StepType.SWAP, + assetInAmount: 1000n, + assetInIndex: 0n, + assetOutIndex: 1n, + minimumAssetOut: 1n, + }, + ], + }); +} + +async function _cancelStableExample(lucid: Lucid): Promise { + const orderUtxos = await lucid.utxosByOutRef([ + { + txHash: + "c3ad8e0aa159a22a14088474908e5c23ba6772a6aa82f8250e7e8eaa1016b2d8", + outputIndex: 0, + }, + { + txHash: + "72e57a1fd90bf0b9291a6fa8e04793099d51df7844813689dde67ce3eea03c1f", + outputIndex: 0, + }, + ]); + invariant(orderUtxos.length === 2, "Can not find order to cancel"); + return new Stableswap(lucid).buildCancelOrdersTx({ + orderUtxos: orderUtxos, + }); +} + /** * Initialize Lucid Instance for Browser Environment * @param network Network you're working on diff --git a/package-lock.json b/package-lock.json index 85a38ba..39abbef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "big.js": "^6.2.1", "bignumber.js": "^9.1.2", "lucid-cardano": "0.10.10", + "remeda": "^2.12.1", "sha3": "^2.1.4" }, "devDependencies": { @@ -2421,12 +2422,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3626,9 +3627,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -5120,12 +5121,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -5894,6 +5895,25 @@ "regexp-tree": "bin/regexp-tree" } }, + "node_modules/remeda": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.12.1.tgz", + "integrity": "sha512-hKFAbxbQe8PMd4+CYO1DYCrCbcZsUSa7e21g7+4co91GBy7BD+Ub6JdaLy76yPOp7PCPTAXRz/9NXtZ9w15jbg==", + "dependencies": { + "type-fest": "^4.26.1" + } + }, + "node_modules/remeda/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index ed4248a..7ac25ec 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "big.js": "^6.2.1", "bignumber.js": "^9.1.2", "lucid-cardano": "0.10.10", + "remeda": "^2.12.1", "sha3": "^2.1.4" }, "devDependencies": { diff --git a/src/adapter.ts b/src/adapter.ts index c5780cb..3b97280 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -362,6 +362,27 @@ export class BlockfrostAdapter { return [priceAB, priceBA]; } + private async parseStablePoolState( + utxo: Awaited>[0] + ): Promise { + let datum: string; + if (utxo.inline_datum) { + datum = utxo.inline_datum; + } else if (utxo.data_hash) { + datum = await this.getDatumByDatumHash(utxo.data_hash); + } else { + throw new Error("Cannot find datum of Stable Pool"); + } + const pool = new StablePool.State( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.output_index }, + utxo.amount, + datum + ); + return pool; + } + public async getAllStablePools(): Promise<{ pools: StablePool.State[]; errors: unknown[]; @@ -375,21 +396,7 @@ export class BlockfrostAdapter { const utxos = await this.api.addressesUtxosAll(poolAddr); try { for (const utxo of utxos) { - let datum: string; - if (utxo.inline_datum) { - datum = utxo.inline_datum; - } else if (utxo.data_hash) { - datum = await this.getDatumByDatumHash(utxo.data_hash); - } else { - throw new Error("Cannot find datum of Stable Pool"); - } - const pool = new StablePool.State( - this.networkId, - utxo.address, - { txHash: utxo.tx_hash, index: utxo.output_index }, - utxo.amount, - datum - ); + const pool = await this.parseStablePoolState(utxo); pools.push(pool); } } catch (err) { @@ -403,6 +410,29 @@ export class BlockfrostAdapter { }; } + public async getStablePoolByLpAsset( + lpAsset: Asset + ): Promise { + const config = StableswapConstant.CONFIG[this.networkId].find( + (cfg) => cfg.lpAsset === Asset.toString(lpAsset) + ); + invariant( + config, + `getStablePoolByLpAsset: Can not find stableswap config by LP Asset ${Asset.toString( + lpAsset + )}` + ); + const poolUtxos = await this.api.addressesUtxosAssetAll( + config.poolAddress, + config.nftAsset + ); + if (poolUtxos.length === 1) { + const poolUtxo = poolUtxos[0]; + return await this.parseStablePoolState(poolUtxo); + } + return null; + } + public async getStablePoolByNFT( nft: Asset ): Promise { @@ -414,29 +444,14 @@ export class BlockfrostAdapter { `Cannot find Stable Pool having NFT ${Asset.toString(nft)}` ); } - const utxos = await this.api.addressesUtxosAssetAll( + const poolUtxos = await this.api.addressesUtxosAssetAll( poolAddress, Asset.toString(nft) ); - for (const utxo of utxos) { - let datum: string; - if (utxo.inline_datum) { - datum = utxo.inline_datum; - } else if (utxo.data_hash) { - datum = await this.getDatumByDatumHash(utxo.data_hash); - } else { - throw new Error("Cannot find datum of Stable Pool"); - } - const pool = new StablePool.State( - this.networkId, - utxo.address, - { txHash: utxo.tx_hash, index: utxo.output_index }, - utxo.amount, - datum - ); - return pool; + if (poolUtxos.length === 1) { + const poolUtxo = poolUtxos[0]; + return await this.parseStablePoolState(poolUtxo); } - return null; } diff --git a/src/calculate.ts b/src/calculate.ts index 7d6330e..d7aa73d 100644 --- a/src/calculate.ts +++ b/src/calculate.ts @@ -1,4 +1,6 @@ +import invariant from "@minswap/tiny-invariant"; import Big from "big.js"; +import { zipWith } from "remeda"; import { OrderV2 } from "./types/order"; import { PoolV2 } from "./types/pool"; @@ -473,3 +475,540 @@ export namespace DexV2Calculation { } } } + +export namespace StableswapCalculation { + export function getD(mulBalances: bigint[], amp: bigint): bigint { + const sumMulBalances = mulBalances.reduce( + (sum, balance) => sum + balance, + 0n + ); + if (sumMulBalances === 0n) { + return 0n; + } + + const length = BigInt(mulBalances.length); + let dPrev = 0n; + let d = sumMulBalances; + const ann = amp * length; + + for (let i = 0; i < 255; i++) { + let dp = d; + for (const mulBalance of mulBalances) { + dp = (dp * d) / (mulBalance * length); + } + dPrev = d; + d = + ((ann * sumMulBalances + dp * length) * d) / + ((ann - 1n) * d + (length + 1n) * dp); + if (d > dPrev) { + if (d - dPrev <= 1n) { + break; + } + } else { + if (dPrev - d <= 1n) { + break; + } + } + } + return d; + } + + export function getY( + i: number, + j: number, + x: bigint, + xp: bigint[], + amp: bigint + ): bigint { + if (i === j || i < 0 || j < 0 || i >= xp.length || j >= xp.length) { + throw Error( + `getY failed: i and j must be different and less than length of xp` + ); + } + const length = BigInt(xp.length); + const d = getD(xp, amp); + let c = d; + let s = 0n; + const ann = amp * length; + + let _x = 0n; + for (let index = 0; index < Number(length); index++) { + if (index === i) { + _x = x; + } else if (index !== j) { + _x = xp[index]; + } else { + continue; + } + s += _x; + c = (c * d) / (_x * length); + } + + c = (c * d) / (ann * length); + const b = s + d / ann; + let yPrev = 0n; + let y = d; + for (let index = 0; index < 255; index++) { + yPrev = y; + y = (y * y + c) / (2n * y + b - d); + if (y > yPrev) { + if (y - yPrev <= 1n) { + break; + } + } else { + if (yPrev - y <= 1n) { + break; + } + } + } + return y; + } + + export function getYD( + i: number, + xp: bigint[], + amp: bigint, + d: bigint + ): bigint { + const length = BigInt(xp.length); + invariant( + 0 <= i && i < xp.length, + `getYD failed: i must be less than length of xp` + ); + let c = d; + let s = 0n; + const ann = amp * length; + + let _x = 0n; + for (let index = 0; index < Number(length); index++) { + if (index !== i) { + _x = xp[index]; + } else { + continue; + } + s += _x; + c = (c * d) / (_x * length); + } + c = (c * d) / (ann * length); + const b = s + d / ann; + let yPrev = 0n; + let y = d; + for (let index = 0; index < 255; index++) { + yPrev = y; + y = (y * y + c) / (2n * y + b - d); + if (y > yPrev) { + if (y - yPrev <= 1n) { + break; + } + } else { + if (yPrev - y <= 1n) { + break; + } + } + } + return y; + } + + export function getDMem( + balances: bigint[], + multiples: bigint[], + amp: bigint + ): bigint { + const mulBalances = zipWith(balances, multiples, (a, b) => a * b); + return getD(mulBalances, amp); + } + + type CommonStableswapCalculationOptions = { + amp: bigint; + multiples: bigint[]; + datumBalances: bigint[]; + fee: bigint; + adminFee: bigint; + feeDenominator: bigint; + }; + + /** + * @property {number} inIndex - index of asset in config assets that you want to swap + * @property {bigint} amountIn - amount of asset that you want to swap + * @property {number} outIndex - index of asset in config assets that you want to receive + */ + export type StableswapCalculateSwapOptions = + CommonStableswapCalculationOptions & { + inIndex: number; + outIndex: number; + amountIn: bigint; + }; + + /** + * @property {bigint[]} amountIns - amount of assets that you want to deposit ordering by assets in config + * @property {bigint} totalLiquidity - amount of asset that you want to swap + */ + export type StableswapCalculateDepositOptions = + CommonStableswapCalculationOptions & { + amountIns: bigint[]; + totalLiquidity: bigint; + }; + + export type StableswapCalculateWithdrawOptions = Omit< + CommonStableswapCalculationOptions, + "amp" | "fee" | "adminFee" | "feeDenominator" + > & { + withdrawalLPAmount: bigint; + totalLiquidity: bigint; + }; + + /** + * @property {bigint[]} withdrawAmounts - exactly amount of assets that you want to withdraw ordering by assets in config + */ + export type StableswapCalculateWithdrawImbalanceOptions = + CommonStableswapCalculationOptions & { + withdrawAmounts: bigint[]; + totalLiquidity: bigint; + }; + + /** + * @property {bigint} amountLpIn - exactly LP amount that you want to withdraw + * @property {number} outIndex - index of asset that you want to zap out in config assets + */ + export type StableswapCalculateZapOutOptions = + CommonStableswapCalculationOptions & { + amountLpIn: bigint; + outIndex: number; + totalLiquidity: bigint; + }; + + /** + * @returns amount of asset that you want to receive. + */ + export function calculateSwapAmount({ + inIndex, + outIndex, + amountIn, + amp, + multiples, + datumBalances, + fee, + adminFee, + feeDenominator, + }: StableswapCalculateSwapOptions): bigint { + const tempDatumBalances = [...datumBalances]; + + const length = multiples.length; + invariant( + amountIn > 0, + `calculateExchange error: amountIn ${amountIn} must be positive.` + ); + invariant( + 0 <= inIndex && inIndex < length, + `calculateExchange error: inIndex ${inIndex} is not valid, must be within 0-${ + length - 1 + }` + ); + invariant( + 0 <= outIndex && outIndex < length, + `calculateExchange error: outIndex ${outIndex} is not valid, must be within 0-${ + length - 1 + }` + ); + invariant(inIndex !== outIndex, `inIndex must be different from outIndex`); + const mulBalances = zipWith(tempDatumBalances, multiples, (a, b) => a * b); + const mulIn = multiples[inIndex]; + const mulOut = multiples[outIndex]; + const x = mulBalances[inIndex] + amountIn * mulIn; + const y = getY(inIndex, outIndex, x, mulBalances, amp); + + const dy = mulBalances[outIndex] - y; + const dyFee = (dy * fee) / feeDenominator; + const dyAdminFee = (dyFee * adminFee) / feeDenominator; + const amountOut = (dy - dyFee) / mulOut; + const newDatumBalanceOut = (y + (dyFee - dyAdminFee)) / mulOut; + + invariant( + amountOut > 0, + `calculateExchange error: amountIn is too small, amountOut (${amountOut}) must be positive.` + ); + invariant( + newDatumBalanceOut > 0, + `calculateExchange error: newDatumBalanceOut (${newDatumBalanceOut}) must be positive.` + ); + return amountOut; + } + + /** + * @returns amount of liquidity asset you receive. + */ + export function calculateDeposit({ + amountIns, + amp, + multiples, + datumBalances, + totalLiquidity, + fee, + adminFee, + feeDenominator, + }: StableswapCalculateDepositOptions): bigint { + const tempDatumBalances = [...datumBalances]; + + const length = multiples.length; + invariant( + amountIns.length === length, + `calculateDeposit error: amountIns's length ${amountIns.length} is invalid, amountIns's length must be ${length}` + ); + + let newDatumBalances: bigint[] = []; + let lpAmount = 0n; + if (totalLiquidity === 0n) { + for (let i = 0; i < length; ++i) { + invariant( + amountIns[i] > 0n, + `calculateDeposit error: amount index ${i} must be positive in case totalLiquidity = 0` + ); + } + newDatumBalances = zipWith(tempDatumBalances, amountIns, (a, b) => a + b); + const d1 = getDMem(newDatumBalances, multiples, amp); + invariant( + d1 > 0, + `calculateDeposit: d1 must be greater than 0 in case totalLiquidity = 0` + ); + lpAmount = d1; + } else { + let sumIns = 0n; + for (let i = 0; i < length; ++i) { + if (amountIns[i] < 0n) { + invariant( + amountIns[i] > 0n, + `calculateDeposit error: amountIns index ${i} must be non-negative` + ); + } + sumIns += amountIns[i]; + } + invariant( + sumIns > 0, + `calculateDeposit error: sum of amountIns must be positive` + ); + + const newDatumBalanceWithoutFee = zipWith( + tempDatumBalances, + amountIns, + (a, b) => a + b + ); + + const d0 = getDMem(tempDatumBalances, multiples, amp); + const d1 = getDMem(newDatumBalanceWithoutFee, multiples, amp); + + invariant( + d1 > d0, + `calculateDeposit: d1 must be greater than d0 in case totalLiquidity > 0, d1: ${d1}, d0: ${d0}` + ); + + const specialFee = (fee * BigInt(length)) / (4n * (BigInt(length) - 1n)); + + const newDatBalancesWithTradingFee: bigint[] = []; + for (let i = 0; i < tempDatumBalances.length; i++) { + const oldBalance = tempDatumBalances[i]; + const newBalance = newDatumBalanceWithoutFee[i]; + + const idealBalance = (d1 * oldBalance) / d0; + let different = 0n; + // In this case, liquidity pool has to swap the amount of other assets to get @different assets[i] + if (newBalance > idealBalance) { + different = newBalance - idealBalance; + } else { + different = idealBalance - newBalance; + } + const tradingFeeAmount = (specialFee * different) / feeDenominator; + const adminFeeAmount = (tradingFeeAmount * adminFee) / feeDenominator; + newDatumBalances.push(newBalance - adminFeeAmount); + newDatBalancesWithTradingFee.push(newBalance - tradingFeeAmount); + } + for (let i = 0; i < length; ++i) { + invariant( + newDatBalancesWithTradingFee[i] > 0, + `calculateDeposit error: deposit amount is too small, newDatBalancesWithTradingFee must be positive` + ); + } + const d2 = getDMem(newDatBalancesWithTradingFee, multiples, amp); + lpAmount = (totalLiquidity * (d2 - d0)) / d0; + } + + invariant( + lpAmount > 0, + `calculateDeposit error: deposit amount is too small, lpAmountOut ${lpAmount} must be positive` + ); + return lpAmount; + } + + /** + * @returns amounts of asset you can receive ordering by config assets + */ + export function calculateWithdraw({ + withdrawalLPAmount, + multiples, + datumBalances, + totalLiquidity, + }: StableswapCalculateWithdrawOptions): bigint[] { + const tempDatumBalances = [...datumBalances]; + + const length = multiples.length; + invariant( + withdrawalLPAmount > 0, + `calculateWithdraw error: withdrawalLPAmount must be positive` + ); + const amountOuts = tempDatumBalances.map( + (balance) => (balance * withdrawalLPAmount) / totalLiquidity + ); + let sumOuts = 0n; + for (let i = 0; i < length; ++i) { + invariant( + amountOuts[i] >= 0n, + `calculateWithdraw error: amountOuts must be non-negative` + ); + sumOuts += amountOuts[i]; + } + invariant( + sumOuts > 0n, + `calculateWithdraw error: sum of amountOuts must be positive` + ); + + return amountOuts; + } + + /** + * @returns lp asset amount you need to provide to receive exactly amount of assets in the pool + */ + export function calculateWithdrawImbalance({ + withdrawAmounts, + amp, + multiples, + datumBalances, + totalLiquidity, + fee, + feeDenominator, + }: StableswapCalculateWithdrawImbalanceOptions): bigint { + const tempDatumBalances = [...datumBalances]; + + const length = multiples.length; + + invariant( + withdrawAmounts.length === length, + `calculateWithdrawImbalance error: withdrawAmounts's length ${withdrawAmounts.length} is invalid, withdrawAmounts's length must be ${length}` + ); + + let sumOuts = 0n; + for (let i = 0; i < length; ++i) { + invariant( + withdrawAmounts[i] >= 0n, + `calculateDeposit error: amountIns must be non-negative` + ); + + sumOuts += withdrawAmounts[i]; + } + invariant( + sumOuts > 0n, + `calculateWithdrawImbalance error: sum of withdrawAmounts must be positive` + ); + + const specialFee = (fee * BigInt(length)) / (4n * (BigInt(length) - 1n)); + + const newDatBalancesWithoutFee = zipWith( + tempDatumBalances, + withdrawAmounts, + (a, b) => a - b + ); + for (let i = 0; i < length; ++i) { + invariant( + newDatBalancesWithoutFee[i] > 0n, + `calculateWithdrawImbalance error: not enough asset index ${i}` + ); + } + const d0 = getDMem(tempDatumBalances, multiples, amp); + const d1 = getDMem(newDatBalancesWithoutFee, multiples, amp); + + const newDatBalancesWithTradingFee = []; + for (let i = 0; i < length; ++i) { + const idealBalance = (d1 * tempDatumBalances[i]) / d0; + let different = 0n; + if (newDatBalancesWithoutFee[i] > idealBalance) { + different = newDatBalancesWithoutFee[i] - idealBalance; + } else { + different = idealBalance - newDatBalancesWithoutFee[i]; + } + const tradingFeeAmount = (specialFee * different) / feeDenominator; + newDatBalancesWithTradingFee.push( + newDatBalancesWithoutFee[i] - tradingFeeAmount + ); + } + for (let i = 0; i < length; ++i) { + invariant( + newDatBalancesWithTradingFee[i] > 0n, + `calculateWithdrawImbalance error: not enough asset index ${i}` + ); + } + + const d2 = getDMem(newDatBalancesWithTradingFee, multiples, amp); + let lpAmount = ((d0 - d2) * totalLiquidity) / d0; + + invariant( + lpAmount > 0n, + `calculateWithdrawImbalance error: required lpAmount ${lpAmount} must be positive` + ); + + lpAmount += 1n; + return lpAmount; + } + + /** + * @returns amount asset amount you want receive + */ + export function calculateZapOut({ + amountLpIn, + outIndex, + amp, + multiples, + datumBalances, + totalLiquidity, + fee, + adminFee, + feeDenominator, + }: StableswapCalculateZapOutOptions): bigint { + const tempDatumBalances = [...datumBalances]; + + const length = multiples.length; + invariant( + amountLpIn > 0, + `calculateZapOut error: amountLpIn ${amountLpIn} must be positive.` + ); + + invariant( + 0 <= outIndex && outIndex < length, + `calculateZapOut error: outIndex ${outIndex} is not valid, must be within 0-${ + length - 1 + }` + ); + + const mulBalances = zipWith(tempDatumBalances, multiples, (a, b) => a * b); + const mulOut = multiples[outIndex]; + const d0 = getD(mulBalances, amp); + const d1 = d0 - (amountLpIn * d0) / totalLiquidity; + const mulBalancesReduced = mulBalances; + // newY is new MulBal[outIndex] + const newYWithoutFee = getYD(outIndex, mulBalances, amp, d1); + const specialFee = (fee * BigInt(length)) / (4n * (BigInt(length) - 1n)); + + const amountOutWithoutFee = + (mulBalances[outIndex] - newYWithoutFee) / mulOut; + for (let i = 0; i < length; ++i) { + const diff = + i === outIndex + ? (mulBalances[i] * d1) / d0 - newYWithoutFee + : mulBalances[i] - (mulBalances[i] * d1) / d0; + mulBalancesReduced[i] -= (diff * specialFee) / feeDenominator; + } + const newY = getYD(outIndex, mulBalancesReduced, amp, d1); + const amountOut = (mulBalancesReduced[outIndex] - newY - 1n) / mulOut; + tempDatumBalances[outIndex] -= + amountOut + + ((amountOutWithoutFee - amountOut) * adminFee) / feeDenominator; + return amountOut; + } +} diff --git a/src/dex-v2.ts b/src/dex-v2.ts index 3ff10c1..d43bee0 100644 --- a/src/dex-v2.ts +++ b/src/dex-v2.ts @@ -68,7 +68,6 @@ export type CreatePoolV2Options = { export type BulkOrdersOption = { sender: Address; - customReceiver?: V2CustomReceiver; orderOptions: OrderOptions[]; expiredOptions?: OrderV2.ExpirySetting; availableUtxos: UTxO[]; @@ -182,6 +181,7 @@ export type OrderOptions = ( | MultiRoutingOptions ) & { lpAsset: Asset; + customReceiver?: V2CustomReceiver; }; export type CancelBulkOrdersOptions = { @@ -752,7 +752,6 @@ export class DexV2 { async createBulkOrdersTx({ sender, - customReceiver, orderOptions, expiredOptions, availableUtxos, @@ -786,7 +785,7 @@ export class DexV2 { }[] = []; for (let i = 0; i < orderOptions.length; i++) { const option = orderOptions[i]; - const { type, lpAsset } = option; + const { type, lpAsset, customReceiver } = option; const orderAssets = this.buildOrderValue(option); const orderStep = this.buildOrderStep(option, batcherFee); if (type === OrderV2.StepType.SWAP_EXACT_IN && option.isLimitOrder) { diff --git a/src/stableswap.ts b/src/stableswap.ts new file mode 100644 index 0000000..3198fbb --- /dev/null +++ b/src/stableswap.ts @@ -0,0 +1,426 @@ +import invariant from "@minswap/tiny-invariant"; +import { + Address, + Assets, + Constr, + Data, + Lucid, + TxComplete, + UTxO, +} from "lucid-cardano"; + +import { + FIXED_DEPOSIT_ADA, + MetadataMessage, + StableOrder, + StableswapConstant, + V1AndStableswapCustomReceiver, +} from "."; +import { calculateBatcherFee } from "./batcher-fee-reduction/calculate"; +import { DexVersion } from "./batcher-fee-reduction/types.internal"; +import { Asset } from "./types/asset"; +import { NetworkEnvironment, NetworkId } from "./types/network"; +import { lucidToNetworkEnv } from "./utils/network.internal"; +import { buildUtxoToStoreDatum } from "./utils/tx.internal"; + +/** + * @property {bigint} assetInIndex - Index of asset you want to swap in config assets + * @property {bigint} assetOutIndex - Index of asset you want to receive in config assets + */ +export type SwapOptions = { + type: StableOrder.StepType.SWAP; + assetInAmount: bigint; + assetInIndex: bigint; + assetOutIndex: bigint; + minimumAssetOut: bigint; +}; + +export type DepositOptions = { + type: StableOrder.StepType.DEPOSIT; + assetsAmount: [Asset, bigint][]; + minimumLPReceived: bigint; + totalLiquidity: bigint; +}; + +export type WithdrawOptions = { + type: StableOrder.StepType.WITHDRAW; + lpAmount: bigint; + minimumAmounts: bigint[]; +}; + +export type WithdrawImbalanceOptions = { + type: StableOrder.StepType.WITHDRAW_IMBALANCE; + lpAmount: bigint; + withdrawAmounts: bigint[]; +}; + +/** + * @property {bigint} assetOutIndex - Index of asset you want to receive in config assets + */ +export type ZapOutOptions = { + type: StableOrder.StepType.ZAP_OUT; + lpAmount: bigint; + assetOutIndex: bigint; + minimumAssetOut: bigint; +}; + +export type OrderOptions = ( + | DepositOptions + | WithdrawOptions + | SwapOptions + | WithdrawImbalanceOptions + | ZapOutOptions +) & { + lpAsset: Asset; + customReceiver?: V1AndStableswapCustomReceiver; +}; + +export type BulkOrdersOption = { + options: OrderOptions[]; + sender: Address; + availableUtxos: UTxO[]; +}; + +export type BuildCancelOrderOptions = { + orderUtxos: UTxO[]; +}; + +export class Stableswap { + private readonly lucid: Lucid; + private readonly networkId: NetworkId; + private readonly networkEnv: NetworkEnvironment; + private readonly dexVersion = DexVersion.STABLESWAP; + + constructor(lucid: Lucid) { + this.lucid = lucid; + this.networkId = + lucid.network === "Mainnet" ? NetworkId.MAINNET : NetworkId.TESTNET; + this.networkEnv = lucidToNetworkEnv(lucid.network); + } + + buildOrderValue(option: OrderOptions): Assets { + const orderAssets: Assets = {}; + + switch (option.type) { + case StableOrder.StepType.DEPOSIT: { + const { minimumLPReceived, assetsAmount, totalLiquidity } = option; + invariant( + minimumLPReceived > 0n, + "minimum LP received must be non-negative" + ); + let sumAmount = 0n; + for (const [asset, amount] of assetsAmount) { + if (totalLiquidity === 0n) { + invariant( + amount > 0n, + "amount must be positive when total liquidity = 0" + ); + } else { + invariant(amount >= 0n, "amount must be non-negative"); + } + if (amount > 0n) { + orderAssets[Asset.toString(asset)] = amount; + } + sumAmount += amount; + } + invariant(sumAmount > 0n, "sum of amount must be positive"); + break; + } + case StableOrder.StepType.SWAP: { + const { assetInAmount, assetInIndex, lpAsset } = option; + const poolConfig = StableswapConstant.getConfigByLpAsset( + lpAsset, + this.networkId + ); + invariant(assetInAmount > 0n, "asset in amount must be positive"); + orderAssets[poolConfig.assets[Number(assetInIndex)]] = assetInAmount; + break; + } + case StableOrder.StepType.WITHDRAW: + case StableOrder.StepType.WITHDRAW_IMBALANCE: + case StableOrder.StepType.ZAP_OUT: { + const { lpAmount, lpAsset } = option; + invariant(lpAmount > 0n, "Lp amount must be positive number"); + orderAssets[Asset.toString(lpAsset)] = lpAmount; + break; + } + } + + if ("lovelace" in orderAssets) { + orderAssets["lovelace"] += FIXED_DEPOSIT_ADA; + } else { + orderAssets["lovelace"] = FIXED_DEPOSIT_ADA; + } + return orderAssets; + } + + buildOrderStep(option: OrderOptions): StableOrder.Step { + switch (option.type) { + case StableOrder.StepType.DEPOSIT: { + const { minimumLPReceived } = option; + invariant( + minimumLPReceived > 0n, + "minimum LP received must be non-negative" + ); + return { + type: StableOrder.StepType.DEPOSIT, + minimumLP: minimumLPReceived, + }; + } + case StableOrder.StepType.WITHDRAW: { + const { minimumAmounts } = option; + let sumAmount = 0n; + for (const amount of minimumAmounts) { + invariant(amount >= 0n, "minimum amount must be non-negative"); + sumAmount += amount; + } + invariant(sumAmount > 0n, "sum of withdaw amount must be positive"); + return { + type: StableOrder.StepType.WITHDRAW, + minimumAmounts: minimumAmounts, + }; + } + case StableOrder.StepType.SWAP: { + const { lpAsset, assetInIndex, assetOutIndex, minimumAssetOut } = + option; + const poolConfig = StableswapConstant.getConfigByLpAsset( + lpAsset, + this.networkId + ); + invariant( + poolConfig, + `Not found Stableswap config matching with LP Asset ${lpAsset.toString()}` + ); + const assetLength = BigInt(poolConfig.assets.length); + invariant( + assetInIndex >= 0n && assetInIndex < assetLength, + `Invalid amountInIndex, must be between 0-${assetLength - 1n}` + ); + invariant( + assetOutIndex >= 0n && assetOutIndex < assetLength, + `Invalid assetOutIndex, must be between 0-${assetLength - 1n}` + ); + invariant( + assetInIndex !== assetOutIndex, + `assetOutIndex and amountInIndex must be different` + ); + invariant( + minimumAssetOut > 0n, + "minimum asset out amount must be positive" + ); + return { + type: StableOrder.StepType.SWAP, + assetInIndex: assetInIndex, + assetOutIndex: assetOutIndex, + minimumAssetOut: minimumAssetOut, + }; + } + case StableOrder.StepType.WITHDRAW_IMBALANCE: { + const { withdrawAmounts } = option; + let sum = 0n; + for (const amount of withdrawAmounts) { + invariant(amount >= 0n, "withdraw amount must be unsigned number"); + sum += amount; + } + invariant(sum > 0n, "sum of withdraw amount must be positive"); + return { + type: StableOrder.StepType.WITHDRAW_IMBALANCE, + withdrawAmounts: withdrawAmounts, + }; + } + case StableOrder.StepType.ZAP_OUT: { + const { assetOutIndex, minimumAssetOut, lpAsset } = option; + const poolConfig = StableswapConstant.getConfigByLpAsset( + lpAsset, + this.networkId + ); + invariant( + poolConfig, + `Not found Stableswap config matching with LP Asset ${lpAsset.toString()}` + ); + const assetLength = BigInt(poolConfig.assets.length); + invariant( + minimumAssetOut > 0n, + "Minimum amount out must be positive number" + ); + invariant( + assetOutIndex >= 0n && assetOutIndex < assetLength, + `Invalid assetOutIndex, must be between 0-${assetLength - 1n}` + ); + return { + type: StableOrder.StepType.ZAP_OUT, + assetOutIndex: assetOutIndex, + minimumAssetOut: minimumAssetOut, + }; + } + } + } + + private getOrderMetadata(options: OrderOptions): string { + switch (options.type) { + case StableOrder.StepType.SWAP: { + return MetadataMessage.SWAP_EXACT_IN_ORDER; + } + case StableOrder.StepType.DEPOSIT: { + let assetInputCnt = 0; + for (const [_, amount] of options.assetsAmount) { + if (amount > 0) { + assetInputCnt++; + } + } + if (assetInputCnt === 1) { + return MetadataMessage.ZAP_IN_ORDER; + } else { + return MetadataMessage.DEPOSIT_ORDER; + } + } + case StableOrder.StepType.WITHDRAW: { + return MetadataMessage.WITHDRAW_ORDER; + } + case StableOrder.StepType.WITHDRAW_IMBALANCE: { + return MetadataMessage.WITHDRAW_ORDER; + } + case StableOrder.StepType.ZAP_OUT: { + return MetadataMessage.ZAP_OUT_ORDER; + } + } + } + + async createBulkOrdersTx(options: BulkOrdersOption): Promise { + const { sender, availableUtxos, options: orderOptions } = options; + + invariant( + orderOptions.length > 0, + "Stableswap.buildCreateTx: Need at least 1 order to build" + ); + // calculate total order value + const totalOrderAssets: Record = {}; + for (const option of orderOptions) { + const orderAssets = this.buildOrderValue(option); + for (const [asset, amt] of Object.entries(orderAssets)) { + if (asset in totalOrderAssets) { + totalOrderAssets[asset] += amt; + } else { + totalOrderAssets[asset] = amt; + } + } + } + + // calculate batcher fee + const { batcherFee, reductionAssets } = calculateBatcherFee({ + utxos: availableUtxos, + orderAssets: totalOrderAssets, + networkEnv: this.networkEnv, + dexVersion: this.dexVersion, + }); + + const tx = this.lucid.newTx(); + for (const orderOption of orderOptions) { + const config = StableswapConstant.getConfigByLpAsset( + orderOption.lpAsset, + this.networkId + ); + const { customReceiver } = orderOption; + const orderAssets = this.buildOrderValue(orderOption); + const step = this.buildOrderStep(orderOption); + if ("lovelace" in orderAssets) { + orderAssets["lovelace"] += batcherFee; + } else { + orderAssets["lovelace"] = batcherFee; + } + const datum: StableOrder.Datum = { + sender: sender, + receiver: customReceiver ? customReceiver.receiver : sender, + receiverDatumHash: customReceiver?.receiverDatum?.hash, + step: step, + batcherFee: batcherFee, + depositADA: FIXED_DEPOSIT_ADA, + }; + tx.payToContract( + config.orderAddress, + { + inline: Data.to(StableOrder.Datum.toPlutusData(datum)), + }, + orderAssets + ); + + if (customReceiver && customReceiver.receiverDatum) { + const utxoForStoringDatum = buildUtxoToStoreDatum( + this.lucid, + sender, + customReceiver.receiver, + customReceiver.receiverDatum.datum + ); + if (utxoForStoringDatum) { + tx.payToAddressWithData( + utxoForStoringDatum.address, + utxoForStoringDatum.outputData, + utxoForStoringDatum.assets + ); + } + } + } + if (Object.keys(reductionAssets).length !== 0) { + tx.payToAddress(sender, reductionAssets); + } + tx.attachMetadata(674, { + msg: [ + orderOptions.length > 1 + ? MetadataMessage.MIXED_ORDERS + : this.getOrderMetadata(orderOptions[0]), + ], + }); + return await tx.complete(); + } + + async buildCancelOrdersTx( + options: BuildCancelOrderOptions + ): Promise { + const tx = this.lucid.newTx(); + + const redeemer = Data.to(new Constr(StableOrder.Redeemer.CANCEL_ORDER, [])); + for (const utxo of options.orderUtxos) { + const config = StableswapConstant.getConfigFromStableswapOrderAddress( + utxo.address, + this.networkId + ); + const referencesScript = StableswapConstant.getStableswapReferencesScript( + Asset.fromString(config.nftAsset), + this.networkId + ); + let datum: StableOrder.Datum; + if (utxo.datum) { + const rawDatum = utxo.datum; + datum = StableOrder.Datum.fromPlutusData( + this.networkId, + Data.from(rawDatum) + ); + } else if (utxo.datumHash) { + const rawDatum = await this.lucid.datumOf(utxo); + datum = StableOrder.Datum.fromPlutusData( + this.networkId, + rawDatum as Constr + ); + } else { + throw new Error( + "Utxo without Datum Hash or Inline Datum can not be spent" + ); + } + + const orderRefs = await this.lucid.utxosByOutRef([ + referencesScript.order, + ]); + invariant( + orderRefs.length === 1, + "cannot find deployed script for V2 Order" + ); + + const orderRef = orderRefs[0]; + tx.readFrom([orderRef]) + .collectFrom([utxo], redeemer) + .addSigner(datum.sender); + } + tx.attachMetadata(674, { msg: [MetadataMessage.CANCEL_ORDER] }); + return await tx.complete(); + } +} diff --git a/src/types/constants.ts b/src/types/constants.ts index 3872994..bbba78e 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -1,5 +1,7 @@ +import invariant from "@minswap/tiny-invariant"; import { Address, OutRef, Script } from "lucid-cardano"; +import { Asset } from ".."; import { NetworkId } from "./network"; export namespace DexV1Constant { @@ -351,6 +353,41 @@ export namespace StableswapConstant { }, }, }; + + export function getConfigByLpAsset( + lpAsset: Asset, + networkId: NetworkId + ): StableswapConstant.Config { + const config = StableswapConstant.CONFIG[networkId].find( + (config) => config.lpAsset === Asset.toString(lpAsset) + ); + invariant(config, `Invalid Stableswap LP Asset ${Asset.toString(lpAsset)}`); + return config; + } + + export function getConfigFromStableswapOrderAddress( + address: Address, + networkId: NetworkId + ): StableswapConstant.Config { + const config = StableswapConstant.CONFIG[networkId].find((config) => { + return address === config.orderAddress; + }); + invariant(config, `Invalid Stableswap Order Address: ${address}`); + return config; + } + + export function getStableswapReferencesScript( + nftAsset: Asset, + networkId: NetworkId + ): StableswapConstant.DeployedScripts { + const refScript = + StableswapConstant.DEPLOYED_SCRIPTS[networkId][Asset.toString(nftAsset)]; + invariant( + refScript, + `Invalid Stableswap Nft Asset ${Asset.toString(nftAsset)}` + ); + return refScript; + } } export namespace DexV2Constant { diff --git a/test/adapter.test.ts b/test/adapter.test.ts index fc0a220..87f2b95 100644 --- a/test/adapter.test.ts +++ b/test/adapter.test.ts @@ -142,6 +142,29 @@ test("getAllStablePools", async () => { expect(mainnetPools.length === numberOfStablePoolsMainnet); }); +test("getStablePoolByLPAsset", async () => { + const testnetCfgs = StableswapConstant.CONFIG[NetworkId.TESTNET]; + const mainnetCfgs = StableswapConstant.CONFIG[NetworkId.MAINNET]; + + for (const cfg of testnetCfgs) { + const pool = await adapterTestnet.getStablePoolByLpAsset( + Asset.fromString(cfg.lpAsset) + ); + expect(pool).not.toBeNull(); + expect(pool?.nft).toEqual(cfg.nftAsset); + expect(pool?.assets).toEqual(cfg.assets); + } + + for (const cfg of mainnetCfgs) { + const pool = await adapterMainnet.getStablePoolByLpAsset( + Asset.fromString(cfg.lpAsset) + ); + expect(pool).not.toBeNull(); + expect(pool?.nft).toEqual(cfg.nftAsset); + expect(pool?.assets).toEqual(cfg.assets); + } +}); + test("getStablePoolByNFT", async () => { const testnetCfgs = StableswapConstant.CONFIG[NetworkId.TESTNET]; const mainnetCfgs = StableswapConstant.CONFIG[NetworkId.MAINNET];