diff --git a/src/entities/join/weighted/weightedJoin.ts b/src/entities/join/weighted/weightedJoin.ts index d2a43f25..cb963f33 100644 --- a/src/entities/join/weighted/weightedJoin.ts +++ b/src/entities/join/weighted/weightedJoin.ts @@ -32,7 +32,7 @@ export class WeightedJoin implements BaseJoin { const userData = this.encodeUserData(input.kind, amounts); - const queryArgs = parseJoinArgs({ + const { args, tokensIn } = parseJoinArgs({ useNativeAssetAsWrappedAmountIn: !!input.useNativeAssetAsWrappedAmountIn, chainId: input.chainId, @@ -48,14 +48,14 @@ export class WeightedJoin implements BaseJoin { const queryResult = await doQueryJoin( input.rpcUrl, input.chainId, - queryArgs, + args, ); const bpt = new Token(input.chainId, poolState.address, 18); const bptOut = TokenAmount.fromRawAmount(bpt, queryResult.bptOut); const amountsIn = queryResult.amountsIn.map((a, i) => - TokenAmount.fromRawAmount(sortedTokens[i], a), + TokenAmount.fromRawAmount(tokensIn[i], a), ); return { @@ -79,7 +79,7 @@ export class WeightedJoin implements BaseJoin { const userData = this.encodeUserData(input.joinKind, amounts); - const args = parseJoinArgs({ + const { args } = parseJoinArgs({ ...input, sortedTokens: input.amountsIn.map((a) => a.token), maxAmountsIn: amounts.maxAmountsIn, diff --git a/src/entities/utils/parseJoinArgs.ts b/src/entities/utils/parseJoinArgs.ts index 78e5dd8c..8611a2ff 100644 --- a/src/entities/utils/parseJoinArgs.ts +++ b/src/entities/utils/parseJoinArgs.ts @@ -36,5 +36,8 @@ export function parseJoinArgs({ fromInternalBalance, }; - return [poolId, sender, recipient, joinPoolRequest] as const; + return { + args: [poolId, sender, recipient, joinPoolRequest] as const, + tokensIn, + }; } diff --git a/test/weightedExit.integration.test.ts b/test/weightedExit.integration.test.ts index b1a39240..95aeff1b 100644 --- a/test/weightedExit.integration.test.ts +++ b/test/weightedExit.integration.test.ts @@ -31,27 +31,32 @@ import { Address, Hex } from '../src/types'; import { CHAINS, ChainId, getPoolAddress } from '../src/utils'; import { forkSetup, sendTransactionGetBalances } from './lib/utils/helper'; +const chainId = ChainId.MAINNET; +const rpcUrl = 'http://127.0.0.1:8545/'; +const blockNumber = 18043296n; const testAddress = '0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f'; // Balancer DAO Multisig +const poolId = + '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014'; // 80BAL-20WETH +const slippage = Slippage.fromPercentage('1'); // 1% describe('weighted exit test', () => { let api: MockApi; - let chainId: ChainId; - let rpcUrl: string; - let blockNumber: bigint; let client: Client & PublicActions & TestActions & WalletActions; - let poolId: Address; let poolFromApi: PoolState; let weightedExit: BaseExit; - let tokenBpt: Token; + let bpt: Token; beforeAll(async () => { // setup mock api api = new MockApi(); - // setup chain and test client - chainId = ChainId.MAINNET; - rpcUrl = 'http://127.0.0.1:8545/'; - blockNumber = 18043296n; + // get pool state from api + poolFromApi = await api.getPool(poolId); + + // setup exit helper + const exitParser = new ExitParser(); + weightedExit = exitParser.getExit(poolFromApi.type); + client = createTestClient({ mode: 'hardhat', chain: CHAINS[chainId], @@ -60,14 +65,11 @@ describe('weighted exit test', () => { .extend(publicActions) .extend(walletActions); - poolId = - '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014'; // 80BAL-20WETH + // setup BPT token + bpt = new Token(chainId, poolFromApi.address, 18, 'BPT'); }); beforeEach(async () => { - // get pool state from api - poolFromApi = await api.getPool(poolId); - await forkSetup( client, testAddress, @@ -77,18 +79,13 @@ describe('weighted exit test', () => { process.env.ETHEREUM_RPC_URL as string, blockNumber, ); - - // setup join helper - const exitParser = new ExitParser(); - weightedExit = exitParser.getExit(poolFromApi.type); }); test('single asset exit', async () => { - tokenBpt = new Token(chainId, poolFromApi.address, 18, 'BPT'); - const bptIn = TokenAmount.fromHumanAmount(tokenBpt, '1'); + const bptIn = TokenAmount.fromHumanAmount(bpt, '1'); const tokenOut = '0xba100000625a3754423978a60c9317c58a424e3D'; // BAL - // perform join query to get expected bpt out + // perform exit query to get expected bpt out const exitInput: SingleAssetExitInput = { chainId, rpcUrl, @@ -96,106 +93,64 @@ describe('weighted exit test', () => { tokenOut, kind: ExitKind.SINGLE_ASSET, }; - const queryResult = await weightedExit.query(exitInput, poolFromApi); + const { queryResult, maxBptIn, minAmountsOut } = await doTransaction( + exitInput, + poolFromApi.tokens.map((t) => t.address), + bpt.address, + slippage, + ); + // Query should use correct BPT amount expect(queryResult.bptIn.amount).to.eq(bptIn.amount); + + // We only expect single asset to have a value for exit + expect(queryResult.tokenOutIndex).to.be.toBeDefined; queryResult.amountsOut.forEach((a, i) => { if (i === queryResult.tokenOutIndex) expect(a.amount > 0n).to.be.true; else expect(a.amount === 0n).to.be.true; }); - // build call with slippage applied - const slippage = Slippage.fromPercentage('1'); // 1% - const { call, to, value, maxBptIn, minAmountsOut } = - weightedExit.buildCall({ - ...queryResult, - slippage, - sender: testAddress, - recipient: testAddress, - }); - - // send transaction and check balance changes - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - ...queryResult.amountsOut.map((a) => a.token.address), - queryResult.bptIn.token.address, - ], - client, - testAddress, - to, - call, - value, - ); - - expect(transactionReceipt.status).to.eq('success'); - expect(maxBptIn).to.eq(bptIn.amount); - const expectedDeltas = [ - ...queryResult.amountsOut.map((a) => a.amount), - queryResult.bptIn.amount, - ]; - expect(expectedDeltas).to.deep.eq(balanceDeltas); + // Confirm slippage - only to amounts out not bpt in const expectedMinAmountsOut = queryResult.amountsOut.map((a) => slippage.removeFrom(a.amount), ); expect(expectedMinAmountsOut).to.deep.eq(minAmountsOut); + expect(maxBptIn).to.eq(bptIn.amount); }); test('proportional exit', async () => { - tokenBpt = new Token(chainId, poolFromApi.address, 18, 'BPT'); - const bptIn = TokenAmount.fromHumanAmount(tokenBpt, '1'); + const bptIn = TokenAmount.fromHumanAmount(bpt, '1'); - // perform join query to get expected bpt out + // perform exit query to get expected bpt out const exitInput: ProportionalExitInput = { chainId, rpcUrl, bptIn, kind: ExitKind.PROPORTIONAL, }; - const queryResult = await weightedExit.query(exitInput, poolFromApi); + const { queryResult, maxBptIn, minAmountsOut } = await doTransaction( + exitInput, + poolFromApi.tokens.map((t) => t.address), + bpt.address, + slippage, + ); + // Query should use correct BPT amount expect(queryResult.bptIn.amount).to.eq(bptIn.amount); + // We expect all assets to have a value for exit + expect(queryResult.tokenOutIndex).to.be.undefined; queryResult.amountsOut.forEach((a) => { expect(a.amount > 0n).to.be.true; }); - // build call with slippage applied - const slippage = Slippage.fromPercentage('1'); // 1% - const { call, to, value, maxBptIn, minAmountsOut } = - weightedExit.buildCall({ - ...queryResult, - slippage, - sender: testAddress, - recipient: testAddress, - }); - - // send transaction and check balance changes - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - ...queryResult.amountsOut.map((a) => a.token.address), - queryResult.bptIn.token.address, - ], - client, - testAddress, - to, - call, - value, - ); - - expect(transactionReceipt.status).to.eq('success'); - expect(maxBptIn).to.eq(bptIn.amount); - const expectedDeltas = [ - ...queryResult.amountsOut.map((a) => a.amount), - queryResult.bptIn.amount, - ]; - expect(expectedDeltas).to.deep.eq(balanceDeltas); + // Confirm slippage - only to amounts out not bpt in const expectedMinAmountsOut = queryResult.amountsOut.map((a) => slippage.removeFrom(a.amount), ); expect(expectedMinAmountsOut).to.deep.eq(minAmountsOut); + expect(maxBptIn).to.eq(bptIn.amount); }); test('unbalanced exit', async () => { @@ -205,64 +160,40 @@ describe('weighted exit test', () => { const amountsOut = poolTokens.map((t) => TokenAmount.fromHumanAmount(t, '0.001'), ); - // perform join query to get expected bpt out + // perform exit query to get expected bpt out const exitInput: UnbalancedExitInput = { chainId, rpcUrl, amountsOut, kind: ExitKind.UNBALANCED, }; - const queryResult = await weightedExit.query(exitInput, poolFromApi); + const { queryResult, maxBptIn, minAmountsOut } = await doTransaction( + exitInput, + poolFromApi.tokens.map((t) => t.address), + bpt.address, + slippage, + ); + // We expect a BPT input amount > 0 expect(queryResult.bptIn.amount > 0n).to.be.true; + // We expect assets to have same amount out as user defined + expect(queryResult.tokenOutIndex).to.be.undefined; queryResult.amountsOut.forEach((a, i) => { expect(a.amount).to.eq(amountsOut[i].amount); }); - // build call with slippage applied - const slippage = Slippage.fromPercentage('1'); // 1% - const { call, to, value, maxBptIn, minAmountsOut } = - weightedExit.buildCall({ - ...queryResult, - slippage, - sender: testAddress, - recipient: testAddress, - }); - - // send transaction and check balance changes - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - ...queryResult.amountsOut.map((a) => a.token.address), - queryResult.bptIn.token.address, - ], - client, - testAddress, - to, - call, - value, - ); - - expect(transactionReceipt.status).to.eq('success'); - minAmountsOut.forEach((a, i) => { - expect(a).to.eq(amountsOut[i].amount); - }); - const expectedDeltas = [ - ...queryResult.amountsOut.map((a) => a.amount), - queryResult.bptIn.amount, - ]; - expect(expectedDeltas).to.deep.eq(balanceDeltas); + // Confirm slippage - only to bpt in, not amounts out + const expectedMinAmountsOut = amountsOut.map((a) => a.amount); + expect(expectedMinAmountsOut).to.deep.eq(minAmountsOut); const expectedMaxBptIn = slippage.applyTo(queryResult.bptIn.amount); expect(expectedMaxBptIn).to.deep.eq(maxBptIn); }); test('exit with native asset', async () => { - // TODO - This should be failing?? - tokenBpt = new Token(chainId, poolFromApi.address, 18, 'BPT'); - const bptIn = TokenAmount.fromHumanAmount(tokenBpt, '1'); + const bptIn = TokenAmount.fromHumanAmount(bpt, '1'); - // perform join query to get expected bpt out + // perform exit query to get expected bpt out const exitInput: ProportionalExitInput = { chainId, rpcUrl, @@ -270,16 +201,45 @@ describe('weighted exit test', () => { kind: ExitKind.PROPORTIONAL, exitWithNativeAsset: true, }; - const queryResult = await weightedExit.query(exitInput, poolFromApi); + // We have to use zero address for balanceDeltas + const poolTokens = poolFromApi.tokens.map( + (t) => new Token(chainId, t.address, t.decimals), + ); + const { queryResult, maxBptIn, minAmountsOut } = await doTransaction( + exitInput, + replaceWrapped(poolTokens, chainId).map((a) => a.address), + bpt.address, + slippage, + ); + // Query should use correct BPT amount expect(queryResult.bptIn.amount).to.eq(bptIn.amount); + // We expect all assets to have a value for exit + expect(queryResult.tokenOutIndex).to.be.undefined; queryResult.amountsOut.forEach((a) => { expect(a.amount > 0n).to.be.true; }); - // build call with slippage applied - const slippage = Slippage.fromPercentage('1'); // 1% + // Confirm slippage - only to amounts out not bpt in + const expectedMinAmountsOut = queryResult.amountsOut.map((a) => + slippage.removeFrom(a.amount), + ); + expect(expectedMinAmountsOut).to.deep.eq(minAmountsOut); + expect(maxBptIn).to.eq(bptIn.amount); + }); + + async function doTransaction( + exitInput: + | SingleAssetExitInput + | ProportionalExitInput + | UnbalancedExitInput, + poolTokens: Address[], + bptToken: Address, + slippage: Slippage, + ) { + const queryResult = await weightedExit.query(exitInput, poolFromApi); + const { call, to, value, maxBptIn, minAmountsOut } = weightedExit.buildCall({ ...queryResult, @@ -288,38 +248,31 @@ describe('weighted exit test', () => { recipient: testAddress, }); - const poolTokens = poolFromApi.tokens.map( - (t) => new Token(chainId, t.address, t.decimals), - ); - // send transaction and check balance changes const { transactionReceipt, balanceDeltas } = await sendTransactionGetBalances( - [ - ...replaceWrapped(poolTokens, chainId).map( - (a) => a.address, - ), - queryResult.bptIn.token.address, - ], + [...poolTokens, bptToken], client, testAddress, to, call, value, ); - expect(transactionReceipt.status).to.eq('success'); - expect(maxBptIn).to.eq(bptIn.amount); + + // Confirm final balance changes match query result const expectedDeltas = [ ...queryResult.amountsOut.map((a) => a.amount), queryResult.bptIn.amount, ]; expect(expectedDeltas).to.deep.eq(balanceDeltas); - const expectedMinAmountsOut = queryResult.amountsOut.map((a) => - slippage.removeFrom(a.amount), - ); - expect(expectedMinAmountsOut).to.deep.eq(minAmountsOut); - }); + + return { + queryResult, + maxBptIn, + minAmountsOut, + }; + } }); /*********************** Mock To Represent API Requirements **********************/ diff --git a/test/weightedJoin.integration.test.ts b/test/weightedJoin.integration.test.ts index 80c32ab7..7ee6ffa9 100644 --- a/test/weightedJoin.integration.test.ts +++ b/test/weightedJoin.integration.test.ts @@ -24,6 +24,7 @@ import { Slippage, Token, TokenAmount, + replaceWrapped, } from '../src/entities'; import { JoinParser } from '../src/entities/join/parser'; import { Address, Hex } from '../src/types'; @@ -32,26 +33,25 @@ import { CHAINS, ChainId, getPoolAddress } from '../src/utils'; import { forkSetup, sendTransactionGetBalances } from './lib/utils/helper'; +const chainId = ChainId.MAINNET; +const rpcUrl = 'http://127.0.0.1:8545/'; +const blockNumber = 18043296n; const testAddress = '0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f'; // Balancer DAO Multisig +const slippage = Slippage.fromPercentage('1'); // 1% +const poolId = + '0x68e3266c9c8bbd44ad9dca5afbfe629022aee9fe000200000000000000000512'; // Balancer 50COMP-50wstETH describe('weighted join test', () => { let api: MockApi; - let chainId: ChainId; - let rpcUrl: string; - let blockNumber: bigint; let client: Client & PublicActions & TestActions & WalletActions; - let poolId: Hex; let poolFromApi: PoolState; let weightedJoin: BaseJoin; + let bpt: Token; beforeAll(async () => { // setup mock api api = new MockApi(); - // setup chain and test client - chainId = ChainId.MAINNET; - rpcUrl = 'http://127.0.0.1:8545/'; - blockNumber = 18043296n; client = createTestClient({ mode: 'hardhat', chain: CHAINS[chainId], @@ -59,310 +59,267 @@ describe('weighted join test', () => { }) .extend(publicActions) .extend(walletActions); - }); - beforeEach(async () => { // get pool state from api poolFromApi = await api.getPool(poolId); + // setup join helper + const joinParser = new JoinParser(); + weightedJoin = joinParser.getJoin(poolFromApi.type); + + // setup BPT token + bpt = new Token(chainId, poolFromApi.address, 18, 'BPT'); + }); + + beforeEach(async () => { await forkSetup( client, testAddress, - poolFromApi.tokens.map((t) => t.address), + [...poolFromApi.tokens.map((t) => t.address), poolFromApi.address], undefined, // TODO: hardcode these values to improve test performance - poolFromApi.tokens.map((t) => parseUnits('100', t.decimals)), + [ + ...poolFromApi.tokens.map((t) => parseUnits('100', t.decimals)), + parseUnits('100', 18), + ], process.env.ETHEREUM_RPC_URL as string, blockNumber, ); - - // setup join helper - const joinParser = new JoinParser(); - weightedJoin = joinParser.getJoin(poolFromApi.type); }); - describe('exact in', async () => { - let tokenIn: Token; + test('unbalanced join', async () => { + const poolTokens = poolFromApi.tokens.map( + (t) => new Token(chainId, t.address, t.decimals), + ); + const amountsIn = poolTokens.map((t) => + TokenAmount.fromHumanAmount(t, '1'), + ); - beforeAll(() => { - poolId = - '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014'; // 80BAL-20WETH - }); + // perform join query to get expected bpt out + const joinInput: UnbalancedJoinInput = { + amountsIn, + chainId, + rpcUrl, + kind: JoinKind.Unbalanced, + }; - test('single asset join', async () => { - tokenIn = new Token( - chainId, - '0xba100000625a3754423978a60c9317c58a424e3D', - 18, - 'BAL', - ); - const amountIn = TokenAmount.fromHumanAmount(tokenIn, '1'); - - // perform join query to get expected bpt out - const joinInput: UnbalancedJoinInput = { - amountsIn: [amountIn], - chainId, - rpcUrl, - kind: JoinKind.Unbalanced, - }; - const queryResult = await weightedJoin.query( + const { queryResult, maxAmountsIn, minBptOut, value } = + await doTransaction( joinInput, - poolFromApi, + poolFromApi.tokens.map((t) => t.address), + bpt.address, + slippage, ); - // build join call with expected minBpOut based on slippage - const slippage = Slippage.fromPercentage('1'); // 1% - const { call, to, value, minBptOut } = weightedJoin.buildCall({ - ...queryResult, - slippage, - sender: testAddress, - recipient: testAddress, - }); + // Query should use same amountsIn as user sets + expect(queryResult.amountsIn).to.deep.eq(amountsIn); + expect(queryResult.tokenInIndex).to.be.undefined; - // send join transaction and check balance changes - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - ...queryResult.amountsIn.map((a) => a.token.address), - queryResult.bptOut.token.address, - ], - client, - testAddress, - to, - call, - value, - ); - - expect(transactionReceipt.status).to.eq('success'); - expect(queryResult.bptOut.amount > 0n).to.be.true; - const expectedDeltas = [ - ...queryResult.amountsIn.map((a) => a.amount), - queryResult.bptOut.amount, - ]; - expect(expectedDeltas).to.deep.eq(balanceDeltas); - const expectedMinBpt = slippage.removeFrom( - queryResult.bptOut.amount, - ); - expect(expectedMinBpt).to.deep.eq(minBptOut); - }); + // Should be no native value + expect(value).toBeUndefined; - test('native asset join', async () => { - tokenIn = new Token( - chainId, - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', - 18, - 'WETH', - ); - const amountIn = TokenAmount.fromHumanAmount(tokenIn, '1'); - - // perform join query to get expected bpt out - const joinInput: UnbalancedJoinInput = { - amountsIn: [amountIn], - chainId, - rpcUrl, - kind: JoinKind.Unbalanced, - useNativeAssetAsWrappedAmountIn: true, - }; - const queryResult = await weightedJoin.query( + // Expect some bpt amount + expect(queryResult.bptOut.amount > 0n).to.be.true; + + // Confirm slippage - only bpt out + const expectedMinBpt = slippage.removeFrom(queryResult.bptOut.amount); + expect(expectedMinBpt).to.deep.eq(minBptOut); + const expectedMaxAmountsIn = amountsIn.map((a) => a.amount); + expect(expectedMaxAmountsIn).to.deep.eq(maxAmountsIn); + }); + + test('native asset join', async () => { + const poolTokens = poolFromApi.tokens.map( + (t) => new Token(chainId, t.address, t.decimals), + ); + const amountsIn = poolTokens.map((t) => + TokenAmount.fromHumanAmount(t, '1'), + ); + + // perform join query to get expected bpt out + const joinInput: UnbalancedJoinInput = { + amountsIn, + chainId, + rpcUrl, + kind: JoinKind.Unbalanced, + useNativeAssetAsWrappedAmountIn: true, + }; + + // We have to use zero address for balanceDeltas + const { queryResult, maxAmountsIn, minBptOut, value } = + await doTransaction( joinInput, - poolFromApi, + replaceWrapped(poolTokens, chainId).map((a) => a.address), + bpt.address, + slippage, ); - // build join call with expected minBpOut based on slippage - const slippage = Slippage.fromPercentage('1'); // 1% - const { call, to, value, minBptOut } = weightedJoin.buildCall({ - ...queryResult, - slippage, - sender: testAddress, - recipient: testAddress, - }); + // Query should use same amountsIn as user sets + expect(queryResult.amountsIn.map((a) => a.amount)).to.deep.eq( + amountsIn.map((a) => a.amount), + ); + expect(queryResult.tokenInIndex).to.be.undefined; + // Should have native value equal to input amount + expect(value).eq(amountsIn[0].amount); + + // Expect some bpt amount + expect(queryResult.bptOut.amount > 0n).to.be.true; + + // Confirm slippage - only bpt out + const expectedMinBpt = slippage.removeFrom(queryResult.bptOut.amount); + expect(expectedMinBpt).to.deep.eq(minBptOut); + const expectedMaxAmountsIn = amountsIn.map((a) => a.amount); + expect(expectedMaxAmountsIn).to.deep.eq(maxAmountsIn); + }); - // send join transaction and check balance changes - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - ...queryResult.amountsIn.map((a) => a.token.address), - queryResult.bptOut.token.address, - ], - client, - testAddress, - to, - call, - value, - ); - - expect(transactionReceipt.status).to.eq('success'); - expect(queryResult.bptOut.amount > 0n).to.be.true; - const expectedDeltas = [ - ...queryResult.amountsIn.map((a) => a.amount), - queryResult.bptOut.amount, - ]; - expect(expectedDeltas).to.deep.eq(balanceDeltas); - const expectedMinBpt = slippage.removeFrom( - queryResult.bptOut.amount, + test('single asset join', async () => { + const bptOut = TokenAmount.fromHumanAmount(bpt, '1'); + const tokenIn = '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f'; + + // perform join query to get expected bpt out + const joinInput: SingleAssetJoinInput = { + bptOut, + tokenIn, + chainId, + rpcUrl, + kind: JoinKind.SingleAsset, + }; + + const { queryResult, maxAmountsIn, minBptOut, value } = + await doTransaction( + joinInput, + poolFromApi.tokens.map((t) => t.address), + bpt.address, + slippage, ); - expect(expectedMinBpt).to.deep.eq(minBptOut); + + // Query should use same bpt out as user sets + expect(queryResult.bptOut.amount).to.deep.eq(bptOut.amount); + + // We only expect single asset to have a value for amount in + expect(queryResult.tokenInIndex).toBeDefined; + queryResult.amountsIn.forEach((a, i) => { + if (i === queryResult.tokenInIndex) + expect(a.amount > 0n).to.be.true; + else expect(a.amount === 0n).to.be.true; }); + + // Should be no native value + expect(value).toBeUndefined; + + // Confirm slippage - only to amount in not bpt out + const expectedMaxAmountsIn = queryResult.amountsIn.map((a) => + slippage.applyTo(a.amount), + ); + expect(expectedMaxAmountsIn).to.deep.eq(maxAmountsIn); + expect(minBptOut).to.eq(bptOut.amount); }); - describe('exact out', async () => { - let tokenOut: Token; - - beforeAll(() => { - poolId = - '0x87a867f5d240a782d43d90b6b06dea470f3f8f22000200000000000000000516'; // Balancer 50COMP-50wstETH - tokenOut = new Token( - chainId, - '0x87a867f5d240a782d43d90b6b06dea470f3f8f22', - 18, - 'Balancer 50COMP-50wstETH', - ); - }); + test('proportional join', async () => { + const bptOut = TokenAmount.fromHumanAmount(bpt, '1'); + + // perform join query to get expected bpt out + const joinInput: ProportionalJoinInput = { + bptOut, + chainId, + rpcUrl, + kind: JoinKind.Proportional, + }; - test('single asset join', async () => { - const amountOut = TokenAmount.fromHumanAmount(tokenOut, '1'); - const tokenIn = '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0'; - - // perform join query to get expected bpt out - const joinInput: SingleAssetJoinInput = { - bptOut: amountOut, - tokenIn, - chainId, - rpcUrl, - kind: JoinKind.SingleAsset, - }; - const queryResult = await weightedJoin.query( + const { queryResult, maxAmountsIn, minBptOut, value } = + await doTransaction( joinInput, - poolFromApi, + poolFromApi.tokens.map((t) => t.address), + bpt.address, + slippage, ); - // build join call with expected minBpOut based on slippage - const slippage = Slippage.fromPercentage('1'); // 1% - const { call, to, value, maxAmountsIn } = weightedJoin.buildCall({ - ...queryResult, - slippage, - sender: testAddress, - recipient: testAddress, - }); + // Query should use same bpt out as user sets + expect(queryResult.bptOut.amount).to.deep.eq(bptOut.amount); - // send join transaction and check balance changes - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - ...queryResult.amountsIn.map((a) => a.token.address), - queryResult.bptOut.token.address, - ], - client, - testAddress, - to, - call, - value, - ); - - expect(transactionReceipt.status).to.eq('success'); - expect(queryResult.bptOut.amount > 0n).to.be.true; - const expectedDeltas = [ - ...queryResult.amountsIn.map((a) => a.amount), - queryResult.bptOut.amount, - ]; - expect(expectedDeltas).to.deep.eq(balanceDeltas); - const expectedMaxAmountsIn = queryResult.amountsIn.map((a) => - slippage.applyTo(a.amount), - ); - expect(expectedMaxAmountsIn).to.deep.eq(maxAmountsIn); + // Expect all assets to have a value for amount in + expect(queryResult.tokenInIndex).toBeDefined; + queryResult.amountsIn.forEach((a) => { + expect(a.amount > 0n).to.be.true; }); + expect(queryResult.tokenInIndex).toBeUndefined; - test('proportional join', async () => { - const amountOut = TokenAmount.fromHumanAmount(tokenOut, '1'); - - // perform join query to get expected bpt out - const joinInput: ProportionalJoinInput = { - bptOut: amountOut, - chainId, - rpcUrl, - kind: JoinKind.Proportional, - }; - const queryResult = await weightedJoin.query( - joinInput, - poolFromApi, - ); + // Should be no native value + expect(value).toBeUndefined; - // build join call with expected minBpOut based on slippage - const slippage = Slippage.fromPercentage('1'); // 1% - const { call, to, value, maxAmountsIn } = weightedJoin.buildCall({ + // Confirm slippage - only to amount in not bpt out + const expectedMaxAmountsIn = queryResult.amountsIn.map((a) => + slippage.applyTo(a.amount), + ); + expect(expectedMaxAmountsIn).to.deep.eq(maxAmountsIn); + expect(minBptOut).to.eq(bptOut.amount); + }); + + async function doTransaction( + joinInput: + | UnbalancedJoinInput + | ProportionalJoinInput + | SingleAssetJoinInput, + poolTokens: Address[], + bptToken: Address, + slippage: Slippage, + ) { + const queryResult = await weightedJoin.query(joinInput, poolFromApi); + + const { call, to, value, maxAmountsIn, minBptOut } = + weightedJoin.buildCall({ ...queryResult, slippage, sender: testAddress, recipient: testAddress, }); - // send join transaction and check balance changes - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - ...queryResult.amountsIn.map((a) => a.token.address), - queryResult.bptOut.token.address, - ], - client, - testAddress, - to, - call, - value, - ); - - expect(transactionReceipt.status).to.eq('success'); - expect(queryResult.bptOut.amount > 0n).to.be.true; - const expectedDeltas = [ - ...queryResult.amountsIn.map((a) => a.amount), - queryResult.bptOut.amount, - ]; - expect(expectedDeltas).to.deep.eq(balanceDeltas); - const expectedMaxAmountsIn = queryResult.amountsIn.map((a) => - slippage.applyTo(a.amount), + // send transaction and check balance changes + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + [...poolTokens, bptToken], + client, + testAddress, + to, + call, + value, ); - expect(expectedMaxAmountsIn).to.deep.eq(maxAmountsIn); - }); - }); + expect(transactionReceipt.status).to.eq('success'); + + // Confirm final balance changes match query result + const expectedDeltas = [ + ...queryResult.amountsIn.map((a) => a.amount), + queryResult.bptOut.amount, + ]; + expect(expectedDeltas).to.deep.eq(balanceDeltas); + + return { + queryResult, + maxAmountsIn, + minBptOut, + value, + }; + } }); /*********************** 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, - }, - ]; - } + 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,