diff --git a/src/entities/exit/index.ts b/src/entities/exit/index.ts index fcb073fe..318c97dc 100644 --- a/src/entities/exit/index.ts +++ b/src/entities/exit/index.ts @@ -1 +1,2 @@ +export * from './poolExit'; export * from './types'; diff --git a/src/entities/exit/parser.ts b/src/entities/exit/parser.ts deleted file mode 100644 index b2cf670b..00000000 --- a/src/entities/exit/parser.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BaseExit, ExitConfig } from './types'; -import { WeightedExit } from './weighted/weightedExit'; - -/*********************** Basic Helper to get exit class from pool type *************/ - -export class ExitParser { - private readonly poolExits: Record = {}; - - constructor(config?: ExitConfig) { - const { customPoolExits } = config || {}; - this.poolExits = { - Weighted: new WeightedExit(), - // custom pool Exits take precedence over base Exits - ...customPoolExits, - }; - } - - public getExit(poolType: string): BaseExit { - return this.poolExits[poolType]; - } -} diff --git a/src/entities/exit/poolExit.ts b/src/entities/exit/poolExit.ts new file mode 100644 index 00000000..6d2a373f --- /dev/null +++ b/src/entities/exit/poolExit.ts @@ -0,0 +1,52 @@ +import { + BaseExit, + ExitBuildOutput, + ExitCallInput, + ExitConfig, + ExitInput, + ExitQueryResult, +} from './types'; +import { WeightedExit } from './weighted/weightedExit'; +import { PoolStateInput } from '../types'; +import { validateInputs } from './weighted/validateInputs'; +import { getSortedTokens } from '../utils/getSortedTokens'; + +export class PoolExit { + private readonly poolExits: Record = {}; + + constructor(config?: ExitConfig) { + const { customPoolExits } = config || {}; + this.poolExits = { + Weighted: new WeightedExit(), + // custom pool Exits take precedence over base Exits + ...customPoolExits, + }; + } + + public getExit(poolType: string): BaseExit { + if (!this.poolExits[poolType]) { + throw new Error('Unsupported pool type'); + } + + return this.poolExits[poolType]; + } + + public async query( + input: ExitInput, + poolState: PoolStateInput, + ): Promise { + validateInputs(input, poolState); + + const sortedTokens = getSortedTokens(poolState.tokens, input.chainId); + const mappedPoolState = { + ...poolState, + tokens: sortedTokens, + }; + + return this.getExit(poolState.type).query(input, mappedPoolState); + } + + public buildCall(input: ExitCallInput): ExitBuildOutput { + return this.getExit(input.poolType).buildCall(input); + } +} diff --git a/src/entities/exit/types.ts b/src/entities/exit/types.ts index 2e50da4d..3938bbc7 100644 --- a/src/entities/exit/types.ts +++ b/src/entities/exit/types.ts @@ -40,6 +40,7 @@ export type ExitInput = // Returned from a exit query export type ExitQueryResult = { + poolType: string; id: Address; exitKind: ExitKind; bptIn: TokenAmount; @@ -54,7 +55,7 @@ export type ExitCallInput = ExitQueryResult & { recipient: Address; }; -export type BuildOutput = { +export type ExitBuildOutput = { call: Address; to: Address; value: bigint | undefined; @@ -64,7 +65,7 @@ export type BuildOutput = { export interface BaseExit { query(input: ExitInput, poolState: PoolState): Promise; - buildCall(input: ExitCallInput): BuildOutput; + buildCall(input: ExitCallInput): ExitBuildOutput; } export type ExitConfig = { diff --git a/src/entities/exit/weighted/validateInputs.ts b/src/entities/exit/weighted/validateInputs.ts index 08f7b6fb..2b89aab7 100644 --- a/src/entities/exit/weighted/validateInputs.ts +++ b/src/entities/exit/weighted/validateInputs.ts @@ -1,8 +1,8 @@ import { ExitInput, ExitKind } from '..'; -import { PoolState } from '../../types'; +import { PoolStateInput } from '../../types'; import { areTokensInArray } from '../../utils/areTokensInArray'; -export function validateInputs(input: ExitInput, poolState: PoolState) { +export function validateInputs(input: ExitInput, poolState: PoolStateInput) { switch (input.kind) { case ExitKind.UNBALANCED: areTokensInArray( diff --git a/src/entities/exit/weighted/weightedExit.ts b/src/entities/exit/weighted/weightedExit.ts index 58d54fe3..28fecb7d 100644 --- a/src/entities/exit/weighted/weightedExit.ts +++ b/src/entities/exit/weighted/weightedExit.ts @@ -1,32 +1,32 @@ import { encodeFunctionData } from 'viem'; -import { Token, TokenAmount, WeightedEncoder } from '../../..'; +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 { + BALANCER_VAULT, + MAX_UINT256, + ZERO_ADDRESS, +} from '../../../utils/constants'; import { vaultAbi } from '../../../abi'; import { parseExitArgs } from '../../utils/parseExitArgs'; import { BaseExit, - BuildOutput, + ExitBuildOutput, ExitCallInput, ExitInput, ExitKind, ExitQueryResult, } from '../types'; -import { getSortedTokens } from '../../utils'; -import { PoolState, AmountsExit } from '../../types'; +import { AmountsExit, PoolState } from '../../types'; import { doQueryExit } from '../../utils/doQueryExit'; -import { validateInputs } from './validateInputs'; export class WeightedExit implements BaseExit { public async query( input: ExitInput, poolState: PoolState, ): Promise { - validateInputs(input, poolState); - - const sortedTokens = getSortedTokens(poolState.tokens, input.chainId); - - const amounts = this.getAmountsQuery(sortedTokens, input); + const amounts = this.getAmountsQuery(poolState.tokens, input); const userData = this.encodeUserData(input.kind, amounts); @@ -35,7 +35,7 @@ export class WeightedExit implements BaseExit { chainId: input.chainId, exitWithNativeAsset: !!input.exitWithNativeAsset, poolId: poolState.id, - sortedTokens, + sortedTokens: poolState.tokens, sender: ZERO_ADDRESS, recipient: ZERO_ADDRESS, minAmountsOut: amounts.minAmountsOut, @@ -57,6 +57,7 @@ export class WeightedExit implements BaseExit { ); return { + poolType: poolState.type, exitKind: input.kind, id: poolState.id, bptIn, @@ -94,7 +95,7 @@ export class WeightedExit implements BaseExit { } } - public buildCall(input: ExitCallInput): BuildOutput { + public buildCall(input: ExitCallInput): ExitBuildOutput { const amounts = this.getAmountsCall(input); const userData = this.encodeUserData(input.exitKind, amounts); diff --git a/src/entities/join/index.ts b/src/entities/join/index.ts index b968ca7a..d20b54f9 100644 --- a/src/entities/join/index.ts +++ b/src/entities/join/index.ts @@ -1,73 +1,2 @@ -import { TokenAmount } from '../tokenAmount'; -import { Slippage } from '../slippage'; -import { PoolState } from '../types'; -import { Address, Hex } from '../../types'; - -export enum JoinKind { - Init = 'Init', - Unbalanced = 'Unbalanced', - SingleAsset = 'SingleAsset', - Proportional = 'Proportional', -} - -// This will be extended for each pools specific input requirements -export type BaseJoinInput = { - chainId: number; - rpcUrl: string; - useNativeAssetAsWrappedAmountIn?: boolean; - fromInternalBalance?: boolean; -}; - -export type InitJoinInput = BaseJoinInput & { - amountsIn: TokenAmount[]; - kind: JoinKind.Init; -}; - -export type UnbalancedJoinInput = BaseJoinInput & { - amountsIn: TokenAmount[]; - kind: JoinKind.Unbalanced; -}; - -export type SingleAssetJoinInput = BaseJoinInput & { - bptOut: TokenAmount; - tokenIn: Address; - kind: JoinKind.SingleAsset; -}; - -export type ProportionalJoinInput = BaseJoinInput & { - bptOut: TokenAmount; - kind: JoinKind.Proportional; -}; - -export type JoinInput = - | InitJoinInput - | UnbalancedJoinInput - | SingleAssetJoinInput - | ProportionalJoinInput; - -// Returned from a join query -export type JoinQueryResult = { - poolId: Hex; - joinKind: JoinKind; - bptOut: TokenAmount; - amountsIn: TokenAmount[]; - fromInternalBalance: boolean; - tokenInIndex?: number; -}; - -export type JoinCallInput = JoinQueryResult & { - slippage: Slippage; - sender: Address; - recipient: Address; -}; - -export interface BaseJoin { - query(input: JoinInput, poolState: PoolState): Promise; - buildCall(input: JoinCallInput): { - call: Hex; - to: Address; - value: bigint | undefined; - minBptOut: bigint; - maxAmountsIn: bigint[]; - }; -} +export * from './poolJoin'; +export * from './types'; diff --git a/src/entities/join/parser.ts b/src/entities/join/parser.ts deleted file mode 100644 index 7506e603..00000000 --- a/src/entities/join/parser.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { BaseJoin } from '.'; -import { WeightedJoin } from './weighted/weightedJoin'; - -/*********************** Basic Helper to get join class from pool type *************/ -export type JoinConfig = { - customPoolJoins: Record; -}; - -export class JoinParser { - private readonly poolJoins: Record = {}; - - constructor(config?: JoinConfig) { - const { customPoolJoins } = config || {}; - this.poolJoins = { - Weighted: new WeightedJoin(), - // custom pool Joins take precedence over base Joins - ...customPoolJoins, - }; - } - - public getJoin(poolType: string): BaseJoin { - return this.poolJoins[poolType]; - } -} diff --git a/src/entities/join/poolJoin.ts b/src/entities/join/poolJoin.ts new file mode 100644 index 00000000..04d8ced5 --- /dev/null +++ b/src/entities/join/poolJoin.ts @@ -0,0 +1,52 @@ +import { + BaseJoin, + JoinBuildOutput, + JoinConfig, + JoinInput, + JoinQueryResult, + JoinCallInput, +} from './types'; +import { WeightedJoin } from './weighted/weightedJoin'; +import { PoolStateInput } from '../types'; +import { validateInputs } from './weighted/validateInputs'; +import { getSortedTokens } from '../utils/getSortedTokens'; + +export class PoolJoin { + private readonly poolJoins: Record = {}; + + constructor(config?: JoinConfig) { + const { customPoolJoins } = config || {}; + this.poolJoins = { + Weighted: new WeightedJoin(), + // custom pool Joins take precedence over base Joins + ...customPoolJoins, + }; + } + + public getJoin(poolType: string): BaseJoin { + if (!this.poolJoins[poolType]) { + throw new Error('Unsupported pool type'); + } + + return this.poolJoins[poolType]; + } + + public async query( + input: JoinInput, + poolState: PoolStateInput, + ): Promise { + validateInputs(input, poolState); + + const sortedTokens = getSortedTokens(poolState.tokens, input.chainId); + const mappedPoolState = { + ...poolState, + tokens: sortedTokens, + }; + + return this.getJoin(poolState.type).query(input, mappedPoolState); + } + + public buildCall(input: JoinCallInput): JoinBuildOutput { + return this.getJoin(input.poolType).buildCall(input); + } +} diff --git a/src/entities/join/types.ts b/src/entities/join/types.ts new file mode 100644 index 00000000..318b8ce6 --- /dev/null +++ b/src/entities/join/types.ts @@ -0,0 +1,86 @@ +import { TokenAmount } from '../tokenAmount'; +import { Slippage } from '../slippage'; +import { PoolState } from '../types'; +import { Address, Hex } from '../../types'; + +export enum JoinKind { + Init = 'Init', + Unbalanced = 'Unbalanced', + SingleAsset = 'SingleAsset', + Proportional = 'Proportional', +} + +// This will be extended for each pools specific input requirements +export type BaseJoinInput = { + chainId: number; + rpcUrl: string; + useNativeAssetAsWrappedAmountIn?: boolean; + fromInternalBalance?: boolean; +}; + +export type InitJoinInput = BaseJoinInput & { + amountsIn: TokenAmount[]; + kind: JoinKind.Init; +}; + +export type UnbalancedJoinInput = BaseJoinInput & { + amountsIn: TokenAmount[]; + kind: JoinKind.Unbalanced; +}; + +export type SingleAssetJoinInput = BaseJoinInput & { + bptOut: TokenAmount; + tokenIn: Address; + kind: JoinKind.SingleAsset; +}; + +export type ProportionalJoinInput = BaseJoinInput & { + bptOut: TokenAmount; + kind: JoinKind.Proportional; +}; + +export type JoinInput = + | InitJoinInput + | UnbalancedJoinInput + | SingleAssetJoinInput + | ProportionalJoinInput; + +// Returned from a join query +export type JoinQueryResult = { + poolType: string; + poolId: Hex; + joinKind: JoinKind; + bptOut: TokenAmount; + amountsIn: TokenAmount[]; + fromInternalBalance: boolean; + tokenInIndex?: number; +}; + +export type JoinCallInput = JoinQueryResult & { + slippage: Slippage; + sender: Address; + recipient: Address; +}; + +export interface BaseJoin { + query(input: JoinInput, poolState: PoolState): Promise; + buildCall(input: JoinCallInput): { + call: Hex; + to: Address; + value: bigint | undefined; + minBptOut: bigint; + maxAmountsIn: bigint[]; + }; +} + +export type JoinBuildOutput = { + call: Hex; + to: Address; + value: bigint | undefined; + minBptOut: bigint; + maxAmountsIn: bigint[]; +}; + +export type JoinConfig = { + customPoolJoins: Record; +}; diff --git a/src/entities/join/weighted/validateInputs.ts b/src/entities/join/weighted/validateInputs.ts index 02916875..02a9b053 100644 --- a/src/entities/join/weighted/validateInputs.ts +++ b/src/entities/join/weighted/validateInputs.ts @@ -1,8 +1,8 @@ import { JoinInput, JoinKind } from '..'; -import { PoolState } from '../../types'; +import { PoolStateInput } from '../../types'; import { areTokensInArray } from '../../utils/areTokensInArray'; -export function validateInputs(input: JoinInput, poolState: PoolState) { +export function validateInputs(input: JoinInput, poolState: PoolStateInput) { switch (input.kind) { case JoinKind.Init: case JoinKind.Unbalanced: diff --git a/src/entities/join/weighted/weightedJoin.ts b/src/entities/join/weighted/weightedJoin.ts index cb963f33..b10fe0fc 100644 --- a/src/entities/join/weighted/weightedJoin.ts +++ b/src/entities/join/weighted/weightedJoin.ts @@ -1,34 +1,25 @@ import { encodeFunctionData } from 'viem'; import { Token, TokenAmount, WeightedEncoder } from '../../..'; -import { Address, Hex } from '../../../types'; +import { Address } from '../../../types'; import { BALANCER_VAULT, MAX_UINT256, ZERO_ADDRESS } from '../../../utils'; import { vaultAbi } from '../../../abi'; -import { validateInputs } from './validateInputs'; import { BaseJoin, + JoinBuildOutput, JoinCallInput, JoinInput, JoinKind, JoinQueryResult, } from '..'; -import { PoolState, AmountsJoin } from '../../types'; -import { - doQueryJoin, - getAmounts, - parseJoinArgs, - getSortedTokens, -} from '../../utils'; +import { AmountsJoin, PoolState } from '../../types'; +import { doQueryJoin, getAmounts, parseJoinArgs } from '../../utils'; export class WeightedJoin implements BaseJoin { public async query( input: JoinInput, poolState: PoolState, ): Promise { - validateInputs(input, poolState); - - const sortedTokens = getSortedTokens(poolState.tokens, input.chainId); - - const amounts = this.getAmountsQuery(sortedTokens, input); + const amounts = this.getAmountsQuery(poolState.tokens, input); const userData = this.encodeUserData(input.kind, amounts); @@ -36,7 +27,7 @@ export class WeightedJoin implements BaseJoin { useNativeAssetAsWrappedAmountIn: !!input.useNativeAssetAsWrappedAmountIn, chainId: input.chainId, - sortedTokens, + sortedTokens: poolState.tokens, poolId: poolState.id, sender: ZERO_ADDRESS, recipient: ZERO_ADDRESS, @@ -59,6 +50,7 @@ export class WeightedJoin implements BaseJoin { ); return { + poolType: poolState.type, joinKind: input.kind, poolId: poolState.id, bptOut, @@ -68,13 +60,7 @@ export class WeightedJoin implements BaseJoin { }; } - public buildCall(input: JoinCallInput): { - call: Hex; - to: Address; - value: bigint | undefined; - minBptOut: bigint; - maxAmountsIn: bigint[]; - } { + public buildCall(input: JoinCallInput): JoinBuildOutput { const amounts = this.getAmountsCall(input); const userData = this.encodeUserData(input.joinKind, amounts); diff --git a/src/entities/types.ts b/src/entities/types.ts index d1286ebf..4be403fe 100644 --- a/src/entities/types.ts +++ b/src/entities/types.ts @@ -1,8 +1,16 @@ 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; diff --git a/test/weightedExit.integration.test.ts b/test/weightedExit.integration.test.ts index 95aeff1b..50cad9d7 100644 --- a/test/weightedExit.integration.test.ts +++ b/test/weightedExit.integration.test.ts @@ -15,49 +15,52 @@ import { walletActions, } from 'viem'; import { - BaseExit, SingleAssetExitInput, ProportionalExitInput, UnbalancedExitInput, ExitKind, - PoolState, Slippage, Token, TokenAmount, replaceWrapped, -} from '../src/entities'; -import { ExitParser } from '../src/entities/exit/parser'; -import { Address, Hex } from '../src/types'; -import { CHAINS, ChainId, getPoolAddress } from '../src/utils'; + PoolStateInput, + PoolExit, + Address, + Hex, + CHAINS, + ChainId, + getPoolAddress, + ExitInput, +} from '../src'; import { forkSetup, sendTransactionGetBalances } from './lib/utils/helper'; +type TxInput = { + client: Client & PublicActions & TestActions & WalletActions; + poolExit: PoolExit; + exitInput: ExitInput; + slippage: Slippage; + poolInput: PoolStateInput; + testAddress: Address; + checkNativeBalance: boolean; +}; + 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 client: Client & PublicActions & TestActions & WalletActions; - let poolFromApi: PoolState; - let weightedExit: BaseExit; - let bpt: Token; - + let txInput: TxInput; + let bptToken: Token; beforeAll(async () => { // setup mock api - api = new MockApi(); + const api = new MockApi(); // get pool state from api - poolFromApi = await api.getPool(poolId); - - // setup exit helper - const exitParser = new ExitParser(); - weightedExit = exitParser.getExit(poolFromApi.type); + const poolInput = await api.getPool(poolId); - client = createTestClient({ + const client = createTestClient({ mode: 'hardhat', chain: CHAINS[chainId], transport: http(rpcUrl), @@ -65,15 +68,23 @@ describe('weighted exit test', () => { .extend(publicActions) .extend(walletActions); - // setup BPT token - bpt = new Token(chainId, poolFromApi.address, 18, 'BPT'); + txInput = { + client, + poolExit: new PoolExit(), + slippage: Slippage.fromPercentage('1'), // 1% + poolInput, + testAddress: '0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f', // Balancer DAO Multisig + exitInput: {} as ExitInput, + checkNativeBalance: false, + }; + bptToken = new Token(chainId, poolInput.address, 18, 'BPT'); }); beforeEach(async () => { await forkSetup( - client, - testAddress, - [poolFromApi.address], + txInput.client, + txInput.testAddress, + [txInput.poolInput.address], undefined, // TODO: hardcode these values to improve test performance [parseUnits('1', 18)], process.env.ETHEREUM_RPC_URL as string, @@ -82,10 +93,10 @@ describe('weighted exit test', () => { }); test('single asset exit', async () => { - const bptIn = TokenAmount.fromHumanAmount(bpt, '1'); + const { slippage } = txInput; + const bptIn = TokenAmount.fromHumanAmount(bptToken, '1'); const tokenOut = '0xba100000625a3754423978a60c9317c58a424e3D'; // BAL - // perform exit query to get expected bpt out const exitInput: SingleAssetExitInput = { chainId, rpcUrl, @@ -93,12 +104,10 @@ describe('weighted exit test', () => { tokenOut, kind: ExitKind.SINGLE_ASSET, }; - const { queryResult, maxBptIn, minAmountsOut } = await doTransaction( + const { queryResult, maxBptIn, minAmountsOut } = await doTransaction({ + ...txInput, exitInput, - poolFromApi.tokens.map((t) => t.address), - bpt.address, - slippage, - ); + }); // Query should use correct BPT amount expect(queryResult.bptIn.amount).to.eq(bptIn.amount); @@ -120,21 +129,19 @@ describe('weighted exit test', () => { }); test('proportional exit', async () => { - const bptIn = TokenAmount.fromHumanAmount(bpt, '1'); + const { slippage } = txInput; + const bptIn = TokenAmount.fromHumanAmount(bptToken, '1'); - // perform exit query to get expected bpt out const exitInput: ProportionalExitInput = { chainId, rpcUrl, bptIn, kind: ExitKind.PROPORTIONAL, }; - const { queryResult, maxBptIn, minAmountsOut } = await doTransaction( + const { queryResult, maxBptIn, minAmountsOut } = await doTransaction({ + ...txInput, exitInput, - poolFromApi.tokens.map((t) => t.address), - bpt.address, - slippage, - ); + }); // Query should use correct BPT amount expect(queryResult.bptIn.amount).to.eq(bptIn.amount); @@ -154,25 +161,25 @@ describe('weighted exit test', () => { }); test('unbalanced exit', async () => { - const poolTokens = poolFromApi.tokens.map( + const { poolInput, slippage } = txInput; + + const poolTokens = poolInput.tokens.map( (t) => new Token(chainId, t.address, t.decimals), ); const amountsOut = poolTokens.map((t) => TokenAmount.fromHumanAmount(t, '0.001'), ); - // perform exit query to get expected bpt out + const exitInput: UnbalancedExitInput = { chainId, rpcUrl, amountsOut, kind: ExitKind.UNBALANCED, }; - const { queryResult, maxBptIn, minAmountsOut } = await doTransaction( + const { queryResult, maxBptIn, minAmountsOut } = await doTransaction({ + ...txInput, 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; @@ -191,9 +198,9 @@ describe('weighted exit test', () => { }); test('exit with native asset', async () => { - const bptIn = TokenAmount.fromHumanAmount(bpt, '1'); + const { slippage } = txInput; + const bptIn = TokenAmount.fromHumanAmount(bptToken, '1'); - // perform exit query to get expected bpt out const exitInput: ProportionalExitInput = { chainId, rpcUrl, @@ -202,16 +209,13 @@ describe('weighted exit test', () => { exitWithNativeAsset: true, }; - // 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( + // Note - checking native balance + const { queryResult, maxBptIn, minAmountsOut } = await doTransaction({ + ...txInput, exitInput, - replaceWrapped(poolTokens, chainId).map((a) => a.address), - bpt.address, - slippage, - ); + checkNativeBalance: true, + }); + // Query should use correct BPT amount expect(queryResult.bptIn.amount).to.eq(bptIn.amount); @@ -229,29 +233,40 @@ describe('weighted exit test', () => { 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({ + async function doTransaction(txIp: TxInput) { + const { + poolExit, + poolInput, + exitInput, + testAddress, + client, + slippage, + checkNativeBalance, + } = txIp; + const queryResult = await poolExit.query(exitInput, poolInput); + + const { call, to, value, maxBptIn, minAmountsOut } = poolExit.buildCall( + { ...queryResult, slippage, sender: testAddress, recipient: testAddress, - }); + }, + ); + + const poolTokens = poolInput.tokens.map( + (t) => new Token(chainId, t.address, t.decimals), + ); + + // Replace with native asset if required + const poolTokensAddr = checkNativeBalance + ? replaceWrapped(poolTokens, chainId).map((t) => t.address) + : poolTokens.map((t) => t.address); // send transaction and check balance changes const { transactionReceipt, balanceDeltas } = await sendTransactionGetBalances( - [...poolTokens, bptToken], + [...poolTokensAddr, poolInput.address], client, testAddress, to, @@ -278,7 +293,7 @@ describe('weighted exit test', () => { /*********************** Mock To Represent API Requirements **********************/ export class MockApi { - public async getPool(id: Hex): Promise { + public async getPool(id: Hex): Promise { let tokens: { address: Address; decimals: number; index: number }[] = []; if ( diff --git a/test/weightedJoin.integration.test.ts b/test/weightedJoin.integration.test.ts index 7ee6ffa9..afd6497a 100644 --- a/test/weightedJoin.integration.test.ts +++ b/test/weightedJoin.integration.test.ts @@ -16,7 +16,6 @@ import { } from 'viem'; import { - BaseJoin, UnbalancedJoinInput, ProportionalJoinInput, SingleAssetJoinInput, @@ -25,34 +24,45 @@ import { Token, TokenAmount, replaceWrapped, -} from '../src/entities'; -import { JoinParser } from '../src/entities/join/parser'; -import { Address, Hex } from '../src/types'; -import { PoolState } from '../src/entities/types'; -import { CHAINS, ChainId, getPoolAddress } from '../src/utils'; - + Address, + Hex, + PoolStateInput, + CHAINS, + ChainId, + getPoolAddress, + PoolJoin, + JoinInput, +} from '../src'; import { forkSetup, sendTransactionGetBalances } from './lib/utils/helper'; +type TxInput = { + client: Client & PublicActions & TestActions & WalletActions; + poolJoin: PoolJoin; + joinInput: JoinInput; + slippage: Slippage; + poolInput: PoolStateInput; + testAddress: Address; + checkNativeBalance: boolean; +}; + 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 client: Client & PublicActions & TestActions & WalletActions; - let poolFromApi: PoolState; - let weightedJoin: BaseJoin; - let bpt: Token; + let txInput: TxInput; + let bptToken: Token; beforeAll(async () => { // setup mock api - api = new MockApi(); + const api = new MockApi(); - client = createTestClient({ + // get pool state from api + const poolInput = await api.getPool(poolId); + + const client = createTestClient({ mode: 'hardhat', chain: CHAINS[chainId], transport: http(rpcUrl), @@ -60,25 +70,33 @@ describe('weighted join test', () => { .extend(publicActions) .extend(walletActions); - // get pool state from api - poolFromApi = await api.getPool(poolId); - - // setup join helper - const joinParser = new JoinParser(); - weightedJoin = joinParser.getJoin(poolFromApi.type); + txInput = { + client, + poolJoin: new PoolJoin(), + slippage: Slippage.fromPercentage('1'), // 1% + poolInput, + testAddress: '0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f', // Balancer DAO Multisig + joinInput: {} as JoinInput, + checkNativeBalance: false, + }; // setup BPT token - bpt = new Token(chainId, poolFromApi.address, 18, 'BPT'); + bptToken = new Token(chainId, poolInput.address, 18, 'BPT'); }); beforeEach(async () => { await forkSetup( - client, - testAddress, - [...poolFromApi.tokens.map((t) => t.address), poolFromApi.address], + txInput.client, + txInput.testAddress, + [ + ...txInput.poolInput.tokens.map((t) => t.address), + txInput.poolInput.address, + ], undefined, // TODO: hardcode these values to improve test performance [ - ...poolFromApi.tokens.map((t) => parseUnits('100', t.decimals)), + ...txInput.poolInput.tokens.map((t) => + parseUnits('100', t.decimals), + ), parseUnits('100', 18), ], process.env.ETHEREUM_RPC_URL as string, @@ -87,7 +105,7 @@ describe('weighted join test', () => { }); test('unbalanced join', async () => { - const poolTokens = poolFromApi.tokens.map( + const poolTokens = txInput.poolInput.tokens.map( (t) => new Token(chainId, t.address, t.decimals), ); const amountsIn = poolTokens.map((t) => @@ -103,12 +121,10 @@ describe('weighted join test', () => { }; const { queryResult, maxAmountsIn, minBptOut, value } = - await doTransaction( + await doTransaction({ + ...txInput, joinInput, - poolFromApi.tokens.map((t) => t.address), - bpt.address, - slippage, - ); + }); // Query should use same amountsIn as user sets expect(queryResult.amountsIn).to.deep.eq(amountsIn); @@ -121,14 +137,16 @@ describe('weighted join test', () => { expect(queryResult.bptOut.amount > 0n).to.be.true; // Confirm slippage - only bpt out - const expectedMinBpt = slippage.removeFrom(queryResult.bptOut.amount); + const expectedMinBpt = txInput.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( + const poolTokens = txInput.poolInput.tokens.map( (t) => new Token(chainId, t.address, t.decimals), ); const amountsIn = poolTokens.map((t) => @@ -146,12 +164,11 @@ describe('weighted join test', () => { // We have to use zero address for balanceDeltas const { queryResult, maxAmountsIn, minBptOut, value } = - await doTransaction( + await doTransaction({ + ...txInput, joinInput, - replaceWrapped(poolTokens, chainId).map((a) => a.address), - bpt.address, - slippage, - ); + checkNativeBalance: true, + }); // Query should use same amountsIn as user sets expect(queryResult.amountsIn.map((a) => a.amount)).to.deep.eq( @@ -165,14 +182,16 @@ describe('weighted join test', () => { expect(queryResult.bptOut.amount > 0n).to.be.true; // Confirm slippage - only bpt out - const expectedMinBpt = slippage.removeFrom(queryResult.bptOut.amount); + const expectedMinBpt = txInput.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('single asset join', async () => { - const bptOut = TokenAmount.fromHumanAmount(bpt, '1'); + const bptOut = TokenAmount.fromHumanAmount(bptToken, '1'); const tokenIn = '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f'; // perform join query to get expected bpt out @@ -185,12 +204,10 @@ describe('weighted join test', () => { }; const { queryResult, maxAmountsIn, minBptOut, value } = - await doTransaction( + await doTransaction({ + ...txInput, joinInput, - poolFromApi.tokens.map((t) => t.address), - bpt.address, - slippage, - ); + }); // Query should use same bpt out as user sets expect(queryResult.bptOut.amount).to.deep.eq(bptOut.amount); @@ -208,14 +225,14 @@ describe('weighted join test', () => { // Confirm slippage - only to amount in not bpt out const expectedMaxAmountsIn = queryResult.amountsIn.map((a) => - slippage.applyTo(a.amount), + txInput.slippage.applyTo(a.amount), ); expect(expectedMaxAmountsIn).to.deep.eq(maxAmountsIn); expect(minBptOut).to.eq(bptOut.amount); }); test('proportional join', async () => { - const bptOut = TokenAmount.fromHumanAmount(bpt, '1'); + const bptOut = TokenAmount.fromHumanAmount(bptToken, '1'); // perform join query to get expected bpt out const joinInput: ProportionalJoinInput = { @@ -226,12 +243,10 @@ describe('weighted join test', () => { }; const { queryResult, maxAmountsIn, minBptOut, value } = - await doTransaction( + await doTransaction({ + ...txInput, joinInput, - poolFromApi.tokens.map((t) => t.address), - bpt.address, - slippage, - ); + }); // Query should use same bpt out as user sets expect(queryResult.bptOut.amount).to.deep.eq(bptOut.amount); @@ -248,35 +263,46 @@ describe('weighted join test', () => { // Confirm slippage - only to amount in not bpt out const expectedMaxAmountsIn = queryResult.amountsIn.map((a) => - slippage.applyTo(a.amount), + txInput.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({ + async function doTransaction(txIp: TxInput) { + const { + poolJoin, + poolInput, + joinInput, + testAddress, + client, + slippage, + checkNativeBalance, + } = txIp; + const queryResult = await poolJoin.query(joinInput, poolInput); + + const { call, to, value, maxAmountsIn, minBptOut } = poolJoin.buildCall( + { ...queryResult, slippage, sender: testAddress, recipient: testAddress, - }); + }, + ); + + const poolTokens = poolInput.tokens.map( + (t) => new Token(chainId, t.address, t.decimals), + ); + + // Replace with native asset if required + const poolTokensAddr = checkNativeBalance + ? replaceWrapped(poolTokens, chainId).map((t) => t.address) + : poolTokens.map((t) => t.address); // send transaction and check balance changes const { transactionReceipt, balanceDeltas } = await sendTransactionGetBalances( - [...poolTokens, bptToken], + [...poolTokensAddr, poolInput.address], client, testAddress, to, @@ -304,7 +330,7 @@ describe('weighted join test', () => { /*********************** Mock To Represent API Requirements **********************/ export class MockApi { - public async getPool(id: Hex): Promise { + public async getPool(id: Hex): Promise { const tokens = [ { address: