diff --git a/.changeset/large-socks-exist.md b/.changeset/large-socks-exist.md new file mode 100644 index 00000000..79fc865b --- /dev/null +++ b/.changeset/large-socks-exist.md @@ -0,0 +1,9 @@ +--- +"@balancer/sdk": minor +--- + +- Add add/remove liquidity pool support (non-nested pools) +- Weighted pool type +- ComposableStable pool type +- Uses balancerHelpers to query amounts in/out rather than relying on specific pool math and associated data +- Integration tests run against local viem fork diff --git a/.changeset/neat-glasses-brush.md b/.changeset/neat-glasses-brush.md new file mode 100644 index 00000000..bad55acf --- /dev/null +++ b/.changeset/neat-glasses-brush.md @@ -0,0 +1,5 @@ +--- +"@balancer/sdk": minor +--- + +Adds Balancer API Provider. A utility module designed to fetch pool data from [API](https://github.com/beethovenxfi/beethovenx-backend/blob/v3-main/README.md#branching-and-deployment-environments). diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 45400b72..9ece59c2 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -29,7 +29,6 @@ jobs: uses: ./.github/actions/setup - name: Lint & format code run: pnpm format & pnpm lint - test: name: Test needs: install @@ -42,13 +41,14 @@ jobs: - uses: actions/checkout@v3 - name: Setup uses: ./.github/actions/setup + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 - name: Test run: pnpm test:ci env: ETHEREUM_RPC_URL: ${{ secrets.ETHEREUM_RPC_URL }} POLYGON_RPC_URL: ${{ secrets.POLYGON_RPC_URL }} FANTOM_RPC_URL: ${{ secrets.FANTOM_RPC_URL }} - build: name: Build needs: install diff --git a/.gitignore b/.gitignore index 7bbcc288..c1308de5 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,19 @@ jspm_packages/ .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz -.pnp.* \ No newline at end of file +.pnp.* + +# hardhat +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts + +#IDE +.idea diff --git a/README.md b/README.md index 0c0e32ce..e16433c2 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,95 @@ If your platform does not support one of the required features, it is also possi Testing requires access to an archive node for onchain quote comparisons. This can be done using Infura. `pnpm test` + +## Balancer Api Provider + +The Balancer API Provider is a provider that facilitates +data fetching from the Balancer API, +it can be used for: +- Fetch Pool State for AddLiquidity; +- Fetch Pool State for RemoveLiquidity. + +### Usage for adding liquidity to a Pool + +```ts + import { BalancerApi, AddLiquidity } from "@balancer/sdk"; + ... + const addLiquidityInput: AddLiquidityProportionalInput = { + bptOut, + chainId, + rpcUrl, + kind: AddLiquidityKind.Proportional, + }; + + const balancerApi = new BalancerApi('https://backend-v3-canary.beets-ftm-node.com/graphql', 1); + const poolState = await balancerApi.pools.fetchPoolState('0x5f1d6874cb1e7156e79a7563d2b61c6cbce03150000200000000000000000586'); + const addLiquidity = new AddLiquidity(); + const queryOutput = await addLiquidity.query(addLiquidityInput, poolState); + const { call, to, value, maxAmountsIn, minBptOut } = + addLiquidity.buildCall({ + ...queryOutput, + slippage, + sender: signerAddress, + recipient: signerAddress, + }); + const client = createClient({ + ... + }) + + await client.sendTransaction({ + account: signerAddress, + chain: client.chain, + data: call, + to, + value, + }); +``` +Full working add liquidity example: [examples/addLiquidity.ts](./examples/addLiquidity.ts) + +### Usage for removing liquidity from a Pool +```ts +import { BalancerApi, RemoveLiquidity } from "@balancer/sdk"; +... +const removeLiquidityInput: RemoveLiquiditySingleTokenInput = { + chainId, + rpcUrl, + bptIn, + tokenOut, + kind: RemoveLiquidityKind.SingleToken, +}; + +const balancerApi = new BalancerApi('https://backend-v3-canary.beets-ftm-node.com/graphql', 1); +const poolState = await balancerApi.pools.fetchPoolState('0x5f1d6874cb1e7156e79a7563d2b61c6cbce03150000200000000000000000586'); +const removeLiquidity = new RemoveLiquidity(); +const queryOutput = await removeLiquidity.query(removeLiquidityInput, poolState); +const { call, to, value, maxAmountsIn, minBptOut } = + removeLiquidity.buildCall({ + ...queryOutput, + slippage, + sender: signerAddress, + recipient: signerAddress, + }); +const client = createClient({ + ... +}) + +await client.sendTransaction({ + account: signerAddress, + chain: client.chain, + data: call, + to, + value, +}); +``` +Full working remove liquidity example: [examples/removeLiquidity.ts](./examples/removeLiquidity.ts) + +## Anvil client +To download and install the anvil client, run the following commands (MacOS): +- `curl -L https://foundry.paradigm.xyz | bash` +- `brew install libusb` +- `source /Users/$(whoami)/.zshenv` +- `foundryup` + +For other SO's check https://book.getfoundry.sh/getting-started/installation +``` diff --git a/examples/addLiquidity.ts b/examples/addLiquidity.ts new file mode 100644 index 00000000..b44c1285 --- /dev/null +++ b/examples/addLiquidity.ts @@ -0,0 +1,103 @@ +/** + * Example showing how to add liquidity to a pool. + * (Runs against a local Anvil fork) + * + * Run with: + * pnpm example ./examples/addLiquidity.ts + */ +import { config } from 'dotenv'; +config(); + +import { + BalancerApi, + ChainId, + AddLiquidityInput, + AddLiquidityKind, + AddLiquidity, + Slippage, +} from '../src'; +import { parseUnits } from 'viem'; +import { ANVIL_NETWORKS, startFork } from '../test/anvil/anvil-global-setup'; +import { makeForkTx } from './utils/makeForkTx'; + +const addLiquidity = async () => { + // User defined + const chainId = ChainId.MAINNET; + const userAccount = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + const poolId = + '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014'; // 80BAL-20WETH + const slippage = Slippage.fromPercentage('1'); // 1% + + // Start a local anvil fork that will be used to query/tx against + const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET); + + // API is used to fetch relevant pool data + const balancerApi = new BalancerApi( + 'https://backend-v3-canary.beets-ftm-node.com/graphql', + chainId, + ); + const poolStateInput = await balancerApi.pools.fetchPoolState(poolId); + + // We create arbitrary amounts in but these would usually be set by user + const amountsIn = poolStateInput.tokens.map((t) => ({ + rawAmount: parseUnits('1', t.decimals), + decimals: t.decimals, + address: t.address, + })); + + // Construct the AddLiquidityInput, in this case an AddLiquidityUnbalanced + const addLiquidityInput: AddLiquidityInput = { + amountsIn, + chainId, + rpcUrl, + kind: AddLiquidityKind.Unbalanced, + }; + + // Simulate addLiquidity to get the amount of BPT out + const addLiquidity = new AddLiquidity(); + const queryOutput = await addLiquidity.query( + addLiquidityInput, + poolStateInput, + ); + + console.log('Add Liquidity Query Output:'); + console.log('Tokens In:'); + queryOutput.amountsIn.map((a) => + console.log(a.token.address, a.amount.toString()), + ); + console.log(`BPT Out: ${queryOutput.bptOut.amount.toString()}`); + + // Apply slippage to the BPT amount received from the query and construct the call + const call = addLiquidity.buildCall({ + ...queryOutput, + slippage, + sender: userAccount, + recipient: userAccount, + }); + + console.log('\nWith slippage applied:'); + console.log('Max tokens in:'); + call.maxAmountsIn.forEach((a) => + console.log(a.token.address, a.amount.toString()), + ); + console.log(`Min BPT Out: ${call.minBptOut.amount.toString()}`); + + // Make the tx against the local fork and print the result + const slots = [1, 3, 0]; + await makeForkTx( + call, + { + rpcUrl, + chainId, + impersonateAccount: userAccount, + forkTokens: amountsIn.map((a, i) => ({ + address: a.address, + slot: slots[i], + rawBalance: a.rawAmount, + })), + }, + poolStateInput, + ); +}; + +addLiquidity().then(() => {}); diff --git a/examples/removeLiquidity.ts b/examples/removeLiquidity.ts new file mode 100644 index 00000000..d286d909 --- /dev/null +++ b/examples/removeLiquidity.ts @@ -0,0 +1,106 @@ +/** + * Example showing how to remove liquidity from a pool. + * (Runs against a local Anvil fork) + * + * Run with: + * pnpm example ./examples/removeLiquidity.ts + */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { + ChainId, + RemoveLiquidityKind, + RemoveLiquidity, + PoolStateInput, + Slippage, + InputAmount, + RemoveLiquidityInput, + BalancerApi, +} from '../src'; +import { parseEther } from 'viem'; +import { ANVIL_NETWORKS, startFork } from '../test/anvil/anvil-global-setup'; +import { makeForkTx } from './utils/makeForkTx'; + +const removeLiquidity = async () => { + // User defined: + const chainId = ChainId.MAINNET; + const userAccount = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + const poolId = + '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014'; // 80BAL-20WETH + const tokenOut = '0xba100000625a3754423978a60c9317c58a424e3D'; // BAL + const slippage = Slippage.fromPercentage('1'); // 1% + + // Start a local anvil fork that will be used to query/tx against + const { rpcUrl } = await startFork(ANVIL_NETWORKS[ChainId[chainId]]); + + // API is used to fetch relevant pool data + const balancerApi = new BalancerApi( + 'https://backend-v3-canary.beets-ftm-node.com/graphql', + chainId, + ); + const poolStateInput: PoolStateInput = + await balancerApi.pools.fetchPoolState(poolId); + + // Construct the RemoveLiquidityInput, in this case a RemoveLiquiditySingleToken + const bptIn: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + const removeLiquidityInput: RemoveLiquidityInput = { + chainId, + rpcUrl, + bptIn, + tokenOut, + kind: RemoveLiquidityKind.SingleToken, + }; + + // Simulate removing liquidity to get the tokens out + const removeLiquidity = new RemoveLiquidity(); + const queryOutput = await removeLiquidity.query( + removeLiquidityInput, + poolStateInput, + ); + + console.log('Remove Liquidity Query Output:'); + console.log(`BPT In: ${queryOutput.bptIn.amount.toString()}\nTokens Out:`); + queryOutput.amountsOut.map((a) => + console.log(a.token.address, a.amount.toString()), + ); + + // Apply slippage to the tokens out received from the query and construct the call + const call = removeLiquidity.buildCall({ + ...queryOutput, + slippage, + sender: userAccount, + recipient: userAccount, + }); + + console.log('\nWith slippage applied:'); + console.log(`Max BPT In: ${call.maxBptIn.amount}`); + console.log('Min amounts out: '); + call.minAmountsOut.forEach((a) => + console.log(a.token.address, a.amount.toString()), + ); + + // Make the tx against the local fork and print the result + await makeForkTx( + call, + { + rpcUrl, + chainId, + impersonateAccount: userAccount, + forkTokens: [ + { + address: bptIn.address, + slot: 0, + rawBalance: bptIn.rawAmount, + }, + ], + }, + poolStateInput, + ); +}; + +removeLiquidity(); diff --git a/examples/utils/makeForkTx.ts b/examples/utils/makeForkTx.ts new file mode 100644 index 00000000..338a430d --- /dev/null +++ b/examples/utils/makeForkTx.ts @@ -0,0 +1,83 @@ +import { CHAINS, PoolStateInput } from '../../src'; +import { + createTestClient, + http, + publicActions, + walletActions, + Address, + Hex, +} from 'viem'; +import { + forkSetup, + sendTransactionGetBalances, +} from '../../test/lib/utils/helper'; + +type Tx = { + to: Address; + call: Hex; + value: bigint; +}; + +type ForkToken = { + address: Address; + slot: number; + rawBalance: bigint; +}; + +/** + * Sets balances for forkTokens, send tx to Anvil fork and print pool token deltas for account + * @param tx + * @param impersonateAccount + * @param rpcUrl + * @param poolStateInput + * @param forkTokens + */ +export async function makeForkTx( + tx: Tx, + forkConfig: { + rpcUrl: string; + chainId: number; + impersonateAccount: Address; + forkTokens: ForkToken[]; + }, + poolStateInput: PoolStateInput, +) { + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[forkConfig.chainId], + transport: http(forkConfig.rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + await forkSetup( + client, + forkConfig.impersonateAccount, + forkConfig.forkTokens.map((t) => t.address), + forkConfig.forkTokens.map((t) => t.slot), + forkConfig.forkTokens.map((t) => t.rawBalance), + ); + + console.log('\nSending tx...'); + + const tokensForBalanceCheck = [ + ...poolStateInput.tokens.map(({ address }) => address), + poolStateInput.address, + ]; + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + tokensForBalanceCheck, + client, + forkConfig.impersonateAccount, + tx.to, + tx.call, + tx.value, + ); + if (transactionReceipt.status === 'reverted') + throw Error('Transaction reverted'); + + console.log('Token balance deltas:'); + tokensForBalanceCheck.forEach((t, i) => { + console.log(`${t} ${balanceDeltas[i]}`); + }); +} diff --git a/package.json b/package.json index c4cca041..84a97dbf 100644 --- a/package.json +++ b/package.json @@ -22,25 +22,28 @@ "test": "vitest dev", "test:ci": "vitest run", "changeset": "changeset", - "changeset:release": "pnpm build && changeset publish" + "changeset:release": "pnpm build && changeset publish", + "example": "npx tsx" }, "dependencies": { "async-retry": "^1.3.3", "decimal.js-light": "^2.5.1", "pino": "^8.11.0", - "viem": "^1.4.1" + "viem": "^1.9.3" }, "devDependencies": { "@changesets/cli": "^2.26.1", "@types/async-retry": "^1.4.4", "@types/node": "^18.11.18", + "@viem/anvil": "^0.0.6", "dotenv": "^16.0.3", "pino-pretty": "^10.0.0", "rome": "12.1.3", + "ts-node": "^10.9.1", "tsup": "^6.6.0", "typescript": "^5.0.4", "vite": "^4.4.2", - "vitest": "~0.30.1" + "vitest": "^0.34.6" }, "packageManager": "^pnpm@8.6.0", "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57ab0c1b..01d6fe62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ dependencies: specifier: ^8.11.0 version: 8.11.0 viem: - specifier: ^1.4.1 - version: 1.4.1(typescript@5.1.3) + specifier: ^1.9.3 + version: 1.16.6(typescript@5.1.3) devDependencies: '@changesets/cli': @@ -28,6 +28,9 @@ devDependencies: '@types/node': specifier: ^18.11.18 version: 18.15.11 + '@viem/anvil': + specifier: ^0.0.6 + version: 0.0.6 dotenv: specifier: ^16.0.3 version: 16.0.3 @@ -37,9 +40,12 @@ devDependencies: rome: specifier: 12.1.3 version: 12.1.3 + ts-node: + specifier: ^10.9.1 + version: 10.9.1(@types/node@18.15.11)(typescript@5.1.3) tsup: specifier: ^6.6.0 - version: 6.7.0(typescript@5.1.3) + version: 6.7.0(ts-node@10.9.1)(typescript@5.1.3) typescript: specifier: ^5.0.4 version: 5.1.3 @@ -47,13 +53,13 @@ devDependencies: specifier: ^4.4.2 version: 4.4.7(@types/node@18.15.11) vitest: - specifier: ~0.30.1 - version: 0.30.1 + specifier: ^0.34.6 + version: 0.34.6 packages: - /@adraffy/ens-normalize@1.9.0: - resolution: {integrity: sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ==} + /@adraffy/ens-normalize@1.9.4: + resolution: {integrity: sha512-UK0bHA7hh9cR39V+4gl2/NnBBjoXIxkuWAPCaY4X7fbH4L/azIi7ilWOCjMUYfpJgraLUAqkRi2BqrjME8Rynw==} dev: false /@babel/code-frame@7.21.4: @@ -268,6 +274,13 @@ packages: prettier: 2.8.8 dev: true + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + /@esbuild/android-arm64@0.17.15: resolution: {integrity: sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==} engines: {node: '>=12'} @@ -664,10 +677,29 @@ packages: dev: true optional: true + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: @@ -688,14 +720,15 @@ packages: read-yaml-file: 1.1.0 dev: true - /@noble/curves@1.0.0: - resolution: {integrity: sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==} + /@noble/curves@1.2.0: + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} dependencies: - '@noble/hashes': 1.3.0 + '@noble/hashes': 1.3.2 dev: false - /@noble/hashes@1.3.0: - resolution: {integrity: sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==} + /@noble/hashes@1.3.2: + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} dev: false /@nodelib/fs.scandir@2.1.5: @@ -767,25 +800,45 @@ packages: dev: true optional: true - /@scure/base@1.1.1: - resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==} + /@scure/base@1.1.3: + resolution: {integrity: sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==} dev: false - /@scure/bip32@1.3.0: - resolution: {integrity: sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q==} + /@scure/bip32@1.3.2: + resolution: {integrity: sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==} dependencies: - '@noble/curves': 1.0.0 - '@noble/hashes': 1.3.0 - '@scure/base': 1.1.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/base': 1.1.3 dev: false - /@scure/bip39@1.2.0: - resolution: {integrity: sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==} + /@scure/bip39@1.2.1: + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} dependencies: - '@noble/hashes': 1.3.0 - '@scure/base': 1.1.1 + '@noble/hashes': 1.3.2 + '@scure/base': 1.1.3 dev: false + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + /@types/async-retry@1.4.5: resolution: {integrity: sha512-YrdjSD+yQv7h6d5Ip+PMxh3H6ZxKyQk0Ts+PvaNRInxneG9PFVZjFg77ILAN+N6qYf7g4giSJ1l+ZjQ1zeegvA==} dependencies: @@ -832,58 +885,59 @@ packages: resolution: {integrity: sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==} dev: true - /@vitest/expect@0.30.1: - resolution: {integrity: sha512-c3kbEtN8XXJSeN81iDGq29bUzSjQhjES2WR3aColsS4lPGbivwLtas4DNUe0jD9gg/FYGIteqOenfU95EFituw==} + /@viem/anvil@0.0.6: + resolution: {integrity: sha512-OjKR/+FVwzuygXYFqP8MBal1SXG8bT2gbZwqqB0XuLw81LNBBvmE/Repm6+5kkBh4IUj0PhYdrqOsnayS14Gtg==} + dependencies: + execa: 7.2.0 + get-port: 6.1.2 + http-proxy: 1.18.1 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + dev: true + + /@vitest/expect@0.34.6: + resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} dependencies: - '@vitest/spy': 0.30.1 - '@vitest/utils': 0.30.1 - chai: 4.3.7 + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 + chai: 4.3.10 dev: true - /@vitest/runner@0.30.1: - resolution: {integrity: sha512-W62kT/8i0TF1UBCNMRtRMOBWJKRnNyv9RrjIgdUryEe0wNpGZvvwPDLuzYdxvgSckzjp54DSpv1xUbv4BQ0qVA==} + /@vitest/runner@0.34.6: + resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} dependencies: - '@vitest/utils': 0.30.1 - concordance: 5.0.4 + '@vitest/utils': 0.34.6 p-limit: 4.0.0 pathe: 1.1.1 dev: true - /@vitest/snapshot@0.30.1: - resolution: {integrity: sha512-fJZqKrE99zo27uoZA/azgWyWbFvM1rw2APS05yB0JaLwUIg9aUtvvnBf4q7JWhEcAHmSwbrxKFgyBUga6tq9Tw==} + /@vitest/snapshot@0.34.6: + resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} dependencies: magic-string: 0.30.1 pathe: 1.1.1 - pretty-format: 27.5.1 + pretty-format: 29.7.0 dev: true - /@vitest/spy@0.30.1: - resolution: {integrity: sha512-YfJeIf37GvTZe04ZKxzJfnNNuNSmTEGnla2OdL60C8od16f3zOfv9q9K0nNii0NfjDJRt/CVN/POuY5/zTS+BA==} + /@vitest/spy@0.34.6: + resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} dependencies: tinyspy: 2.1.1 dev: true - /@vitest/utils@0.30.1: - resolution: {integrity: sha512-/c8Xv2zUVc+rnNt84QF0Y0zkfxnaGhp87K2dYJMLtLOIckPzuxLVzAtFCicGFdB4NeBHNzTRr1tNn7rCtQcWFA==} + /@vitest/utils@0.34.6: + resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} dependencies: - concordance: 5.0.4 + diff-sequences: 29.6.3 loupe: 2.3.6 - pretty-format: 27.5.1 + pretty-format: 29.7.0 dev: true - /@wagmi/chains@1.6.0(typescript@5.1.3): - resolution: {integrity: sha512-5FRlVxse5P4ZaHG3GTvxwVANSmYJas1eQrTBHhjxVtqXoorm0aLmCHbhmN8Xo1yu09PaWKlleEvfE98yH4AgIw==} - peerDependencies: - typescript: '>=5.0.4' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - typescript: 5.1.3 - dev: false - - /abitype@0.9.3(typescript@5.1.3): - resolution: {integrity: sha512-dz4qCQLurx97FQhnb/EIYTk/ldQ+oafEDUqC0VVIeQS1Q48/YWt/9YNfMmp9SLFqN41ktxny3c8aYxHjmFIB/w==} + /abitype@0.9.8(typescript@5.1.3): + resolution: {integrity: sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ==} peerDependencies: typescript: '>=5.0.4' zod: ^3 >=3.19.1 @@ -954,6 +1008,10 @@ packages: picomatch: 2.3.1 dev: true + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -1025,10 +1083,6 @@ packages: engines: {node: '>=8'} dev: true - /blueimp-md5@2.19.0: - resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} - dev: true - /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1097,14 +1151,14 @@ packages: engines: {node: '>=6'} dev: true - /chai@4.3.7: - resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + /chai@4.3.10: + resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} dependencies: assertion-error: 1.1.0 - check-error: 1.0.2 + check-error: 1.0.3 deep-eql: 4.1.3 - get-func-name: 2.0.0 + get-func-name: 2.0.2 loupe: 2.3.6 pathval: 1.1.1 type-detect: 4.0.8 @@ -1131,8 +1185,10 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true - /check-error@1.0.2: - resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 dev: true /chokidar@3.5.3: @@ -1147,7 +1203,7 @@ packages: normalize-path: 3.0.0 readdirp: 3.6.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /ci-info@3.8.0: @@ -1211,18 +1267,8 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true - /concordance@5.0.4: - resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==} - engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'} - dependencies: - date-time: 3.1.0 - esutils: 2.0.3 - fast-diff: 1.3.0 - js-string-escape: 1.0.1 - lodash: 4.17.21 - md5-hex: 3.0.1 - semver: 7.3.8 - well-known-symbols: 2.0.0 + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true /cross-spawn@5.1.0: @@ -1264,13 +1310,6 @@ packages: stream-transform: 2.1.3 dev: true - /date-time@3.1.0: - resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} - engines: {node: '>=6'} - dependencies: - time-zone: 1.0.0 - dev: true - /dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} dev: true @@ -1330,6 +1369,16 @@ packages: engines: {node: '>=8'} dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1505,15 +1554,14 @@ packages: hasBin: true dev: true - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true - /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: true + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -1533,6 +1581,21 @@ packages: strip-final-newline: 2.0.0 dev: true + /execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + dev: true + /extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} dev: true @@ -1550,10 +1613,6 @@ packages: resolution: {integrity: sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==} dev: true - /fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - dev: true - /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -1610,6 +1669,16 @@ packages: pkg-dir: 4.2.0 dev: true + /follow-redirects@1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: true + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -1638,8 +1707,8 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true @@ -1669,8 +1738,8 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: true - /get-func-name@2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true /get-intrinsic@1.2.1: @@ -1682,6 +1751,11 @@ packages: has-symbols: 1.0.3 dev: true + /get-port@6.1.2: + resolution: {integrity: sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -1817,6 +1891,17 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true + /http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.2 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + dev: true + /human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} dev: true @@ -1826,6 +1911,11 @@ packages: engines: {node: '>=10.17.0'} dev: true + /human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + dev: true + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -1982,6 +2072,11 @@ packages: engines: {node: '>=8'} dev: true + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -2029,12 +2124,12 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /isomorphic-ws@5.0.0(ws@8.12.0): - resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + /isows@1.0.3(ws@8.13.0): + resolution: {integrity: sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==} peerDependencies: ws: '*' dependencies: - ws: 8.12.0 + ws: 8.13.0 dev: false /joycon@3.1.1: @@ -2042,11 +2137,6 @@ packages: engines: {node: '>=10'} dev: true - /js-string-escape@1.0.1: - resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} - engines: {node: '>= 0.8'} - dev: true - /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true @@ -2134,14 +2224,10 @@ packages: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} dev: true - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true - /loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: - get-func-name: 2.0.0 + get-func-name: 2.0.2 dev: true /lru-cache@4.1.5: @@ -2151,13 +2237,6 @@ packages: yallist: 2.1.2 dev: true - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - /magic-string@0.30.1: resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} engines: {node: '>=12'} @@ -2165,6 +2244,10 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -2175,13 +2258,6 @@ packages: engines: {node: '>=8'} dev: true - /md5-hex@3.0.1: - resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} - engines: {node: '>=8'} - dependencies: - blueimp-md5: 2.19.0 - dev: true - /meow@6.1.1: resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} engines: {node: '>=8'} @@ -2221,6 +2297,11 @@ packages: engines: {node: '>=6'} dev: true + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -2305,6 +2386,13 @@ packages: path-key: 3.1.1 dev: true + /npm-run-path@5.1.0: + resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2345,6 +2433,13 @@ packages: mimic-fn: 2.1.0 dev: true + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + /os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -2431,6 +2526,11 @@ packages: engines: {node: '>=8'} dev: true + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true @@ -2529,7 +2629,7 @@ packages: pathe: 1.1.1 dev: true - /postcss-load-config@3.1.4: + /postcss-load-config@3.1.4(ts-node@10.9.1): resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} peerDependencies: @@ -2542,6 +2642,7 @@ packages: optional: true dependencies: lilconfig: 2.1.0 + ts-node: 10.9.1(@types/node@18.15.11)(typescript@5.1.3) yaml: 1.10.2 dev: true @@ -2570,13 +2671,13 @@ packages: hasBin: true dev: true - /pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - ansi-regex: 5.0.1 + '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 - react-is: 17.0.2 + react-is: 18.2.0 dev: true /process-warning@2.1.0: @@ -2616,8 +2717,8 @@ packages: engines: {node: '>=8'} dev: true - /react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true /read-pkg-up@7.0.1: @@ -2709,6 +2810,10 @@ packages: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} dev: true + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + /resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -2738,7 +2843,7 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /rollup@3.26.3: @@ -2746,7 +2851,7 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /rome@12.1.3: @@ -2799,14 +2904,6 @@ packages: hasBin: true dev: true - /semver@7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: true @@ -2879,11 +2976,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: true - /source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} @@ -2999,6 +3091,11 @@ packages: engines: {node: '>=6'} dev: true + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + /strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -3073,17 +3170,12 @@ packages: real-require: 0.2.0 dev: false - /time-zone@1.0.0: - resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} - engines: {node: '>=4'} - dev: true - /tinybench@2.5.0: resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} dev: true - /tinypool@0.4.0: - resolution: {integrity: sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==} + /tinypool@0.7.0: + resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} engines: {node: '>=14.0.0'} dev: true @@ -3126,7 +3218,38 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true - /tsup@6.7.0(typescript@5.1.3): + /ts-node@10.9.1(@types/node@18.15.11)(typescript@5.1.3): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.15.11 + acorn: 8.10.0 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.1.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /tsup@6.7.0(ts-node@10.9.1)(typescript@5.1.3): resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} engines: {node: '>=14.18'} hasBin: true @@ -3150,7 +3273,7 @@ packages: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 3.1.4 + postcss-load-config: 3.1.4(ts-node@10.9.1) resolve-from: 5.0.0 rollup: 3.20.2 source-map: 0.8.0-beta.0 @@ -3231,6 +3354,10 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -3238,32 +3365,31 @@ packages: spdx-expression-parse: 3.0.1 dev: true - /viem@1.4.1(typescript@5.1.3): - resolution: {integrity: sha512-MtaoBHDSJDqa+QyXKG5d+S6EQSebRO0tzw6anSP4zC7AbC614vMeg9Y8LbkmEkWCw8swFYkort+H9l7GkWB0uA==} + /viem@1.16.6(typescript@5.1.3): + resolution: {integrity: sha512-jcWcFQ+xzIfDwexwPJRvCuCRJKEkK9iHTStG7mpU5MmuSBpACs4nATBDyXNFtUiyYTFzLlVEwWkt68K0nCSImg==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: typescript: optional: true dependencies: - '@adraffy/ens-normalize': 1.9.0 - '@noble/curves': 1.0.0 - '@noble/hashes': 1.3.0 - '@scure/bip32': 1.3.0 - '@scure/bip39': 1.2.0 - '@wagmi/chains': 1.6.0(typescript@5.1.3) - abitype: 0.9.3(typescript@5.1.3) - isomorphic-ws: 5.0.0(ws@8.12.0) + '@adraffy/ens-normalize': 1.9.4 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/bip32': 1.3.2 + '@scure/bip39': 1.2.1 + abitype: 0.9.8(typescript@5.1.3) + isows: 1.0.3(ws@8.13.0) typescript: 5.1.3 - ws: 8.12.0 + ws: 8.13.0 transitivePeerDependencies: - bufferutil - utf-8-validate - zod dev: false - /vite-node@0.30.1(@types/node@18.15.11): - resolution: {integrity: sha512-vTikpU/J7e6LU/8iM3dzBo8ZhEiKZEKRznEMm+mJh95XhWaPrJQraT/QsT2NWmuEf+zgAoMe64PKT7hfZ1Njmg==} + /vite-node@0.34.6(@types/node@18.15.11): + resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} hasBin: true dependencies: @@ -3317,11 +3443,11 @@ packages: postcss: 8.4.27 rollup: 3.26.3 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true - /vitest@0.30.1: - resolution: {integrity: sha512-y35WTrSTlTxfMLttgQk4rHcaDkbHQwDP++SNwPb+7H8yb13Q3cu2EixrtHzF27iZ8v0XCciSsLg00RkPAzB/aA==} + /vitest@0.34.6: + resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} engines: {node: '>=v14.18.0'} hasBin: true peerDependencies: @@ -3354,28 +3480,26 @@ packages: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 '@types/node': 18.15.11 - '@vitest/expect': 0.30.1 - '@vitest/runner': 0.30.1 - '@vitest/snapshot': 0.30.1 - '@vitest/spy': 0.30.1 - '@vitest/utils': 0.30.1 + '@vitest/expect': 0.34.6 + '@vitest/runner': 0.34.6 + '@vitest/snapshot': 0.34.6 + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 acorn: 8.10.0 acorn-walk: 8.2.0 cac: 6.7.14 - chai: 4.3.7 - concordance: 5.0.4 + chai: 4.3.10 debug: 4.3.4 local-pkg: 0.4.3 magic-string: 0.30.1 pathe: 1.1.1 picocolors: 1.0.0 - source-map: 0.6.1 std-env: 3.3.3 strip-literal: 1.0.1 tinybench: 2.5.0 - tinypool: 0.4.0 + tinypool: 0.7.0 vite: 4.4.7(@types/node@18.15.11) - vite-node: 0.30.1(@types/node@18.15.11) + vite-node: 0.34.6(@types/node@18.15.11) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -3397,11 +3521,6 @@ packages: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true - /well-known-symbols@2.0.0: - resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} - engines: {node: '>=6'} - dev: true - /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: @@ -3490,8 +3609,8 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true - /ws@8.12.0: - resolution: {integrity: sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==} + /ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -3503,6 +3622,19 @@ packages: optional: true dev: false + /ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} dev: true @@ -3516,10 +3648,6 @@ packages: resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} dev: true - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -3568,6 +3696,11 @@ packages: yargs-parser: 21.1.1 dev: true + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/src/abi/balancerHelpers.ts b/src/abi/balancerHelpers.ts new file mode 100644 index 00000000..96388c91 --- /dev/null +++ b/src/abi/balancerHelpers.ts @@ -0,0 +1,148 @@ +export const balancerHelpersAbi = [ + { + inputs: [ + { + internalType: 'contract IVault', + name: '_vault', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + components: [ + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { + internalType: 'uint256[]', + name: 'minAmountsOut', + type: 'uint256[]', + }, + { + internalType: 'bytes', + name: 'userData', + type: 'bytes', + }, + { + internalType: 'bool', + name: 'toInternalBalance', + type: 'bool', + }, + ], + internalType: 'struct IVault.ExitPoolRequest', + name: 'request', + type: 'tuple', + }, + ], + name: 'queryExit', + outputs: [ + { + internalType: 'uint256', + name: 'bptIn', + type: 'uint256', + }, + { + internalType: 'uint256[]', + name: 'amountsOut', + type: 'uint256[]', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + components: [ + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { + internalType: 'uint256[]', + name: 'maxAmountsIn', + type: 'uint256[]', + }, + { + internalType: 'bytes', + name: 'userData', + type: 'bytes', + }, + { + internalType: 'bool', + name: 'fromInternalBalance', + type: 'bool', + }, + ], + internalType: 'struct IVault.JoinPoolRequest', + name: 'request', + type: 'tuple', + }, + ], + name: 'queryJoin', + outputs: [ + { + internalType: 'uint256', + name: 'bptOut', + type: 'uint256', + }, + { + internalType: 'uint256[]', + name: 'amountsIn', + type: 'uint256[]', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'vault', + outputs: [ + { + internalType: 'contract IVault', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const; diff --git a/src/abi/index.ts b/src/abi/index.ts index a13e6d1b..7232ba99 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -1,3 +1,4 @@ export * from './vault'; +export * from './balancerHelpers'; export * from './balancerQueries'; export * from './erc20'; diff --git a/src/data/enrichers/onChainPoolDataEnricher.ts b/src/data/enrichers/onChainPoolDataEnricher.ts index 2522c07e..857cc59f 100644 --- a/src/data/enrichers/onChainPoolDataEnricher.ts +++ b/src/data/enrichers/onChainPoolDataEnricher.ts @@ -11,10 +11,10 @@ import { RawPool, RawPoolTokenWithRate, RawWeightedPoolToken, + HumanAmount, } from '../types'; - import { CHAINS } from '../../utils'; -import { HumanAmount, SwapOptions } from '../../types'; +import { SwapOptions } from '../../types'; import { fetchAdditionalPoolData } from '../onChainPoolDataViaReadContract'; export interface OnChainPoolData { diff --git a/src/data/index.ts b/src/data/index.ts index e02a4aa2..ed1dbc6a 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,3 +1,4 @@ export * from './enrichers/onChainPoolDataEnricher'; export * from './providers/subgraphPoolProvider'; +export * from './providers/balancer-api'; export * from './types'; diff --git a/src/data/providers/balancer-api/client/index.ts b/src/data/providers/balancer-api/client/index.ts new file mode 100644 index 00000000..91cd0d2d --- /dev/null +++ b/src/data/providers/balancer-api/client/index.ts @@ -0,0 +1,26 @@ +import { ChainId } from '../../../../utils'; + +export class BalancerApiClient { + apiUrl: string; + chainId: ChainId; + constructor(apiUrl: string, chainId: ChainId) { + this.apiUrl = apiUrl; + this.chainId = chainId; + } + + async fetch(requestQuery: { + operationName?: string; + query: string; + variables?: any; + }) { + const response = await fetch(this.apiUrl, { + method: 'post', + body: JSON.stringify(requestQuery), + headers: { + 'Content-Type': 'application/json', + ChainId: this.chainId.toString(), + }, + }); + return response.json(); + } +} diff --git a/src/data/providers/balancer-api/index.ts b/src/data/providers/balancer-api/index.ts new file mode 100644 index 00000000..1dc16831 --- /dev/null +++ b/src/data/providers/balancer-api/index.ts @@ -0,0 +1,13 @@ +import { Pools } from './modules/pool-state'; +import { BalancerApiClient } from './client'; +import { ChainId } from '../../../utils'; + +export class BalancerApi { + balancerApiClient: BalancerApiClient; + pools: Pools; + + constructor(balancerApiUrl: string, chainId: ChainId) { + this.balancerApiClient = new BalancerApiClient(balancerApiUrl, chainId); + this.pools = new Pools(this.balancerApiClient); + } +} diff --git a/src/data/providers/balancer-api/modules/pool-state/index.ts b/src/data/providers/balancer-api/modules/pool-state/index.ts new file mode 100644 index 00000000..5cfca48d --- /dev/null +++ b/src/data/providers/balancer-api/modules/pool-state/index.ts @@ -0,0 +1,122 @@ +import { BalancerApiClient } from '../../client'; +import { PoolStateInput } from '../../../../../entities'; + +export class Pools { + readonly poolStateQuery = `query GetPool($id: String!){ + poolGetPool(id:$id) { + id + address + name + type + version + ... on GqlPoolWeighted { + tokens { + ... on GqlPoolTokenBase { + address + decimals + index + } + } + } + ... on GqlPoolStable { + tokens { + ... on GqlPoolTokenBase { + address + decimals + index + } + } + } + ... on GqlPoolPhantomStable { + tokens { + ... on GqlPoolTokenBase { + address + decimals + index + } + } + } + ... on GqlPoolGyro { + tokens { + ... on GqlPoolTokenBase { + address + decimals + index + } + } + } + ... on GqlPoolLiquidityBootstrapping { + tokens { + ... on GqlPoolTokenBase { + address + decimals + index + } + } + } + ... on GqlPoolElement { + tokens { + ... on GqlPoolTokenBase { + address + decimals + index + } + } + } + ... on GqlPoolLiquidityBootstrapping { + tokens { + ... on GqlPoolTokenBase { + address + decimals + index + } + } + } + } +}`; + + constructor(private readonly balancerApiClient: BalancerApiClient) {} + + async fetchPoolState(id: string): Promise { + const { data } = await this.balancerApiClient.fetch({ + query: this.poolStateQuery, + variables: { + id, + }, + }); + const poolGetPool: PoolStateInput = data.poolGetPool; + + if (poolGetPool.type === 'PHANTOM_STABLE') { + if ( + !poolGetPool.tokens.some( + (t) => t.address === poolGetPool.address, + ) + ) { + /** + * TODO: + * We are working on assumption that API will return BPT token in token list (as current SG does) + * If it doesn't (as of 09/11/23) we have to add it manually + */ + let missingBPTIndex = 0; + const sortedIndexes = poolGetPool.tokens + .map((t) => t.index) + .sort(); + for (let i = 0; i < poolGetPool.tokens.length + 1; i++) { + if ( + i === poolGetPool.tokens.length || + sortedIndexes[i] !== i + ) { + missingBPTIndex = i; + break; + } + } + poolGetPool.tokens.splice(missingBPTIndex, 0, { + index: missingBPTIndex, + address: poolGetPool.address, + decimals: 18, + }); + } + } + return poolGetPool; + } +} diff --git a/src/data/types.ts b/src/data/types.ts index a913a37d..f33f6e4d 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -1,5 +1,6 @@ import { Address, Hex } from 'viem'; -import { HumanAmount } from '../types'; + +export type HumanAmount = `${number}`; // These are only the known pool types, additional pool types can be added via // extension through custom PoolFactories and PoolDataProviders @@ -118,12 +119,15 @@ export interface RawFxPool extends RawBasePool { epsilon: HumanAmount; } -export interface RawPoolToken { +export interface MinimalToken { address: Address; + decimals: number; index: number; +} + +export interface RawPoolToken extends MinimalToken { symbol: string; name: string; - decimals: number; balance: HumanAmount; } diff --git a/src/entities/addLiquidity/addLiquidity.ts b/src/entities/addLiquidity/addLiquidity.ts new file mode 100644 index 00000000..d7554fba --- /dev/null +++ b/src/entities/addLiquidity/addLiquidity.ts @@ -0,0 +1,62 @@ +import { + AddLiquidityBase, + AddLiquidityBuildOutput, + AddLiquidityConfig, + AddLiquidityInput, + AddLiquidityQueryOutput, + AddLiquidityCall, +} from './types'; +import { AddLiquidityWeighted } from './weighted/addLiquidityWeighted'; +import { PoolStateInput } from '../types'; +import { validateInputs } from './utils/validateInputs'; +import { getSortedTokens } from '../utils/getSortedTokens'; +import { AddLiquidityComposableStable } from './composable-stable/addLiquidityComposableStable'; + +export class AddLiquidity { + private readonly addLiquidityTypes: Record = {}; + + constructor(config?: AddLiquidityConfig) { + const { customAddLiquidityTypes } = config || {}; + this.addLiquidityTypes = { + //GYRO2, GYRO3, GYROE pool types only support Add Liquidity Proportional (3 - ALL_TOKENS_IN_FOR_BPT_OUT) + GYRO2: new AddLiquidityWeighted(), + GYRO3: new AddLiquidityWeighted(), + GYROE: new AddLiquidityWeighted(), + WEIGHTED: new AddLiquidityWeighted(), + // PHANTOM_STABLE === ComposableStables in API + PHANTOM_STABLE: new AddLiquidityComposableStable(), + // custom add liquidity types take precedence over base types + ...customAddLiquidityTypes, + }; + } + + public getAddLiquidity(poolType: string): AddLiquidityBase { + if (!this.addLiquidityTypes[poolType]) { + throw new Error('Unsupported pool type'); + } + + return this.addLiquidityTypes[poolType]; + } + + public async query( + input: AddLiquidityInput, + poolState: PoolStateInput, + ): Promise { + validateInputs(input, poolState); + + const sortedTokens = getSortedTokens(poolState.tokens, input.chainId); + const mappedPoolState = { + ...poolState, + tokens: sortedTokens, + }; + + return this.getAddLiquidity(poolState.type).query( + input, + mappedPoolState, + ); + } + + public buildCall(input: AddLiquidityCall): AddLiquidityBuildOutput { + return this.getAddLiquidity(input.poolType).buildCall(input); + } +} diff --git a/src/entities/addLiquidity/composable-stable/addLiquidityComposableStable.ts b/src/entities/addLiquidity/composable-stable/addLiquidityComposableStable.ts new file mode 100644 index 00000000..72a1c60d --- /dev/null +++ b/src/entities/addLiquidity/composable-stable/addLiquidityComposableStable.ts @@ -0,0 +1,245 @@ +import { encodeFunctionData } from 'viem'; +import { Token } from '../../token'; +import { TokenAmount } from '../../tokenAmount'; +import { Address } from '../../../types'; +import { BALANCER_VAULT, MAX_UINT256, ZERO_ADDRESS } from '../../../utils'; +import { vaultAbi } from '../../../abi'; +import { + AddLiquidityBase, + AddLiquidityBuildOutput, + AddLiquidityInput, + AddLiquidityKind, + AddLiquidityComposableStableQueryOutput, + AddLiquidityComposableStableCall, +} from '../types'; +import { + AddLiquidityAmounts as AddLiquidityAmountsBase, + PoolState, +} from '../../types'; +import { + doAddLiquidityQuery, + getAmounts, + parseAddLiquidityArgs, +} from '../../utils'; +import { ComposableStableEncoder } from '../../encoders/composableStable'; + +type AddLiquidityAmounts = AddLiquidityAmountsBase & { + maxAmountsInNoBpt: bigint[]; +}; + +export class AddLiquidityComposableStable implements AddLiquidityBase { + public async query( + input: AddLiquidityInput, + poolState: PoolState, + ): Promise { + const bptIndex = poolState.tokens.findIndex( + (t) => t.address === poolState.address, + ); + const amounts = this.getAmountsQuery(poolState.tokens, input, bptIndex); + + const userData = this.encodeUserData(input.kind, amounts); + + const { args, tokensIn } = parseAddLiquidityArgs({ + useNativeAssetAsWrappedAmountIn: + !!input.useNativeAssetAsWrappedAmountIn, + chainId: input.chainId, + sortedTokens: poolState.tokens, + poolId: poolState.id, + sender: ZERO_ADDRESS, + recipient: ZERO_ADDRESS, + maxAmountsIn: amounts.maxAmountsIn, + userData, + fromInternalBalance: input.fromInternalBalance ?? false, + }); + + const queryOutput = await doAddLiquidityQuery( + input.rpcUrl, + input.chainId, + args, + ); + + const bpt = new Token(input.chainId, poolState.address, 18); + const bptOut = TokenAmount.fromRawAmount(bpt, queryOutput.bptOut); + + const amountsIn = queryOutput.amountsIn.map((a, i) => + TokenAmount.fromRawAmount(tokensIn[i], a), + ); + + return { + poolType: poolState.type, + addLiquidityKind: input.kind, + poolId: poolState.id, + bptOut, + amountsIn, + tokenInIndex: amounts.tokenInIndex, + fromInternalBalance: !!input.fromInternalBalance, + bptIndex, + }; + } + + public buildCall( + input: AddLiquidityComposableStableCall, + ): AddLiquidityBuildOutput { + const amounts = this.getAmountsCall(input); + + const userData = this.encodeUserData(input.addLiquidityKind, amounts); + + const { args } = parseAddLiquidityArgs({ + ...input, + sortedTokens: input.amountsIn.map((a) => a.token), + maxAmountsIn: amounts.maxAmountsIn, + userData, + fromInternalBalance: input.fromInternalBalance, + }); + + const call = encodeFunctionData({ + abi: vaultAbi, + functionName: 'joinPool', + args, + }); + + const value = input.amountsIn.find( + (a) => a.token.address === ZERO_ADDRESS, + )?.amount; + + return { + call, + to: BALANCER_VAULT, + value: value === undefined ? 0n : value, + minBptOut: TokenAmount.fromRawAmount( + input.bptOut.token, + amounts.minimumBpt, + ), + maxAmountsIn: input.amountsIn.map((a, i) => + TokenAmount.fromRawAmount(a.token, amounts.maxAmountsIn[i]), + ), + }; + } + + private getAmountsQuery( + poolTokens: Token[], + input: AddLiquidityInput, + bptIndex: number, + ): AddLiquidityAmounts { + let addLiquidityAmounts: AddLiquidityAmountsBase; + switch (input.kind) { + case AddLiquidityKind.Init: + case AddLiquidityKind.Unbalanced: { + addLiquidityAmounts = { + minimumBpt: 0n, + maxAmountsIn: getAmounts( + poolTokens, + input.amountsIn, + BigInt(0), + ), + tokenInIndex: undefined, + }; + break; + } + case AddLiquidityKind.SingleToken: { + const tokenInIndex = poolTokens + .filter((_, index) => index !== bptIndex) // Need to remove Bpt + .findIndex((t) => t.isSameAddress(input.tokenIn)); + if (tokenInIndex === -1) + throw Error("Can't find index of SingleToken"); + const maxAmountsIn = Array(poolTokens.length).fill(0n); + maxAmountsIn[tokenInIndex] = MAX_UINT256; + addLiquidityAmounts = { + minimumBpt: input.bptOut.rawAmount, + maxAmountsIn, + tokenInIndex, + }; + break; + } + case AddLiquidityKind.Proportional: { + addLiquidityAmounts = { + minimumBpt: input.bptOut.rawAmount, + maxAmountsIn: Array(poolTokens.length).fill(MAX_UINT256), + tokenInIndex: undefined, + }; + break; + } + default: + throw Error('Unsupported Add Liquidity Kind'); + } + + return { + ...addLiquidityAmounts, + maxAmountsInNoBpt: [ + ...addLiquidityAmounts.maxAmountsIn.slice(0, bptIndex), + ...addLiquidityAmounts.maxAmountsIn.slice(bptIndex + 1), + ], + }; + } + + private getAmountsCall( + input: AddLiquidityComposableStableCall, + ): AddLiquidityAmounts { + let addLiquidityAmounts: AddLiquidityAmountsBase; + switch (input.addLiquidityKind) { + case AddLiquidityKind.Init: + case AddLiquidityKind.Unbalanced: { + const minimumBpt = input.slippage.removeFrom( + input.bptOut.amount, + ); + addLiquidityAmounts = { + minimumBpt, + maxAmountsIn: input.amountsIn.map((a) => a.amount), + tokenInIndex: input.tokenInIndex, + }; + break; + } + case AddLiquidityKind.SingleToken: + case AddLiquidityKind.Proportional: { + addLiquidityAmounts = { + minimumBpt: input.bptOut.amount, + maxAmountsIn: input.amountsIn.map((a) => + input.slippage.applyTo(a.amount), + ), + tokenInIndex: input.tokenInIndex, + }; + break; + } + default: + throw Error('Unsupported Add Liquidity Kind'); + } + return { + ...addLiquidityAmounts, + maxAmountsInNoBpt: [ + ...addLiquidityAmounts.maxAmountsIn.slice(0, input.bptIndex), + ...addLiquidityAmounts.maxAmountsIn.slice(input.bptIndex + 1), + ], + }; + } + + private encodeUserData( + kind: AddLiquidityKind, + amounts: AddLiquidityAmounts, + ): Address { + switch (kind) { + case AddLiquidityKind.Init: + return ComposableStableEncoder.addLiquidityInit( + amounts.maxAmountsInNoBpt, + ); + case AddLiquidityKind.Unbalanced: + return ComposableStableEncoder.addLiquidityUnbalanced( + amounts.maxAmountsInNoBpt, + amounts.minimumBpt, + ); + case AddLiquidityKind.SingleToken: { + if (amounts.tokenInIndex === undefined) throw Error('No Index'); + return ComposableStableEncoder.addLiquiditySingleToken( + amounts.minimumBpt, + amounts.tokenInIndex, // Has to be index without BPT + ); + } + case AddLiquidityKind.Proportional: { + return ComposableStableEncoder.addLiquidityProportional( + amounts.minimumBpt, + ); + } + default: + throw Error('Unsupported Add Liquidity Kind'); + } + } +} diff --git a/src/entities/addLiquidity/index.ts b/src/entities/addLiquidity/index.ts new file mode 100644 index 00000000..6beed28f --- /dev/null +++ b/src/entities/addLiquidity/index.ts @@ -0,0 +1,2 @@ +export * from './addLiquidity'; +export * from './types'; diff --git a/src/entities/addLiquidity/types.ts b/src/entities/addLiquidity/types.ts new file mode 100644 index 00000000..3d743f84 --- /dev/null +++ b/src/entities/addLiquidity/types.ts @@ -0,0 +1,108 @@ +import { TokenAmount } from '../tokenAmount'; +import { Slippage } from '../slippage'; +import { PoolState } from '../types'; +import { Address, Hex, InputAmount } from '../../types'; + +export enum AddLiquidityKind { + Init = 'Init', + Unbalanced = 'Unbalanced', + SingleToken = 'SingleToken', + Proportional = 'Proportional', +} + +// This will be extended for each pools specific input requirements +type AddLiquidityBaseInput = { + chainId: number; + rpcUrl: string; + useNativeAssetAsWrappedAmountIn?: boolean; + fromInternalBalance?: boolean; +}; + +export type AddLiquidityInitInput = AddLiquidityBaseInput & { + amountsIn: InputAmount[]; + kind: AddLiquidityKind.Init; +}; + +export type AddLiquidityUnbalancedInput = AddLiquidityBaseInput & { + amountsIn: InputAmount[]; + kind: AddLiquidityKind.Unbalanced; +}; + +export type AddLiquiditySingleTokenInput = AddLiquidityBaseInput & { + bptOut: InputAmount; + tokenIn: Address; + kind: AddLiquidityKind.SingleToken; +}; + +export type AddLiquidityProportionalInput = AddLiquidityBaseInput & { + bptOut: InputAmount; + kind: AddLiquidityKind.Proportional; +}; + +export type AddLiquidityInput = + | AddLiquidityInitInput + | AddLiquidityUnbalancedInput + | AddLiquiditySingleTokenInput + | AddLiquidityProportionalInput; + +type AddLiquidityBaseQueryOutput = { + poolType: string; + poolId: Hex; + addLiquidityKind: AddLiquidityKind; + bptOut: TokenAmount; + amountsIn: TokenAmount[]; + fromInternalBalance: boolean; + tokenInIndex?: number; +}; + +export type AddLiquidityWeightedQueryOutput = AddLiquidityBaseQueryOutput; + +export type AddLiquidityComposableStableQueryOutput = + AddLiquidityBaseQueryOutput & { + bptIndex: number; + }; + +export type AddLiquidityQueryOutput = + | AddLiquidityWeightedQueryOutput + | AddLiquidityComposableStableQueryOutput; + +type AddLiquidityBaseCall = { + slippage: Slippage; + sender: Address; + recipient: Address; +}; + +export type AddLiquidityComposableStableCall = AddLiquidityBaseCall & + AddLiquidityComposableStableQueryOutput; +export type AddLiquidityWeightedCall = AddLiquidityBaseCall & + AddLiquidityBaseQueryOutput; + +export type AddLiquidityCall = + | AddLiquidityWeightedCall + | AddLiquidityComposableStableCall; + +export interface AddLiquidityBase { + query( + input: AddLiquidityInput, + poolState: PoolState, + ): Promise; + buildCall(input: AddLiquidityCall): { + call: Hex; + to: Address; + value: bigint; + minBptOut: TokenAmount; + maxAmountsIn: TokenAmount[]; + }; +} + +export type AddLiquidityBuildOutput = { + call: Hex; + to: Address; + value: bigint; + minBptOut: TokenAmount; + maxAmountsIn: TokenAmount[]; +}; + +export type AddLiquidityConfig = { + customAddLiquidityTypes: Record; +}; diff --git a/src/entities/addLiquidity/utils/validateInputs.ts b/src/entities/addLiquidity/utils/validateInputs.ts new file mode 100644 index 00000000..b6160c78 --- /dev/null +++ b/src/entities/addLiquidity/utils/validateInputs.ts @@ -0,0 +1,63 @@ +import { AddLiquidityInput, AddLiquidityKind } from '../types'; +import { PoolStateInput } from '../../types'; +import { areTokensInArray } from '../../utils/areTokensInArray'; +import { Address } from 'viem'; +import { MinimalToken } from '../../../data'; + +export function validateInputs( + input: AddLiquidityInput, + poolState: PoolStateInput, +) { + validateComposableStableWithBPT( + poolState.type, + poolState.address, + poolState.tokens, + ); + validateAddLiquidityGyroIsProportional(input.kind, poolState.type); + switch (input.kind) { + case AddLiquidityKind.Init: + case AddLiquidityKind.Unbalanced: + areTokensInArray( + input.amountsIn.map((a) => a.address), + poolState.tokens.map((t) => t.address), + ); + break; + case AddLiquidityKind.SingleToken: + areTokensInArray( + [input.tokenIn], + poolState.tokens.map((t) => t.address), + ); + case AddLiquidityKind.Proportional: + areTokensInArray([input.bptOut.address], [poolState.address]); + default: + break; + } +} + +export const addLiquidityKindNotSupportedByGyro = + 'INPUT_ERROR: Gyro pools do not implement this add liquidity kind, only Add Liquidity Proportional (3 - ALL_TOKENS_IN_FOR_BPT_OUT) is supported'; + +function validateAddLiquidityGyroIsProportional( + kind: AddLiquidityKind, + poolType: string, +) { + if ( + ['GYROE', 'GYRO2', 'GYRO3'].includes(poolType) && + kind !== AddLiquidityKind.Proportional + ) { + throw new Error(addLiquidityKindNotSupportedByGyro); + } +} + +function validateComposableStableWithBPT( + poolType: string, + poolAddress: Address, + poolTokens: MinimalToken[], +) { + const bptIndex = poolTokens.findIndex((t) => t.address === poolAddress); + if (['PHANTOM_STABLE'].includes(poolType) && bptIndex < 0) { + throw new Error( + 'INPUT_ERROR: Composable Stable Pool State should have BPT token included', + ); + } +} diff --git a/src/entities/addLiquidity/weighted/addLiquidityWeighted.ts b/src/entities/addLiquidity/weighted/addLiquidityWeighted.ts new file mode 100644 index 00000000..0a6b9021 --- /dev/null +++ b/src/entities/addLiquidity/weighted/addLiquidityWeighted.ts @@ -0,0 +1,203 @@ +import { encodeFunctionData } from 'viem'; +import { Token } from '../../token'; +import { TokenAmount } from '../../tokenAmount'; +import { WeightedEncoder } from '../../encoders/weighted'; +import { Address } from '../../../types'; +import { BALANCER_VAULT, MAX_UINT256, ZERO_ADDRESS } from '../../../utils'; +import { vaultAbi } from '../../../abi'; +import { + AddLiquidityBase, + AddLiquidityBuildOutput, + AddLiquidityInput, + AddLiquidityKind, + AddLiquidityWeightedQueryOutput, + AddLiquidityWeightedCall, +} from '../types'; +import { AddLiquidityAmounts, PoolState } from '../../types'; +import { + doAddLiquidityQuery, + getAmounts, + parseAddLiquidityArgs, +} from '../../utils'; + +export class AddLiquidityWeighted implements AddLiquidityBase { + public async query( + input: AddLiquidityInput, + poolState: PoolState, + ): Promise { + const amounts = this.getAmountsQuery(poolState.tokens, input); + + const userData = this.encodeUserData(input.kind, amounts); + + const { args, tokensIn } = parseAddLiquidityArgs({ + useNativeAssetAsWrappedAmountIn: + !!input.useNativeAssetAsWrappedAmountIn, + chainId: input.chainId, + sortedTokens: poolState.tokens, + poolId: poolState.id, + sender: ZERO_ADDRESS, + recipient: ZERO_ADDRESS, + maxAmountsIn: amounts.maxAmountsIn, + userData, + fromInternalBalance: input.fromInternalBalance ?? false, + }); + + const queryOutput = await doAddLiquidityQuery( + input.rpcUrl, + input.chainId, + args, + ); + + const bpt = new Token(input.chainId, poolState.address, 18); + const bptOut = TokenAmount.fromRawAmount(bpt, queryOutput.bptOut); + + const amountsIn = queryOutput.amountsIn.map((a, i) => + TokenAmount.fromRawAmount(tokensIn[i], a), + ); + + return { + poolType: poolState.type, + addLiquidityKind: input.kind, + poolId: poolState.id, + bptOut, + amountsIn, + tokenInIndex: amounts.tokenInIndex, + fromInternalBalance: !!input.fromInternalBalance, + }; + } + + public buildCall(input: AddLiquidityWeightedCall): AddLiquidityBuildOutput { + const amounts = this.getAmountsCall(input); + + const userData = this.encodeUserData(input.addLiquidityKind, amounts); + + const { args } = parseAddLiquidityArgs({ + ...input, + sortedTokens: input.amountsIn.map((a) => a.token), + maxAmountsIn: amounts.maxAmountsIn, + userData, + fromInternalBalance: input.fromInternalBalance, + }); + + const call = encodeFunctionData({ + abi: vaultAbi, + functionName: 'joinPool', + args, + }); + + const value = input.amountsIn.find( + (a) => a.token.address === ZERO_ADDRESS, + )?.amount; + + return { + call, + to: BALANCER_VAULT, + value: value === undefined ? 0n : value, + minBptOut: TokenAmount.fromRawAmount( + input.bptOut.token, + amounts.minimumBpt, + ), + maxAmountsIn: input.amountsIn.map((a, i) => + TokenAmount.fromRawAmount(a.token, amounts.maxAmountsIn[i]), + ), + }; + } + + private getAmountsQuery( + poolTokens: Token[], + input: AddLiquidityInput, + ): AddLiquidityAmounts { + switch (input.kind) { + case AddLiquidityKind.Init: + case AddLiquidityKind.Unbalanced: { + return { + minimumBpt: 0n, + maxAmountsIn: getAmounts(poolTokens, input.amountsIn), + tokenInIndex: undefined, + }; + } + case AddLiquidityKind.SingleToken: { + const tokenInIndex = poolTokens.findIndex((t) => + t.isSameAddress(input.tokenIn), + ); + if (tokenInIndex === -1) + throw Error("Can't find index of SingleToken"); + const maxAmountsIn = Array(poolTokens.length).fill(0n); + maxAmountsIn[tokenInIndex] = MAX_UINT256; + return { + minimumBpt: input.bptOut.rawAmount, + maxAmountsIn, + tokenInIndex, + }; + } + case AddLiquidityKind.Proportional: { + return { + minimumBpt: input.bptOut.rawAmount, + maxAmountsIn: Array(poolTokens.length).fill(MAX_UINT256), + tokenInIndex: undefined, + }; + } + default: + throw Error('Unsupported Add Liquidity Kind'); + } + } + + private getAmountsCall( + input: AddLiquidityWeightedCall, + ): AddLiquidityAmounts { + switch (input.addLiquidityKind) { + case AddLiquidityKind.Init: + case AddLiquidityKind.Unbalanced: { + const minimumBpt = input.slippage.removeFrom( + input.bptOut.amount, + ); + return { + minimumBpt, + maxAmountsIn: input.amountsIn.map((a) => a.amount), + tokenInIndex: input.tokenInIndex, + }; + } + case AddLiquidityKind.SingleToken: + case AddLiquidityKind.Proportional: { + return { + minimumBpt: input.bptOut.amount, + maxAmountsIn: input.amountsIn.map((a) => + input.slippage.applyTo(a.amount), + ), + tokenInIndex: input.tokenInIndex, + }; + } + default: + throw Error('Unsupported Add Liquidity Kind'); + } + } + + private encodeUserData( + kind: AddLiquidityKind, + amounts: AddLiquidityAmounts, + ): Address { + switch (kind) { + case AddLiquidityKind.Init: + return WeightedEncoder.addLiquidityInit(amounts.maxAmountsIn); + case AddLiquidityKind.Unbalanced: + return WeightedEncoder.addLiquidityUnbalanced( + amounts.maxAmountsIn, + amounts.minimumBpt, + ); + case AddLiquidityKind.SingleToken: { + if (amounts.tokenInIndex === undefined) throw Error('No Index'); + return WeightedEncoder.addLiquiditySingleToken( + amounts.minimumBpt, + amounts.tokenInIndex, + ); + } + case AddLiquidityKind.Proportional: { + return WeightedEncoder.addLiquidityProportional( + amounts.minimumBpt, + ); + } + default: + throw Error('Unsupported Add Liquidity Kind'); + } + } +} diff --git a/src/entities/encoders/composableStable.ts b/src/entities/encoders/composableStable.ts new file mode 100644 index 00000000..689d0a51 --- /dev/null +++ b/src/entities/encoders/composableStable.ts @@ -0,0 +1,146 @@ +import { encodeAbiParameters } from 'viem'; +import { Address } from '../../types'; + +export enum ComposableStablePoolJoinKind { + INIT = 0, + EXACT_TOKENS_IN_FOR_BPT_OUT = 1, + TOKEN_IN_FOR_EXACT_BPT_OUT = 2, + ALL_TOKENS_IN_FOR_EXACT_BPT_OUT = 3, +} + +export enum ComposableStablePoolExitKind { + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0, + BPT_IN_FOR_EXACT_TOKENS_OUT = 1, + EXACT_BPT_IN_FOR_ALL_TOKENS_OUT = 2, +} + +export class ComposableStableEncoder { + /** + * Cannot be constructed. + */ + private constructor() { + // eslint-disable-next-line @typescript-eslint/no-empty-function + } + + /** + * Encodes the userData parameter for providing the initial liquidity to a ComposableStablePool + * @param initialBalances - the amounts of tokens to send to the pool to form the initial balances + */ + static addLiquidityInit = (amountsIn: bigint[]): Address => + encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256[]' }], + [BigInt(ComposableStablePoolJoinKind.INIT), amountsIn], + ); + + /** + * Encodes the userData parameter for adding liquidity to a ComposableStablePool with exact token inputs + * @param amountsIn - the amounts each of token to deposit in the pool as liquidity + * @param minimumBPT - the minimum acceptable BPT to receive in return for deposited tokens + */ + static addLiquidityUnbalanced = ( + amountsIn: bigint[], + minimumBPT: bigint, + ): Address => + encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256[]' }, { type: 'uint256' }], + [ + BigInt( + ComposableStablePoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, + ), + amountsIn, + minimumBPT, + ], + ); + + /** + * Encodes the userData parameter for adding liquidity to a ComposableStablePool with a single token to receive an exact amount of BPT + * @param bptAmountOut - the amount of BPT to be minted + * @param tokenIndex - the index of the token to be provided as liquidity + */ + static addLiquiditySingleToken = ( + bptAmountOut: bigint, + tokenIndex: number, + ): Address => { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }], + [ + BigInt(ComposableStablePoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT), + bptAmountOut, + BigInt(tokenIndex), + ], + ); + }; + + /** + * Encodes the userData parameter for adding liquidity to a ComposableStablePool proportionally to receive an exact amount of BPT + * @param bptAmountOut - the amount of BPT to be minted + */ + static addLiquidityProportional = (bptAmountOut: bigint): Address => { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }], + [ + BigInt( + ComposableStablePoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, + ), + bptAmountOut, + ], + ); + }; + + /** + * Encodes the userData parameter for removing liquidity from a ComposableStablePool by removing tokens in return for an exact amount of BPT + * @param bptAmountIn - the amount of BPT to be burned + * @param tokenIndex - the index of the token to be removed from the pool + */ + static removeLiquiditySingleToken = ( + bptAmountIn: bigint, + tokenIndex: number, + ): Address => { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }], + [ + BigInt( + ComposableStablePoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, + ), + bptAmountIn, + BigInt(tokenIndex), + ], + ); + }; + + /** + * Encodes the userData parameter for removing liquidity from a ComposableStablePool by removing tokens in return for an exact amount of BPT + * @param bptAmountIn - the amount of BPT to be burned + */ + static removeLiquidityProportional = (bptAmountIn: bigint): Address => { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }], + [ + BigInt( + ComposableStablePoolExitKind.EXACT_BPT_IN_FOR_ALL_TOKENS_OUT, + ), + bptAmountIn, + ], + ); + }; + + /** + * Encodes the userData parameter for removing liquidity from a ComposableStablePool by removing exact amounts of tokens + * @param amountsOut - the amounts of each token to be withdrawn from the pool + * @param maxBPTAmountIn - the minimum acceptable BPT to burn in return for withdrawn tokens + */ + static removeLiquidityUnbalanced = ( + amountsOut: bigint[], + maxBPTAmountIn: bigint, + ): Address => + encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256[]' }, { type: 'uint256' }], + [ + BigInt( + ComposableStablePoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT, + ), + amountsOut, + maxBPTAmountIn, + ], + ); +} diff --git a/src/entities/encoders/index.ts b/src/entities/encoders/index.ts new file mode 100644 index 00000000..2746a28e --- /dev/null +++ b/src/entities/encoders/index.ts @@ -0,0 +1,15 @@ +import { SupportedRawPoolTypes } from '../../data/types'; +import { WeightedEncoder } from './weighted'; + +export * from './weighted'; + +export const getEncoder = ( + poolType: SupportedRawPoolTypes | string, +): typeof WeightedEncoder | undefined => { + switch (poolType) { + case 'Weighted': + return WeightedEncoder; + default: + return undefined; + } +}; diff --git a/src/entities/encoders/weighted.ts b/src/entities/encoders/weighted.ts new file mode 100644 index 00000000..3d8f065f --- /dev/null +++ b/src/entities/encoders/weighted.ts @@ -0,0 +1,138 @@ +import { encodeAbiParameters } from 'viem'; +import { Address } from '../../types'; + +export enum WeightedPoolJoinKind { + INIT = 0, + EXACT_TOKENS_IN_FOR_BPT_OUT = 1, + TOKEN_IN_FOR_EXACT_BPT_OUT = 2, + ALL_TOKENS_IN_FOR_EXACT_BPT_OUT = 3, +} + +export enum WeightedPoolExitKind { + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0, + EXACT_BPT_IN_FOR_TOKENS_OUT = 1, + BPT_IN_FOR_EXACT_TOKENS_OUT = 2, + MANAGEMENT_FEE_TOKENS_OUT = 3, +} + +export class WeightedEncoder { + /** + * Cannot be constructed. + */ + private constructor() { + // eslint-disable-next-line @typescript-eslint/no-empty-function + } + + /** + * Encodes the userData parameter for providing the initial liquidity to a WeightedPool + * @param initialBalances - the amounts of tokens to send to the pool to form the initial balances + */ + static addLiquidityInit = (amountsIn: bigint[]): Address => + encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256[]' }], + [BigInt(WeightedPoolJoinKind.INIT), amountsIn], + ); + + /** + * Encodes the userData parameter for adding liquidity to a WeightedPool with exact token inputs + * @param amountsIn - the amounts each of token to deposit in the pool as liquidity + * @param minimumBPT - the minimum acceptable BPT to receive in return for deposited tokens + */ + static addLiquidityUnbalanced = ( + amountsIn: bigint[], + minimumBPT: bigint, + ): Address => + encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256[]' }, { type: 'uint256' }], + [ + BigInt(WeightedPoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT), + amountsIn, + minimumBPT, + ], + ); + + /** + * Encodes the userData parameter for adding liquidity to a WeightedPool with a single token to receive an exact amount of BPT + * @param bptAmountOut - the amount of BPT to be minted + * @param tokenIndex - the index of the token to be provided as liquidity + */ + static addLiquiditySingleToken = ( + bptAmountOut: bigint, + tokenIndex: number, + ): Address => { + // if tokenIndex is provided, it's assumed to be an allTokensIn + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }], + [ + BigInt(WeightedPoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT), + bptAmountOut, + BigInt(tokenIndex), + ], + ); + }; + + /** + * Encodes the userData parameter for adding liquidity to a WeightedPool proportionally to receive an exact amount of BPT + * @param bptAmountOut - the amount of BPT to be minted + */ + static addLiquidityProportional = (bptAmountOut: bigint): Address => { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }], + [ + BigInt(WeightedPoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT), + bptAmountOut, + ], + ); + }; + + /** + * Encodes the userData parameter for removing liquidity from a WeightedPool by removing tokens in return for an exact amount of BPT + * @param bptAmountIn - the amount of BPT to be burned + * @param tokenIndex - the index of the token to removed from the pool + */ + static removeLiquiditySingleToken = ( + bptAmountIn: bigint, + tokenIndex: number, + ): Address => { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }], + [ + BigInt(WeightedPoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT), + bptAmountIn, + BigInt(tokenIndex), + ], + ); + }; + + /** + * Encodes the userData parameter for removing liquidity from a WeightedPool by removing tokens in return for an exact amount of BPT + * @param bptAmountIn - the amount of BPT to be burned + */ + static removeLiquidityProportional = (bptAmountIn: bigint): Address => { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }], + [ + BigInt(WeightedPoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT), + bptAmountIn, + ], + ); + }; + + /** + * Encodes the userData parameter for removing liquidity from a WeightedPool by removing exact amounts of tokens + * @param amountsOut - the amounts of each token to be withdrawn from the pool + * @param maxBPTAmountIn - the minimum acceptable BPT to burn in return for withdrawn tokens + */ + static removeLiquidityUnbalanced = ( + amountsOut: bigint[], + maxBPTAmountIn: bigint, + ): Address => + encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256[]' }, { type: 'uint256' }], + [ + BigInt(WeightedPoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT), + amountsOut, + maxBPTAmountIn, + ], + ); +} diff --git a/src/entities/index.ts b/src/entities/index.ts index 66d20d26..19c78a9d 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -1,5 +1,11 @@ +export * from './encoders'; +export * from './addLiquidity'; +export * from './removeLiquidity'; export * from './path'; export * from './swap'; +export * from './slippage'; export * from './token'; export * from './tokenAmount'; export * from './pools/'; +export * from './utils'; +export * from './types'; diff --git a/src/entities/path.ts b/src/entities/path.ts index 8055a2e2..d9053a8d 100644 --- a/src/entities/path.ts +++ b/src/entities/path.ts @@ -1,5 +1,6 @@ import { BasePool } from './pools/'; -import { Token, TokenAmount } from './'; +import { Token } from './token'; +import { TokenAmount } from './tokenAmount'; import { SwapKind } from '../types'; export class Path { diff --git a/src/entities/pools/fx/fxFactory.ts b/src/entities/pools/fx/fxFactory.ts index 04d17822..b2c9dca9 100644 --- a/src/entities/pools/fx/fxFactory.ts +++ b/src/entities/pools/fx/fxFactory.ts @@ -1,5 +1,5 @@ import { BasePool, BasePoolFactory } from '../'; -import { FxPool } from './'; +import { FxPool } from './fxPool'; import { RawFxPool, RawPool } from '../../../data/types'; export class FxPoolFactory implements BasePoolFactory { diff --git a/src/entities/pools/fx/fxMath.ts b/src/entities/pools/fx/fxMath.ts index e73f6ead..70b15b21 100644 --- a/src/entities/pools/fx/fxMath.ts +++ b/src/entities/pools/fx/fxMath.ts @@ -1,6 +1,6 @@ import { parseUnits } from 'viem'; -import { RAY } from '../../../utils'; -import { FxPoolPairData } from './fxPool'; +import { RAY } from '../../../utils/math'; +import { FxPoolPairData } from './types'; import { SwapKind } from '../../../types'; export const CURVEMATH_MAX_DIFF = parseUnits('-0.000001000000000000024', 36); diff --git a/src/entities/pools/fx/fxPool.ts b/src/entities/pools/fx/fxPool.ts index afc163f9..d264efc5 100644 --- a/src/entities/pools/fx/fxPool.ts +++ b/src/entities/pools/fx/fxPool.ts @@ -1,11 +1,14 @@ import { Hex, parseEther, parseUnits } from 'viem'; -import { HumanAmount, PoolType, SwapKind } from '../../../types'; -import { BigintIsh, Token, TokenAmount } from '../../'; +import { PoolType, SwapKind } from '../../../types'; +import { Token } from '../../token'; +import { TokenAmount } from '../../tokenAmount'; import { BasePool } from '../../pools'; -import { RAY, WAD, getPoolAddress } from '../../../utils'; +import { RAY, getPoolAddress } from '../../../utils'; import { _calcInGivenOut, _calcOutGivenIn } from './fxMath'; import { RawFxPool } from '../../../data/types'; import { MathFx, parseFixedCurveParam } from './helpers'; +import { FxPoolPairData } from './types'; +import { FxPoolToken } from './fxPoolToken'; const isUSDC = (address: string): boolean => { return ( @@ -15,104 +18,6 @@ const isUSDC = (address: string): boolean => { ); }; -export type FxPoolPairData = { - tIn: FxPoolToken; - tOut: FxPoolToken; - alpha: bigint; - beta: bigint; - delta: bigint; - lambda: bigint; - _oGLiq: bigint; - _nGLiq: bigint; - _oBals: bigint[]; - _nBals: bigint[]; - givenToken: FxPoolToken; - swapKind: SwapKind; -}; - -export class FxPoolToken extends TokenAmount { - public readonly index: number; - public readonly latestFXPrice: HumanAmount; - public readonly fxOracleDecimals: number; - public numeraire: bigint; // in 36 decimals - private readonly scalar36 = this.scalar * WAD; - - public constructor( - token: Token, - amount: BigintIsh, - latestFXPrice: HumanAmount, - fxOracleDecimals: number, - index: number, - ) { - super(token, amount); - this.latestFXPrice = latestFXPrice; - this.fxOracleDecimals = fxOracleDecimals; - const truncatedNumeraire = MathFx.mulDownFixed( - this.amount, - parseUnits(this.latestFXPrice, this.fxOracleDecimals), - this.fxOracleDecimals, - ); - this.numeraire = truncatedNumeraire * this.scalar36; - this.index = index; - } - - public increase(amount: bigint): TokenAmount { - this.amount = this.amount + amount; - this.scale18 = this.amount * this.scalar; - const truncatedNumeraire = MathFx.mulDownFixed( - this.amount, - parseUnits(this.latestFXPrice, this.fxOracleDecimals), - this.fxOracleDecimals, - ); - this.numeraire = truncatedNumeraire * this.scalar36; - return this; - } - - public decrease(amount: bigint): TokenAmount { - this.amount = this.amount - amount; - this.scale18 = this.amount * this.scalar; - const truncatedNumeraire = MathFx.mulDownFixed( - this.amount, - parseUnits(this.latestFXPrice, this.fxOracleDecimals), - this.fxOracleDecimals, - ); - this.numeraire = truncatedNumeraire * this.scalar36; - return this; - } - - public static fromNumeraire( - poolToken: FxPoolToken, - numeraire: BigintIsh, - divUp?: boolean, - ): FxPoolToken { - const truncatedNumeraire = BigInt(numeraire) / poolToken.scalar36; // loss of precision required to match SC implementation - const amount = divUp - ? MathFx.divUpFixed( - BigInt(truncatedNumeraire), - parseUnits( - poolToken.latestFXPrice, - poolToken.fxOracleDecimals, - ), - poolToken.fxOracleDecimals, - ) - : MathFx.divDownFixed( - BigInt(truncatedNumeraire), - parseUnits( - poolToken.latestFXPrice, - poolToken.fxOracleDecimals, - ), - poolToken.fxOracleDecimals, - ); - return new FxPoolToken( - poolToken.token, - amount, - poolToken.latestFXPrice, - poolToken.fxOracleDecimals, - poolToken.index, - ); - } -} - export class FxPool implements BasePool { public readonly chainId: number; public readonly id: Hex; diff --git a/src/entities/pools/fx/fxPoolToken.ts b/src/entities/pools/fx/fxPoolToken.ts new file mode 100644 index 00000000..75f2d1f4 --- /dev/null +++ b/src/entities/pools/fx/fxPoolToken.ts @@ -0,0 +1,90 @@ +import { parseUnits } from 'viem'; +import { Token } from '../../token'; +import { TokenAmount, BigintIsh } from '../../tokenAmount'; +import { WAD } from '../../../utils/math'; +import { _calcInGivenOut, _calcOutGivenIn } from './fxMath'; +import { HumanAmount } from '../../../data/types'; +import { MathFx } from './helpers'; + +export class FxPoolToken extends TokenAmount { + public readonly index: number; + public readonly latestFXPrice: HumanAmount; + public readonly fxOracleDecimals: number; + public numeraire: bigint; // in 36 decimals + private readonly scalar36 = this.scalar * WAD; + + public constructor( + token: Token, + amount: BigintIsh, + latestFXPrice: HumanAmount, + fxOracleDecimals: number, + index: number, + ) { + super(token, amount); + this.latestFXPrice = latestFXPrice; + this.fxOracleDecimals = fxOracleDecimals; + const truncatedNumeraire = MathFx.mulDownFixed( + this.amount, + parseUnits(this.latestFXPrice, this.fxOracleDecimals), + this.fxOracleDecimals, + ); + this.numeraire = truncatedNumeraire * this.scalar36; + this.index = index; + } + + public increase(amount: bigint): TokenAmount { + this.amount = this.amount + amount; + this.scale18 = this.amount * this.scalar; + const truncatedNumeraire = MathFx.mulDownFixed( + this.amount, + parseUnits(this.latestFXPrice, this.fxOracleDecimals), + this.fxOracleDecimals, + ); + this.numeraire = truncatedNumeraire * this.scalar36; + return this; + } + + public decrease(amount: bigint): TokenAmount { + this.amount = this.amount - amount; + this.scale18 = this.amount * this.scalar; + const truncatedNumeraire = MathFx.mulDownFixed( + this.amount, + parseUnits(this.latestFXPrice, this.fxOracleDecimals), + this.fxOracleDecimals, + ); + this.numeraire = truncatedNumeraire * this.scalar36; + return this; + } + + public static fromNumeraire( + poolToken: FxPoolToken, + numeraire: BigintIsh, + divUp?: boolean, + ): FxPoolToken { + const truncatedNumeraire = BigInt(numeraire) / poolToken.scalar36; // loss of precision required to match SC implementation + const amount = divUp + ? MathFx.divUpFixed( + BigInt(truncatedNumeraire), + parseUnits( + poolToken.latestFXPrice, + poolToken.fxOracleDecimals, + ), + poolToken.fxOracleDecimals, + ) + : MathFx.divDownFixed( + BigInt(truncatedNumeraire), + parseUnits( + poolToken.latestFXPrice, + poolToken.fxOracleDecimals, + ), + poolToken.fxOracleDecimals, + ); + return new FxPoolToken( + poolToken.token, + amount, + poolToken.latestFXPrice, + poolToken.fxOracleDecimals, + poolToken.index, + ); + } +} diff --git a/src/entities/pools/fx/helpers.ts b/src/entities/pools/fx/helpers.ts index ddd9bb52..cfedbb46 100644 --- a/src/entities/pools/fx/helpers.ts +++ b/src/entities/pools/fx/helpers.ts @@ -1,5 +1,5 @@ import { parseUnits } from 'viem'; -import { HumanAmount } from '../../../types'; +import { HumanAmount } from '../../../data/types'; export class MathFx { static mulDownFixed(a: bigint, b: bigint, decimals = 36): bigint { diff --git a/src/entities/pools/fx/index.ts b/src/entities/pools/fx/index.ts index 6686b03e..ca789301 100644 --- a/src/entities/pools/fx/index.ts +++ b/src/entities/pools/fx/index.ts @@ -1,3 +1,5 @@ export * from './fxFactory'; export * from './fxPool'; export * from './fxMath'; +export * from './types'; +export * from './fxPoolToken'; diff --git a/src/entities/pools/fx/types.ts b/src/entities/pools/fx/types.ts new file mode 100644 index 00000000..c1a28fb0 --- /dev/null +++ b/src/entities/pools/fx/types.ts @@ -0,0 +1,17 @@ +import { SwapKind } from '../../../types'; +import { FxPoolToken } from './fxPoolToken'; + +export type FxPoolPairData = { + tIn: FxPoolToken; + tOut: FxPoolToken; + alpha: bigint; + beta: bigint; + delta: bigint; + lambda: bigint; + _oGLiq: bigint; + _nGLiq: bigint; + _oBals: bigint[]; + _nBals: bigint[]; + givenToken: FxPoolToken; + swapKind: SwapKind; +}; diff --git a/src/entities/pools/gyro2/gyro2Factory.ts b/src/entities/pools/gyro2/gyro2Factory.ts index a57272fa..9014c7a4 100644 --- a/src/entities/pools/gyro2/gyro2Factory.ts +++ b/src/entities/pools/gyro2/gyro2Factory.ts @@ -1,5 +1,5 @@ -import { Gyro2Pool } from '.'; -import { BasePool, BasePoolFactory } from '..'; +import { Gyro2Pool } from './gyro2Pool'; +import { BasePool, BasePoolFactory } from '../index'; import { RawGyro2Pool, RawPool } from '../../../data/types'; export class Gyro2PoolFactory implements BasePoolFactory { diff --git a/src/entities/pools/gyro2/gyro2Pool.ts b/src/entities/pools/gyro2/gyro2Pool.ts index 4e0711dd..bf9274a4 100644 --- a/src/entities/pools/gyro2/gyro2Pool.ts +++ b/src/entities/pools/gyro2/gyro2Pool.ts @@ -7,7 +7,8 @@ import { _findVirtualParams, } from './gyro2Math'; import { BasePool } from '..'; -import { BigintIsh, Token, TokenAmount } from '../..'; +import { Token } from '../../token'; +import { TokenAmount, BigintIsh } from '../../tokenAmount'; import { RawGyro2Pool } from '../../../data/types'; import { PoolType, SwapKind } from '../../../types'; import { getPoolAddress, MathSol, WAD } from '../../../utils'; diff --git a/src/entities/pools/gyro3/gyro3Factory.ts b/src/entities/pools/gyro3/gyro3Factory.ts index e938f5e0..75d038a3 100644 --- a/src/entities/pools/gyro3/gyro3Factory.ts +++ b/src/entities/pools/gyro3/gyro3Factory.ts @@ -1,5 +1,5 @@ -import { Gyro3Pool } from '.'; -import { BasePool, BasePoolFactory } from '..'; +import { Gyro3Pool } from './gyro3Pool'; +import { BasePool, BasePoolFactory } from '../index'; import { RawGyro3Pool, RawPool } from '../../../data/types'; export class Gyro3PoolFactory implements BasePoolFactory { diff --git a/src/entities/pools/gyro3/gyro3Pool.ts b/src/entities/pools/gyro3/gyro3Pool.ts index 0a7d3093..b4f7a6a2 100644 --- a/src/entities/pools/gyro3/gyro3Pool.ts +++ b/src/entities/pools/gyro3/gyro3Pool.ts @@ -6,7 +6,8 @@ import { _calculateInvariant, } from './gyro3Math'; import { BasePool } from '..'; -import { BigintIsh, Token, TokenAmount } from '../..'; +import { Token } from '../../token'; +import { TokenAmount, BigintIsh } from '../../tokenAmount'; import { RawGyro3Pool } from '../../../data/types'; import { PoolType, SwapKind } from '../../../types'; import { getPoolAddress, MathSol, WAD } from '../../../utils'; diff --git a/src/entities/pools/gyroE/gyroEFactory.ts b/src/entities/pools/gyroE/gyroEFactory.ts index 98a527d6..63e0816e 100644 --- a/src/entities/pools/gyroE/gyroEFactory.ts +++ b/src/entities/pools/gyroE/gyroEFactory.ts @@ -1,5 +1,5 @@ -import { GyroEPool } from '.'; -import { BasePool, BasePoolFactory } from '..'; +import { GyroEPool } from './gyroEPool'; +import { BasePool, BasePoolFactory } from '../index'; import { RawGyroEPool, RawPool } from '../../../data/types'; export class GyroEPoolFactory implements BasePoolFactory { diff --git a/src/entities/pools/gyroE/gyroEMath.ts b/src/entities/pools/gyroE/gyroEMath.ts index c5c909cc..690244ab 100644 --- a/src/entities/pools/gyroE/gyroEMath.ts +++ b/src/entities/pools/gyroE/gyroEMath.ts @@ -11,7 +11,7 @@ import { normalizedLiquidityXIn, normalizedLiquidityYIn, } from './gyroEMathFunctions'; -import { DerivedGyroEParams, GyroEParams, Vector2 } from './gyroEPool'; +import { DerivedGyroEParams, GyroEParams, Vector2 } from './types'; import { MathGyro, ONE_XP, SMALL } from '../../../utils/gyroHelpers/math'; export function calculateNormalizedLiquidity( diff --git a/src/entities/pools/gyroE/gyroEMathFunctions.ts b/src/entities/pools/gyroE/gyroEMathFunctions.ts index 48ad7bd7..2893c5d6 100644 --- a/src/entities/pools/gyroE/gyroEMathFunctions.ts +++ b/src/entities/pools/gyroE/gyroEMathFunctions.ts @@ -1,7 +1,7 @@ import { WAD } from '../../../utils'; import { MathGyro } from '../../../utils/gyroHelpers/math'; import { virtualOffset0, virtualOffset1 } from './gyroEMathHelpers'; -import { DerivedGyroEParams, GyroEParams, Vector2 } from './gyroEPool'; +import { DerivedGyroEParams, GyroEParams, Vector2 } from './types'; ///////// /// SPOT PRICE DERIVATIVE CALCULATIONS diff --git a/src/entities/pools/gyroE/gyroEMathHelpers.ts b/src/entities/pools/gyroE/gyroEMathHelpers.ts index 799d7970..7ba22411 100644 --- a/src/entities/pools/gyroE/gyroEMathHelpers.ts +++ b/src/entities/pools/gyroE/gyroEMathHelpers.ts @@ -1,5 +1,5 @@ import { MAX_BALANCES } from './constants'; -import { DerivedGyroEParams, GyroEParams, Vector2 } from './gyroEPool'; +import { DerivedGyroEParams, GyroEParams, Vector2 } from './types'; import { MathGyro, ONE_XP } from '../../../utils/gyroHelpers/math'; ///////// diff --git a/src/entities/pools/gyroE/gyroEPool.ts b/src/entities/pools/gyroE/gyroEPool.ts index 13868f64..4f165152 100644 --- a/src/entities/pools/gyroE/gyroEPool.ts +++ b/src/entities/pools/gyroE/gyroEPool.ts @@ -1,7 +1,8 @@ import { Hex, parseEther, parseUnits } from 'viem'; -import { BasePool } from '..'; -import { BigintIsh, Token, TokenAmount } from '../..'; +import { BasePool } from '../index'; +import { Token } from '../../token'; +import { TokenAmount, BigintIsh } from '../../tokenAmount'; import { RawGyroEPool } from '../../../data/types'; import { PoolType, SwapKind } from '../../../types'; import { MathSol, WAD, getPoolAddress } from '../../../utils'; @@ -16,6 +17,7 @@ import { calculateInvariantWithError, } from './gyroEMath'; import { MathGyro, SWAP_LIMIT_FACTOR } from '../../../utils/gyroHelpers/math'; +import { GyroEParams, Vector2, DerivedGyroEParams } from './types'; export class GyroEPoolToken extends TokenAmount { public readonly rate: bigint; @@ -46,29 +48,6 @@ export class GyroEPoolToken extends TokenAmount { } } -export type GyroEParams = { - alpha: bigint; - beta: bigint; - c: bigint; - s: bigint; - lambda: bigint; -}; - -export type Vector2 = { - x: bigint; - y: bigint; -}; - -export type DerivedGyroEParams = { - tauAlpha: Vector2; - tauBeta: Vector2; - u: bigint; - v: bigint; - w: bigint; - z: bigint; - dSq: bigint; -}; - export class GyroEPool implements BasePool { public readonly chainId: number; public readonly id: Hex; diff --git a/src/entities/pools/gyroE/index.ts b/src/entities/pools/gyroE/index.ts index 0fabeb53..d9d4c7c7 100644 --- a/src/entities/pools/gyroE/index.ts +++ b/src/entities/pools/gyroE/index.ts @@ -1,3 +1,4 @@ export * from './gyroEFactory'; export * from './gyroEPool'; export * from './gyroEMath'; +export * from './types'; diff --git a/src/entities/pools/gyroE/types.ts b/src/entities/pools/gyroE/types.ts new file mode 100644 index 00000000..5fda0b2b --- /dev/null +++ b/src/entities/pools/gyroE/types.ts @@ -0,0 +1,22 @@ +export type GyroEParams = { + alpha: bigint; + beta: bigint; + c: bigint; + s: bigint; + lambda: bigint; +}; + +export type Vector2 = { + x: bigint; + y: bigint; +}; + +export type DerivedGyroEParams = { + tauAlpha: Vector2; + tauBeta: Vector2; + u: bigint; + v: bigint; + w: bigint; + z: bigint; + dSq: bigint; +}; diff --git a/src/entities/pools/index.ts b/src/entities/pools/index.ts index fc7d6e9a..23298f2f 100644 --- a/src/entities/pools/index.ts +++ b/src/entities/pools/index.ts @@ -1,6 +1,7 @@ import { Hex } from 'viem'; import { PoolType, SwapKind } from '../../types'; -import { Token, TokenAmount } from '../'; +import { Token } from '../token'; +import { TokenAmount } from '../tokenAmount'; import { RawPool } from '../../data/types'; export interface BasePool { diff --git a/src/entities/pools/linear/index.ts b/src/entities/pools/linear/index.ts index e64a3b8e..f2a02c35 100644 --- a/src/entities/pools/linear/index.ts +++ b/src/entities/pools/linear/index.ts @@ -1,3 +1,4 @@ export * from './linearFactory'; export * from './linearPool'; export * from './linearMath'; +export * from './types'; diff --git a/src/entities/pools/linear/linearFactory.ts b/src/entities/pools/linear/linearFactory.ts index e53c7abb..d7e3aac1 100644 --- a/src/entities/pools/linear/linearFactory.ts +++ b/src/entities/pools/linear/linearFactory.ts @@ -1,5 +1,5 @@ -import { BasePool, BasePoolFactory } from '../'; -import { LinearPool } from './'; +import { BasePool, BasePoolFactory } from '../index'; +import { LinearPool } from './linearPool'; import { RawLinearPool, RawPool } from '../../../data/types'; export class LinearPoolFactory implements BasePoolFactory { diff --git a/src/entities/pools/linear/linearMath.ts b/src/entities/pools/linear/linearMath.ts index 56383314..8c18e0bb 100644 --- a/src/entities/pools/linear/linearMath.ts +++ b/src/entities/pools/linear/linearMath.ts @@ -1,5 +1,5 @@ -import { MathSol, WAD } from '../../../utils/'; -import { Params } from './'; +import { MathSol, WAD } from '../../../utils/math'; +import { Params } from './types'; export function _calcWrappedOutPerMainIn( mainIn: bigint, diff --git a/src/entities/pools/linear/linearPool.ts b/src/entities/pools/linear/linearPool.ts index 840831e4..2cefa1ec 100644 --- a/src/entities/pools/linear/linearPool.ts +++ b/src/entities/pools/linear/linearPool.ts @@ -1,6 +1,7 @@ import { Hex, parseEther } from 'viem'; import { PoolType, SwapKind } from '../../../types'; -import { BigintIsh, Token, TokenAmount } from '../../'; +import { Token } from '../../token'; +import { TokenAmount, BigintIsh } from '../../tokenAmount'; import { BasePool } from '../../pools'; import { getPoolAddress, MAX_UINT112, WAD } from '../../../utils'; import { @@ -19,6 +20,7 @@ import { } from './linearMath'; import { StablePoolToken } from '../stable/stablePool'; import { RawLinearPool } from '../../../data/types'; +import { Params } from './types'; const MAX_RATIO = parseEther('10'); const MAX_TOKEN_BALANCE = MAX_UINT112 - 1n; @@ -52,13 +54,6 @@ class BPT extends TokenAmount { } } -export type Params = { - fee: bigint; - rate: bigint; - lowerTarget: bigint; - upperTarget: bigint; -}; - export class LinearPool implements BasePool { public readonly chainId: number; public readonly id: Hex; diff --git a/src/entities/pools/linear/types.ts b/src/entities/pools/linear/types.ts new file mode 100644 index 00000000..399c93c1 --- /dev/null +++ b/src/entities/pools/linear/types.ts @@ -0,0 +1,6 @@ +export type Params = { + fee: bigint; + rate: bigint; + lowerTarget: bigint; + upperTarget: bigint; +}; diff --git a/src/entities/pools/metastable/metastableFactory.ts b/src/entities/pools/metastable/metastableFactory.ts index bd86bd09..e5190d79 100644 --- a/src/entities/pools/metastable/metastableFactory.ts +++ b/src/entities/pools/metastable/metastableFactory.ts @@ -1,5 +1,5 @@ -import { BasePool, BasePoolFactory } from '..'; -import { MetaStablePool } from '.'; +import { BasePool, BasePoolFactory } from '../index'; +import { MetaStablePool } from './metastablePool'; import { RawMetaStablePool, RawPool } from '../../../data/types'; export class MetaStablePoolFactory implements BasePoolFactory { diff --git a/src/entities/pools/stable/stableFactory.ts b/src/entities/pools/stable/stableFactory.ts index 3bc486af..1c313030 100644 --- a/src/entities/pools/stable/stableFactory.ts +++ b/src/entities/pools/stable/stableFactory.ts @@ -1,5 +1,5 @@ -import { BasePool, BasePoolFactory } from '../'; -import { StablePool } from './'; +import { BasePool, BasePoolFactory } from '../index'; +import { StablePool } from './stablePool'; import { RawComposableStablePool, RawPool } from '../../../data/types'; export class StablePoolFactory implements BasePoolFactory { diff --git a/src/entities/pools/stable/stablePool.ts b/src/entities/pools/stable/stablePool.ts index 3d4b2bfd..c3b8ce94 100644 --- a/src/entities/pools/stable/stablePool.ts +++ b/src/entities/pools/stable/stablePool.ts @@ -1,6 +1,7 @@ import { Hex, parseEther, parseUnits } from 'viem'; import { PoolType, SwapKind } from '../../../types'; -import { BigintIsh, Token, TokenAmount } from '../../'; +import { Token } from '../../token'; +import { TokenAmount, BigintIsh } from '../../tokenAmount'; import { BasePool } from '../'; import { getPoolAddress, MathSol, WAD } from '../../../utils'; import { diff --git a/src/entities/pools/weighted/weightedFactory.ts b/src/entities/pools/weighted/weightedFactory.ts index 53d5f300..5a36c0d8 100644 --- a/src/entities/pools/weighted/weightedFactory.ts +++ b/src/entities/pools/weighted/weightedFactory.ts @@ -1,5 +1,5 @@ -import { BasePool, BasePoolFactory } from '../'; -import { WeightedPool } from './'; +import { BasePool, BasePoolFactory } from '../index'; +import { WeightedPool } from './weightedPool'; import { RawPool, RawWeightedPool } from '../../../data/types'; export class WeightedPoolFactory implements BasePoolFactory { diff --git a/src/entities/pools/weighted/weightedPool.ts b/src/entities/pools/weighted/weightedPool.ts index baecc0f9..f47a5104 100644 --- a/src/entities/pools/weighted/weightedPool.ts +++ b/src/entities/pools/weighted/weightedPool.ts @@ -1,6 +1,7 @@ import { Hex, parseEther } from 'viem'; import { PoolType, SwapKind } from '../../../types'; -import { Token, TokenAmount, BigintIsh } from '../../'; +import { Token } from '../../token'; +import { TokenAmount, BigintIsh } from '../../tokenAmount'; import { BasePool } from '../'; import { MathSol, WAD, getPoolAddress } from '../../../utils'; import { _calcOutGivenIn, _calcInGivenOut } from './weightedMath'; diff --git a/src/entities/removeLiquidity/composable-stable/removeLiquidityComposableStable.ts b/src/entities/removeLiquidity/composable-stable/removeLiquidityComposableStable.ts new file mode 100644 index 00000000..e75e978c --- /dev/null +++ b/src/entities/removeLiquidity/composable-stable/removeLiquidityComposableStable.ts @@ -0,0 +1,215 @@ +import { encodeFunctionData } from 'viem'; +import { Token } from '../../token'; +import { TokenAmount } from '../../tokenAmount'; +import { Address } from '../../../types'; +import { + BALANCER_VAULT, + MAX_UINT256, + ZERO_ADDRESS, +} from '../../../utils/constants'; +import { vaultAbi } from '../../../abi'; +import { parseRemoveLiquidityArgs } from '../../utils/parseRemoveLiquidityArgs'; +import { + RemoveLiquidityBase, + RemoveLiquidityComposableStableCall, + RemoveLiquidityBuildOutput, + RemoveLiquidityInput, + RemoveLiquidityKind, + RemoveLiquidityQueryOutput, +} from '../types'; +import { RemoveLiquidityAmounts, PoolState } from '../../types'; +import { doRemoveLiquidityQuery } from '../../utils/doRemoveLiquidityQuery'; +import { ComposableStableEncoder } from '../../encoders/composableStable'; +import { getAmounts } from '../../utils'; + +export class RemoveLiquidityComposableStable implements RemoveLiquidityBase { + public async query( + input: RemoveLiquidityInput, + poolState: PoolState, + ): Promise { + const bptIndex = poolState.tokens.findIndex( + (t) => t.address === poolState.address, + ); + const amounts = this.getAmountsQuery(poolState.tokens, input, bptIndex); + const amountsWithoutBpt = { + ...amounts, + minAmountsOut: [ + ...amounts.minAmountsOut.slice(0, bptIndex), + ...amounts.minAmountsOut.slice(bptIndex + 1), + ], + }; + const userData = this.encodeUserData(input.kind, amountsWithoutBpt); + + // tokensOut will have zero address if removing liquidity to native asset + const { args, tokensOut } = parseRemoveLiquidityArgs({ + chainId: input.chainId, + toNativeAsset: !!input.toNativeAsset, + poolId: poolState.id, + sortedTokens: poolState.tokens, + sender: ZERO_ADDRESS, + recipient: ZERO_ADDRESS, + minAmountsOut: amounts.minAmountsOut, + userData, + toInternalBalance: !!input.toInternalBalance, + }); + const queryOutput = await doRemoveLiquidityQuery( + input.rpcUrl, + input.chainId, + args, + ); + const bpt = new Token(input.chainId, poolState.address, 18); + const bptIn = TokenAmount.fromRawAmount(bpt, queryOutput.bptIn); + + const amountsOut = queryOutput.amountsOut.map((a, i) => + TokenAmount.fromRawAmount(tokensOut[i], a), + ); + + return { + poolType: poolState.type, + removeLiquidityKind: input.kind, + poolId: poolState.id, + bptIn, + amountsOut, + tokenOutIndex: amounts.tokenOutIndex, + toInternalBalance: !!input.toInternalBalance, + bptIndex, + }; + } + + private getAmountsQuery( + tokens: Token[], + input: RemoveLiquidityInput, + bptIndex: number, + ): RemoveLiquidityAmounts { + switch (input.kind) { + case RemoveLiquidityKind.Unbalanced: + return { + minAmountsOut: getAmounts(tokens, input.amountsOut), + tokenOutIndex: undefined, + maxBptAmountIn: MAX_UINT256, + }; + case RemoveLiquidityKind.SingleToken: + return { + minAmountsOut: Array(tokens.length).fill(0n), + tokenOutIndex: tokens + .filter((_, index) => index !== bptIndex) + .findIndex((t) => t.isSameAddress(input.tokenOut)), + maxBptAmountIn: input.bptIn.rawAmount, + }; + case RemoveLiquidityKind.Proportional: + return { + minAmountsOut: Array(tokens.length).fill(0n), + tokenOutIndex: undefined, + maxBptAmountIn: input.bptIn.rawAmount, + }; + } + } + + public buildCall( + input: RemoveLiquidityComposableStableCall, + ): RemoveLiquidityBuildOutput { + const amounts = this.getAmountsCall(input); + const amountsWithoutBpt = { + ...amounts, + minAmountsOut: [ + ...amounts.minAmountsOut.slice(0, input.bptIndex), + ...amounts.minAmountsOut.slice(input.bptIndex + 1), + ], + }; + const userData = this.encodeUserData( + input.removeLiquidityKind, + amountsWithoutBpt, + ); + + const { args } = parseRemoveLiquidityArgs({ + poolId: input.poolId, + sortedTokens: input.amountsOut.map((a) => a.token), + sender: input.sender, + recipient: input.recipient, + minAmountsOut: amounts.minAmountsOut, + userData, + toInternalBalance: !!input.toInternalBalance, + }); + const call = encodeFunctionData({ + abi: vaultAbi, + functionName: 'exitPool', + args, + }); + + return { + call, + to: BALANCER_VAULT, + value: 0n, + maxBptIn: TokenAmount.fromRawAmount( + input.bptIn.token, + amounts.maxBptAmountIn, + ), + minAmountsOut: input.amountsOut.map((a, i) => + TokenAmount.fromRawAmount(a.token, amounts.minAmountsOut[i]), + ), + }; + } + + private getAmountsCall( + input: RemoveLiquidityComposableStableCall, + ): RemoveLiquidityAmounts { + switch (input.removeLiquidityKind) { + case RemoveLiquidityKind.Unbalanced: + return { + minAmountsOut: input.amountsOut.map((a) => a.amount), + tokenOutIndex: input.tokenOutIndex, + maxBptAmountIn: input.slippage.applyTo(input.bptIn.amount), + }; + case RemoveLiquidityKind.SingleToken: + if (input.tokenOutIndex === undefined) { + throw new Error( + 'tokenOutIndex must be defined for RemoveLiquiditySingleToken', + ); + } + return { + minAmountsOut: input.amountsOut.map((a) => + input.slippage.removeFrom(a.amount), + ), + tokenOutIndex: input.tokenOutIndex, + maxBptAmountIn: input.bptIn.amount, + }; + case RemoveLiquidityKind.Proportional: + return { + minAmountsOut: input.amountsOut.map((a) => + input.slippage.removeFrom(a.amount), + ), + tokenOutIndex: input.tokenOutIndex, + maxBptAmountIn: input.bptIn.amount, + }; + default: + throw Error('Unsupported Remove Liquidity Kind'); + } + } + + private encodeUserData( + kind: RemoveLiquidityKind, + amounts: RemoveLiquidityAmounts, + ): Address { + switch (kind) { + case RemoveLiquidityKind.Unbalanced: + return ComposableStableEncoder.removeLiquidityUnbalanced( + amounts.minAmountsOut, + amounts.maxBptAmountIn, + ); + case RemoveLiquidityKind.SingleToken: + if (amounts.tokenOutIndex === undefined) + throw Error('No Index'); + + return ComposableStableEncoder.removeLiquiditySingleToken( + amounts.maxBptAmountIn, + amounts.tokenOutIndex, + ); + case RemoveLiquidityKind.Proportional: + return ComposableStableEncoder.removeLiquidityProportional( + amounts.maxBptAmountIn, + ); + default: + throw Error('Unsupported Remove Liquidity Kind'); + } + } +} diff --git a/src/entities/removeLiquidity/index.ts b/src/entities/removeLiquidity/index.ts new file mode 100644 index 00000000..9c1cf5b4 --- /dev/null +++ b/src/entities/removeLiquidity/index.ts @@ -0,0 +1,2 @@ +export * from './removeLiquidity'; +export * from './types'; diff --git a/src/entities/removeLiquidity/removeLiquidity.ts b/src/entities/removeLiquidity/removeLiquidity.ts new file mode 100644 index 00000000..207c7e27 --- /dev/null +++ b/src/entities/removeLiquidity/removeLiquidity.ts @@ -0,0 +1,63 @@ +import { + RemoveLiquidityBase, + RemoveLiquidityBuildOutput, + RemoveLiquidityCall, + RemoveLiquidityConfig, + RemoveLiquidityInput, + RemoveLiquidityQueryOutput, +} from './types'; +import { RemoveLiquidityWeighted } from './weighted/removeLiquidityWeighted'; +import { PoolStateInput } from '../types'; +import { validateInputs } from './utils/validateInputs'; +import { getSortedTokens } from '../utils/getSortedTokens'; +import { RemoveLiquidityComposableStable } from './composable-stable/removeLiquidityComposableStable'; + +export class RemoveLiquidity { + private readonly removeLiquidityTypes: Record = + {}; + + constructor(config?: RemoveLiquidityConfig) { + const { customRemoveLiquidityTypes } = config || {}; + this.removeLiquidityTypes = { + //GYRO2, GYRO3, GYROE only support Remove Liquidity Proportional(1 - EXACT_BPT_IN_FOR_TOKENS_OUT) + GYRO2: new RemoveLiquidityWeighted(), + GYRO3: new RemoveLiquidityWeighted(), + GYROE: new RemoveLiquidityWeighted(), + WEIGHTED: new RemoveLiquidityWeighted(), + // PHANTOM_STABLE === ComposableStables in API + PHANTOM_STABLE: new RemoveLiquidityComposableStable(), + // custom remove liquidity types take precedence over base types + ...customRemoveLiquidityTypes, + }; + } + + public getRemoveLiquidity(poolType: string): RemoveLiquidityBase { + if (!this.removeLiquidityTypes[poolType]) { + throw new Error('Unsupported pool type'); + } + + return this.removeLiquidityTypes[poolType]; + } + + public async query( + input: RemoveLiquidityInput, + poolState: PoolStateInput, + ): Promise { + validateInputs(input, poolState); + + const sortedTokens = getSortedTokens(poolState.tokens, input.chainId); + const mappedPoolState = { + ...poolState, + tokens: sortedTokens, + }; + + return this.getRemoveLiquidity(poolState.type).query( + input, + mappedPoolState, + ); + } + + public buildCall(input: RemoveLiquidityCall): RemoveLiquidityBuildOutput { + return this.getRemoveLiquidity(input.poolType).buildCall(input); + } +} diff --git a/src/entities/removeLiquidity/types.ts b/src/entities/removeLiquidity/types.ts new file mode 100644 index 00000000..b90d2fcb --- /dev/null +++ b/src/entities/removeLiquidity/types.ts @@ -0,0 +1,100 @@ +import { TokenAmount } from '../tokenAmount'; +import { Slippage } from '../slippage'; +import { Address, InputAmount } from '../../types'; +import { PoolState } from '../types'; + +export enum RemoveLiquidityKind { + Unbalanced = 'Unbalanced', // exact out + SingleToken = 'SingleToken', // exact in (single token out) + Proportional = 'Proportional', // exact in (all tokens out) +} + +// This will be extended for each pools specific output requirements +export type RemoveLiquidityBaseInput = { + chainId: number; + rpcUrl: string; + toNativeAsset?: boolean; + toInternalBalance?: boolean; +}; + +export type RemoveLiquidityUnbalancedInput = RemoveLiquidityBaseInput & { + amountsOut: InputAmount[]; + kind: RemoveLiquidityKind.Unbalanced; +}; + +export type RemoveLiquiditySingleTokenInput = RemoveLiquidityBaseInput & { + bptIn: InputAmount; + tokenOut: Address; + kind: RemoveLiquidityKind.SingleToken; +}; + +export type RemoveLiquidityProportionalInput = RemoveLiquidityBaseInput & { + bptIn: InputAmount; + kind: RemoveLiquidityKind.Proportional; +}; + +export type RemoveLiquidityInput = + | RemoveLiquidityUnbalancedInput + | RemoveLiquiditySingleTokenInput + | RemoveLiquidityProportionalInput; + +export type RemoveLiquidityQueryOutput = + | RemoveLiquidityBaseQueryOutput + | RemoveLiquidityComposableStableQueryOutput; + +// Returned from a remove liquidity query +export type RemoveLiquidityBaseQueryOutput = { + poolType: string; + poolId: Address; + removeLiquidityKind: RemoveLiquidityKind; + bptIn: TokenAmount; + amountsOut: TokenAmount[]; + tokenOutIndex?: number; + toInternalBalance: boolean; +}; + +export type RemoveLiquidityComposableStableQueryOutput = + RemoveLiquidityBaseQueryOutput & { + bptIndex: number; + }; + +type RemoveLiquidityBaseCall = { + slippage: Slippage; + sender: Address; + recipient: Address; +}; +export type RemoveLiquidityComposableStableCall = RemoveLiquidityBaseCall & + RemoveLiquidityComposableStableQueryOutput; +export type RemoveLiquidityWeightedCall = RemoveLiquidityBaseCall & + RemoveLiquidityBaseQueryOutput; + +export type RemoveLiquidityCall = + | RemoveLiquidityComposableStableCall + | RemoveLiquidityWeightedCall; + +export type RemoveLiquidityBuildOutput = { + call: Address; + to: Address; + value: bigint; + maxBptIn: TokenAmount; + minAmountsOut: TokenAmount[]; +}; + +export interface RemoveLiquidityBase { + query( + input: RemoveLiquidityInput, + poolState: PoolState, + ): Promise; + buildCall(input: RemoveLiquidityCall): RemoveLiquidityBuildOutput; +} + +export type RemoveLiquidityConfig = { + customRemoveLiquidityTypes: Record; +}; + +export type ExitPoolRequest = { + assets: Address[]; + minAmountsOut: bigint[]; + userData: Address; + toInternalBalance: boolean; +}; diff --git a/src/entities/removeLiquidity/utils/validateInputs.ts b/src/entities/removeLiquidity/utils/validateInputs.ts new file mode 100644 index 00000000..beb84b27 --- /dev/null +++ b/src/entities/removeLiquidity/utils/validateInputs.ts @@ -0,0 +1,62 @@ +import { RemoveLiquidityInput, RemoveLiquidityKind } from '../types'; +import { PoolStateInput } from '../../types'; +import { areTokensInArray } from '../../utils/areTokensInArray'; +import { Address } from 'viem'; +import { MinimalToken } from '../../../data'; + +export function validateInputs( + input: RemoveLiquidityInput, + poolState: PoolStateInput, +) { + validateComposableStableWithBPT( + poolState.type, + poolState.address, + poolState.tokens, + ); + validateRemoveLiquidityGyroIsProportional(input.kind, poolState.type); + switch (input.kind) { + case RemoveLiquidityKind.Unbalanced: + areTokensInArray( + input.amountsOut.map((a) => a.address), + poolState.tokens.map((t) => t.address), + ); + break; + case RemoveLiquidityKind.SingleToken: + areTokensInArray( + [input.tokenOut], + poolState.tokens.map((t) => t.address), + ); + case RemoveLiquidityKind.Proportional: + areTokensInArray([input.bptIn.address], [poolState.address]); + default: + break; + } +} + +export const removeLiquidityKindNotSupportedByGyro = + 'INPUT_ERROR: Gyro pools do not implement this remove liquidity kind, only Remove Liquidity Proportional (1 - EXACT_BPT_IN_FOR_TOKENS_OUT) is supported'; + +function validateRemoveLiquidityGyroIsProportional( + kind: RemoveLiquidityKind, + poolType: string, +) { + if ( + ['GYROE', 'GYRO2', 'GYRO3'].includes(poolType) && + kind !== RemoveLiquidityKind.Proportional + ) { + throw new Error(removeLiquidityKindNotSupportedByGyro); + } +} + +function validateComposableStableWithBPT( + poolType: string, + poolAddress: Address, + poolTokens: MinimalToken[], +) { + const bptIndex = poolTokens.findIndex((t) => t.address === poolAddress); + if (['PHANTOM_STABLE'].includes(poolType) && bptIndex < 0) { + throw new Error( + 'INPUT_ERROR: Composable Stable Pool State should have BPT token included', + ); + } +} diff --git a/src/entities/removeLiquidity/weighted/removeLiquidityWeighted.ts b/src/entities/removeLiquidity/weighted/removeLiquidityWeighted.ts new file mode 100644 index 00000000..2b1aa6fa --- /dev/null +++ b/src/entities/removeLiquidity/weighted/removeLiquidityWeighted.ts @@ -0,0 +1,200 @@ +import { encodeFunctionData } from 'viem'; +import { Token } from '../../token'; +import { TokenAmount } from '../../tokenAmount'; +import { WeightedEncoder } from '../../encoders/weighted'; +import { Address } from '../../../types'; +import { + BALANCER_VAULT, + MAX_UINT256, + ZERO_ADDRESS, +} from '../../../utils/constants'; +import { vaultAbi } from '../../../abi'; +import { parseRemoveLiquidityArgs } from '../../utils/parseRemoveLiquidityArgs'; +import { + RemoveLiquidityBase, + RemoveLiquidityBuildOutput, + RemoveLiquidityCall, + RemoveLiquidityInput, + RemoveLiquidityKind, + RemoveLiquidityQueryOutput, + RemoveLiquidityWeightedCall, +} from '../types'; +import { RemoveLiquidityAmounts, PoolState } from '../../types'; +import { doRemoveLiquidityQuery } from '../../utils/doRemoveLiquidityQuery'; +import { getAmounts } from '../../utils'; + +export class RemoveLiquidityWeighted implements RemoveLiquidityBase { + public async query( + input: RemoveLiquidityInput, + poolState: PoolState, + ): Promise { + const amounts = this.getAmountsQuery(poolState.tokens, input); + + const userData = this.encodeUserData(input.kind, amounts); + + // tokensOut will have zero address if removing liquidity to native asset + const { args, tokensOut } = parseRemoveLiquidityArgs({ + chainId: input.chainId, + toNativeAsset: !!input.toNativeAsset, + poolId: poolState.id, + sortedTokens: poolState.tokens, + sender: ZERO_ADDRESS, + recipient: ZERO_ADDRESS, + minAmountsOut: amounts.minAmountsOut, + userData, + toInternalBalance: !!input.toInternalBalance, + }); + + const queryOutput = await doRemoveLiquidityQuery( + input.rpcUrl, + input.chainId, + args, + ); + + const bpt = new Token(input.chainId, poolState.address, 18); + const bptIn = TokenAmount.fromRawAmount(bpt, queryOutput.bptIn); + + const amountsOut = queryOutput.amountsOut.map((a, i) => + TokenAmount.fromRawAmount(tokensOut[i], a), + ); + + return { + poolType: poolState.type, + removeLiquidityKind: input.kind, + poolId: poolState.id, + bptIn, + amountsOut, + tokenOutIndex: amounts.tokenOutIndex, + toInternalBalance: !!input.toInternalBalance, + }; + } + + private getAmountsQuery( + tokens: Token[], + input: RemoveLiquidityInput, + ): RemoveLiquidityAmounts { + switch (input.kind) { + case RemoveLiquidityKind.Unbalanced: + return { + minAmountsOut: getAmounts(tokens, input.amountsOut), + tokenOutIndex: undefined, + maxBptAmountIn: MAX_UINT256, + }; + case RemoveLiquidityKind.SingleToken: + return { + minAmountsOut: Array(tokens.length).fill(0n), + tokenOutIndex: tokens.findIndex((t) => + t.isSameAddress(input.tokenOut), + ), + maxBptAmountIn: input.bptIn.rawAmount, + }; + case RemoveLiquidityKind.Proportional: + return { + minAmountsOut: Array(tokens.length).fill(0n), + tokenOutIndex: undefined, + maxBptAmountIn: input.bptIn.rawAmount, + }; + } + } + + public buildCall( + input: RemoveLiquidityWeightedCall, + ): RemoveLiquidityBuildOutput { + const amounts = this.getAmountsCall(input); + + const userData = this.encodeUserData( + input.removeLiquidityKind, + amounts, + ); + + const { args } = parseRemoveLiquidityArgs({ + poolId: input.poolId, + sortedTokens: input.amountsOut.map((a) => a.token), + sender: input.sender, + recipient: input.recipient, + minAmountsOut: amounts.minAmountsOut, + userData, + toInternalBalance: !!input.toInternalBalance, + }); + + const call = encodeFunctionData({ + abi: vaultAbi, + functionName: 'exitPool', + args, + }); + + return { + call, + to: BALANCER_VAULT, + value: 0n, + maxBptIn: TokenAmount.fromRawAmount( + input.bptIn.token, + amounts.maxBptAmountIn, + ), + minAmountsOut: input.amountsOut.map((a, i) => + TokenAmount.fromRawAmount(a.token, amounts.minAmountsOut[i]), + ), + }; + } + + private getAmountsCall(input: RemoveLiquidityCall): RemoveLiquidityAmounts { + switch (input.removeLiquidityKind) { + case RemoveLiquidityKind.Unbalanced: + return { + minAmountsOut: input.amountsOut.map((a) => a.amount), + tokenOutIndex: input.tokenOutIndex, + maxBptAmountIn: input.slippage.applyTo(input.bptIn.amount), + }; + case RemoveLiquidityKind.SingleToken: + if (input.tokenOutIndex === undefined) { + throw new Error( + 'tokenOutIndex must be defined for RemoveLiquiditySingleToken', + ); + } + return { + minAmountsOut: input.amountsOut.map((a) => + input.slippage.removeFrom(a.amount), + ), + tokenOutIndex: input.tokenOutIndex, + maxBptAmountIn: input.bptIn.amount, + }; + case RemoveLiquidityKind.Proportional: + return { + minAmountsOut: input.amountsOut.map((a) => + input.slippage.removeFrom(a.amount), + ), + tokenOutIndex: input.tokenOutIndex, + maxBptAmountIn: input.bptIn.amount, + }; + default: + throw Error('Unsupported Remove Liquidity Kind'); + } + } + + private encodeUserData( + kind: RemoveLiquidityKind, + amounts: RemoveLiquidityAmounts, + ): Address { + switch (kind) { + case RemoveLiquidityKind.Unbalanced: + return WeightedEncoder.removeLiquidityUnbalanced( + amounts.minAmountsOut, + amounts.maxBptAmountIn, + ); + case RemoveLiquidityKind.SingleToken: + if (amounts.tokenOutIndex === undefined) + throw Error('No Index'); + + return WeightedEncoder.removeLiquiditySingleToken( + amounts.maxBptAmountIn, + amounts.tokenOutIndex, + ); + case RemoveLiquidityKind.Proportional: + return WeightedEncoder.removeLiquidityProportional( + amounts.maxBptAmountIn, + ); + default: + throw Error('Unsupported Remove Liquidity Kind'); + } + } +} diff --git a/src/entities/slippage.ts b/src/entities/slippage.ts new file mode 100644 index 00000000..7234ac0c --- /dev/null +++ b/src/entities/slippage.ts @@ -0,0 +1,44 @@ +import { formatEther, parseEther } from 'viem'; +import { BigintIsh } from './tokenAmount'; +import { MathSol, WAD } from '../utils'; + +export class Slippage { + public amount: bigint; + public decimal: number; + public percentage: number; + public bps: number; + + public static fromRawAmount(rawAmount: BigintIsh) { + return new Slippage(rawAmount); + } + + public static fromDecimal(decimalAmount: `${number}`) { + const rawAmount = parseEther(decimalAmount); + return Slippage.fromRawAmount(rawAmount); + } + + public static fromPercentage(percentageAmount: `${number}`) { + const decimalAmount = Number(percentageAmount) / 100; + return Slippage.fromDecimal(`${decimalAmount}`); + } + + public static fromBasisPoints(bpsAmount: `${number}`) { + const decimalAmount = Number(bpsAmount) / 10000; + return Slippage.fromDecimal(`${decimalAmount}`); + } + + protected constructor(amount: BigintIsh) { + this.amount = BigInt(amount); + this.decimal = parseFloat(formatEther(this.amount)); + this.percentage = this.decimal * 100; + this.bps = this.decimal * 10000; + } + + public applyTo(amount: bigint): bigint { + return MathSol.mulDownFixed(amount, this.amount + WAD); + } + + public removeFrom(amount: bigint): bigint { + return MathSol.divDownFixed(amount, this.amount + WAD); + } +} diff --git a/src/entities/token.ts b/src/entities/token.ts index 59dbdd9f..c27bb98b 100644 --- a/src/entities/token.ts +++ b/src/entities/token.ts @@ -5,7 +5,7 @@ export class Token { public readonly decimals: number; public readonly symbol?: string; public readonly name?: string; - public readonly wrapped: string; + public readonly wrapped: Address; public constructor( chainId: number, @@ -13,7 +13,7 @@ export class Token { decimals: number, symbol?: string, name?: string, - wrapped?: string, + wrapped?: Address, ) { this.chainId = chainId; // Addresses are always lowercased for speed @@ -21,7 +21,9 @@ export class Token { this.decimals = decimals; this.symbol = symbol; this.name = name; - this.wrapped = wrapped ? wrapped.toLowerCase() : address.toLowerCase(); + this.wrapped = ( + wrapped ? wrapped.toLowerCase() : address.toLowerCase() + ) as Address; } public isEqual(token: Token) { @@ -31,4 +33,8 @@ export class Token { public isUnderlyingEqual(token: Token) { return this.chainId === token.chainId && this.wrapped === token.wrapped; } + + public isSameAddress(address: Address) { + return this.address === address.toLowerCase(); + } } diff --git a/src/entities/tokenAmount.ts b/src/entities/tokenAmount.ts index e41397bd..580098ee 100644 --- a/src/entities/tokenAmount.ts +++ b/src/entities/tokenAmount.ts @@ -1,7 +1,9 @@ import { Token } from './token'; import _Decimal from 'decimal.js-light'; import { parseUnits } from 'viem'; -import { DECIMAL_SCALES, WAD } from '../utils'; +import { DECIMAL_SCALES } from '../utils/constants'; +import { WAD } from '../utils/math'; +import { InputAmount } from '../types'; export type BigintIsh = bigint | string | number; @@ -76,4 +78,12 @@ export class TokenAmount { .toDecimalPlaces(significantDigits) .toString(); } + + public toInputAmount(): InputAmount { + return { + address: this.token.address, + decimals: this.token.decimals, + rawAmount: this.amount, + }; + } } diff --git a/src/entities/types.ts b/src/entities/types.ts new file mode 100644 index 00000000..864f99ba --- /dev/null +++ b/src/entities/types.ts @@ -0,0 +1,30 @@ +import { MinimalToken } from '../data'; +import { Address, Hex } from '../types'; +import { Token } from './token'; + +// Returned from API and used as input +export type PoolState = { + id: Hex; + address: Address; + type: string; + tokens: Token[]; +}; + +export type PoolStateInput = { + id: Hex; + address: Address; + type: string; + tokens: MinimalToken[]; +}; + +export type AddLiquidityAmounts = { + maxAmountsIn: bigint[]; + tokenInIndex: number | undefined; + minimumBpt: bigint; +}; + +export type RemoveLiquidityAmounts = { + minAmountsOut: bigint[]; + tokenOutIndex: number | undefined; + maxBptAmountIn: bigint; +}; diff --git a/src/entities/utils/areTokensInArray.ts b/src/entities/utils/areTokensInArray.ts new file mode 100644 index 00000000..568e2dce --- /dev/null +++ b/src/entities/utils/areTokensInArray.ts @@ -0,0 +1,11 @@ +import { Address } from '../../types'; + +export function areTokensInArray(tokens: Address[], tokenArray: Address[]) { + const sanitisedTokens = tokens.map((t) => t.toLowerCase() as Address); + const sanitisedTokenArray = tokenArray.map((t) => t.toLowerCase()); + for (const token of sanitisedTokens) { + if (!sanitisedTokenArray.includes(token)) { + throw new Error(`Token ${token} not found in array`); + } + } +} diff --git a/src/entities/utils/doAddLiquidityQuery.ts b/src/entities/utils/doAddLiquidityQuery.ts new file mode 100644 index 00000000..d328f7be --- /dev/null +++ b/src/entities/utils/doAddLiquidityQuery.ts @@ -0,0 +1,41 @@ +import { createPublicClient, http } from 'viem'; +import { Address } from '../../types'; +import { BALANCER_HELPERS, CHAINS } from '../../utils'; +import { balancerHelpersAbi } from '../../abi'; + +export async function doAddLiquidityQuery( + rpcUrl: string, + chainId: number, + args: readonly [ + Address, + Address, + Address, + { + assets: readonly Address[]; + maxAmountsIn: readonly bigint[]; + userData: Address; + fromInternalBalance: boolean; + }, + ], +): Promise<{ + bptOut: bigint; + amountsIn: readonly bigint[]; +}> { + const client = createPublicClient({ + transport: http(rpcUrl), + chain: CHAINS[chainId], + }); + + const { + result: [bptOut, amountsIn], + } = await client.simulateContract({ + address: BALANCER_HELPERS[chainId], + abi: balancerHelpersAbi, + functionName: 'queryJoin', + args, + }); + return { + bptOut, + amountsIn, + }; +} diff --git a/src/entities/utils/doRemoveLiquidityQuery.ts b/src/entities/utils/doRemoveLiquidityQuery.ts new file mode 100644 index 00000000..3001fa3d --- /dev/null +++ b/src/entities/utils/doRemoveLiquidityQuery.ts @@ -0,0 +1,33 @@ +import { createPublicClient, http } from 'viem'; +import { Address } from '../../types'; +import { BALANCER_HELPERS, CHAINS } from '../../utils/constants'; +import { balancerHelpersAbi } from '../../abi'; +import { ExitPoolRequest } from '../removeLiquidity/types'; + +export async function doRemoveLiquidityQuery( + rpcUrl: string, + chainId: number, + args: readonly [Address, Address, Address, ExitPoolRequest], +): Promise<{ + bptIn: bigint; + amountsOut: readonly bigint[]; +}> { + const client = createPublicClient({ + transport: http(rpcUrl), + chain: CHAINS[chainId], + }); + + const { + result: [bptIn, amountsOut], + } = await client.simulateContract({ + address: BALANCER_HELPERS[chainId], + abi: balancerHelpersAbi, + functionName: 'queryExit', + args, + }); + + return { + bptIn, + amountsOut, + }; +} diff --git a/src/entities/utils/getAmounts.ts b/src/entities/utils/getAmounts.ts new file mode 100644 index 00000000..7fc1d351 --- /dev/null +++ b/src/entities/utils/getAmounts.ts @@ -0,0 +1,21 @@ +import { InputAmount } from '../../types'; +import { Token } from '../token'; + +/** + * Get amounts from array of TokenAmounts returning default if not a value for tokens. + * @param tokens + * @param amounts + * @param defaultAmount + * @returns + */ +export function getAmounts( + tokens: Token[], + amounts: InputAmount[], + defaultAmount = 0n, +): bigint[] { + return tokens.map( + (t) => + amounts.find((a) => t.isSameAddress(a.address))?.rawAmount ?? + defaultAmount, + ); +} diff --git a/src/entities/utils/getSortedTokens.ts b/src/entities/utils/getSortedTokens.ts new file mode 100644 index 00000000..3d28df09 --- /dev/null +++ b/src/entities/utils/getSortedTokens.ts @@ -0,0 +1,11 @@ +import { MinimalToken } from '../../data/types'; +import { Token } from '../token'; + +export function getSortedTokens( + tokens: MinimalToken[], + chainId: number, +): Token[] { + return tokens + .sort((a, b) => a.index - b.index) + .map((t) => new Token(chainId, t.address, t.decimals)); +} diff --git a/src/entities/utils/index.ts b/src/entities/utils/index.ts new file mode 100644 index 00000000..ed6f7e2f --- /dev/null +++ b/src/entities/utils/index.ts @@ -0,0 +1,5 @@ +export * from './doAddLiquidityQuery'; +export * from './getAmounts'; +export * from './getSortedTokens'; +export * from './parseAddLiquidityArgs'; +export * from './replaceWrapped'; diff --git a/src/entities/utils/parseAddLiquidityArgs.ts b/src/entities/utils/parseAddLiquidityArgs.ts new file mode 100644 index 00000000..77051a08 --- /dev/null +++ b/src/entities/utils/parseAddLiquidityArgs.ts @@ -0,0 +1,43 @@ +import { Address, Hex } from '../../types'; +import { Token } from '../token'; +import { replaceWrapped } from './replaceWrapped'; + +export function parseAddLiquidityArgs({ + useNativeAssetAsWrappedAmountIn, + chainId, + sortedTokens, + poolId, + sender, + recipient, + maxAmountsIn, + userData, + fromInternalBalance, +}: { + chainId?: number; + useNativeAssetAsWrappedAmountIn?: boolean; + sortedTokens: Token[]; + poolId: Hex; + sender: Address; + recipient: Address; + maxAmountsIn: readonly bigint[]; + userData: Hex; + fromInternalBalance: boolean; +}) { + // replace wrapped token with native asset if needed + const tokensIn = + chainId && useNativeAssetAsWrappedAmountIn + ? replaceWrapped([...sortedTokens], chainId) + : [...sortedTokens]; + + const joinPoolRequest = { + assets: tokensIn.map((t) => t.address), // with BPT + maxAmountsIn, // with BPT + userData, // wihtout BPT + fromInternalBalance, + }; + + return { + args: [poolId, sender, recipient, joinPoolRequest] as const, + tokensIn, + }; +} diff --git a/src/entities/utils/parseRemoveLiquidityArgs.ts b/src/entities/utils/parseRemoveLiquidityArgs.ts new file mode 100644 index 00000000..6e54b5c9 --- /dev/null +++ b/src/entities/utils/parseRemoveLiquidityArgs.ts @@ -0,0 +1,44 @@ +import { Address } from '../../types'; +import { Token } from '../token'; +import { ExitPoolRequest } from '../removeLiquidity/types'; +import { replaceWrapped } from './replaceWrapped'; + +export function parseRemoveLiquidityArgs({ + chainId, + toNativeAsset, + sortedTokens, + poolId, + sender, + recipient, + minAmountsOut, + userData, + toInternalBalance, +}: { + chainId?: number; + toNativeAsset?: boolean; + sortedTokens: Token[]; + poolId: Address; + sender: Address; + recipient: Address; + minAmountsOut: bigint[]; + userData: Address; + toInternalBalance: boolean; +}) { + // replace wrapped token with native asset if needed + const tokensOut = + chainId && toNativeAsset + ? replaceWrapped([...sortedTokens], chainId) + : [...sortedTokens]; + + const exitPoolRequest: ExitPoolRequest = { + assets: tokensOut.map((t) => t.address), // with BPT + minAmountsOut, // with BPT + userData, // wihtout BPT + toInternalBalance, + }; + + return { + args: [poolId, sender, recipient, exitPoolRequest] as const, + tokensOut, + }; +} diff --git a/src/entities/utils/replaceWrapped.ts b/src/entities/utils/replaceWrapped.ts new file mode 100644 index 00000000..2da576d4 --- /dev/null +++ b/src/entities/utils/replaceWrapped.ts @@ -0,0 +1,12 @@ +import { Token } from '../token'; +import { NATIVE_ASSETS, ZERO_ADDRESS } from '../../utils'; + +export function replaceWrapped(tokens: Token[], chainId: number): Token[] { + return tokens.map((token) => { + if (token.isUnderlyingEqual(NATIVE_ASSETS[chainId])) { + return new Token(chainId, ZERO_ADDRESS, 18); + } else { + return token; + } + }); +} diff --git a/src/pathGraph/pathGraphTypes.ts b/src/pathGraph/pathGraphTypes.ts index 673a4a99..4a08bbf2 100644 --- a/src/pathGraph/pathGraphTypes.ts +++ b/src/pathGraph/pathGraphTypes.ts @@ -1,5 +1,12 @@ -import { PoolTokenPair } from '../types'; -import { BasePool, Token } from '../entities'; +import { Token } from '../entities/token'; +import { BasePool } from '../entities/pools/index'; + +export interface PoolTokenPair { + id: string; + pool: BasePool; + tokenIn: Token; + tokenOut: Token; +} export type PoolAddressDictionary = { [address: string]: BasePool; diff --git a/src/types.ts b/src/types.ts index c7a66698..b0159b2d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ -import { BigintIsh, Token, BasePool, BasePoolFactory } from './entities'; import { PoolDataEnricher, PoolDataProvider } from './data/types'; import { PathGraphTraversalConfig } from './pathGraph/pathGraphTypes'; +import { BigintIsh } from './entities/tokenAmount'; +import { BasePoolFactory } from './entities/pools/index'; export type Address = `0x${string}`; export type Hex = `0x${string}`; @@ -45,13 +46,6 @@ export type SorConfig = { customPoolFactories?: BasePoolFactory[]; }; -export interface PoolTokenPair { - id: string; - pool: BasePool; - tokenIn: Token; - tokenOut: Token; -} - export interface SingleSwap { poolId: Hex; kind: SwapKind; @@ -69,4 +63,8 @@ export interface BatchSwapStep { userData: Hex; } -export type HumanAmount = `${number}`; +export type InputAmount = { + address: Address; + decimals: number; + rawAmount: bigint; +}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 02bd4737..d6b031db 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -22,6 +22,8 @@ export const NATIVE_ADDRESS: Address = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; export const MAX_UINT112 = 5192296858534827628530496329220095n; +export const MAX_UINT256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; export const PREMINTED_STABLE_BPT = 2596148429267413814265248164610048n; // 2**111 export const DECIMAL_SCALES = { @@ -149,6 +151,18 @@ export const BATCHSIZE: Record = { [ChainId.FANTOM]: 128, }; +export const BALANCER_VAULT = '0xBA12222222228d8Ba445958a75a0704d566BF2C8'; + +export const BALANCER_HELPERS: Record = { + [ChainId.ARBITRUM_ONE]: '0x77d46184d22ca6a3726a2f500c776767b6a3d6ab', + [ChainId.AVALANCHE]: '0x8e9aa87e45e92bad84d5f8dd1bff34fb92637de9', + [ChainId.GNOSIS_CHAIN]: '0x8e9aa87e45e92bad84d5f8dd1bff34fb92637de9', + [ChainId.MAINNET]: '0x5addcca35b7a0d07c74063c48700c8590e87864e', + [ChainId.OPTIMISM]: '0x8e9aa87e45e92bad84d5f8dd1bff34fb92637de9', + [ChainId.POLYGON]: '0x239e55f427d44c3cc793f49bfb507ebe76638a2b', + [ChainId.ZKEVM]: '0x8e9aa87e45e92bad84d5f8dd1bff34fb92637de9', +}; + export const NATIVE_ASSETS = { [ChainId.MAINNET]: new Token( ChainId.MAINNET, diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 02620be0..f895d370 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,4 +1,5 @@ -import { BigintIsh, Token, TokenAmount } from '../entities'; +import { Token } from '../entities/token'; +import { TokenAmount, BigintIsh } from '../entities/tokenAmount'; import { SwapKind } from '../types'; export function checkInputs( diff --git a/test/addLiquidityComposableStable.integration.test.ts b/test/addLiquidityComposableStable.integration.test.ts new file mode 100644 index 00000000..9262c258 --- /dev/null +++ b/test/addLiquidityComposableStable.integration.test.ts @@ -0,0 +1,281 @@ +// pnpm test -- addLiquidityComposableStable.integration.test.ts +import { config } from 'dotenv'; +config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + walletActions, + parseEther, +} from 'viem'; + +import { + AddLiquidityUnbalancedInput, + AddLiquidityProportionalInput, + AddLiquiditySingleTokenInput, + AddLiquidityKind, + Slippage, + Address, + Hex, + PoolStateInput, + CHAINS, + ChainId, + getPoolAddress, + AddLiquidity, + AddLiquidityInput, + InputAmount, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { AddLiquidityTxInput } from './lib/utils/types'; +import { + doAddLiquidity, + assertAddLiquidityUnbalanced, + assertAddLiquiditySingleToken, + assertAddLiquidityProportional, +} from './lib/utils/addLiquidityHelper'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; + +const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET); +const chainId = ChainId.MAINNET; +const poolId = + '0x156c02f3f7fef64a3a9d80ccf7085f23cce91d76000000000000000000000570'; // Balancer vETH/WETH StablePool + +describe('add liquidity composable stable test', () => { + let txInput: AddLiquidityTxInput; + let poolStateInput: PoolStateInput; + + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolStateInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + addLiquidity: new AddLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput: poolStateInput, + testAddress: '0x10a19e7ee7d7f8a52822f6817de8ea18204f2e4f', // Balancer DAO Multisig + addLiquidityInput: {} as AddLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [...txInput.poolStateInput.tokens.map((t) => t.address)], + [0, 0, 3], + [ + ...txInput.poolStateInput.tokens.map((t) => + parseUnits('100', t.decimals), + ), + ], + ); + }); + + describe('add liquidity unbalanced', () => { + let input: Omit; + let amountsIn: InputAmount[]; + beforeAll(() => { + const bptIndex = txInput.poolStateInput.tokens.findIndex( + (t) => t.address === txInput.poolStateInput.address, + ); + amountsIn = txInput.poolStateInput.tokens + .map((t) => ({ + rawAmount: parseUnits('1', t.decimals), + decimals: t.decimals, + address: t.address, + })) + .filter((_, index) => index !== bptIndex); + input = { + chainId, + rpcUrl, + kind: AddLiquidityKind.Unbalanced, + }; + }); + test('token inputs', async () => { + const addLiquidityInput = { + ...input, + amountsIn: [...amountsIn.splice(0, 1)], + }; + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + assertAddLiquidityUnbalanced( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + + test('with native', async () => { + const addLiquidityInput = { + ...input, + amountsIn, + useNativeAssetAsWrappedAmountIn: true, + }; + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + assertAddLiquidityUnbalanced( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('add liquidity single asset', () => { + let input: AddLiquiditySingleTokenInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + const tokenIn = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + input = { + bptOut, + tokenIn, + chainId, + rpcUrl, + kind: AddLiquidityKind.SingleToken, + }; + }); + test('with token', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput: input, + }); + + assertAddLiquiditySingleToken( + txInput.client.chain?.id as number, + txInput.poolStateInput, + input, + addLiquidityOutput, + txInput.slippage, + ); + }); + + test('with native', async () => { + const addLiquidityInput = { + ...input, + useNativeAssetAsWrappedAmountIn: true, + }; + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + + assertAddLiquiditySingleToken( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('add liquidity proportional', () => { + let input: AddLiquidityProportionalInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + input = { + bptOut, + chainId, + rpcUrl, + kind: AddLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput: input, + }); + + assertAddLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + input, + addLiquidityOutput, + txInput.slippage, + ); + }); + test('with native', async () => { + const addLiquidityInput = { + ...input, + useNativeAssetAsWrappedAmountIn: true, + }; + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + assertAddLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + const tokens = [ + { + address: + '0x156c02f3f7fef64a3a9d80ccf7085f23cce91d76' as Address, // vETH/WETH BPT + decimals: 18, + index: 0, + }, + { + address: + '0x4bc3263eb5bb2ef7ad9ab6fb68be80e43b43801f' as Address, // VETH + decimals: 18, + index: 1, + }, + { + address: + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' as Address, // WETH + decimals: 18, + index: 2, + }, + ]; + + return { + id, + address: getPoolAddress(id) as Address, + type: 'PHANTOM_STABLE', + tokens, + }; + } +} + +/******************************************************************************/ diff --git a/test/addLiquidityGyro2.integration.test.ts b/test/addLiquidityGyro2.integration.test.ts new file mode 100644 index 00000000..2c71b980 --- /dev/null +++ b/test/addLiquidityGyro2.integration.test.ts @@ -0,0 +1,206 @@ +//0xdac42eeb17758daa38caf9a3540c808247527ae3000200000000000000000a2b - 2CLP-USDC-DAI +// pnpm test -- addLiquidityGyro2.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + walletActions, + parseEther, + Address, +} from 'viem'; + +import { + AddLiquidityProportionalInput, + AddLiquidityKind, + Slippage, + Hex, + PoolStateInput, + CHAINS, + ChainId, + AddLiquidity, + AddLiquidityInput, + InputAmount, + getPoolAddress, + AddLiquidityUnbalancedInput, + AddLiquiditySingleTokenInput, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { + assertAddLiquidityProportional, + doAddLiquidity, +} from './lib/utils/addLiquidityHelper'; +import { AddLiquidityTxInput } from './lib/utils/types'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; +import { addLiquidityKindNotSupportedByGyro } from '../src/entities/addLiquidity/utils/validateInputs'; + +const { rpcUrl } = await startFork(ANVIL_NETWORKS.POLYGON); +const chainId = ChainId.POLYGON; +const poolId = + '0xdac42eeb17758daa38caf9a3540c808247527ae3000200000000000000000a2b'; // 2CLP-USDC-DAI + +describe('Gyro2 add liquidity test', () => { + let txInput: AddLiquidityTxInput; + let poolStateInput: PoolStateInput; + + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolStateInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + addLiquidity: new AddLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput, + testAddress: '0xe84f75fc9caa49876d0ba18d309da4231d44e94d', // MATIC Holder Wallet, must hold amount of matic to approve tokens + addLiquidityInput: {} as AddLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [ + ...txInput.poolStateInput.tokens.map((t) => t.address), + txInput.poolStateInput.address, + ], + [0, 0, 0], + [ + ...txInput.poolStateInput.tokens.map((t) => { + return parseUnits('1', t.decimals); + }), + parseUnits('1', 18), + ], + ); + }); + + describe('proportional', () => { + let addLiquidityInput: AddLiquidityProportionalInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + addLiquidityInput = { + bptOut, + chainId, + rpcUrl, + kind: AddLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + + assertAddLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + //Removed test with native, because there are no GyroE V1 pool with wrapped native asset in any network + }); + + describe('unbalanced', () => { + let input: Omit; + let amountsIn: InputAmount[]; + beforeAll(() => { + amountsIn = txInput.poolStateInput.tokens.map((t) => ({ + rawAmount: parseUnits('1', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: AddLiquidityKind.Unbalanced, + }; + }); + test('must throw add liquidity kind not supported error', async () => { + const addLiquidityInput = { + ...input, + amountsIn: [...amountsIn.splice(0, 1)], + }; + await expect(() => + doAddLiquidity({ + ...txInput, + addLiquidityInput, + }), + ).rejects.toThrowError(addLiquidityKindNotSupportedByGyro); + }); + }); + + describe('single token', () => { + let addLiquidityInput: AddLiquiditySingleTokenInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + const tokenIn = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + addLiquidityInput = { + bptOut, + tokenIn, + chainId, + rpcUrl, + kind: AddLiquidityKind.SingleToken, + }; + }); + + test('must throw add liquidity kind not supported error', async () => { + await expect(() => + doAddLiquidity({ + ...txInput, + addLiquidityInput, + }), + ).rejects.toThrowError(addLiquidityKindNotSupportedByGyro); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + return { + id, + address: getPoolAddress(id) as Address, + type: 'GYRO2', + tokens: [ + { + address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC(PoS) + decimals: 6, + index: 0, + }, + { + address: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', // DAI + decimals: 18, + index: 1, + }, + ], + }; + } +} + +/******************************************************************************/ diff --git a/test/addLiquidityGyro3.integration.test.ts b/test/addLiquidityGyro3.integration.test.ts new file mode 100644 index 00000000..bd5a080f --- /dev/null +++ b/test/addLiquidityGyro3.integration.test.ts @@ -0,0 +1,210 @@ +//0x17f1ef81707811ea15d9ee7c741179bbe2a63887000100000000000000000799 - 3CLP-BUSD-USDC-USDT +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + walletActions, + parseEther, + Address, +} from 'viem'; + +import { + AddLiquidityProportionalInput, + AddLiquidityKind, + Slippage, + Hex, + PoolStateInput, + CHAINS, + ChainId, + AddLiquidity, + AddLiquidityInput, + InputAmount, + getPoolAddress, + AddLiquidityUnbalancedInput, + AddLiquiditySingleTokenInput, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { + assertAddLiquidityProportional, + doAddLiquidity, +} from './lib/utils/addLiquidityHelper'; +import { AddLiquidityTxInput } from './lib/utils/types'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; +import { addLiquidityKindNotSupportedByGyro } from '../src/entities/addLiquidity/utils/validateInputs'; + +const { rpcUrl } = await startFork(ANVIL_NETWORKS.POLYGON); +const chainId = ChainId.POLYGON; +const poolId = + '0x17f1ef81707811ea15d9ee7c741179bbe2a63887000100000000000000000799'; // 3CLP-BUSD-USDC-USDT + +describe('Gyro3 add liquidity test', () => { + let txInput: AddLiquidityTxInput; + let poolStateInput: PoolStateInput; + + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolStateInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + addLiquidity: new AddLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput, + testAddress: '0xe84f75fc9caa49876d0ba18d309da4231d44e94d', // MATIC Holder Wallet, must hold amount of matic to approve tokens + addLiquidityInput: {} as AddLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [ + ...txInput.poolStateInput.tokens.map((t) => t.address), + txInput.poolStateInput.address, + ], + [0, 51, 0, 0], + [ + ...txInput.poolStateInput.tokens.map((t) => + parseUnits('10000', t.decimals), + ), + parseUnits('10000', 18), + ], + ); + }); + + describe('proportional', () => { + let addLiquidityInput: AddLiquidityProportionalInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + addLiquidityInput = { + bptOut, + chainId, + rpcUrl, + kind: AddLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + + assertAddLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + //Removed test with native, because there are no GyroE V1 pool with wrapped native asset in any network + }); + + describe('unbalanced', () => { + let input: Omit; + let amountsIn: InputAmount[]; + beforeAll(() => { + amountsIn = txInput.poolStateInput.tokens.map((t) => ({ + rawAmount: parseUnits('1', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: AddLiquidityKind.Unbalanced, + }; + }); + test('must throw add liquidity kind not supported error', async () => { + const addLiquidityInput = { + ...input, + amountsIn: [...amountsIn.splice(0, 1)], + }; + await expect(() => + doAddLiquidity({ + ...txInput, + addLiquidityInput, + }), + ).rejects.toThrowError(addLiquidityKindNotSupportedByGyro); + }); + }); + + describe('single token', () => { + let addLiquidityInput: AddLiquiditySingleTokenInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + const tokenIn = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + addLiquidityInput = { + bptOut, + tokenIn, + chainId, + rpcUrl, + kind: AddLiquidityKind.SingleToken, + }; + }); + + test('must throw add liquidity kind not supported error', async () => { + await expect(() => + doAddLiquidity({ + ...txInput, + addLiquidityInput, + }), + ).rejects.toThrowError(addLiquidityKindNotSupportedByGyro); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + return { + id, + address: getPoolAddress(id) as Address, + type: 'GYRO3', + tokens: [ + { + address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC(PoS) + decimals: 6, + index: 0, + }, + { + address: '0x9c9e5fd8bbc25984b178fdce6117defa39d2db39', // BUSD + decimals: 18, + index: 1, + }, + { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', // USDT(PoS) + decimals: 6, + index: 2, + }, + ], + }; + } +} + +/******************************************************************************/ diff --git a/test/addLiquidityGyroE.integration.test.ts b/test/addLiquidityGyroE.integration.test.ts new file mode 100644 index 00000000..3ab15337 --- /dev/null +++ b/test/addLiquidityGyroE.integration.test.ts @@ -0,0 +1,205 @@ +// pnpm test -- addLiquidityGyro3.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + walletActions, + parseEther, + Address, +} from 'viem'; + +import { + AddLiquidityProportionalInput, + AddLiquidityKind, + Slippage, + Hex, + PoolStateInput, + CHAINS, + ChainId, + AddLiquidity, + AddLiquidityInput, + InputAmount, + getPoolAddress, + AddLiquidityUnbalancedInput, + AddLiquiditySingleTokenInput, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { + assertAddLiquidityProportional, + doAddLiquidity, +} from './lib/utils/addLiquidityHelper'; +import { AddLiquidityTxInput } from './lib/utils/types'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; +import { addLiquidityKindNotSupportedByGyro } from '../src/entities/addLiquidity/utils/validateInputs'; + +const { rpcUrl } = await startFork(ANVIL_NETWORKS.POLYGON); +const chainId = ChainId.POLYGON; +const poolId = + '0xa489c057de6c3177380ea264ebdf686b7f564f510002000000000000000008e2'; // ECLP-wstETH-wETH + +describe('gyroE V2 add liquidity test', () => { + let txInput: AddLiquidityTxInput; + let poolStateInput: PoolStateInput; + + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolStateInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + addLiquidity: new AddLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput, + testAddress: '0xe84f75fc9caa49876d0ba18d309da4231d44e94d', // MATIC Holder Wallet, must hold amount of matic to approve tokens + addLiquidityInput: {} as AddLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [ + ...txInput.poolStateInput.tokens.map((t) => t.address), + txInput.poolStateInput.address, + ], + undefined, + [ + ...txInput.poolStateInput.tokens.map((t) => + parseUnits('10000', t.decimals), + ), + parseUnits('10000', 18), + ], + ); + }); + + describe('proportional', () => { + let addLiquidityInput: AddLiquidityProportionalInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + addLiquidityInput = { + bptOut, + chainId, + rpcUrl, + kind: AddLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + + assertAddLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('unbalanced', () => { + let input: Omit; + let amountsIn: InputAmount[]; + beforeAll(() => { + amountsIn = txInput.poolStateInput.tokens.map((t) => ({ + rawAmount: parseUnits('1', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: AddLiquidityKind.Unbalanced, + }; + }); + test('with tokens', async () => { + const addLiquidityInput = { + ...input, + amountsIn: [...amountsIn.splice(0, 1)], + }; + await expect(() => + doAddLiquidity({ + ...txInput, + addLiquidityInput, + }), + ).rejects.toThrowError(addLiquidityKindNotSupportedByGyro); + }); + //Removed test with native, because there are no GyroE V1 pool with wrapped native asset in any network + }); + + describe('single token', () => { + let addLiquidityInput: AddLiquiditySingleTokenInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + const tokenIn = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + addLiquidityInput = { + bptOut, + tokenIn, + chainId, + rpcUrl, + kind: AddLiquidityKind.SingleToken, + }; + }); + + test('must throw add liquidity kind not supported error', async () => { + await expect(() => + doAddLiquidity({ + ...txInput, + addLiquidityInput, + }), + ).rejects.toThrowError(addLiquidityKindNotSupportedByGyro); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + return { + id, + address: getPoolAddress(id) as Address, + type: 'GYROE', + tokens: [ + { + address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC + decimals: 6, + index: 0, + }, + { + address: '0x2e1ad108ff1d8c782fcbbb89aad783ac49586756', // TUSD + decimals: 18, + index: 1, + }, + ], + }; + } +} + +/******************************************************************************/ diff --git a/test/addLiquidityGyroEV2.integration.test.ts b/test/addLiquidityGyroEV2.integration.test.ts new file mode 100644 index 00000000..78b4b7b2 --- /dev/null +++ b/test/addLiquidityGyroEV2.integration.test.ts @@ -0,0 +1,223 @@ +// pnpm test -- addLiquidityGyroEV2.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + walletActions, + parseEther, + Address, +} from 'viem'; + +import { + AddLiquidityProportionalInput, + AddLiquidityKind, + Slippage, + Hex, + PoolStateInput, + CHAINS, + ChainId, + AddLiquidity, + AddLiquidityInput, + InputAmount, + getPoolAddress, + AddLiquidityUnbalancedInput, + AddLiquiditySingleTokenInput, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { + assertAddLiquidityProportional, + doAddLiquidity, +} from './lib/utils/addLiquidityHelper'; +import { AddLiquidityTxInput } from './lib/utils/types'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; +import { addLiquidityKindNotSupportedByGyro } from '../src/entities/addLiquidity/utils/validateInputs'; + +const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET); +const chainId = ChainId.MAINNET; +const poolId = + '0xf01b0684c98cd7ada480bfdf6e43876422fa1fc10002000000000000000005de'; // ECLP-wstETH-wETH + +describe('GyroE V2 add liquidity test', () => { + let txInput: AddLiquidityTxInput; + let poolStateInput: PoolStateInput; + + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolStateInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + addLiquidity: new AddLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput, + testAddress: '0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f', // Balancer DAO Multisig + addLiquidityInput: {} as AddLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [ + ...txInput.poolStateInput.tokens.map((t) => t.address), + txInput.poolStateInput.address, + ], + [0, 98, 0], + [ + ...txInput.poolStateInput.tokens.map((t) => + parseUnits('100', t.decimals), + ), + parseUnits('100', 18), + ], + ); + }); + + describe('proportional', () => { + let addLiquidityInput: AddLiquidityProportionalInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('2'), + decimals: 18, + address: poolStateInput.address, + }; + addLiquidityInput = { + bptOut, + chainId, + rpcUrl, + kind: AddLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + + assertAddLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + test('with native', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput: { + ...addLiquidityInput, + useNativeAssetAsWrappedAmountIn: true, + }, + }); + assertAddLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + { + ...addLiquidityInput, + useNativeAssetAsWrappedAmountIn: true, + }, + addLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('unbalanced', () => { + let input: Omit; + let amountsIn: InputAmount[]; + beforeAll(() => { + amountsIn = txInput.poolStateInput.tokens.map((t) => ({ + rawAmount: parseUnits('1', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: AddLiquidityKind.Unbalanced, + }; + }); + test('with tokens', async () => { + const addLiquidityInput = { + ...input, + amountsIn: [...amountsIn.splice(0, 1)], + }; + await expect(() => + doAddLiquidity({ + ...txInput, + addLiquidityInput, + }), + ).rejects.toThrowError(addLiquidityKindNotSupportedByGyro); + }); + }); + + describe('single token', () => { + let addLiquidityInput: AddLiquiditySingleTokenInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + const tokenIn = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + addLiquidityInput = { + bptOut, + tokenIn, + chainId, + rpcUrl, + kind: AddLiquidityKind.SingleToken, + }; + }); + + test('must throw add liquidity kind not supported error', async () => { + await expect(() => + doAddLiquidity({ + ...txInput, + addLiquidityInput, + }), + ).rejects.toThrowError(addLiquidityKindNotSupportedByGyro); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + return { + id, + address: getPoolAddress(id) as Address, + type: 'GYROE', + tokens: [ + { + address: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', // wstETH + decimals: 18, + index: 0, + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // wETH + decimals: 18, + index: 1, + }, + ], + }; + } +} + +/******************************************************************************/ diff --git a/test/addLiquidityWeighted.integration.test.ts b/test/addLiquidityWeighted.integration.test.ts new file mode 100644 index 00000000..e52d85d2 --- /dev/null +++ b/test/addLiquidityWeighted.integration.test.ts @@ -0,0 +1,281 @@ +// pnpm test -- addLiquidityWeighted.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + walletActions, + parseEther, +} from 'viem'; + +import { + AddLiquidityUnbalancedInput, + AddLiquidityProportionalInput, + AddLiquiditySingleTokenInput, + AddLiquidityKind, + Slippage, + Address, + Hex, + PoolStateInput, + CHAINS, + ChainId, + getPoolAddress, + AddLiquidity, + AddLiquidityInput, + InputAmount, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { + assertAddLiquidityProportional, + assertAddLiquiditySingleToken, + assertAddLiquidityUnbalanced, + doAddLiquidity, +} from './lib/utils/addLiquidityHelper'; +import { AddLiquidityTxInput } from './lib/utils/types'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; + +const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET); +const chainId = ChainId.MAINNET; +const poolId = + '0x68e3266c9c8bbd44ad9dca5afbfe629022aee9fe000200000000000000000512'; // 80wjAURA-20WETH + +describe('add liquidity weighted test', () => { + let txInput: AddLiquidityTxInput; + let poolStateInput: PoolStateInput; + + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolStateInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + addLiquidity: new AddLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput, + testAddress: '0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f', // Balancer DAO Multisig + addLiquidityInput: {} as AddLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [ + ...txInput.poolStateInput.tokens.map((t) => t.address), + txInput.poolStateInput.address, + ], + undefined, // TODO: hardcode these values to improve test performance + [ + ...txInput.poolStateInput.tokens.map((t) => + parseUnits('100', t.decimals), + ), + parseUnits('100', 18), + ], + ); + }); + + describe('add liquidity unbalanced', () => { + let input: Omit; + let amountsIn: InputAmount[]; + beforeAll(() => { + amountsIn = txInput.poolStateInput.tokens.map((t) => ({ + rawAmount: parseUnits('0.001', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: AddLiquidityKind.Unbalanced, + }; + }); + test('with tokens', async () => { + const addLiquidityInput = { + ...input, + amountsIn: [...amountsIn.splice(0, 1)], + }; + + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + assertAddLiquidityUnbalanced( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + + test('with native', async () => { + const addLiquidityInput = { + ...input, + amountsIn, + useNativeAssetAsWrappedAmountIn: true, + }; + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + assertAddLiquidityUnbalanced( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('add liquidity single asset', () => { + let addLiquidityInput: AddLiquiditySingleTokenInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + const tokenIn = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + addLiquidityInput = { + bptOut, + tokenIn, + chainId, + rpcUrl, + kind: AddLiquidityKind.SingleToken, + }; + }); + + test('with token', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + + assertAddLiquiditySingleToken( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + + test('with native', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput: { + ...addLiquidityInput, + useNativeAssetAsWrappedAmountIn: true, + }, + }); + + assertAddLiquiditySingleToken( + txInput.client.chain?.id as number, + txInput.poolStateInput, + { + ...addLiquidityInput, + useNativeAssetAsWrappedAmountIn: true, + }, + addLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('add liquidity proportional', () => { + let addLiquidityInput: AddLiquidityProportionalInput; + beforeAll(() => { + const bptOut: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolStateInput.address, + }; + addLiquidityInput = { + bptOut, + chainId, + rpcUrl, + kind: AddLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput, + }); + + assertAddLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + addLiquidityInput, + addLiquidityOutput, + txInput.slippage, + ); + }); + test('with native', async () => { + const addLiquidityOutput = await doAddLiquidity({ + ...txInput, + addLiquidityInput: { + ...addLiquidityInput, + useNativeAssetAsWrappedAmountIn: true, + }, + }); + + assertAddLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + { + ...addLiquidityInput, + useNativeAssetAsWrappedAmountIn: true, + }, + addLiquidityOutput, + txInput.slippage, + ); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + const tokens = [ + { + address: + '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f' as Address, // wjAURA + decimals: 18, + index: 0, + }, + { + address: + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' as Address, // WETH + decimals: 18, + index: 1, + }, + ]; + + return { + id, + address: getPoolAddress(id) as Address, + type: 'WEIGHTED', + tokens, + }; + } +} + +/******************************************************************************/ diff --git a/test/anvil/anvil-global-setup.ts b/test/anvil/anvil-global-setup.ts new file mode 100644 index 00000000..1d1e93e4 --- /dev/null +++ b/test/anvil/anvil-global-setup.ts @@ -0,0 +1,132 @@ +import { Anvil, CreateAnvilOptions, createAnvil } from '@viem/anvil'; +import { sleep } from '../lib/utils/promises'; +import { ChainId } from '../../src/utils/constants'; + +type NetworkSetup = { + rpcEnv: string; + fallBackRpc: string | undefined; + port: number; + forkBlockNumber: bigint; +}; + +type NetworksWithFork = Extract< + keyof typeof ChainId, + 'MAINNET' | 'POLYGON' | 'FANTOM' +>; + +const ANVIL_PORTS: Record = { + //Ports separated by 100 to avoid port collision when running tests in parallel + MAINNET: 8645, + POLYGON: 8745, + FANTOM: 8845, +}; + +export const ANVIL_NETWORKS: Record = { + MAINNET: { + rpcEnv: 'ETHEREUM_RPC_URL', + fallBackRpc: 'https://cloudflare-eth.com', + port: ANVIL_PORTS.MAINNET, + forkBlockNumber: 18043296n, + }, + POLYGON: { + rpcEnv: 'POLYGON_RPC_URL', + // Public Polygon RPCs are usually unreliable + fallBackRpc: undefined, + port: ANVIL_PORTS.POLYGON, + // Note - this has to be >= highest blockNo used in tests + forkBlockNumber: 44215395n, + }, + FANTOM: { + rpcEnv: 'FANTOM_RPC_URL', + // Public Fantom RPCs are usually unreliable + fallBackRpc: undefined, + port: ANVIL_PORTS.FANTOM, + forkBlockNumber: 65313450n, + }, +}; + +function getAnvilOptions(network: NetworkSetup): CreateAnvilOptions { + let forkUrl: string; + if (process.env[network.rpcEnv] !== 'undefined') { + forkUrl = process.env[network.rpcEnv] as string; + } else { + if (!network.fallBackRpc) + throw Error( + `Please add a environment variable for: ${network.rpcEnv}`, + ); + forkUrl = network.fallBackRpc; + console.warn( + `\`${network.rpcEnv}\` not found. Falling back to \`${forkUrl}\`.`, + ); + } + const port = network.port; + const forkBlockNumber = network.forkBlockNumber; + return { + forkUrl, + port, + forkBlockNumber, + }; +} + +// Controls the current running forks to avoid starting the same fork twice +let runningForks: Record = {}; + +// Make sure that forks are stopped after each test suite +export async function stopAnvilForks() { + await Promise.all( + Object.values(runningForks).map(async (anvil) => { + // console.log('Stopping anvil fork', anvil.options); + return anvil.stop(); + }), + ); + runningForks = {}; +} + +/* + Starts an anvil fork with the given options. + In vitest, each thread is assigned a unique, numerical id (`process.env.VITEST_POOL_ID`). + When jobId is provided, the fork uses this id to create a different local rpc url (e.g. `http://127.0.0.1:<8545+jobId>/` + so that tests can be run in parallel (depending on the number of threads of the host machine) +*/ +export async function startFork( + network: NetworkSetup, + jobId = Number(process.env.VITEST_POOL_ID) || 0, +) { + const anvilOptions = getAnvilOptions(network); + + const defaultAnvilPort = 8545; + const port = (anvilOptions.port || defaultAnvilPort) + jobId; + + if (!anvilOptions.forkUrl) { + throw Error( + 'Anvil forkUrl must have a value. Please review your anvil setup', + ); + } + const rpcUrl = `http://127.0.0.1:${port}`; + + console.log('checking rpcUrl', port, runningForks); + + // Avoid starting fork if it was running already + if (runningForks[port]) return { rpcUrl }; + + // https://www.npmjs.com/package/@viem/anvil + const anvil = createAnvil({ ...anvilOptions, port }); + // Save reference to running fork + runningForks[port] = anvil; + + if (process.env.SKIP_GLOBAL_SETUP === 'true') { + console.warn(`🛠️ Skipping global anvil setup. You must run the anvil fork manually. Example: +anvil --fork-url https://eth-mainnet.alchemyapi.io/v2/ --port 8545 --fork-block-number=17878719 +`); + await sleep(5000); + return { rpcUrl }; + } + console.log('🛠️ Starting anvil', { + port, + forkBlockNumber: anvilOptions.forkBlockNumber, + }); + await anvil.start(); + return { + rpcUrl, + }; +} diff --git a/test/balancerApi.test.ts b/test/balancerApi.test.ts new file mode 100644 index 00000000..ed8dfdff --- /dev/null +++ b/test/balancerApi.test.ts @@ -0,0 +1,30 @@ +// pnpm test -- balancerApi.test.ts +import { BalancerApi, PoolStateInput, ChainId } from '../src'; + +describe( + 'BalancerApi Provider', + () => { + test('CS Pool - Should add BPT to tokens', async () => { + const chainId = ChainId.MAINNET; + // wstEth/WETH CS Pool + const poolId = + '0x93d199263632a4ef4bb438f1feb99e57b4b5f0bd0000000000000000000005c2'; + + // API is used to fetch relevant pool data + const balancerApi = new BalancerApi( + 'https://backend-v3-canary.beets-ftm-node.com/graphql', + chainId, + ); + const poolStateInput: PoolStateInput = + await balancerApi.pools.fetchPoolState(poolId); + + expect(poolStateInput.tokens.length).toEqual(3); + expect(poolStateInput.tokens[1].address).toEqual( + poolStateInput.address, + ); + }); + }, + { + timeout: 60000, + }, +); diff --git a/test/composableStable.integration.test.ts b/test/composableStable.integration.test.ts new file mode 100644 index 00000000..9aae5581 --- /dev/null +++ b/test/composableStable.integration.test.ts @@ -0,0 +1,108 @@ +// pnpm test -- test/composableStable.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { SmartOrderRouter } from '../src/sor'; +import { sorGetSwapsWithPools } from '../src/static'; +import { ChainId, BATCHSIZE, VAULT } from '../src/utils'; +import { Token, TokenAmount } from '../src/entities'; +import { OnChainPoolDataEnricher } from '../src/data/enrichers/onChainPoolDataEnricher'; +import { SwapKind, SwapOptions } from '../src/types'; +import { BasePool } from '../src/entities/pools'; +import { MockPoolProvider } from './lib/utils/mockPoolProvider'; + +import testPools from './lib/testData/testPools/composableStable_17473810.json'; +import { RawStablePool } from '../src'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; + +const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET); + +describe('ComposableStable Swap tests', () => { + const chainId = ChainId.MAINNET; + const mockPoolProvider = new MockPoolProvider( + testPools.pools as RawStablePool[], + ); + const onChainPoolDataEnricher = new OnChainPoolDataEnricher( + chainId, + rpcUrl, + BATCHSIZE[chainId], + VAULT[chainId], + ); + + const sor = new SmartOrderRouter({ + chainId, + poolDataProviders: mockPoolProvider, + poolDataEnrichers: onChainPoolDataEnricher, + rpcUrl: rpcUrl, + }); + + const USDC = new Token( + chainId, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + 6, + 'USDC', + ); + const USDT = new Token( + chainId, + '0xdAC17F958D2ee523a2206206994597C13D831ec7', + 6, + 'USDT', + ); + const DAI = new Token( + chainId, + '0x6B175474E89094C44Da98b954EedeAC495271d0F', + 18, + 'DAI', + ); + + const swapOptions: SwapOptions = { + block: 17473810n, + }; + + let pools: BasePool[]; + // Since constructing a Swap mutates the pool balances, we refetch for each test + // May be a better way to deep clone a BasePool[] class instead + beforeEach(async () => { + pools = await sor.fetchAndCachePools(swapOptions.block); + }); + + test('DAI -> USDT givenIn ComposableStable', async () => { + const inputAmount = TokenAmount.fromHumanAmount(DAI, '100000'); + + const swap = await sorGetSwapsWithPools( + DAI, + USDT, + SwapKind.GivenIn, + inputAmount, + pools, + swapOptions, + ); + + if (!swap) throw new Error('Swap is undefined'); + + const onchain = await swap.query(rpcUrl, swapOptions.block); + expect(swap.quote.amount).toEqual(onchain.amount); + expect(swap.inputAmount.amount).toEqual(inputAmount.amount); + expect(swap.outputAmount.amount).toEqual(swap.quote.amount); + expect(swap.paths.length).toEqual(1); + expect(swap.paths[0].pools.length).toEqual(1); + }); + + test('USDC -> DAI givenOut ComposableStable', async () => { + const outputAmount = TokenAmount.fromHumanAmount(DAI, '1000000'); + + const swap = await sorGetSwapsWithPools( + USDC, + DAI, + SwapKind.GivenOut, + outputAmount, + pools, + swapOptions, + ); + + if (!swap) throw new Error('Swap is undefined'); + + const onchain = await swap.query(rpcUrl, swapOptions.block); + expect(swap.quote.amount).toEqual(onchain.amount); + }); +}); diff --git a/test/fantom.integration.test.ts b/test/fantom.integration.test.ts new file mode 100644 index 00000000..df53981b --- /dev/null +++ b/test/fantom.integration.test.ts @@ -0,0 +1,102 @@ +// pnpm test -- test/fantom.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { SmartOrderRouter } from '../src/sor'; +import { sorGetSwapsWithPools } from '../src/static'; +import { ChainId, NATIVE_ASSETS, BATCHSIZE, VAULT } from '../src/utils'; +import { Token, TokenAmount } from '../src/entities'; +import { OnChainPoolDataEnricher } from '../src/data/enrichers/onChainPoolDataEnricher'; +import { SwapKind, SwapOptions } from '../src/types'; +import { BasePool } from '../src/entities/pools'; +import { MockPoolProvider } from './lib/utils/mockPoolProvider'; + +import testPools from './lib/testData/testPools/fantom_65313450.json'; +import { RawBasePool } from '../src'; + +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; + +const chainId = ChainId.FANTOM; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.FANTOM); + +describe('Fantom SOR', () => { + const inputToken = NATIVE_ASSETS[chainId]; + const mockPoolProvider = new MockPoolProvider( + testPools.pools as RawBasePool[], + ); + const onChainPoolDataEnricher = new OnChainPoolDataEnricher( + chainId, + rpcUrl, + BATCHSIZE[chainId], + VAULT[chainId], + ); + + const sor = new SmartOrderRouter({ + chainId, + poolDataProviders: mockPoolProvider, + poolDataEnrichers: onChainPoolDataEnricher, + rpcUrl: rpcUrl, + }); + + const BEETS = new Token( + chainId, + '0xF24Bcf4d1e507740041C9cFd2DddB29585aDCe1e', + 18, + 'BEETS', + ); + + const swapOptions: SwapOptions = { + block: 65313450n, + }; + + let pools: BasePool[]; + // Since constructing a Swap mutates the pool balances, we refetch for each test + // May be a better way to deep clone a BasePool[] class instead + beforeEach(async () => { + pools = await sor.fetchAndCachePools(swapOptions.block); + }); + + describe('Native Swaps', () => { + test('Native -> Token givenIn', async () => { + const inputAmount = TokenAmount.fromHumanAmount(inputToken, '100'); + + const swap = await sorGetSwapsWithPools( + inputToken, + BEETS, + SwapKind.GivenIn, + inputAmount, + pools, + swapOptions, + ); + + if (!swap) throw new Error('Swap is undefined'); + + const onchain = await swap.query(rpcUrl, swapOptions.block); + + expect(swap.quote.amount).toEqual(onchain.amount); + expect(swap.inputAmount.amount).toEqual(inputAmount.amount); + expect(swap.outputAmount.amount).toEqual(swap.quote.amount); + }); + + test('Native ETH -> Token givenOut', async () => { + const outputAmount = TokenAmount.fromHumanAmount(BEETS, '100000'); + + const swap = await sorGetSwapsWithPools( + inputToken, + BEETS, + SwapKind.GivenOut, + outputAmount, + pools, + swapOptions, + ); + + if (!swap) throw new Error('Swap is undefined'); + + const onchain = await swap.query(rpcUrl, swapOptions.block); + + expect(swap.quote.amount).toEqual(onchain.amount); + expect(swap.inputAmount.amount).toEqual(swap.quote.amount); + expect(swap.outputAmount.amount).toEqual(outputAmount.amount); + }); + }); +}); diff --git a/test/fxPool.integration.test.ts b/test/fxPool.integration.test.ts index 04f1d8a1..67cf3a7f 100644 --- a/test/fxPool.integration.test.ts +++ b/test/fxPool.integration.test.ts @@ -6,19 +6,22 @@ import { BATCHSIZE, ChainId, VAULT } from '../src/utils'; import { BasePool, OnChainPoolDataEnricher, + RawFxPool, SmartOrderRouter, - SubgraphPoolProvider, SwapKind, SwapOptions, Token, TokenAmount, sorGetSwapsWithPools, } from '../src'; +import { MockPoolProvider } from './lib/utils/mockPoolProvider'; +import testPools from './lib/testData/testPools/fx_43667355.json'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; -describe('fx integration tests', () => { - const chainId = ChainId.POLYGON; - const rpcUrl = process.env.POLYGON_RPC_URL || 'https://polygon-rpc.com'; +const chainId = ChainId.POLYGON; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.POLYGON); +describe('fx integration tests', () => { const USDC = new Token( chainId, '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', @@ -32,19 +35,15 @@ describe('fx integration tests', () => { 'XSGD', ); const swapOptions: SwapOptions = { - block: 43878700n, + block: 43667355n, }; let sor: SmartOrderRouter; beforeAll(() => { - const subgraphPoolDataService = new SubgraphPoolProvider( - chainId, - undefined, - { - poolTypeIn: ['FX'], - }, - ); + const pools = testPools.pools as RawFxPool[]; + const mockPoolProvider = new MockPoolProvider(pools); + const onChainPoolDataEnricher = new OnChainPoolDataEnricher( chainId, rpcUrl, @@ -54,7 +53,7 @@ describe('fx integration tests', () => { sor = new SmartOrderRouter({ chainId, - poolDataProviders: subgraphPoolDataService, + poolDataProviders: mockPoolProvider, poolDataEnrichers: onChainPoolDataEnricher, rpcUrl: rpcUrl, }); diff --git a/test/fxPool.test.ts b/test/fxPool.test.ts index 8f73aad5..293bc134 100644 --- a/test/fxPool.test.ts +++ b/test/fxPool.test.ts @@ -1,7 +1,6 @@ // pnpm test -- fxPool.test.ts -import { describe, expect, test } from 'vitest'; import { ChainId, RawFxPool, SwapKind, Token } from '../src'; -import testPools from './lib/testData/fxPool_43667355.json'; +import testPools from './lib/testData/testPools/fx_43667355.json'; import { CurveMathRevert, FxPool, FxPoolToken } from '../src/entities/pools/fx'; import { parseFixedCurveParam } from '../src/entities/pools/fx/helpers'; import { parseUnits } from 'viem'; diff --git a/test/gyro2Math.test.ts b/test/gyro2Math.test.ts index cd476448..42a04c0e 100644 --- a/test/gyro2Math.test.ts +++ b/test/gyro2Math.test.ts @@ -1,5 +1,5 @@ // pnpm test -- gyro2Math.test.ts -import testPools from './lib/testData/gyro2TestPool.json'; +import testPools from './lib/testData/testPools/gyro2.json'; import { ChainId, RawGyro2Pool, Token, TokenAmount, WAD } from '../src'; import { _calculateQuadratic, diff --git a/test/gyro2Pool.test.ts b/test/gyro2Pool.test.ts index db3a72d6..6d91af4a 100644 --- a/test/gyro2Pool.test.ts +++ b/test/gyro2Pool.test.ts @@ -4,7 +4,7 @@ dotenv.config(); import { parseEther } from 'viem'; -import testPools from './lib/testData/gyro2TestPool.json'; +import testPools from './lib/testData/testPools/gyro2.json'; import { MockPoolDataEnricher } from './lib/utils/mockPoolEnricher'; import { MockPoolProvider } from './lib/utils/mockPoolProvider'; import { diff --git a/test/gyro3Pool.integration.test.ts b/test/gyro3Pool.integration.test.ts index b62caec9..4c2f5927 100644 --- a/test/gyro3Pool.integration.test.ts +++ b/test/gyro3Pool.integration.test.ts @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; dotenv.config(); -import testPools from './lib/testData/gyro3TestPool.json'; +import testPools from './lib/testData/testPools/gyro3_44133130.json'; import { ChainId } from '../src/utils'; import { RawGyro3Pool } from '../src/data/types'; import { @@ -14,10 +14,12 @@ import { sorGetSwapsWithPools, sorParseRawPools, } from '../src'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; + +const chainId = ChainId.POLYGON; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.POLYGON); describe('gyro3 integration tests', () => { - const chainId = ChainId.POLYGON; - const rpcUrl = process.env['POLYGON_RPC_URL'] || 'https://polygon-rpc.com'; const rawPool = { ...testPools }.pools[1] as RawGyro3Pool; const USDC = new Token( chainId, diff --git a/test/gyro3Pool.test.ts b/test/gyro3Pool.test.ts index 1b23ad85..c5c5b25a 100644 --- a/test/gyro3Pool.test.ts +++ b/test/gyro3Pool.test.ts @@ -1,5 +1,5 @@ // pnpm test -- gyro3Pool.test.ts -import testPools from './lib/testData/gyro3TestPool.json'; +import testPools from './lib/testData/testPools/gyro3_44133130.json'; import { ChainId, RawGyro3Pool, SwapKind, Token, TokenAmount } from '../src'; import { Gyro3Pool } from '../src/entities/pools/gyro3'; diff --git a/test/gyroEMath.test.ts b/test/gyroEMath.test.ts index 7d297e7a..28d5a764 100644 --- a/test/gyroEMath.test.ts +++ b/test/gyroEMath.test.ts @@ -1,8 +1,9 @@ // pnpm test -- gyroEMath.test.ts -import testPools from './lib/testData/gyroETestPool.json'; +import testPools from './lib/testData/testPools/gyroE_44215395.json'; import { RawGyroEPool } from '../src/data/types'; import { ChainId } from '../src/utils'; -import { GyroEPool, Vector2 } from '../src/entities/pools/gyroE/gyroEPool'; +import { GyroEPool } from '../src/entities/pools/gyroE/gyroEPool'; +import { Vector2 } from '../src/entities/pools/gyroE/types'; import { Token, TokenAmount } from '../src/entities'; import { calculateInvariantWithError } from '../src/entities/pools/gyroE/gyroEMath'; import { diff --git a/test/gyroEPool.test.ts b/test/gyroEPool.test.ts index 2b4c875b..f400ddd3 100644 --- a/test/gyroEPool.test.ts +++ b/test/gyroEPool.test.ts @@ -1,5 +1,5 @@ // pnpm test -- test/gyroEPool.test.ts -import testPools from './lib/testData/gyroETestPool.json'; +import testPools from './lib/testData/testPools/gyroE_44215395.json'; import { ChainId, SwapKind, Token, TokenAmount } from '../src'; import { RawGyroEPool } from '../src/data/types'; import { GyroEPool } from '../src/entities/pools/gyroE'; diff --git a/test/gyroEV2Pool.integration.test.ts b/test/gyroEV2Pool.integration.test.ts index e801a3a1..745133dc 100644 --- a/test/gyroEV2Pool.integration.test.ts +++ b/test/gyroEV2Pool.integration.test.ts @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; dotenv.config(); -import testPools from './lib/testData/gyroETestPool.json'; +import testPools from './lib/testData/testPools/gyroE_44215395.json'; import { BATCHSIZE, ChainId, VAULT } from '../src/utils'; import { BasePool, @@ -17,10 +17,12 @@ import { import { parseEther } from 'viem'; import { GyroEPool, GyroEPoolToken } from '../src/entities/pools/gyroE'; import { MockPoolProvider } from './lib/utils/mockPoolProvider'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; + +const chainId = ChainId.POLYGON; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.POLYGON); describe('gyroEV2: WMATIC-stMATIC integration tests', () => { - const chainId = ChainId.POLYGON; - const rpcUrl = process.env.POLYGON_RPC_URL || 'https://polygon-rpc.com'; const WMATIC = new Token( chainId, '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', diff --git a/test/gyroEV2Pool.test.ts b/test/gyroEV2Pool.test.ts index 4f135629..059d0c17 100644 --- a/test/gyroEV2Pool.test.ts +++ b/test/gyroEV2Pool.test.ts @@ -1,7 +1,7 @@ // pnpm test -- gyroEV2Pool.test.ts import { ChainId, RawGyroEPool, SwapKind, Token, TokenAmount } from '../src'; import { GyroEPool } from '../src/entities/pools/gyroE'; -import testPools from './lib/testData/gyroETestPool.json'; +import testPools from './lib/testData/testPools/gyroE_44215395.json'; describe('gyroEPool tests', () => { const testPool = { ...testPools }.pools[1] as RawGyroEPool; diff --git a/test/lib/testData/testPools/composableStable_17473810.json b/test/lib/testData/testPools/composableStable_17473810.json new file mode 100644 index 00000000..c7d21491 --- /dev/null +++ b/test/lib/testData/testPools/composableStable_17473810.json @@ -0,0 +1,94 @@ +{ + "pools": [ + { + "id": "0x79c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7", + "address": "0x79c58f70905f734641735bc61e45c19dd9ad60bc", + "poolType": "ComposableStable", + "poolTypeVersion": 3, + "name": "Balancer USDC-DAI-USDT Stable Pool", + "tokens": [ + { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "balance": "1634347.131341290124462272", + "weight": null, + "priceRate": "1", + "decimals": 18, + "name": "Dai Stablecoin", + "index": 0, + "symbol": "DAI", + "token": { "latestFXPrice": "1.00011" } + }, + { + "address": "0x79c58f70905f734641735bc61e45c19dd9ad60bc", + "balance": "2596148429321223.432121172010498472", + "weight": null, + "priceRate": "1", + "decimals": 18, + "name": "Balancer USDC-DAI-USDT Stable Pool", + "index": 1, + "symbol": "USDC-DAI-USDT", + "token": { "latestFXPrice": null } + }, + { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "balance": "1628729.447793", + "weight": null, + "priceRate": "1", + "decimals": 6, + "name": "USD Coin", + "index": 2, + "symbol": "USDC", + "token": { "latestFXPrice": "1" } + }, + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "balance": "2217712.39191", + "weight": null, + "priceRate": "1", + "decimals": 6, + "name": "Tether USD", + "index": 3, + "symbol": "USDT", + "token": { "latestFXPrice": null } + } + ], + "tokensList": [ + "0x6b175474e89094c44da98b954eedeac495271d0f", + "0x79c58f70905f734641735bc61e45c19dd9ad60bc", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xdac17f958d2ee523a2206206994597c13d831ec7" + ], + "swapEnabled": true, + "swapFee": "0.00005", + "amp": "2000", + "totalLiquidity": "5480788.971044290124462272", + "totalShares": "5476334.783405922127632546", + "mainIndex": null, + "wrappedIndex": null, + "lowerTarget": null, + "upperTarget": null, + "alpha": null, + "beta": null, + "c": null, + "delta": null, + "dSq": null, + "epsilon": null, + "lambda": null, + "root3Alpha": null, + "s": null, + "sqrtAlpha": null, + "sqrtBeta": null, + "tauAlphaX": null, + "tauAlphaY": null, + "tauBetaX": null, + "tauBetaY": null, + "u": null, + "v": null, + "w": null, + "z": null, + "inRecoveryMode": false, + "isPaused": false, + "liquidity": "0" + } + ] +} diff --git a/test/lib/testData/testPools/fantom_65313450.json b/test/lib/testData/testPools/fantom_65313450.json new file mode 100644 index 00000000..558d9145 --- /dev/null +++ b/test/lib/testData/testPools/fantom_65313450.json @@ -0,0 +1,70 @@ +{ + "pools": [ + { + "id": "0x9e4341acef4147196e99d648c5e43b3fc9d026780002000000000000000005ec", + "address": "0x9e4341acef4147196e99d648c5e43b3fc9d02678", + "poolType": "Weighted", + "poolTypeVersion": 2, + "name": "Fresh BEETS", + "tokens": [ + { + "address": "0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83", + "balance": "1529628.909214834943938725", + "weight": "0.2", + "priceRate": "1", + "decimals": 18, + "name": "Wrapped Fantom", + "index": 0, + "symbol": "WFTM", + "token": { "latestFXPrice": null } + }, + { + "address": "0xf24bcf4d1e507740041c9cfd2dddb29585adce1e", + "balance": "81750834.894524234143091925", + "weight": "0.8", + "priceRate": "1", + "decimals": 18, + "name": "BeethovenxToken", + "index": 1, + "symbol": "BEETS", + "token": { "latestFXPrice": null } + } + ], + "tokensList": [ + "0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83", + "0xf24bcf4d1e507740041c9cfd2dddb29585adce1e" + ], + "swapEnabled": true, + "swapFee": "0.01", + "amp": null, + "totalLiquidity": "2679688.778562431478805364412296083", + "totalShares": "72890712.800024961804215653", + "mainIndex": null, + "wrappedIndex": null, + "lowerTarget": null, + "upperTarget": null, + "alpha": null, + "beta": null, + "c": null, + "delta": null, + "dSq": null, + "epsilon": null, + "lambda": null, + "root3Alpha": null, + "s": null, + "sqrtAlpha": null, + "sqrtBeta": null, + "tauAlphaX": null, + "tauAlphaY": null, + "tauBetaX": null, + "tauBetaY": null, + "u": null, + "v": null, + "w": null, + "z": null, + "inRecoveryMode": true, + "isPaused": false, + "liquidity": "0" + } + ] +} diff --git a/test/lib/testData/fxPool_43667355.json b/test/lib/testData/testPools/fx_43667355.json similarity index 100% rename from test/lib/testData/fxPool_43667355.json rename to test/lib/testData/testPools/fx_43667355.json diff --git a/test/lib/testData/gyro2TestPool.json b/test/lib/testData/testPools/gyro2.json similarity index 85% rename from test/lib/testData/gyro2TestPool.json rename to test/lib/testData/testPools/gyro2.json index 09b37799..404bf9c6 100644 --- a/test/lib/testData/gyro2TestPool.json +++ b/test/lib/testData/testPools/gyro2.json @@ -1,14 +1,4 @@ { - "tradeInfo": { - "SwapType": "n/a", - "TokenIn": "n/a", - "TokenOut": "n/a", - "NoPools": 1, - "SwapAmount": "n/a", - "GasPrice": "n/a", - "SwapAmountDecimals": "n/a", - "ReturnAmountDecimals": "n/a" - }, "pools": [ { "id": "0xebfed10e11dc08fcda1af1fda146945e8710f22e0000000000000000000000ff", diff --git a/test/lib/testData/gyro3TestPool.json b/test/lib/testData/testPools/gyro3_44133130.json similarity index 93% rename from test/lib/testData/gyro3TestPool.json rename to test/lib/testData/testPools/gyro3_44133130.json index dec03547..a9bdc58b 100644 --- a/test/lib/testData/gyro3TestPool.json +++ b/test/lib/testData/testPools/gyro3_44133130.json @@ -1,14 +1,4 @@ { - "tradeInfo": { - "SwapType": "n/a", - "TokenIn": "n/a", - "TokenOut": "n/a", - "NoPools": 1, - "SwapAmount": "n/a", - "GasPrice": "n/a", - "SwapAmountDecimals": "n/a", - "ReturnAmountDecimals": "n/a" - }, "pools": [ { "id": "0xebfed10e11dc08fcda1af1fda146945e8710f22e0000000000000000000000ff", diff --git a/test/lib/testData/gyroETestPool.json b/test/lib/testData/testPools/gyroE_44215395.json similarity index 96% rename from test/lib/testData/gyroETestPool.json rename to test/lib/testData/testPools/gyroE_44215395.json index 81cdf43a..b684212f 100644 --- a/test/lib/testData/gyroETestPool.json +++ b/test/lib/testData/testPools/gyroE_44215395.json @@ -1,14 +1,4 @@ { - "tradeInfo": { - "SwapType": "n/a", - "TokenIn": "n/a", - "TokenOut": "n/a", - "NoPools": 1, - "SwapAmount": "n/a", - "GasPrice": "n/a", - "SwapAmountDecimals": "n/a", - "ReturnAmountDecimals": "n/a" - }, "pools": [ { "id": "0x12311573a96806182c01ef6c349948edc6635b040002000000000000000002ab", diff --git a/test/lib/testData/testPools/weighted_17473810.json b/test/lib/testData/testPools/weighted_17473810.json new file mode 100644 index 00000000..a771001f --- /dev/null +++ b/test/lib/testData/testPools/weighted_17473810.json @@ -0,0 +1,70 @@ +{ + "pools": [ + { + "id": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", + "address": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56", + "poolType": "Weighted", + "poolTypeVersion": 1, + "name": "Balancer 80 BAL 20 WETH", + "tokens": [ + { + "address": "0xba100000625a3754423978a60c9317c58a424e3d", + "balance": "31701973.223639147427614953", + "weight": "0.8", + "priceRate": "1", + "decimals": 18, + "name": "Balancer", + "index": 0, + "symbol": "BAL", + "token": { "latestFXPrice": null } + }, + { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "balance": "20294.720800533482038095", + "weight": "0.2", + "priceRate": "1", + "decimals": 18, + "name": "Wrapped Ether", + "index": 1, + "symbol": "WETH", + "token": { "latestFXPrice": null } + } + ], + "tokensList": [ + "0xba100000625a3754423978a60c9317c58a424e3d", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + ], + "swapEnabled": true, + "swapFee": "0.01", + "amp": null, + "totalLiquidity": "173627239.2541944837545942771966962", + "totalShares": "14221698.569579854728322716", + "mainIndex": null, + "wrappedIndex": null, + "lowerTarget": null, + "upperTarget": null, + "alpha": null, + "beta": null, + "c": null, + "delta": null, + "dSq": null, + "epsilon": null, + "lambda": null, + "root3Alpha": null, + "s": null, + "sqrtAlpha": null, + "sqrtBeta": null, + "tauAlphaX": null, + "tauAlphaY": null, + "tauBetaX": null, + "tauBetaY": null, + "u": null, + "v": null, + "w": null, + "z": null, + "inRecoveryMode": false, + "isPaused": false, + "liquidity": "0" + } + ] +} diff --git a/test/lib/utils/addLiquidityHelper.ts b/test/lib/utils/addLiquidityHelper.ts new file mode 100644 index 00000000..103a535b --- /dev/null +++ b/test/lib/utils/addLiquidityHelper.ts @@ -0,0 +1,415 @@ +import { + AddLiquidity, + AddLiquidityInput, + PoolStateInput, + Slippage, + Address, + AddLiquidityBuildOutput, + AddLiquidityQueryOutput, + AddLiquidityUnbalancedInput, + BALANCER_VAULT, + AddLiquiditySingleTokenInput, + AddLiquidityProportionalInput, + Token, + ChainId, + TokenAmount, + AddLiquidityComposableStableQueryOutput, + NATIVE_ASSETS, +} from '../../../src'; +import { TxOutput, sendTransactionGetBalances } from './helper'; +import { AddLiquidityTxInput } from './types'; +import { zeroAddress } from 'viem'; +import { getTokensForBalanceCheck } from './getTokensForBalanceCheck'; + +type AddLiquidityOutput = { + addLiquidityQueryOutput: AddLiquidityQueryOutput; + addLiquidityBuildOutput: AddLiquidityBuildOutput; + txOutput: TxOutput; +}; + +async function sdkAddLiquidity({ + addLiquidity, + addLiquidityInput, + poolStateInput, + slippage, + testAddress, +}: { + addLiquidity: AddLiquidity; + addLiquidityInput: AddLiquidityInput; + poolStateInput: PoolStateInput; + slippage: Slippage; + testAddress: Address; +}): Promise<{ + addLiquidityBuildOutput: AddLiquidityBuildOutput; + addLiquidityQueryOutput: AddLiquidityQueryOutput; +}> { + const addLiquidityQueryOutput = await addLiquidity.query( + addLiquidityInput, + poolStateInput, + ); + const addLiquidityBuildOutput = addLiquidity.buildCall({ + ...addLiquidityQueryOutput, + slippage, + sender: testAddress, + recipient: testAddress, + }); + + return { + addLiquidityBuildOutput, + addLiquidityQueryOutput, + }; +} + +function isAddLiquidityComposableStableQueryOutput( + result: AddLiquidityQueryOutput, +): boolean { + return ( + (result as AddLiquidityComposableStableQueryOutput).bptIndex !== + undefined + ); +} + +function getCheck(result: AddLiquidityQueryOutput, isExactIn: boolean) { + if (isAddLiquidityComposableStableQueryOutput(result)) { + if (isExactIn) { + // Using this destructuring to return only the fields of interest + // rome-ignore lint/correctness/noUnusedVariables: + const { bptOut, bptIndex, ...check } = + result as AddLiquidityComposableStableQueryOutput; + return check; + } else { + // rome-ignore lint/correctness/noUnusedVariables: + const { amountsIn, bptIndex, ...check } = + result as AddLiquidityComposableStableQueryOutput; + return check; + } + } else { + if (isExactIn) { + // rome-ignore lint/correctness/noUnusedVariables: + const { bptOut, ...check } = result; + return check; + } else { + // rome-ignore lint/correctness/noUnusedVariables: + const { amountsIn, ...check } = result; + return check; + } + } +} + +/** + * Create and submit add liquidity transaction. + * @param txInput + * @param addLiquidity: AddLiquidity - The add liquidity class, used to query outputs and build transaction call + * @param poolInput: PoolStateInput - The state of the pool + * @param addLiquidityInput: AddLiquidityInput - The parameters of the transaction, example: bptOut, amountsIn, etc. + * @param testAddress: Address - The address to send the transaction from + * @param client: Client & PublicActions & WalletActions - The RPC client + * @param slippage: Slippage - The slippage tolerance for the transaction + */ +export async function doAddLiquidity(txInput: AddLiquidityTxInput) { + const { + addLiquidity, + poolStateInput, + addLiquidityInput, + testAddress, + client, + slippage, + } = txInput; + + const { addLiquidityQueryOutput, addLiquidityBuildOutput } = + await sdkAddLiquidity({ + addLiquidity, + addLiquidityInput, + poolStateInput, + slippage, + testAddress, + }); + + const tokens = getTokensForBalanceCheck(poolStateInput); + + // send transaction and calculate balance changes + const txOutput = await sendTransactionGetBalances( + tokens, + client, + testAddress, + addLiquidityBuildOutput.to, + addLiquidityBuildOutput.call, + addLiquidityBuildOutput.value, + ); + + return { + addLiquidityQueryOutput, + addLiquidityBuildOutput, + txOutput, + }; +} + +export function assertAddLiquidityUnbalanced( + chainId: ChainId, + poolStateInput: PoolStateInput, + addLiquidityInput: AddLiquidityUnbalancedInput, + addLiquidityOutput: AddLiquidityOutput, + slippage: Slippage, +) { + const { txOutput, addLiquidityQueryOutput, addLiquidityBuildOutput } = + addLiquidityOutput; + + // Get an amount for each pool token defaulting to 0 if not provided as input (this will include BPT token if in tokenList) + const expectedAmountsIn = poolStateInput.tokens.map((t) => { + let token; + if ( + addLiquidityInput.useNativeAssetAsWrappedAmountIn && + t.address === NATIVE_ASSETS[chainId].wrapped + ) + token = new Token(chainId, zeroAddress, t.decimals); + else token = new Token(chainId, t.address, t.decimals); + const input = addLiquidityInput.amountsIn.find( + (a) => a.address === t.address, + ); + if (input === undefined) return TokenAmount.fromRawAmount(token, 0n); + else return TokenAmount.fromRawAmount(token, input.rawAmount); + }); + + const expectedQueryOutput: Omit< + AddLiquidityQueryOutput, + 'bptOut' | 'bptIndex' + > = { + // Query should use same amountsIn as input + amountsIn: expectedAmountsIn, + tokenInIndex: undefined, + // Should match inputs + poolId: poolStateInput.id, + poolType: poolStateInput.type, + fromInternalBalance: !!addLiquidityInput.fromInternalBalance, + addLiquidityKind: addLiquidityInput.kind, + }; + + const queryCheck = getCheck(addLiquidityQueryOutput, true); + + expect(queryCheck).to.deep.eq(expectedQueryOutput); + + // Expect some bpt amount + expect(addLiquidityQueryOutput.bptOut.amount > 0n).to.be.true; + + assertAddLiquidityBuildOutput( + addLiquidityInput, + addLiquidityQueryOutput, + addLiquidityBuildOutput, + true, + slippage, + ); + + assertTokenDeltas( + poolStateInput, + addLiquidityInput, + addLiquidityQueryOutput, + addLiquidityBuildOutput, + txOutput, + ); +} + +export function assertAddLiquiditySingleToken( + chainId: ChainId, + poolStateInput: PoolStateInput, + addLiquidityInput: AddLiquiditySingleTokenInput, + addLiquidityOutput: AddLiquidityOutput, + slippage: Slippage, +) { + const { txOutput, addLiquidityQueryOutput, addLiquidityBuildOutput } = + addLiquidityOutput; + + if (addLiquidityQueryOutput.tokenInIndex === undefined) + throw Error('No index'); + + const bptToken = new Token(chainId, poolStateInput.address, 18); + + const tokensWithoutBpt = poolStateInput.tokens.filter( + (t) => t.address !== poolStateInput.address, + ); + + const expectedQueryOutput: Omit< + AddLiquidityQueryOutput, + 'amountsIn' | 'bptIndex' + > = { + // Query should use same bpt out as user sets + bptOut: TokenAmount.fromRawAmount( + bptToken, + addLiquidityInput.bptOut.rawAmount, + ), + tokenInIndex: tokensWithoutBpt.findIndex( + (t) => t.address === addLiquidityInput.tokenIn, + ), + // Should match inputs + poolId: poolStateInput.id, + poolType: poolStateInput.type, + fromInternalBalance: !!addLiquidityInput.fromInternalBalance, + addLiquidityKind: addLiquidityInput.kind, + }; + + const queryCheck = getCheck(addLiquidityQueryOutput, false); + + expect(queryCheck).to.deep.eq(expectedQueryOutput); + + // Expect only tokenIn to have amount > 0 + // (Note addLiquidityQueryOutput also has value for bpt if pre-minted) + addLiquidityQueryOutput.amountsIn.forEach((a) => { + if ( + !addLiquidityInput.useNativeAssetAsWrappedAmountIn && + a.token.address === addLiquidityInput.tokenIn + ) + expect(a.amount > 0n).to.be.true; + else if ( + addLiquidityInput.useNativeAssetAsWrappedAmountIn && + a.token.address === zeroAddress + ) + expect(a.amount > 0n).to.be.true; + else expect(a.amount).toEqual(0n); + }); + + assertAddLiquidityBuildOutput( + addLiquidityInput, + addLiquidityQueryOutput, + addLiquidityBuildOutput, + false, + slippage, + ); + + assertTokenDeltas( + poolStateInput, + addLiquidityInput, + addLiquidityQueryOutput, + addLiquidityBuildOutput, + txOutput, + ); +} + +export function assertAddLiquidityProportional( + chainId: ChainId, + poolStateInput: PoolStateInput, + addLiquidityInput: AddLiquidityProportionalInput, + addLiquidityOutput: AddLiquidityOutput, + slippage: Slippage, +) { + const { txOutput, addLiquidityQueryOutput, addLiquidityBuildOutput } = + addLiquidityOutput; + + const bptToken = new Token(chainId, poolStateInput.address, 18); + + const expectedQueryOutput: Omit< + AddLiquidityQueryOutput, + 'amountsIn' | 'bptIndex' + > = { + // Query should use same bpt out as user sets + bptOut: TokenAmount.fromRawAmount( + bptToken, + addLiquidityInput.bptOut.rawAmount, + ), + // Only expect tokenInIndex for AddLiquiditySingleToken + tokenInIndex: undefined, + // Should match inputs + poolId: poolStateInput.id, + poolType: poolStateInput.type, + fromInternalBalance: !!addLiquidityInput.fromInternalBalance, + addLiquidityKind: addLiquidityInput.kind, + }; + + const queryCheck = getCheck(addLiquidityQueryOutput, false); + + expect(queryCheck).to.deep.eq(expectedQueryOutput); + + // Expect all assets in to have an amount > 0 apart from BPT if it exists + addLiquidityQueryOutput.amountsIn.forEach((a) => { + if (a.token.address === poolStateInput.address) + expect(a.amount).toEqual(0n); + else expect(a.amount > 0n).to.be.true; + }); + + assertAddLiquidityBuildOutput( + addLiquidityInput, + addLiquidityQueryOutput, + addLiquidityBuildOutput, + false, + slippage, + ); + + assertTokenDeltas( + poolStateInput, + addLiquidityInput, + addLiquidityQueryOutput, + addLiquidityBuildOutput, + txOutput, + ); +} + +function assertTokenDeltas( + poolStateInput: PoolStateInput, + addLiquidityInput: AddLiquidityInput, + addLiquidityQueryOutput: AddLiquidityQueryOutput, + addLiquidityBuildOutput: AddLiquidityBuildOutput, + txOutput: TxOutput, +) { + expect(txOutput.transactionReceipt.status).to.eq('success'); + + // addLiquidityQueryOutput amountsIn will have a value for the BPT token if it is a pre-minted pool + const amountsWithoutBpt = [...addLiquidityQueryOutput.amountsIn].filter( + (t) => t.token.address !== poolStateInput.address, + ); + + // Matching order of getTokens helper: [poolTokens, BPT, native] + const expectedDeltas = [ + ...amountsWithoutBpt.map((a) => a.amount), + addLiquidityQueryOutput.bptOut.amount, + 0n, + ]; + + // If input is wrapped native we must replace it with 0 and update native value instead + if (addLiquidityInput.useNativeAssetAsWrappedAmountIn) { + const index = amountsWithoutBpt.findIndex( + (a) => a.token.address === zeroAddress, + ); + expectedDeltas[index] = 0n; + expectedDeltas[expectedDeltas.length - 1] = + addLiquidityBuildOutput.value; + } + + expect(txOutput.balanceDeltas).to.deep.eq(expectedDeltas); +} + +function assertAddLiquidityBuildOutput( + addLiquidityInput: AddLiquidityInput, + addLiquidityQueryOutput: AddLiquidityQueryOutput, + addLiquidityBuildOutput: AddLiquidityBuildOutput, + isExactIn: boolean, + slippage: Slippage, +) { + // if exactIn maxAmountsIn should use same amountsIn as input else slippage should be applied + const maxAmountsIn = isExactIn + ? [...addLiquidityQueryOutput.amountsIn] + : addLiquidityQueryOutput.amountsIn.map((a) => + TokenAmount.fromRawAmount(a.token, slippage.applyTo(a.amount)), + ); + + // if exactIn slippage should be applied to bptOut else should use same bptOut as input + const minBptOut = isExactIn + ? TokenAmount.fromRawAmount( + addLiquidityQueryOutput.bptOut.token, + slippage.removeFrom(addLiquidityQueryOutput.bptOut.amount), + ) + : ({ ...addLiquidityQueryOutput.bptOut } as TokenAmount); + + const expectedBuildOutput: Omit = { + maxAmountsIn, + minBptOut, + to: BALANCER_VAULT, + // Value should equal value of any wrapped asset if using native + value: addLiquidityInput.useNativeAssetAsWrappedAmountIn + ? (addLiquidityQueryOutput.amountsIn.find( + (a) => a.token.address === zeroAddress, + )?.amount as bigint) + : 0n, + }; + + // rome-ignore lint/correctness/noUnusedVariables: + const { call, ...buildCheck } = addLiquidityBuildOutput; + expect(buildCheck).to.deep.eq(expectedBuildOutput); +} diff --git a/test/lib/utils/getTokensForBalanceCheck.ts b/test/lib/utils/getTokensForBalanceCheck.ts new file mode 100644 index 00000000..f828f461 --- /dev/null +++ b/test/lib/utils/getTokensForBalanceCheck.ts @@ -0,0 +1,17 @@ +import { Address, PoolStateInput, ZERO_ADDRESS } from "../../../src"; + + +/** + * Get tokens from the pool formatted for balance check, i.e. [...poolTokens, bpt, eth] + * @param poolStateInput PoolStateInput + * @returns Address[] + */ +export function getTokensForBalanceCheck(poolStateInput: PoolStateInput): Address[] { + // pool tokens, bpt, eth + const tokens = poolStateInput.tokens + .filter((t) => t.address !== poolStateInput.address) + .map((t) => t.address); + tokens.push(poolStateInput.address); + tokens.push(ZERO_ADDRESS); + return tokens; +} \ No newline at end of file diff --git a/test/lib/utils/helper.ts b/test/lib/utils/helper.ts new file mode 100644 index 00000000..759b368a --- /dev/null +++ b/test/lib/utils/helper.ts @@ -0,0 +1,301 @@ +import { + Address, + Client, + Hex, + PublicActions, + TestActions, + TransactionReceipt, + WalletActions, + concat, + encodeAbiParameters, + hexToBigInt, + keccak256, + pad, + toBytes, + toHex, + trim, +} from 'viem'; +import { erc20Abi } from '../../../src/abi'; +import { BALANCER_VAULT, MAX_UINT256, ZERO_ADDRESS } from '../../../src/utils'; + +export type TxOutput = { + transactionReceipt: TransactionReceipt; + balanceDeltas: bigint[]; + gasUsed: bigint; +}; + +export const approveToken = async ( + client: Client & PublicActions & WalletActions, + account: Address, + token: Address, + amount = MAX_UINT256, // approve max by default +): Promise => { + // approve token on the vault + const hash = await client.writeContract({ + account, + chain: client.chain, + address: token, + abi: erc20Abi, + functionName: 'approve', + args: [BALANCER_VAULT, amount], + }); + + const txReceipt = await client.waitForTransactionReceipt({ + hash, + }); + return txReceipt.status === 'success'; +}; + +export const getErc20Balance = ( + token: Address, + client: Client & PublicActions, + holder: Address, +): Promise => + client.readContract({ + address: token, + abi: erc20Abi, + functionName: 'balanceOf', + args: [holder], + }); + +export const getBalances = async ( + tokens: Address[], + client: Client & PublicActions, + holder: Address, +): Promise> => { + const balances: Promise[] = []; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i] === ZERO_ADDRESS) { + balances[i] = client.getBalance({ + address: holder, + }); + } else { + balances[i] = getErc20Balance(tokens[i], client, holder); + } + } + return Promise.all(balances); +}; + +/** + * Helper function that sends a transaction and calculates balance changes + * + * @param tokensForBalanceCheck Token addresses to check balance deltas + * @param client Client that will perform transactions + * @param clientAddress Account address that will have token balance checked + * @param to Contract Address that will be called + * @param data Transaction encoded data + * @param value ETH value in case of ETH transfer + * @returns Transaction recepit, balance deltas and gas used + */ +export async function sendTransactionGetBalances( + tokensForBalanceCheck: Address[], + client: Client & PublicActions & TestActions & WalletActions, + clientAddress: Address, + to: Address, + data: Address, + value?: bigint, +): Promise { + const balanceBefore = await getBalances( + tokensForBalanceCheck, + client, + clientAddress, + ); + + // Send transaction to local fork + const hash = await client.sendTransaction({ + account: clientAddress, + chain: client.chain, + data, + to, + value, + }); + const transactionReceipt = await client.waitForTransactionReceipt({ + hash, + }); + const { gasUsed, effectiveGasPrice } = transactionReceipt; + const gasPrice = gasUsed * effectiveGasPrice; + + const balancesAfter = await getBalances( + tokensForBalanceCheck, + client, + clientAddress, + ); + + const balanceDeltas = balancesAfter.map((balanceAfter, i) => { + let _balanceAfter = balanceAfter; + if (tokensForBalanceCheck[i] === ZERO_ADDRESS) { + // ignore ETH delta from gas cost + _balanceAfter = balanceAfter + gasPrice; + } + const delta = _balanceAfter - balanceBefore[i]; + return delta >= 0n ? delta : -delta; + }); + + return { + transactionReceipt, + balanceDeltas, + gasUsed, + }; +} + +/** + * Set local ERC20 token balance for a given account address (used for testing) + * + * @param client client that will perform the setStorageAt call + * @param accountAddress Account address that will have token balance set + * @param token Token address which balance will be set + * @param slot Slot memory that stores balance - use npm package `slot20` to identify which slot to provide + * @param balance Balance in EVM amount + * @param isVyperMapping Whether the storage uses Vyper or Solidity mapping + */ +export const setTokenBalance = async ( + client: Client & TestActions, + accountAddress: Address, + token: Address, + slot: number, + balance: bigint, + isVyperMapping = false, +): Promise => { + // Get storage slot index + + const slotBytes = pad(toBytes(slot)); + const accountAddressBytes = pad(toBytes(accountAddress)); + + let index: Address; + if (isVyperMapping) { + index = keccak256(concat([slotBytes, accountAddressBytes])); // slot, key + } else { + index = keccak256(concat([accountAddressBytes, slotBytes])); // key, slot + } + + // Manipulate local balance (needs to be bytes32 string) + await client.setStorageAt({ + address: token, + index, + value: toHex(balance, { size: 32 }), + }); +}; + +/** + * Find ERC20 token balance storage slot (to be used on setTokenBalance) + * + * @param client client that will perform contract calls + * @param accountAddress Account address to probe storage slot changes + * @param tokenAddress Token address which we're looking for the balance slot + * @param isVyperMapping Whether the storage uses Vyper or Solidity mapping + */ +export async function findTokenBalanceSlot( + client: Client & PublicActions & TestActions, + accountAddress: Address, + tokenAddress: Address, + isVyperMapping = false, +): Promise { + const probeA = encodeAbiParameters( + [{ name: 'probeA', type: 'uint256' }], + [BigInt((Math.random() * 10000).toFixed())], + ); + const probeB = encodeAbiParameters( + [{ name: 'probeA', type: 'uint256' }], + [BigInt((Math.random() * 10000).toFixed())], + ); + for (let i = 0; i < 999; i++) { + // encode probed slot + const slotBytes = pad(toBytes(i)); + const accountAddressBytes = pad(toBytes(accountAddress)); + let probedSlot: Address; + if (isVyperMapping) { + probedSlot = keccak256(concat([slotBytes, accountAddressBytes])); // slot, key + } else { + probedSlot = keccak256(concat([accountAddressBytes, slotBytes])); // key, slot + } + + // remove padding for JSON RPC + probedSlot = trim(probedSlot); + + // get storage value + const prev = (await client.getStorageAt({ + address: tokenAddress, + slot: probedSlot, + })) as Hex; + + // set storage slot to new probe + const probe = prev === probeA ? probeB : probeA; + await client.setStorageAt({ + address: tokenAddress, + index: probedSlot, + value: probe, + }); + + // check if balance changed + const balance = await getErc20Balance( + tokenAddress, + client, + accountAddress, + ); + + // reset to previous value + await client.setStorageAt({ + address: tokenAddress, + index: probedSlot, + value: prev, + }); + + // return slot if balance changed + if (balance === hexToBigInt(probe)) return i; + } + throw new Error('Balance slot not found!'); +} + +/** + * Setup local fork with approved token balance for a given account address + * + * @param client Client that will perform transactions + * @param accountAddress Account address that will have token balance set and approved + * @param tokens Token addresses which balance will be set and approved + * @param slots Slot that stores token balance in memory - use npm package `slot20` to identify which slot to provide + * @param balances Balances in EVM amounts + * @param jsonRpcUrl Url with remote node to be forked locally + * @param isVyperMapping Whether the storage uses Vyper or Solidity mapping + */ +export const forkSetup = async ( + client: Client & PublicActions & TestActions & WalletActions, + accountAddress: Address, + tokens: Address[], + slots: number[] | undefined, + balances: bigint[], + isVyperMapping: boolean[] = Array(tokens.length).fill(false), +): Promise => { + await client.impersonateAccount({ address: accountAddress }); + + let _slots: number[]; + if (slots) { + _slots = slots; + } else { + _slots = await Promise.all( + tokens.map(async (token, i) => + findTokenBalanceSlot( + client, + accountAddress, + token, + isVyperMapping[i], + ), + ), + ); + console.log(`slots: ${_slots}`); + } + + for (let i = 0; i < tokens.length; i++) { + // Set initial account balance for each token that will be used to add liquidity to the pool + await setTokenBalance( + client, + accountAddress, + tokens[i], + _slots[i], + balances[i], + isVyperMapping[i], + ); + + // Approve appropriate allowances so that vault contract can move tokens + await approveToken(client, accountAddress, tokens[i]); + } +}; diff --git a/test/lib/utils/promises.ts b/test/lib/utils/promises.ts new file mode 100644 index 00000000..39068174 --- /dev/null +++ b/test/lib/utils/promises.ts @@ -0,0 +1,3 @@ +export function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/test/lib/utils/removeLiquidityHelper.ts b/test/lib/utils/removeLiquidityHelper.ts new file mode 100644 index 00000000..669c7c14 --- /dev/null +++ b/test/lib/utils/removeLiquidityHelper.ts @@ -0,0 +1,395 @@ +import { RemoveLiquidityTxInput } from './types'; +import { + ChainId, + RemoveLiquidityComposableStableQueryOutput, + RemoveLiquidityBuildOutput, + RemoveLiquidityQueryOutput, + NATIVE_ASSETS, + PoolStateInput, + TokenAmount, + Slippage, + Token, + RemoveLiquidityUnbalancedInput, + RemoveLiquiditySingleTokenInput, + BALANCER_VAULT, + RemoveLiquidityInput, + RemoveLiquidityProportionalInput, +} from '../../../src'; +import { sendTransactionGetBalances, TxOutput } from './helper'; +import { zeroAddress } from 'viem'; +import { getTokensForBalanceCheck } from './getTokensForBalanceCheck'; + +type RemoveLiquidityOutput = { + removeLiquidityQueryOutput: RemoveLiquidityQueryOutput; + removeLiquidityBuildOutput: RemoveLiquidityBuildOutput; + txOutput: TxOutput; +}; + +export const sdkRemoveLiquidity = async ({ + removeLiquidity, + removeLiquidityInput, + poolStateInput, + slippage, + testAddress, +}: Omit): Promise<{ + removeLiquidityBuildOutput: RemoveLiquidityBuildOutput; + removeLiquidityQueryOutput: RemoveLiquidityQueryOutput; +}> => { + const removeLiquidityQueryOutput = await removeLiquidity.query( + removeLiquidityInput, + poolStateInput, + ); + const removeLiquidityBuildOutput = removeLiquidity.buildCall({ + ...removeLiquidityQueryOutput, + slippage, + sender: testAddress, + recipient: testAddress, + }); + + return { + removeLiquidityBuildOutput, + removeLiquidityQueryOutput, + }; +}; + +function isRemoveLiquidityComposableStableQueryOutput( + result: RemoveLiquidityQueryOutput, +): boolean { + return ( + (result as RemoveLiquidityComposableStableQueryOutput).bptIndex !== + undefined + ); +} + +function getCheck(result: RemoveLiquidityQueryOutput, isExactIn: boolean) { + if (isRemoveLiquidityComposableStableQueryOutput(result)) { + if (isExactIn) { + // Using this destructuring to return only the fields of interest + // rome-ignore lint/correctness/noUnusedVariables: + const { amountsOut, bptIndex, ...check } = + result as RemoveLiquidityComposableStableQueryOutput; + return check; + } else { + // rome-ignore lint/correctness/noUnusedVariables: + const { bptIn, bptIndex, ...check } = + result as RemoveLiquidityComposableStableQueryOutput; + return check; + } + } else { + if (isExactIn) { + // rome-ignore lint/correctness/noUnusedVariables: + const { amountsOut, ...check } = result; + return check; + } else { + // rome-ignore lint/correctness/noUnusedVariables: + const { bptIn, ...check } = result; + return check; + } + } +} + +/** + * Create and submit remove liquidity transaction. + * @param txInput + * @param client: Client & PublicActions & WalletActions - The RPC client + * @param removeLiquidity: RemoveLiquidity - The remove liquidity class, used to query outputs and build transaction call + * @param removeLiquidityInput: RemoveLiquidityInput - The parameters of the transaction, example: bptIn, amountsOut, etc. + * @param slippage: Slippage - The slippage tolerance for the transaction + * @param poolStateInput: PoolStateInput - The state of the pool + * @param testAddress: Address - The address to send the transaction from + * */ +export async function doRemoveLiquidity(txInput: RemoveLiquidityTxInput) { + const { + removeLiquidity, + poolStateInput, + removeLiquidityInput, + testAddress, + client, + slippage, + } = txInput; + + const { removeLiquidityQueryOutput, removeLiquidityBuildOutput } = + await sdkRemoveLiquidity({ + removeLiquidity, + removeLiquidityInput, + poolStateInput, + slippage, + testAddress, + }); + + // get tokens for balance change - pool tokens, BPT, native + const tokens = getTokensForBalanceCheck(poolStateInput); + + // send transaction and calculate balance changes + const txOutput = await sendTransactionGetBalances( + tokens, + client, + testAddress, + removeLiquidityBuildOutput.to, + removeLiquidityBuildOutput.call, + removeLiquidityBuildOutput.value, + ); + + return { + removeLiquidityQueryOutput, + removeLiquidityBuildOutput, + txOutput, + }; +} + +export function assertRemoveLiquidityUnbalanced( + chainId: ChainId, + poolStateInput: PoolStateInput, + removeLiquidityInput: RemoveLiquidityUnbalancedInput, + removeLiquidityOutput: RemoveLiquidityOutput, + slippage: Slippage, +) { + const { txOutput, removeLiquidityQueryOutput, removeLiquidityBuildOutput } = + removeLiquidityOutput; + + // Get an amount for each pool token defaulting to 0 if not provided as input (this will include BPT token if in tokenList) + const expectedAmountsOut = poolStateInput.tokens.map((t) => { + let token; + if ( + removeLiquidityInput.toNativeAsset && + t.address === NATIVE_ASSETS[chainId].wrapped + ) + token = new Token(chainId, zeroAddress, t.decimals); + else token = new Token(chainId, t.address, t.decimals); + const input = removeLiquidityInput.amountsOut.find( + (a) => a.address === t.address, + ); + if (input === undefined) return TokenAmount.fromRawAmount(token, 0n); + else return TokenAmount.fromRawAmount(token, input.rawAmount); + }); + + const expectedQueryOutput: Omit< + RemoveLiquidityQueryOutput, + 'bptIn' | 'bptIndex' + > = { + // Query should use same amountsOut as input + amountsOut: expectedAmountsOut, + tokenOutIndex: undefined, + // Should match inputs + poolId: poolStateInput.id, + poolType: poolStateInput.type, + toInternalBalance: !!removeLiquidityInput.toInternalBalance, + removeLiquidityKind: removeLiquidityInput.kind, + }; + + const queryCheck = getCheck(removeLiquidityQueryOutput, false); + + expect(queryCheck).to.deep.eq(expectedQueryOutput); + + // Expect some bpt amount + expect(removeLiquidityQueryOutput.bptIn.amount > 0n).to.be.true; + + assertRemoveLiquidityBuildOutput( + removeLiquidityQueryOutput, + removeLiquidityBuildOutput, + false, + slippage, + ); + + assertTokenDeltas( + poolStateInput, + removeLiquidityInput, + removeLiquidityQueryOutput, + txOutput, + ); +} + +export function assertRemoveLiquiditySingleToken( + chainId: ChainId, + poolStateInput: PoolStateInput, + removeLiquidityInput: RemoveLiquiditySingleTokenInput, + removeLiquidityOutput: RemoveLiquidityOutput, + slippage: Slippage, +) { + const { txOutput, removeLiquidityQueryOutput, removeLiquidityBuildOutput } = + removeLiquidityOutput; + + if (removeLiquidityQueryOutput.tokenOutIndex === undefined) + throw Error('No index'); + + const bptToken = new Token(chainId, poolStateInput.address, 18); + + const tokensWithoutBpt = poolStateInput.tokens.filter( + (t) => t.address !== poolStateInput.address, + ); + + const expectedQueryOutput: Omit< + RemoveLiquidityQueryOutput, + 'amountsOut' | 'bptIndex' + > = { + // Query should use same bpt out as user sets + bptIn: TokenAmount.fromRawAmount( + bptToken, + removeLiquidityInput.bptIn.rawAmount, + ), + tokenOutIndex: tokensWithoutBpt.findIndex( + (t) => t.address === removeLiquidityInput.tokenOut, + ), + // Should match inputs + poolId: poolStateInput.id, + poolType: poolStateInput.type, + toInternalBalance: !!removeLiquidityInput.toInternalBalance, + removeLiquidityKind: removeLiquidityInput.kind, + }; + + const queryCheck = getCheck(removeLiquidityQueryOutput, true); + + expect(queryCheck).to.deep.eq(expectedQueryOutput); + + // Expect only tokenOut to have amount > 0 + // (Note removeLiquidityQueryOutput also has value for bpt if pre-minted) + removeLiquidityQueryOutput.amountsOut.forEach((a) => { + if ( + !removeLiquidityInput.toNativeAsset && + a.token.address === removeLiquidityInput.tokenOut + ) + expect(a.amount > 0n).to.be.true; + else if ( + removeLiquidityInput.toNativeAsset && + a.token.address === zeroAddress + ) + expect(a.amount > 0n).to.be.true; + else expect(a.amount).toEqual(0n); + }); + + assertRemoveLiquidityBuildOutput( + removeLiquidityQueryOutput, + removeLiquidityBuildOutput, + true, + slippage, + ); + + assertTokenDeltas( + poolStateInput, + removeLiquidityInput, + removeLiquidityQueryOutput, + txOutput, + ); +} + +export function assertRemoveLiquidityProportional( + chainId: ChainId, + poolStateInput: PoolStateInput, + removeLiquidityInput: RemoveLiquidityProportionalInput, + removeLiquidityOutput: RemoveLiquidityOutput, + slippage: Slippage, +) { + const { txOutput, removeLiquidityQueryOutput, removeLiquidityBuildOutput } = + removeLiquidityOutput; + + const bptToken = new Token(chainId, poolStateInput.address, 18); + + const expectedQueryOutput: Omit< + RemoveLiquidityQueryOutput, + 'amountsOut' | 'bptIndex' + > = { + // Query should use same bpt out as user sets + bptIn: TokenAmount.fromRawAmount( + bptToken, + removeLiquidityInput.bptIn.rawAmount, + ), + // Only expect tokenInIndex for AddLiquiditySingleToken + tokenOutIndex: undefined, + // Should match inputs + poolId: poolStateInput.id, + poolType: poolStateInput.type, + toInternalBalance: !!removeLiquidityInput.toInternalBalance, + removeLiquidityKind: removeLiquidityInput.kind, + }; + + const queryCheck = getCheck(removeLiquidityQueryOutput, true); + + expect(queryCheck).to.deep.eq(expectedQueryOutput); + + // Expect all assets in to have an amount > 0 apart from BPT if it exists + removeLiquidityQueryOutput.amountsOut.forEach((a) => { + if (a.token.address === poolStateInput.address) + expect(a.amount).toEqual(0n); + else expect(a.amount > 0n).to.be.true; + }); + + assertRemoveLiquidityBuildOutput( + removeLiquidityQueryOutput, + removeLiquidityBuildOutput, + true, + slippage, + ); + + assertTokenDeltas( + poolStateInput, + removeLiquidityInput, + removeLiquidityQueryOutput, + txOutput, + ); +} + +function assertTokenDeltas( + poolStateInput: PoolStateInput, + removeLiquidityInput: RemoveLiquidityInput, + removeLiquidityQueryOutput: RemoveLiquidityQueryOutput, + txOutput: TxOutput, +) { + expect(txOutput.transactionReceipt.status).to.eq('success'); + + // removeLiquidityQueryOutput amountsOut will have a value for the BPT token if it is a pre-minted pool + const amountsWithoutBpt = [...removeLiquidityQueryOutput.amountsOut].filter( + (t) => t.token.address !== poolStateInput.address, + ); + + // Matching order of getTokens helper: [poolTokens, BPT, native] + const expectedDeltas = [ + ...amountsWithoutBpt.map((a) => a.amount), + removeLiquidityQueryOutput.bptIn.amount, + 0n, + ]; + + // If removing liquidity to native asset we must replace it with 0 and update native value instead + if (removeLiquidityInput.toNativeAsset) { + const index = amountsWithoutBpt.findIndex( + (a) => a.token.address === zeroAddress, + ); + expectedDeltas[expectedDeltas.length - 1] = expectedDeltas[index]; + expectedDeltas[index] = 0n; + } + + expect(txOutput.balanceDeltas).to.deep.eq(expectedDeltas); +} + +function assertRemoveLiquidityBuildOutput( + removeLiquidityQueryOutput: RemoveLiquidityQueryOutput, + RemoveLiquidityBuildOutput: RemoveLiquidityBuildOutput, + isExactIn: boolean, + slippage: Slippage, +) { + // if exactIn minAmountsOut should use amountsOut with slippage applied, else should use same amountsOut as input + // slippage.removeFrom(a.amount) + const minAmountsOut = isExactIn + ? removeLiquidityQueryOutput.amountsOut.map((a) => + TokenAmount.fromRawAmount(a.token, slippage.removeFrom(a.amount)), + ) + : [...removeLiquidityQueryOutput.amountsOut]; + + // if exactIn slippage cannot be applied to bptIn, else should use bptIn with slippage applied + const maxBptIn = isExactIn + ? ({ ...removeLiquidityQueryOutput.bptIn } as TokenAmount) + : TokenAmount.fromRawAmount( + removeLiquidityQueryOutput.bptIn.token, + slippage.applyTo(removeLiquidityQueryOutput.bptIn.amount), + ); + + const expectedBuildOutput: Omit = { + minAmountsOut, + maxBptIn, + to: BALANCER_VAULT, + value: 0n, // Value should always be 0 when removing liquidity + }; + + // rome-ignore lint/correctness/noUnusedVariables: + const { call, ...buildCheck } = RemoveLiquidityBuildOutput; + expect(buildCheck).to.deep.eq(expectedBuildOutput); +} diff --git a/test/lib/utils/types.ts b/test/lib/utils/types.ts new file mode 100644 index 00000000..da030039 --- /dev/null +++ b/test/lib/utils/types.ts @@ -0,0 +1,28 @@ +import { Client, PublicActions, TestActions, WalletActions } from 'viem'; +import { + Address, + AddLiquidityInput, + RemoveLiquidityInput, + RemoveLiquidity, + AddLiquidity, + PoolStateInput, + Slippage, +} from '../../../src'; + +export type AddLiquidityTxInput = { + client: Client & PublicActions & TestActions & WalletActions; + addLiquidity: AddLiquidity; + addLiquidityInput: AddLiquidityInput; + slippage: Slippage; + poolStateInput: PoolStateInput; + testAddress: Address; +}; + +export type RemoveLiquidityTxInput = { + client: Client & PublicActions & TestActions & WalletActions; + removeLiquidity: RemoveLiquidity; + removeLiquidityInput: RemoveLiquidityInput; + slippage: Slippage; + poolStateInput: PoolStateInput; + testAddress: Address; +}; diff --git a/test/removeLiquidityComposableStable.integration.test.ts b/test/removeLiquidityComposableStable.integration.test.ts new file mode 100644 index 00000000..2b7cdb25 --- /dev/null +++ b/test/removeLiquidityComposableStable.integration.test.ts @@ -0,0 +1,279 @@ +// pnpm test -- removeLiquidityComposableStable.integration.test.ts +import { config } from 'dotenv'; +config(); + +import { + createTestClient, + http, + parseEther, + parseUnits, + publicActions, + walletActions, +} from 'viem'; +import { + RemoveLiquiditySingleTokenInput, + RemoveLiquidityProportionalInput, + RemoveLiquidityUnbalancedInput, + RemoveLiquidityKind, + Slippage, + Token, + PoolStateInput, + RemoveLiquidity, + Address, + Hex, + CHAINS, + ChainId, + getPoolAddress, + RemoveLiquidityInput, + InputAmount, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { RemoveLiquidityTxInput } from './lib/utils/types'; +import { + assertRemoveLiquidityProportional, + assertRemoveLiquiditySingleToken, + assertRemoveLiquidityUnbalanced, + doRemoveLiquidity, +} from './lib/utils/removeLiquidityHelper'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; + +const chainId = ChainId.MAINNET; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET); +const poolId = + '0x1a44e35d5451e0b78621a1b3e7a53dfaa306b1d000000000000000000000051b'; // baoETH-ETH StablePool + +describe('composable stable remove liquidity test', () => { + let txInput: RemoveLiquidityTxInput; + let bptToken: Token; + let poolInput: PoolStateInput; + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + removeLiquidity: new RemoveLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput: poolInput, + testAddress: '0x10a19e7ee7d7f8a52822f6817de8ea18204f2e4f', // Balancer DAO Multisig + removeLiquidityInput: {} as RemoveLiquidityInput, + }; + bptToken = new Token(chainId, poolInput.address, 18, 'BPT'); + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [txInput.poolStateInput.address], + [0], // TODO: hardcode these values to improve test performance + [parseUnits('1000', 18)], + ); + }); + + describe('remove liquidity unbalanced', async () => { + let input: Omit; + let amountsOut: InputAmount[]; + beforeAll(() => { + const bptIndex = txInput.poolStateInput.tokens.findIndex( + (t) => t.address === txInput.poolStateInput.address, + ); + const poolTokensWithoutBpt = txInput.poolStateInput.tokens + .map((t) => new Token(chainId, t.address, t.decimals)) + .filter((_, index) => index !== bptIndex); + + amountsOut = poolTokensWithoutBpt.map((t) => ({ + rawAmount: parseUnits('20', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Unbalanced, + }; + }); + test('with wrapped', async () => { + const removeLiquidityInput = { + ...input, + amountsOut: amountsOut.slice(0, 1), + }; + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput, + }); + assertRemoveLiquidityUnbalanced( + txInput.client.chain?.id as number, + txInput.poolStateInput, + removeLiquidityInput, + removeLiquidityOutput, + txInput.slippage, + ); + }); + test('with native', async () => { + const removeLiquidityInput = { + ...input, + amountsOut: amountsOut.slice(0, 1), + toNativeAsset: true, + }; + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput, + }); + assertRemoveLiquidityUnbalanced( + txInput.client.chain?.id as number, + txInput.poolStateInput, + removeLiquidityInput, + removeLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('remove liquidity single token', () => { + let input: RemoveLiquiditySingleTokenInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolInput.address, + }; + const tokenOut = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; // WETH + input = { + chainId, + rpcUrl, + bptIn, + tokenOut, + kind: RemoveLiquidityKind.SingleToken, + }; + }); + test('with wrapped', async () => { + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput: input, + }); + + assertRemoveLiquiditySingleToken( + txInput.client.chain?.id as number, + txInput.poolStateInput, + input, + removeLiquidityOutput, + txInput.slippage, + ); + }); + + test('with native', async () => { + const removeLiquidityInput = { + ...input, + toNativeAsset: true, + }; + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput, + }); + + assertRemoveLiquiditySingleToken( + txInput.client.chain?.id as number, + txInput.poolStateInput, + removeLiquidityInput, + removeLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('remove liquidity proportional', () => { + let input: RemoveLiquidityProportionalInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolInput.address, + }; + input = { + bptIn, + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput: input, + }); + + assertRemoveLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + input, + removeLiquidityOutput, + txInput.slippage, + ); + }); + test('with native', async () => { + const removeLiquidityInput = { + ...input, + useNativeAssetAsWrappedAmountIn: true, + }; + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput, + }); + assertRemoveLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + removeLiquidityInput, + removeLiquidityOutput, + txInput.slippage, + ); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + const tokens = [ + { + address: + '0x1a44e35d5451e0b78621a1b3e7a53dfaa306b1d0' as Address, // B-baoETH-ETH-BPT + decimals: 18, + index: 0, + }, + { + address: + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' as Address, // WETH + decimals: 18, + index: 1, + }, + { + address: + '0xf4edfad26ee0d23b69ca93112ecce52704e0006f' as Address, // baoETH + decimals: 18, + index: 2, + }, + ]; + + return { + id, + address: getPoolAddress(id) as Address, + type: 'PHANTOM_STABLE', + tokens, + }; + } +} + +/******************************************************************************/ diff --git a/test/removeLiquidityGyro2.integration.test.ts b/test/removeLiquidityGyro2.integration.test.ts new file mode 100644 index 00000000..a2616f71 --- /dev/null +++ b/test/removeLiquidityGyro2.integration.test.ts @@ -0,0 +1,188 @@ +// pnpm test -- weightedExit.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseEther, + parseUnits, + publicActions, + walletActions, +} from 'viem'; +import { + RemoveLiquiditySingleTokenInput, + RemoveLiquidityProportionalInput, + RemoveLiquidityUnbalancedInput, + RemoveLiquidityKind, + Slippage, + PoolStateInput, + RemoveLiquidity, + Address, + Hex, + CHAINS, + ChainId, + getPoolAddress, + RemoveLiquidityInput, + InputAmount, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { + assertRemoveLiquidityProportional, + doRemoveLiquidity, +} from './lib/utils/removeLiquidityHelper'; +import { RemoveLiquidityTxInput } from './lib/utils/types'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; +import { removeLiquidityKindNotSupportedByGyro } from '../src/entities/removeLiquidity/utils/validateInputs'; + +const chainId = ChainId.POLYGON; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.POLYGON); +const poolId = + '0xdac42eeb17758daa38caf9a3540c808247527ae3000200000000000000000a2b'; // 2CLP-USDC-DAI + +describe('Gyro2 remove liquidity test', () => { + let txInput: RemoveLiquidityTxInput; + let poolInput: PoolStateInput; + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + removeLiquidity: new RemoveLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput: poolInput, + testAddress: '0xe84f75fc9caa49876d0ba18d309da4231d44e94d', // MATIC Holder Wallet, must hold amount of matic to approve tokens + removeLiquidityInput: {} as RemoveLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [txInput.poolStateInput.address], + [0], + [parseUnits('1000', 18)], + ); + }); + + describe('proportional', () => { + let input: RemoveLiquidityProportionalInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('0.01'), + decimals: 18, + address: poolInput.address, + }; + input = { + bptIn, + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput: input, + }); + + assertRemoveLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + input, + removeLiquidityOutput, + txInput.slippage, + ); + //Removed test with native, because there are no GyroE V1 pool with wrapped native asset in any network + }); + }); + + describe('unbalanced', async () => { + let input: Omit; + let amountsOut: InputAmount[]; + beforeAll(() => { + amountsOut = poolInput.tokens.map((t) => ({ + rawAmount: parseUnits('0.001', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Unbalanced, + }; + }); + test('must throw remove liquidity kind not supported error', async () => { + const removeLiquidityInput = { + ...input, + amountsOut: amountsOut.slice(0, 1), + }; + await expect(() => + doRemoveLiquidity({ ...txInput, removeLiquidityInput }), + ).rejects.toThrowError(removeLiquidityKindNotSupportedByGyro); + }); + }); + + describe('single token', () => { + let input: RemoveLiquiditySingleTokenInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolInput.address, + }; + const tokenOut = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; // WETH + input = { + chainId, + rpcUrl, + bptIn, + tokenOut, + kind: RemoveLiquidityKind.SingleToken, + }; + }); + test('must throw remove liquidity kind not supported error', async () => { + await expect(() => + doRemoveLiquidity({ ...txInput, removeLiquidityInput: input }), + ).rejects.toThrowError(removeLiquidityKindNotSupportedByGyro); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + return { + id, + address: getPoolAddress(id) as Address, + type: 'GYRO2', + tokens: [ + { + address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC(PoS) + decimals: 6, + index: 0, + }, + { + address: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', // DAI + decimals: 18, + index: 1, + }, + ], + }; + } +} + +/******************************************************************************/ diff --git a/test/removeLiquidityGyro3.integration.test.ts b/test/removeLiquidityGyro3.integration.test.ts new file mode 100644 index 00000000..f88cf336 --- /dev/null +++ b/test/removeLiquidityGyro3.integration.test.ts @@ -0,0 +1,193 @@ +// pnpm test -- weightedExit.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseEther, + parseUnits, + publicActions, + walletActions, +} from 'viem'; +import { + RemoveLiquiditySingleTokenInput, + RemoveLiquidityProportionalInput, + RemoveLiquidityUnbalancedInput, + RemoveLiquidityKind, + Slippage, + PoolStateInput, + RemoveLiquidity, + Address, + Hex, + CHAINS, + ChainId, + getPoolAddress, + RemoveLiquidityInput, + InputAmount, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { + assertRemoveLiquidityProportional, + doRemoveLiquidity, +} from './lib/utils/removeLiquidityHelper'; +import { RemoveLiquidityTxInput } from './lib/utils/types'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; +import { removeLiquidityKindNotSupportedByGyro } from '../src/entities/removeLiquidity/utils/validateInputs'; + +const chainId = ChainId.POLYGON; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.POLYGON); +const poolId = + '0x17f1ef81707811ea15d9ee7c741179bbe2a63887000100000000000000000799'; // 3CLP-BUSD-USDC-USDT + +describe('Gyro3 remove liquidity test', () => { + let txInput: RemoveLiquidityTxInput; + let poolInput: PoolStateInput; + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + removeLiquidity: new RemoveLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput: poolInput, + testAddress: '0xe84f75fc9caa49876d0ba18d309da4231d44e94d', // MATIC Holder Wallet, must hold amount of matic to approve tokens + removeLiquidityInput: {} as RemoveLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [txInput.poolStateInput.address], + [0], + [parseUnits('1000', 18)], + ); + }); + + describe('proportional', () => { + let input: RemoveLiquidityProportionalInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('0.01'), + decimals: 18, + address: poolInput.address, + }; + input = { + bptIn, + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput: input, + }); + + assertRemoveLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + input, + removeLiquidityOutput, + txInput.slippage, + ); + //Removed test with native, because there are no GyroE V1 pool with wrapped native asset in any network + }); + }); + + describe('unbalanced', async () => { + let input: Omit; + let amountsOut: InputAmount[]; + beforeAll(() => { + amountsOut = poolInput.tokens.map((t) => ({ + rawAmount: parseUnits('0.001', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Unbalanced, + }; + }); + test('must throw remove liquidity kind not supported error', async () => { + const removeLiquidityInput = { + ...input, + amountsOut: amountsOut.slice(0, 1), + }; + await expect(() => + doRemoveLiquidity({ ...txInput, removeLiquidityInput }), + ).rejects.toThrowError(removeLiquidityKindNotSupportedByGyro); + }); + }); + + describe('single token', () => { + let input: RemoveLiquiditySingleTokenInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolInput.address, + }; + const tokenOut = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; // WETH + input = { + chainId, + rpcUrl, + bptIn, + tokenOut, + kind: RemoveLiquidityKind.SingleToken, + }; + }); + test('must throw remove liquidity kind not supported error', async () => { + await expect(() => + doRemoveLiquidity({ ...txInput, removeLiquidityInput: input }), + ).rejects.toThrowError(removeLiquidityKindNotSupportedByGyro); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + return { + id, + address: getPoolAddress(id) as Address, + type: 'GYRO3', + tokens: [ + { + address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC(PoS) + decimals: 6, + index: 0, + }, + { + address: '0x9c9e5fd8bbc25984b178fdce6117defa39d2db39', // BUSD + decimals: 18, + index: 1, + }, + { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', // USDT(PoS) + decimals: 6, + index: 2, + }, + ], + }; + } +} + +/******************************************************************************/ diff --git a/test/removeLiquidityGyroE.integration.test.ts b/test/removeLiquidityGyroE.integration.test.ts new file mode 100644 index 00000000..2a977914 --- /dev/null +++ b/test/removeLiquidityGyroE.integration.test.ts @@ -0,0 +1,188 @@ +// pnpm test -- weightedExit.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseEther, + parseUnits, + publicActions, + walletActions, +} from 'viem'; +import { + RemoveLiquiditySingleTokenInput, + RemoveLiquidityProportionalInput, + RemoveLiquidityUnbalancedInput, + RemoveLiquidityKind, + Slippage, + PoolStateInput, + RemoveLiquidity, + Address, + Hex, + CHAINS, + ChainId, + getPoolAddress, + RemoveLiquidityInput, + InputAmount, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { + assertRemoveLiquidityProportional, + doRemoveLiquidity, +} from './lib/utils/removeLiquidityHelper'; +import { RemoveLiquidityTxInput } from './lib/utils/types'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; +import { removeLiquidityKindNotSupportedByGyro } from '../src/entities/removeLiquidity/utils/validateInputs'; + +const chainId = ChainId.POLYGON; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.POLYGON); +const poolId = + '0x97469e6236bd467cd147065f77752b00efadce8a0002000000000000000008c0'; // ECLP-TUSD-USDC + +describe('GyroE V1 remove liquidity test', () => { + let txInput: RemoveLiquidityTxInput; + let poolInput: PoolStateInput; + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + removeLiquidity: new RemoveLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput: poolInput, + testAddress: '0xe84f75fc9caa49876d0ba18d309da4231d44e94d', // MATIC Holder Wallet, must hold amount of matic to approve tokens + removeLiquidityInput: {} as RemoveLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [txInput.poolStateInput.address], + [0], + [parseUnits('1000', 18)], + ); + }); + + describe('proportional', () => { + let input: RemoveLiquidityProportionalInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolInput.address, + }; + input = { + bptIn, + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput: input, + }); + + assertRemoveLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + input, + removeLiquidityOutput, + txInput.slippage, + ); + }); + //Removed test with native, because there are no GyroE V1 pool with wrapped native asset in any network + }); + + describe('unbalanced', async () => { + let input: Omit; + let amountsOut: InputAmount[]; + beforeAll(() => { + amountsOut = poolInput.tokens.map((t) => ({ + rawAmount: parseUnits('1', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Unbalanced, + }; + }); + test('must throw remove liquidity kind not supported error', async () => { + const removeLiquidityInput = { + ...input, + amountsOut: amountsOut.slice(0, 1), + }; + await expect(() => + doRemoveLiquidity({ ...txInput, removeLiquidityInput }), + ).rejects.toThrowError(removeLiquidityKindNotSupportedByGyro); + }); + }); + + describe('single token', () => { + let input: RemoveLiquiditySingleTokenInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolInput.address, + }; + const tokenOut = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; // WETH + input = { + chainId, + rpcUrl, + bptIn, + tokenOut, + kind: RemoveLiquidityKind.SingleToken, + }; + }); + test('must throw remove liquidity kind not supported error', async () => { + await expect(() => + doRemoveLiquidity({ ...txInput, removeLiquidityInput: input }), + ).rejects.toThrowError(removeLiquidityKindNotSupportedByGyro); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + return { + id, + address: getPoolAddress(id) as Address, + type: 'GYROE', + tokens: [ + { + address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC + decimals: 6, + index: 0, + }, + { + address: '0x2e1ad108ff1d8c782fcbbb89aad783ac49586756', // TUSD + decimals: 18, + index: 1, + }, + ], + }; + } +} + +/******************************************************************************/ diff --git a/test/removeLiquidityGyroEV2.integration.test.ts b/test/removeLiquidityGyroEV2.integration.test.ts new file mode 100644 index 00000000..710a73ba --- /dev/null +++ b/test/removeLiquidityGyroEV2.integration.test.ts @@ -0,0 +1,203 @@ +// pnpm test -- weightedExit.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseEther, + parseUnits, + publicActions, + walletActions, +} from 'viem'; +import { + RemoveLiquiditySingleTokenInput, + RemoveLiquidityProportionalInput, + RemoveLiquidityUnbalancedInput, + RemoveLiquidityKind, + Slippage, + PoolStateInput, + RemoveLiquidity, + Address, + Hex, + CHAINS, + ChainId, + getPoolAddress, + RemoveLiquidityInput, + InputAmount, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { + assertRemoveLiquidityProportional, + doRemoveLiquidity, +} from './lib/utils/removeLiquidityHelper'; +import { RemoveLiquidityTxInput } from './lib/utils/types'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; +import { removeLiquidityKindNotSupportedByGyro } from '../src/entities/removeLiquidity/utils/validateInputs'; + +const chainId = ChainId.MAINNET; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET); +const poolId = + '0xf01b0684c98cd7ada480bfdf6e43876422fa1fc10002000000000000000005de'; // ECLP-wstETH-wETH + +describe('GyroE V2 remove liquidity test', () => { + let txInput: RemoveLiquidityTxInput; + let poolInput: PoolStateInput; + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + removeLiquidity: new RemoveLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput: poolInput, + testAddress: '0x10a19e7ee7d7f8a52822f6817de8ea18204f2e4f', // Balancer DAO Multisig + removeLiquidityInput: {} as RemoveLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [txInput.poolStateInput.address], + [0], + [parseUnits('1000', 18)], + ); + }); + + describe('proportional', () => { + let input: RemoveLiquidityProportionalInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('0.01'), + decimals: 18, + address: poolInput.address, + }; + input = { + bptIn, + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput: input, + }); + + assertRemoveLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + input, + removeLiquidityOutput, + txInput.slippage, + ); + }); + test('with native', async () => { + const removeLiquidityInput = { + ...input, + useNativeAssetAsWrappedAmountIn: true, + }; + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput, + }); + assertRemoveLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + removeLiquidityInput, + removeLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('unbalanced', async () => { + let input: Omit; + let amountsOut: InputAmount[]; + beforeAll(() => { + amountsOut = poolInput.tokens.map((t) => ({ + rawAmount: parseUnits('0.001', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Unbalanced, + }; + }); + test('must throw remove liquidity kind not supported error', async () => { + const removeLiquidityInput = { + ...input, + amountsOut: amountsOut.slice(0, 1), + }; + await expect(() => + doRemoveLiquidity({ ...txInput, removeLiquidityInput }), + ).rejects.toThrowError(removeLiquidityKindNotSupportedByGyro); + }); + }); + + describe('single token', () => { + let input: RemoveLiquiditySingleTokenInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolInput.address, + }; + const tokenOut = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; // WETH + input = { + chainId, + rpcUrl, + bptIn, + tokenOut, + kind: RemoveLiquidityKind.SingleToken, + }; + }); + test('must throw remove liquidity kind not supported error', async () => { + await expect(() => + doRemoveLiquidity({ ...txInput, removeLiquidityInput: input }), + ).rejects.toThrowError(removeLiquidityKindNotSupportedByGyro); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ +export class MockApi { + public async getPool(id: Hex): Promise { + return { + id, + address: getPoolAddress(id) as Address, + type: 'GYROE', + tokens: [ + { + address: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', // wstETH + decimals: 18, + index: 0, + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // wETH + decimals: 18, + index: 1, + }, + ], + }; + } +} + +/******************************************************************************/ diff --git a/test/removeLiquidityWeighted.integration.test.ts b/test/removeLiquidityWeighted.integration.test.ts new file mode 100644 index 00000000..97cebf36 --- /dev/null +++ b/test/removeLiquidityWeighted.integration.test.ts @@ -0,0 +1,283 @@ +// pnpm test -- removeLiquidityWeighted.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseEther, + parseUnits, + publicActions, + walletActions, +} from 'viem'; +import { + RemoveLiquiditySingleTokenInput, + RemoveLiquidityProportionalInput, + RemoveLiquidityUnbalancedInput, + RemoveLiquidityKind, + Slippage, + PoolStateInput, + RemoveLiquidity, + Address, + Hex, + CHAINS, + ChainId, + getPoolAddress, + RemoveLiquidityInput, + InputAmount, +} from '../src'; +import { forkSetup } from './lib/utils/helper'; +import { + assertRemoveLiquidityProportional, + assertRemoveLiquiditySingleToken, + assertRemoveLiquidityUnbalanced, + doRemoveLiquidity, +} from './lib/utils/removeLiquidityHelper'; +import { RemoveLiquidityTxInput } from './lib/utils/types'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; + +const chainId = ChainId.MAINNET; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET); +const poolId = + '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014'; // 80BAL-20WETH + +describe('weighted remove liquidity test', () => { + let txInput: RemoveLiquidityTxInput; + let poolInput: PoolStateInput; + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolInput = await api.getPool(poolId); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + txInput = { + client, + removeLiquidity: new RemoveLiquidity(), + slippage: Slippage.fromPercentage('1'), // 1% + poolStateInput: poolInput, + testAddress: '0x10a19e7ee7d7f8a52822f6817de8ea18204f2e4f', // Balancer DAO Multisig + removeLiquidityInput: {} as RemoveLiquidityInput, + }; + }); + + beforeEach(async () => { + await forkSetup( + txInput.client, + txInput.testAddress, + [txInput.poolStateInput.address], + [0], // TODO: hardcode these values to improve test performance + [parseUnits('1000', 18)], + ); + }); + + describe('remove liquidity unbalanced', async () => { + let input: Omit; + let amountsOut: InputAmount[]; + beforeAll(() => { + amountsOut = poolInput.tokens.map((t) => ({ + rawAmount: parseUnits('0.001', t.decimals), + decimals: t.decimals, + address: t.address, + })); + input = { + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Unbalanced, + }; + }); + test('with wrapped', async () => { + const removeLiquidityInput = { + ...input, + amountsOut: amountsOut.slice(0, 1), + }; + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput, + }); + assertRemoveLiquidityUnbalanced( + txInput.client.chain?.id as number, + txInput.poolStateInput, + removeLiquidityInput, + removeLiquidityOutput, + txInput.slippage, + ); + }); + test('with native', async () => { + const removeLiquidityInput = { + ...input, + amountsOut: amountsOut.slice(0, 1), + toNativeAsset: true, + }; + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput, + }); + assertRemoveLiquidityUnbalanced( + txInput.client.chain?.id as number, + txInput.poolStateInput, + removeLiquidityInput, + removeLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('remove liquidity single asset', () => { + let input: RemoveLiquiditySingleTokenInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolInput.address, + }; + const tokenOut = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; // WETH + input = { + chainId, + rpcUrl, + bptIn, + tokenOut, + kind: RemoveLiquidityKind.SingleToken, + }; + }); + test('with wrapped', async () => { + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput: input, + }); + + assertRemoveLiquiditySingleToken( + txInput.client.chain?.id as number, + txInput.poolStateInput, + input, + removeLiquidityOutput, + txInput.slippage, + ); + }); + + test('with native', async () => { + const removeLiquidityInput = { + ...input, + toNativeAsset: true, + }; + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput, + }); + + assertRemoveLiquiditySingleToken( + txInput.client.chain?.id as number, + txInput.poolStateInput, + removeLiquidityInput, + removeLiquidityOutput, + txInput.slippage, + ); + }); + }); + + describe('remove liquidity proportional', () => { + let input: RemoveLiquidityProportionalInput; + beforeAll(() => { + const bptIn: InputAmount = { + rawAmount: parseEther('1'), + decimals: 18, + address: poolInput.address, + }; + input = { + bptIn, + chainId, + rpcUrl, + kind: RemoveLiquidityKind.Proportional, + }; + }); + test('with tokens', async () => { + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput: input, + }); + + assertRemoveLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + input, + removeLiquidityOutput, + txInput.slippage, + ); + }); + test('with native', async () => { + const removeLiquidityInput = { + ...input, + useNativeAssetAsWrappedAmountIn: true, + }; + const removeLiquidityOutput = await doRemoveLiquidity({ + ...txInput, + removeLiquidityInput, + }); + assertRemoveLiquidityProportional( + txInput.client.chain?.id as number, + txInput.poolStateInput, + removeLiquidityInput, + removeLiquidityOutput, + txInput.slippage, + ); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +export class MockApi { + public async getPool(id: Hex): Promise { + let tokens: { address: Address; decimals: number; index: number }[] = + []; + if ( + id === + '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014' + ) { + tokens = [ + { + address: '0xba100000625a3754423978a60c9317c58a424e3d', // BAL + decimals: 18, + index: 0, + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // wETH + decimals: 18, + index: 1, + }, + ]; + } else if ( + id === + '0x87a867f5d240a782d43d90b6b06dea470f3f8f22000200000000000000000516' + ) { + tokens = [ + { + address: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', // wstETH slot 0 + decimals: 18, + index: 0, + }, + { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', // COMP slot 1 + decimals: 18, + index: 1, + }, + ]; + } + return { + id, + address: getPoolAddress(id) as Address, + type: 'WEIGHTED', + tokens, + }; + } +} + +/******************************************************************************/ diff --git a/test/sor.test.ts b/test/sor.test.ts deleted file mode 100644 index 32a8671c..00000000 --- a/test/sor.test.ts +++ /dev/null @@ -1,286 +0,0 @@ -// pnpm test -- test/sor.test.ts -import dotenv from 'dotenv'; -dotenv.config(); - -import { SmartOrderRouter } from '../src/sor'; -import { sorGetSwapsWithPools } from '../src/static'; -import { SubgraphPoolProvider } from '../src/data/providers/subgraphPoolProvider'; -import { ChainId, ETH, NATIVE_ASSETS, BATCHSIZE, VAULT } from '../src/utils'; -import { Token, TokenAmount } from '../src/entities'; -import { OnChainPoolDataEnricher } from '../src/data/enrichers/onChainPoolDataEnricher'; -import { SwapKind, SwapOptions } from '../src/types'; -import { BasePool } from '../src/entities/pools'; - -describe( - 'SmartOrderRouter', - () => { - describe('Mainnet', () => { - const chainId = ChainId.MAINNET; - const rpcUrl = - process.env.ETHEREUM_RPC_URL || 'https://eth.llamarpc.com'; - const subgraphPoolDataService = new SubgraphPoolProvider(chainId); - const onChainPoolDataEnricher = new OnChainPoolDataEnricher( - chainId, - rpcUrl, - BATCHSIZE[chainId], - VAULT[chainId], - ); - - const sor = new SmartOrderRouter({ - chainId, - poolDataProviders: subgraphPoolDataService, - poolDataEnrichers: onChainPoolDataEnricher, - rpcUrl: rpcUrl, - }); - - const BAL = new Token( - chainId, - '0xba100000625a3754423978a60c9317c58a424e3D', - 18, - 'BAL', - ); - const USDC = new Token( - chainId, - '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - 6, - 'USDC', - ); - const USDT = new Token( - chainId, - '0xdAC17F958D2ee523a2206206994597C13D831ec7', - 6, - 'USDT', - ); - const DAI = new Token( - chainId, - '0x6B175474E89094C44Da98b954EedeAC495271d0F', - 18, - 'DAI', - ); - - const swapOptions: SwapOptions = { - block: 17473810n, - }; - - let pools: BasePool[]; - // Since constructing a Swap mutates the pool balances, we refetch for each test - // May be a better way to deep clone a BasePool[] class instead - beforeEach(async () => { - pools = await sor.fetchAndCachePools(swapOptions.block); - }); - - describe('Weighted Pools', () => { - // ETH -> BAL swapGivenIn single hop - // Weighted pool - // 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014 - test('Native ETH -> Token givenIn single hop', async () => { - const inputAmount = TokenAmount.fromHumanAmount(ETH, '1'); - - const swap = await sorGetSwapsWithPools( - ETH, - BAL, - SwapKind.GivenIn, - inputAmount, - pools, - swapOptions, - ); - - if (!swap) throw new Error('Swap is undefined'); - - const onchain = await swap.query(rpcUrl, swapOptions.block); - - expect(swap.quote.amount).toEqual(onchain.amount); - expect(swap.inputAmount.amount).toEqual(inputAmount.amount); - expect(swap.outputAmount.amount).toEqual(swap.quote.amount); - expect(swap.paths.length).toEqual(1); - expect(swap.paths[0].pools.length).toEqual(1); - expect(swap.paths[0].pools[0].id).toEqual( - '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014', - ); - }); - - // ETH -> BAL swapGivenOut single hop - // Weighted pool - // 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014 - test('Native ETH -> Token givenOut single hop', async () => { - const outputAmount = TokenAmount.fromHumanAmount( - BAL, - '100', - ); - - const swap = await sorGetSwapsWithPools( - ETH, - BAL, - SwapKind.GivenOut, - outputAmount, - pools, - swapOptions, - ); - - if (!swap) throw new Error('Swap is undefined'); - - const onchain = await swap.query(rpcUrl, swapOptions.block); - - expect(swap.quote.amount).toEqual(onchain.amount); - expect(swap.inputAmount.amount).toEqual(swap.quote.amount); - expect(swap.outputAmount.amount).toEqual( - outputAmount.amount, - ); - expect(swap.paths.length).toEqual(1); - expect(swap.paths[0].pools.length).toEqual(1); - expect(swap.paths[0].pools[0].id).toEqual( - '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014', - ); - }); - }); - - describe('Stable Pools', () => { - test('DAI -> USDT givenIn ComposableStable', async () => { - const inputAmount = TokenAmount.fromHumanAmount( - DAI, - '100000', - ); - - const swap = await sorGetSwapsWithPools( - DAI, - USDT, - SwapKind.GivenIn, - inputAmount, - pools, - swapOptions, - ); - - if (!swap) throw new Error('Swap is undefined'); - - const onchain = await swap.query(rpcUrl, swapOptions.block); - expect(swap.quote.amount).toEqual(onchain.amount); - expect(swap.inputAmount.amount).toEqual(inputAmount.amount); - expect(swap.outputAmount.amount).toEqual(swap.quote.amount); - expect(swap.paths.length).toEqual(1); - expect(swap.paths[0].pools.length).toEqual(1); - }); - - test('USDC -> DAI givenOut ComposableStable', async () => { - const outputAmount = TokenAmount.fromHumanAmount( - DAI, - '1000000', - ); - - const swap = await sorGetSwapsWithPools( - USDC, - DAI, - SwapKind.GivenOut, - outputAmount, - pools, - swapOptions, - ); - - if (!swap) throw new Error('Swap is undefined'); - - const onchain = await swap.query(rpcUrl, swapOptions.block); - expect(swap.quote.amount).toEqual(onchain.amount); - }); - }); - }); - describe('Fantom', () => { - const chainId = ChainId.FANTOM; - const inputToken = NATIVE_ASSETS[chainId]; - const rpcUrl = process.env.FANTOM_RPC_URL || ''; - const subgraphPoolDataService = new SubgraphPoolProvider( - chainId, - undefined, - { - poolIdIn: [ - '0x9e4341acef4147196e99d648c5e43b3fc9d026780002000000000000000005ec', - ], - }, - ); - const onChainPoolDataEnricher = new OnChainPoolDataEnricher( - chainId, - rpcUrl, - BATCHSIZE[chainId], - VAULT[chainId], - ); - - const sor = new SmartOrderRouter({ - chainId, - poolDataProviders: subgraphPoolDataService, - poolDataEnrichers: onChainPoolDataEnricher, - rpcUrl: rpcUrl, - }); - - const BEETS = new Token( - chainId, - '0xF24Bcf4d1e507740041C9cFd2DddB29585aDCe1e', - 18, - 'BEETS', - ); - - const swapOptions: SwapOptions = { - block: 65313450n, - }; - - let pools: BasePool[]; - // Since constructing a Swap mutates the pool balances, we refetch for each test - // May be a better way to deep clone a BasePool[] class instead - beforeEach(async () => { - pools = await sor.fetchAndCachePools(swapOptions.block); - }); - - describe('Native Swaps', () => { - test('Native -> Token givenIn', async () => { - const inputAmount = TokenAmount.fromHumanAmount( - inputToken, - '100', - ); - - const swap = await sorGetSwapsWithPools( - inputToken, - BEETS, - SwapKind.GivenIn, - inputAmount, - pools, - swapOptions, - ); - - if (!swap) throw new Error('Swap is undefined'); - - const onchain = await swap.query(rpcUrl, swapOptions.block); - - expect(swap.quote.amount).toEqual(onchain.amount); - expect(swap.inputAmount.amount).toEqual(inputAmount.amount); - expect(swap.outputAmount.amount).toEqual(swap.quote.amount); - }); - - test('Native ETH -> Token givenOut', async () => { - const outputAmount = TokenAmount.fromHumanAmount( - BEETS, - '100000', - ); - - const swap = await sorGetSwapsWithPools( - inputToken, - BEETS, - SwapKind.GivenOut, - outputAmount, - pools, - swapOptions, - ); - - if (!swap) throw new Error('Swap is undefined'); - - const onchain = await swap.query(rpcUrl, swapOptions.block); - - expect(swap.quote.amount).toEqual(onchain.amount); - expect(swap.inputAmount.amount).toEqual(swap.quote.amount); - expect(swap.outputAmount.amount).toEqual( - outputAmount.amount, - ); - }); - }); - }); - }, - { - timeout: 120000, - }, -); diff --git a/test/vitest-setup.ts b/test/vitest-setup.ts new file mode 100644 index 00000000..37f2b0aa --- /dev/null +++ b/test/vitest-setup.ts @@ -0,0 +1,5 @@ +import { stopAnvilForks } from './anvil/anvil-global-setup'; + +afterAll(async () => { + await stopAnvilForks(); +}); diff --git a/test/weightedPool.integration.test.ts b/test/weightedPool.integration.test.ts new file mode 100644 index 00000000..8d8ed31f --- /dev/null +++ b/test/weightedPool.integration.test.ts @@ -0,0 +1,113 @@ +// pnpm test -- test/weightedPool.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { SmartOrderRouter } from '../src/sor'; +import { sorGetSwapsWithPools } from '../src/static'; +import { ChainId, ETH, BATCHSIZE, VAULT } from '../src/utils'; +import { Token, TokenAmount } from '../src/entities'; +import { OnChainPoolDataEnricher } from '../src/data/enrichers/onChainPoolDataEnricher'; +import { SwapKind, SwapOptions } from '../src/types'; +import { BasePool } from '../src/entities/pools'; +import { MockPoolProvider } from './lib/utils/mockPoolProvider'; + +import testPools from './lib/testData/testPools/weighted_17473810.json'; +import { RawWeightedPool } from '../src'; +import { ANVIL_NETWORKS, startFork } from './anvil/anvil-global-setup'; + +const chainId = ChainId.MAINNET; +const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET); + +describe('Weighted Swap tests', () => { + const mockPoolProvider = new MockPoolProvider( + testPools.pools as RawWeightedPool[], + ); + const onChainPoolDataEnricher = new OnChainPoolDataEnricher( + chainId, + rpcUrl, + BATCHSIZE[chainId], + VAULT[chainId], + ); + + const sor = new SmartOrderRouter({ + chainId, + poolDataProviders: mockPoolProvider, + poolDataEnrichers: onChainPoolDataEnricher, + rpcUrl: rpcUrl, + }); + + const BAL = new Token( + chainId, + '0xba100000625a3754423978a60c9317c58a424e3D', + 18, + 'BAL', + ); + + const swapOptions: SwapOptions = { + block: 17473810n, + }; + + let pools: BasePool[]; + // Since constructing a Swap mutates the pool balances, we refetch for each test + // May be a better way to deep clone a BasePool[] class instead + beforeEach(async () => { + pools = await sor.fetchAndCachePools(swapOptions.block); + }); + + // ETH -> BAL swapGivenIn single hop + // Weighted pool + // 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014 + test('Native ETH -> Token givenIn single hop', async () => { + const inputAmount = TokenAmount.fromHumanAmount(ETH, '1'); + + const swap = await sorGetSwapsWithPools( + ETH, + BAL, + SwapKind.GivenIn, + inputAmount, + pools, + swapOptions, + ); + + if (!swap) throw new Error('Swap is undefined'); + + const onchain = await swap.query(rpcUrl, swapOptions.block); + expect(swap.quote.amount).toEqual(onchain.amount); + expect(swap.inputAmount.amount).toEqual(inputAmount.amount); + expect(swap.outputAmount.amount).toEqual(swap.quote.amount); + expect(swap.paths.length).toEqual(1); + expect(swap.paths[0].pools.length).toEqual(1); + expect(swap.paths[0].pools[0].id).toEqual( + '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014', + ); + }); + + // ETH -> BAL swapGivenOut single hop + // Weighted pool + // 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014 + test('Native ETH -> Token givenOut single hop', async () => { + const outputAmount = TokenAmount.fromHumanAmount(BAL, '100'); + + const swap = await sorGetSwapsWithPools( + ETH, + BAL, + SwapKind.GivenOut, + outputAmount, + pools, + swapOptions, + ); + + if (!swap) throw new Error('Swap is undefined'); + + const onchain = await swap.query(rpcUrl, swapOptions.block); + + expect(swap.quote.amount).toEqual(onchain.amount); + expect(swap.inputAmount.amount).toEqual(swap.quote.amount); + expect(swap.outputAmount.amount).toEqual(outputAmount.amount); + expect(swap.paths.length).toEqual(1); + expect(swap.paths[0].pools.length).toEqual(1); + expect(swap.paths[0].pools[0].id).toEqual( + '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014', + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 74ae2efd..12e9cd8e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["src", "test"], + "include": ["src", "test", "examples"], "compilerOptions": { "target": "ESNext", "module": "ESNext", diff --git a/tsconfig.testing.json b/tsconfig.testing.json new file mode 100644 index 00000000..2c7b2841 --- /dev/null +++ b/tsconfig.testing.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs" + } +} diff --git a/vitest.config.ts b/vitest.config.ts index 63a655fc..41fb947f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,29 @@ import { defineConfig } from 'vitest/config'; +import { loadEnv } from 'vite'; -export default defineConfig({ - test: { - testTimeout: 10_000, - hookTimeout: 20_000, - globals: true, - }, +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + + return { + define: { + 'process.env.ETHEREUM_RPC_URL': JSON.stringify( + env.ETHEREUM_RPC_URL, + ), + 'process.env.POLYGON_RPC_URL': JSON.stringify(env.POLYGON_RPC_URL), + 'process.env.ARBITRUM_RPC_URL': JSON.stringify( + env.ARBITRUM_RPC_URL, + ), + 'process.env.FANTOM_RPC_URL': JSON.stringify(env.FANTOM_RPC_URL), + }, + test: { + testTimeout: 20_000, + hookTimeout: 30_000, + setupFiles: ['/test/vitest-setup.ts'], + globals: true, + // Uncomment to debug suite excluding some tests + // exclude: ['test/*weighted*.integration.*', 'node_modules', 'dist'], + // Uncomment to run integration tests sequentially + // threads: false, + }, + }; });