From bbf61891cbc4a2746b856d0619dfa8707bafbde8 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Thu, 5 Sep 2024 12:23:00 -0700 Subject: [PATCH 1/8] progress towards example using buildCallWithPermit2 --- examples/swaps/swap.ts | 65 +++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/examples/swaps/swap.ts b/examples/swaps/swap.ts index e8638392..7c3033aa 100644 --- a/examples/swaps/swap.ts +++ b/examples/swaps/swap.ts @@ -18,8 +18,21 @@ import { Swap, SwapBuildOutputExactIn, SwapBuildOutputExactOut, + Permit2Helper, + CHAINS, } from '../../src'; +import { + // createWalletClient, + // createPublicClient, + http, + createTestClient, + publicActions, + walletActions, +} from 'viem'; +// import { privateKeyToAccount } from 'viem/accounts'; +// import { polygon } from 'viem/chains'; + const swap = async () => { // User defined const rpcUrl = process.env.POLYGON_RPC_URL; @@ -77,30 +90,50 @@ const swap = async () => { // Get up to date swap result by querying onchain const queryOutput = await swap.query(rpcUrl); + // const client = createPublicClient({ + // chain: polygon, + // transport: http(), + // }); + + const buildCallInput = { + slippage, + deadline, + queryOutput, + sender, + recipient, + wethIsEth, + }; + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + const permit2 = await Permit2Helper.signSwapApproval({ + ...buildCallInput, + client, + owner: sender, + }); + // Construct transaction to make swap if (queryOutput.swapKind === SwapKind.GivenIn) { console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); - const callData = swap.buildCall({ - slippage, - deadline, - queryOutput, - sender, - recipient, - wethIsEth, - }) as SwapBuildOutputExactIn; + const callData = swap.buildCallWithPermit2( + buildCallInput, + permit2, + ) as SwapBuildOutputExactIn; console.log( `Min Amount Out: ${callData.minAmountOut.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, ); } else { console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); - const callData = swap.buildCall({ - slippage, - deadline, - queryOutput, - sender, - recipient, - wethIsEth, - }) as SwapBuildOutputExactOut; + const callData = swap.buildCallWithPermit2( + buildCallInput, + permit2, + ) as SwapBuildOutputExactOut; console.log( `Max Amount In: ${callData.maxAmountIn.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, ); From afaabf9c82d0fad43309d7dffdb5c3ec8f153d22 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Fri, 6 Sep 2024 15:20:48 -0700 Subject: [PATCH 2/8] progress towards swap example with permit 2 --- examples/swaps/swap.ts | 110 ++++++++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/examples/swaps/swap.ts b/examples/swaps/swap.ts index 7c3033aa..36d884cd 100644 --- a/examples/swaps/swap.ts +++ b/examples/swaps/swap.ts @@ -1,5 +1,5 @@ /** - * Example showing how to find swap information for a token pair. + * Example showing how to find swap information for a token pair * * Run with: * pnpm example ./examples/swaps/swap.ts @@ -18,37 +18,43 @@ import { Swap, SwapBuildOutputExactIn, SwapBuildOutputExactOut, - Permit2Helper, - CHAINS, + Permit2Batch, + MaxAllowanceExpiration, + BALANCER_ROUTER, + PERMIT2, + permit2Abi, + PermitDetails, + MaxSigDeadline, + AllowanceTransfer, } from '../../src'; -import { - // createWalletClient, - // createPublicClient, - http, - createTestClient, - publicActions, - walletActions, -} from 'viem'; -// import { privateKeyToAccount } from 'viem/accounts'; -// import { polygon } from 'viem/chains'; +import { createPublicClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { sepolia } from 'viem/chains'; const swap = async () => { // User defined - const rpcUrl = process.env.POLYGON_RPC_URL; - const chainId = ChainId.POLYGON; + const publicClient = createPublicClient({ + chain: sepolia, + transport: http(), + }); + const account = privateKeyToAccount( + process.env.PRIVATE_KEY as `0x${string}`, + ); + const rpcUrl = process.env.SEPOLIA_RPC_URL; + const chainId = ChainId.SEPOLIA; const swapKind = SwapKind.GivenIn; const tokenIn = new Token( chainId, - '0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6', + '0xE8d4E9Fc8257B77Acb9eb80B5e8176F4f0cBCeBC', 18, - 'MaticX', + 'MockToken1', ); const tokenOut = new Token( chainId, - '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', + '0xF0Bab79D87F51a249AFe316a580C1cDFC111bE10', 18, - 'WMATIC', + 'MockToken2', ); const wethIsEth = false; const slippage = Slippage.fromPercentage('0.1'); @@ -90,11 +96,6 @@ const swap = async () => { // Get up to date swap result by querying onchain const queryOutput = await swap.query(rpcUrl); - // const client = createPublicClient({ - // chain: polygon, - // transport: http(), - // }); - const buildCallInput = { slippage, deadline, @@ -104,36 +105,57 @@ const swap = async () => { wethIsEth, }; - const client = createTestClient({ - mode: 'anvil', - chain: CHAINS[chainId], - transport: http(rpcUrl), - }) - .extend(publicActions) - .extend(walletActions); - - const permit2 = await Permit2Helper.signSwapApproval({ - ...buildCallInput, - client, - owner: sender, + const [, , nonce] = await publicClient.readContract({ + address: PERMIT2[chainId], + abi: permit2Abi, + functionName: 'allowance', + args: [account.address, tokenIn.address, BALANCER_ROUTER[chainId]], + }); + + const details: PermitDetails[] = [ + { + token: tokenIn.address, + amount: swapAmount.amount, + expiration: Number(MaxAllowanceExpiration), + nonce, + }, + ]; + + const batch: Permit2Batch = { + details, + spender: BALANCER_ROUTER[chainId], + sigDeadline: MaxSigDeadline, + }; + + const { domain, types, values } = AllowanceTransfer.getPermitData( + batch, + PERMIT2[chainId], + chainId, + ); + + const signature = await account.signTypedData({ + message: { ...values }, + domain, + primaryType: 'PermitBatch', + types, }); // Construct transaction to make swap if (queryOutput.swapKind === SwapKind.GivenIn) { console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); - const callData = swap.buildCallWithPermit2( - buildCallInput, - permit2, - ) as SwapBuildOutputExactIn; + const callData = swap.buildCallWithPermit2(buildCallInput, { + signature, + batch, + }) as SwapBuildOutputExactIn; console.log( `Min Amount Out: ${callData.minAmountOut.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, ); } else { console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); - const callData = swap.buildCallWithPermit2( - buildCallInput, - permit2, - ) as SwapBuildOutputExactOut; + const callData = swap.buildCallWithPermit2(buildCallInput, { + signature, + batch, + }) as SwapBuildOutputExactOut; console.log( `Max Amount In: ${callData.maxAmountIn.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, ); From dcce8bc8167335db4c7e5ff98bc7cf486e66c5dd Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Fri, 20 Sep 2024 18:24:38 -0700 Subject: [PATCH 3/8] fumbling around --- examples/approvals/approvePermit2OnToken.ts | 0 examples/approvals/approveRouterOnPermit2.ts | 0 examples/approvals/index.ts | 1 + examples/approvals/signPermit2.ts | 34 ++++ examples/swaps/queryCustomPath.ts | 69 ++++++++ examples/swaps/querySmartPath.ts | 82 +++++++++ examples/swaps/swap.ts | 165 ------------------- examples/swaps/swapV3.ts | 108 ++++++++++++ 8 files changed, 294 insertions(+), 165 deletions(-) create mode 100644 examples/approvals/approvePermit2OnToken.ts create mode 100644 examples/approvals/approveRouterOnPermit2.ts create mode 100644 examples/approvals/index.ts create mode 100644 examples/approvals/signPermit2.ts create mode 100644 examples/swaps/queryCustomPath.ts create mode 100644 examples/swaps/querySmartPath.ts delete mode 100644 examples/swaps/swap.ts create mode 100644 examples/swaps/swapV3.ts diff --git a/examples/approvals/approvePermit2OnToken.ts b/examples/approvals/approvePermit2OnToken.ts new file mode 100644 index 00000000..e69de29b diff --git a/examples/approvals/approveRouterOnPermit2.ts b/examples/approvals/approveRouterOnPermit2.ts new file mode 100644 index 00000000..e69de29b diff --git a/examples/approvals/index.ts b/examples/approvals/index.ts new file mode 100644 index 00000000..d80ee9f8 --- /dev/null +++ b/examples/approvals/index.ts @@ -0,0 +1 @@ +export * from './signPermit2'; diff --git a/examples/approvals/signPermit2.ts b/examples/approvals/signPermit2.ts new file mode 100644 index 00000000..a68e59c0 --- /dev/null +++ b/examples/approvals/signPermit2.ts @@ -0,0 +1,34 @@ +import { + AllowanceTransfer, + Permit2Batch, + PermitDetails, + PERMIT2, + BALANCER_ROUTER, + MaxSigDeadline, + ChainId, +} from '../../src'; + +export const signPermit2 = async (client, details: PermitDetails[]) => { + const chainId = ChainId.SEPOLIA; + + const batch: Permit2Batch = { + details, + spender: BALANCER_ROUTER[chainId], + sigDeadline: MaxSigDeadline, + }; + + const { domain, types, values } = AllowanceTransfer.getPermitData( + batch, + PERMIT2[chainId], + chainId, + ); + + const signature = await client.signTypedData({ + message: { ...values }, + domain, + primaryType: 'PermitBatch', + types, + }); + + return { signature, batch }; +}; diff --git a/examples/swaps/queryCustomPath.ts b/examples/swaps/queryCustomPath.ts new file mode 100644 index 00000000..fc9d37b3 --- /dev/null +++ b/examples/swaps/queryCustomPath.ts @@ -0,0 +1,69 @@ +// use custom path to query and return the queryOutput +/** + * Example showing how to query swap using paths from the SOR + * + * Run with: + * pnpm example ./examples/swaps/queryCustomPath.ts + */ +import { config } from 'dotenv'; +config(); + +import { + ChainId, + SwapKind, + Swap, + ExactInQueryOutput, + ExactOutQueryOutput, +} from '../../src'; + +import { Address, parseUnits } from 'viem'; + +const queryCustomPath = async () => { + // User defined + const rpcUrl = process.env.SEPOLIA_RPC_URL; + const chainId = ChainId.SEPOLIA; + const pool = '0xb27aC1DD8192163CFbD6F977C91D31A07E941B87' as Address; // Constant Product Pool from scaffold balancer v3 + const tokenIn = { + address: '0x83f953D2461C6352120E06f5f8EcCD3e4d66d042' as Address, // MockToken1 from scaffold balancer v3 + decimals: 18, + }; + const tokenOut = { + address: '0x9d57eDCe10b7BdDA98997613c33ff7f3e34F4eAd' as Address, + decimals: 18, + }; + const swapKind = SwapKind.GivenIn; + const tokens = [tokenIn, tokenOut]; + const protocolVersion = 3 as const; + const inputAmountRaw = parseUnits('1', 18); + const outputAmountRaw = parseUnits('1', 18); + + const customPaths = [ + { + pools: [pool], + tokens, + protocolVersion, + inputAmountRaw, + outputAmountRaw, + }, + ]; + const swapInput = { chainId, swapKind, paths: customPaths }; + + // Swap object provides useful helpers for re-querying, building call, etc + const swap = new Swap(swapInput); + + // Get up to date swap result by querying onchain + const queryOutput = (await swap.query(rpcUrl)) as + | ExactInQueryOutput + | ExactOutQueryOutput; + + // Construct transaction to make swap + if (queryOutput.swapKind === SwapKind.GivenIn) { + console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); + } else { + console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); + } + + return { swap, queryOutput }; +}; + +export default queryCustomPath; diff --git a/examples/swaps/querySmartPath.ts b/examples/swaps/querySmartPath.ts new file mode 100644 index 00000000..874490ca --- /dev/null +++ b/examples/swaps/querySmartPath.ts @@ -0,0 +1,82 @@ +/** + * Example showing how to query swap using paths from the SOR + * + * Run with: + * pnpm example ./examples/swaps/querySmartPath.ts + */ +import { config } from 'dotenv'; +config(); + +import { + BalancerApi, + API_ENDPOINT, + ChainId, + SwapKind, + Token, + TokenAmount, + Swap, +} from '../../src'; + +const querySmartPath = async () => { + // User defined + const rpcUrl = process.env.SEPOLIA_RPC_URL; + const chainId = ChainId.SEPOLIA; + const swapKind = SwapKind.GivenOut; + const tokenIn = new Token( + chainId, + '0xE8d4E9Fc8257B77Acb9eb80B5e8176F4f0cBCeBC', + 18, + 'MockToken1', + ); + const tokenOut = new Token( + chainId, + '0xF0Bab79D87F51a249AFe316a580C1cDFC111bE10', + 18, + 'MockToken2', + ); + const swapAmount = + swapKind === SwapKind.GivenIn + ? TokenAmount.fromHumanAmount(tokenIn, '1.2345678910') + : TokenAmount.fromHumanAmount(tokenOut, '1.2345678910'); + + // API is used to fetch best path from available liquidity + const balancerApi = new BalancerApi(API_ENDPOINT, chainId); + + const sorPaths = await balancerApi.sorSwapPaths.fetchSorSwapPaths({ + chainId, + tokenIn: tokenIn.address, + tokenOut: tokenOut.address, + swapKind, + swapAmount, + }); + + const swapInput = { + chainId, + paths: sorPaths, + swapKind, + }; + + // Swap object provides useful helpers for re-querying, building call, etc + const swap = new Swap(swapInput); + + console.log( + `Input token: ${swap.inputAmount.token.address}, Amount: ${swap.inputAmount.amount}`, + ); + console.log( + `Output token: ${swap.outputAmount.token.address}, Amount: ${swap.outputAmount.amount}`, + ); + + // Get up to date swap result by querying onchain + const queryOutput = await swap.query(rpcUrl); + + // Construct transaction to make swap + if (queryOutput.swapKind === SwapKind.GivenIn) { + console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); + } else { + console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); + } + + return queryOutput; +}; + +export default querySmartPath; diff --git a/examples/swaps/swap.ts b/examples/swaps/swap.ts deleted file mode 100644 index 36d884cd..00000000 --- a/examples/swaps/swap.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Example showing how to find swap information for a token pair - * - * Run with: - * pnpm example ./examples/swaps/swap.ts - */ -import { config } from 'dotenv'; -config(); - -import { - BalancerApi, - API_ENDPOINT, - ChainId, - Slippage, - SwapKind, - Token, - TokenAmount, - Swap, - SwapBuildOutputExactIn, - SwapBuildOutputExactOut, - Permit2Batch, - MaxAllowanceExpiration, - BALANCER_ROUTER, - PERMIT2, - permit2Abi, - PermitDetails, - MaxSigDeadline, - AllowanceTransfer, -} from '../../src'; - -import { createPublicClient, http } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; -import { sepolia } from 'viem/chains'; - -const swap = async () => { - // User defined - const publicClient = createPublicClient({ - chain: sepolia, - transport: http(), - }); - const account = privateKeyToAccount( - process.env.PRIVATE_KEY as `0x${string}`, - ); - const rpcUrl = process.env.SEPOLIA_RPC_URL; - const chainId = ChainId.SEPOLIA; - const swapKind = SwapKind.GivenIn; - const tokenIn = new Token( - chainId, - '0xE8d4E9Fc8257B77Acb9eb80B5e8176F4f0cBCeBC', - 18, - 'MockToken1', - ); - const tokenOut = new Token( - chainId, - '0xF0Bab79D87F51a249AFe316a580C1cDFC111bE10', - 18, - 'MockToken2', - ); - const wethIsEth = false; - const slippage = Slippage.fromPercentage('0.1'); - const swapAmount = - swapKind === SwapKind.GivenIn - ? TokenAmount.fromHumanAmount(tokenIn, '1.2345678910') - : TokenAmount.fromHumanAmount(tokenOut, '1.2345678910'); - const sender = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; - const recipient = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; - const deadline = 999999999999999999n; // Infinity - - // API is used to fetch best path from available liquidity - const balancerApi = new BalancerApi(API_ENDPOINT, chainId); - - const sorPaths = await balancerApi.sorSwapPaths.fetchSorSwapPaths({ - chainId, - tokenIn: tokenIn.address, - tokenOut: tokenOut.address, - swapKind, - swapAmount, - }); - - const swapInput = { - chainId, - paths: sorPaths, - swapKind, - }; - - // Swap object provides useful helpers for re-querying, building call, etc - const swap = new Swap(swapInput); - - console.log( - `Input token: ${swap.inputAmount.token.address}, Amount: ${swap.inputAmount.amount}`, - ); - console.log( - `Output token: ${swap.outputAmount.token.address}, Amount: ${swap.outputAmount.amount}`, - ); - - // Get up to date swap result by querying onchain - const queryOutput = await swap.query(rpcUrl); - - const buildCallInput = { - slippage, - deadline, - queryOutput, - sender, - recipient, - wethIsEth, - }; - - const [, , nonce] = await publicClient.readContract({ - address: PERMIT2[chainId], - abi: permit2Abi, - functionName: 'allowance', - args: [account.address, tokenIn.address, BALANCER_ROUTER[chainId]], - }); - - const details: PermitDetails[] = [ - { - token: tokenIn.address, - amount: swapAmount.amount, - expiration: Number(MaxAllowanceExpiration), - nonce, - }, - ]; - - const batch: Permit2Batch = { - details, - spender: BALANCER_ROUTER[chainId], - sigDeadline: MaxSigDeadline, - }; - - const { domain, types, values } = AllowanceTransfer.getPermitData( - batch, - PERMIT2[chainId], - chainId, - ); - - const signature = await account.signTypedData({ - message: { ...values }, - domain, - primaryType: 'PermitBatch', - types, - }); - - // Construct transaction to make swap - if (queryOutput.swapKind === SwapKind.GivenIn) { - console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); - const callData = swap.buildCallWithPermit2(buildCallInput, { - signature, - batch, - }) as SwapBuildOutputExactIn; - console.log( - `Min Amount Out: ${callData.minAmountOut.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, - ); - } else { - console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); - const callData = swap.buildCallWithPermit2(buildCallInput, { - signature, - batch, - }) as SwapBuildOutputExactOut; - console.log( - `Max Amount In: ${callData.maxAmountIn.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, - ); - } -}; - -export default swap; diff --git a/examples/swaps/swapV3.ts b/examples/swaps/swapV3.ts new file mode 100644 index 00000000..8ce8ba60 --- /dev/null +++ b/examples/swaps/swapV3.ts @@ -0,0 +1,108 @@ +/** + * Example showing how to find swap information for a token pair + * + * Run with: + * pnpm example ./examples/swaps/swapV3.ts + */ +import { config } from 'dotenv'; +config(); + +import { + ChainId, + Slippage, + SwapKind, + SwapBuildOutputExactIn, + SwapBuildOutputExactOut, + MaxAllowanceExpiration, + BALANCER_ROUTER, + PERMIT2, + permit2Abi, + PermitDetails, + TokenAmount, +} from '../../src'; + +import queryCustomPath from './queryCustomPath'; +import { signPermit2 } from '../approvals'; + +import { createPublicClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { sepolia } from 'viem/chains'; + +const swap = async () => { + // User defined + const publicClient = createPublicClient({ + chain: sepolia, + transport: http(), + }); + const account = privateKeyToAccount( + process.env.PRIVATE_KEY as `0x${string}`, + ); + const chainId = ChainId.SEPOLIA; + // Get up to date swap result by querying onchain + const { swap, queryOutput } = await queryCustomPath(); + + const sender = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + const recipient = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + const slippage = Slippage.fromPercentage('0.1'); + const deadline = 999999999999999999n; // Infinity + const wethIsEth = false; + + const buildCallInput = { + sender, + recipient, + slippage, + deadline, + wethIsEth, + queryOutput, + }; + + let tokenIn: TokenAmount; + + if (queryOutput.swapKind === SwapKind.GivenIn) { + tokenIn = queryOutput.amountIn; + } else { + tokenIn = queryOutput.expectedAmountIn; + } + + const [, , nonce] = await publicClient.readContract({ + address: PERMIT2[chainId], + abi: permit2Abi, + functionName: 'allowance', + args: [ + account.address, + tokenIn.token.address, + BALANCER_ROUTER[chainId], + ], + }); + + const details: PermitDetails[] = [ + { + token: tokenIn.token.address, + amount: tokenIn.amount, + expiration: Number(MaxAllowanceExpiration), + nonce, + }, + ]; + + const signedPermit2Batch = await signPermit2(account, details); + + const callData = swap.buildCallWithPermit2( + buildCallInput, + signedPermit2Batch, + ) as SwapBuildOutputExactOut | SwapBuildOutputExactIn; + + // Construct transaction to make swap + if ('minAmountOut' in callData && 'expectedAmountOut' in queryOutput) { + console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); + console.log( + `Min Amount Out: ${callData.minAmountOut.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, + ); + } else if ('maxAmountIn' in callData && 'expectedAmountIn' in queryOutput) { + console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); + console.log( + `Max Amount In: ${callData.maxAmountIn.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, + ); + } +}; + +export default swap; From 49cff9890f64c24ed8116d3407cf76b33e04a925 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Sat, 21 Sep 2024 13:52:55 -0700 Subject: [PATCH 4/8] figuring out what is possible on fork --- examples/approvals/approvePermit2OnToken.ts | 0 examples/approvals/approveRouterOnPermit2.ts | 0 examples/approvals/approveSpenderOnToken.ts | 19 +++++ examples/approvals/index.ts | 1 + examples/approvals/signPermit2.ts | 16 +++- examples/swaps/queryCustomPath.ts | 26 +++++- examples/swaps/querySmartPath.ts | 45 +++++----- examples/swaps/swapV3.ts | 90 ++++++++++++-------- 8 files changed, 134 insertions(+), 63 deletions(-) delete mode 100644 examples/approvals/approvePermit2OnToken.ts delete mode 100644 examples/approvals/approveRouterOnPermit2.ts create mode 100644 examples/approvals/approveSpenderOnToken.ts diff --git a/examples/approvals/approvePermit2OnToken.ts b/examples/approvals/approvePermit2OnToken.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/approvals/approveRouterOnPermit2.ts b/examples/approvals/approveRouterOnPermit2.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/approvals/approveSpenderOnToken.ts b/examples/approvals/approveSpenderOnToken.ts new file mode 100644 index 00000000..08075e8f --- /dev/null +++ b/examples/approvals/approveSpenderOnToken.ts @@ -0,0 +1,19 @@ +import { erc20Abi, MaxUint256 } from '../../src'; + +import { Address } from 'viem'; + +export const approveSpenderOnToken = async ( + client: any, + account: Address, + token: Address, + spender: Address, +) => { + await client.writeContract({ + account, + address: token, + abi: erc20Abi, + functionName: 'approve', + args: [spender, MaxUint256], + }); + console.log('Approved spender on token'); +}; diff --git a/examples/approvals/index.ts b/examples/approvals/index.ts index d80ee9f8..f52cb84e 100644 --- a/examples/approvals/index.ts +++ b/examples/approvals/index.ts @@ -1 +1,2 @@ export * from './signPermit2'; +export * from './approveSpenderOnToken'; diff --git a/examples/approvals/signPermit2.ts b/examples/approvals/signPermit2.ts index a68e59c0..7b873bc8 100644 --- a/examples/approvals/signPermit2.ts +++ b/examples/approvals/signPermit2.ts @@ -8,12 +8,19 @@ import { ChainId, } from '../../src'; -export const signPermit2 = async (client, details: PermitDetails[]) => { - const chainId = ChainId.SEPOLIA; +import { Address } from 'viem'; + +export const signPermit2 = async ( + client: any, + account: Address, + chainId: ChainId, + details: PermitDetails[], +) => { + const spender = BALANCER_ROUTER[chainId]; const batch: Permit2Batch = { details, - spender: BALANCER_ROUTER[chainId], + spender, sigDeadline: MaxSigDeadline, }; @@ -23,7 +30,10 @@ export const signPermit2 = async (client, details: PermitDetails[]) => { chainId, ); + await client.impersonateAccount({ address: account }); + const signature = await client.signTypedData({ + account: account, message: { ...values }, domain, primaryType: 'PermitBatch', diff --git a/examples/swaps/queryCustomPath.ts b/examples/swaps/queryCustomPath.ts index fc9d37b3..f05d0d12 100644 --- a/examples/swaps/queryCustomPath.ts +++ b/examples/swaps/queryCustomPath.ts @@ -31,7 +31,7 @@ const queryCustomPath = async () => { address: '0x9d57eDCe10b7BdDA98997613c33ff7f3e34F4eAd' as Address, decimals: 18, }; - const swapKind = SwapKind.GivenIn; + const swapKind = SwapKind.GivenIn as SwapKind; const tokens = [tokenIn, tokenOut]; const protocolVersion = 3 as const; const inputAmountRaw = parseUnits('1', 18); @@ -51,6 +51,18 @@ const queryCustomPath = async () => { // Swap object provides useful helpers for re-querying, building call, etc const swap = new Swap(swapInput); + if (swapKind === SwapKind.GivenIn) { + console.log('Given tokenIn:', { + address: tokenIn.address, + amount: inputAmountRaw, + }); + } else { + console.log('Given tokenOut:', { + address: tokenOut.address, + amount: outputAmountRaw, + }); + } + // Get up to date swap result by querying onchain const queryOutput = (await swap.query(rpcUrl)) as | ExactInQueryOutput @@ -58,12 +70,18 @@ const queryCustomPath = async () => { // Construct transaction to make swap if (queryOutput.swapKind === SwapKind.GivenIn) { - console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); + console.log('tokenOut:', { + address: tokenOut.address, + expectedAmount: queryOutput.expectedAmountOut.amount, + }); } else { - console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); + console.log('tokenIn:', { + address: tokenIn.address, + expectedAmount: queryOutput.expectedAmountIn.amount, + }); } - return { swap, queryOutput }; + return { swap, chainId, queryOutput }; }; export default queryCustomPath; diff --git a/examples/swaps/querySmartPath.ts b/examples/swaps/querySmartPath.ts index 874490ca..f22e9a5d 100644 --- a/examples/swaps/querySmartPath.ts +++ b/examples/swaps/querySmartPath.ts @@ -19,25 +19,25 @@ import { const querySmartPath = async () => { // User defined - const rpcUrl = process.env.SEPOLIA_RPC_URL; - const chainId = ChainId.SEPOLIA; - const swapKind = SwapKind.GivenOut; + const rpcUrl = process.env.MAINNET_RPC_URL; + const chainId = ChainId.MAINNET; + const swapKind = SwapKind.GivenIn as SwapKind; const tokenIn = new Token( chainId, - '0xE8d4E9Fc8257B77Acb9eb80B5e8176F4f0cBCeBC', - 18, - 'MockToken1', + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + 6, + 'USDC', ); const tokenOut = new Token( chainId, - '0xF0Bab79D87F51a249AFe316a580C1cDFC111bE10', + '0xba100000625a3754423978a60c9317c58a424e3D', 18, - 'MockToken2', + 'BAL', ); const swapAmount = swapKind === SwapKind.GivenIn - ? TokenAmount.fromHumanAmount(tokenIn, '1.2345678910') - : TokenAmount.fromHumanAmount(tokenOut, '1.2345678910'); + ? TokenAmount.fromHumanAmount(tokenIn, '100') + : TokenAmount.fromHumanAmount(tokenOut, '100'); // API is used to fetch best path from available liquidity const balancerApi = new BalancerApi(API_ENDPOINT, chainId); @@ -59,24 +59,31 @@ const querySmartPath = async () => { // Swap object provides useful helpers for re-querying, building call, etc const swap = new Swap(swapInput); - console.log( - `Input token: ${swap.inputAmount.token.address}, Amount: ${swap.inputAmount.amount}`, - ); - console.log( - `Output token: ${swap.outputAmount.token.address}, Amount: ${swap.outputAmount.amount}`, - ); + console.table({ + Address: { + tokenIn: swap.inputAmount.token.address, + tokenOut: swap.outputAmount.token.address, + }, + Amount: { + tokenIn: swap.inputAmount.amount, + tokenOut: swap.outputAmount.amount, + }, + }); // Get up to date swap result by querying onchain const queryOutput = await swap.query(rpcUrl); // Construct transaction to make swap if (queryOutput.swapKind === SwapKind.GivenIn) { - console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); + console.log( + 'Expected Amount Out:', + queryOutput.expectedAmountOut.amount, + ); } else { - console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); + console.log('Expected Amount In:', queryOutput.expectedAmountIn.amount); } - return queryOutput; + return { swap, chainId, queryOutput }; }; export default querySmartPath; diff --git a/examples/swaps/swapV3.ts b/examples/swaps/swapV3.ts index 8ce8ba60..dbb7df09 100644 --- a/examples/swaps/swapV3.ts +++ b/examples/swaps/swapV3.ts @@ -8,7 +8,6 @@ import { config } from 'dotenv'; config(); import { - ChainId, Slippage, SwapKind, SwapBuildOutputExactIn, @@ -19,42 +18,26 @@ import { permit2Abi, PermitDetails, TokenAmount, + CHAINS, } from '../../src'; import queryCustomPath from './queryCustomPath'; -import { signPermit2 } from '../approvals'; +import { approveSpenderOnToken, signPermit2 } from '../approvals'; -import { createPublicClient, http } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; -import { sepolia } from 'viem/chains'; +import { createTestClient, http, publicActions, walletActions } from 'viem'; +import { ANVIL_NETWORKS, startFork } from '../../test/anvil/anvil-global-setup'; -const swap = async () => { - // User defined - const publicClient = createPublicClient({ - chain: sepolia, - transport: http(), - }); - const account = privateKeyToAccount( - process.env.PRIVATE_KEY as `0x${string}`, - ); - const chainId = ChainId.SEPOLIA; - // Get up to date swap result by querying onchain - const { swap, queryOutput } = await queryCustomPath(); - - const sender = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; - const recipient = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; +const swapV3 = async () => { + const { rpcUrl } = await startFork(ANVIL_NETWORKS.SEPOLIA); + // User defined; + const sender = '0x5036388C540994Ed7b74b82F71175a441F85BdA1'; + const recipient = '0x5036388C540994Ed7b74b82F71175a441F85BdA1'; const slippage = Slippage.fromPercentage('0.1'); const deadline = 999999999999999999n; // Infinity const wethIsEth = false; - const buildCallInput = { - sender, - recipient, - slippage, - deadline, - wethIsEth, - queryOutput, - }; + // Get up to date swap result by querying onchain + const { chainId, swap, queryOutput } = await queryCustomPath(); let tokenIn: TokenAmount; @@ -64,17 +47,35 @@ const swap = async () => { tokenIn = queryOutput.expectedAmountIn; } - const [, , nonce] = await publicClient.readContract({ + const client = createTestClient({ + // account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`), + chain: CHAINS[chainId], + mode: 'anvil', + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + // Impersonate sender so we don't need private key + await client.impersonateAccount({ address: sender }); + + // Approve Permit2 contract as spender of tokenIn + await approveSpenderOnToken( + client, + sender, + tokenIn.token.address, + PERMIT2[chainId], + ); + + // Get Permit2 nonce for PermitDetails + const [, , nonce] = await client.readContract({ address: PERMIT2[chainId], abi: permit2Abi, functionName: 'allowance', - args: [ - account.address, - tokenIn.token.address, - BALANCER_ROUTER[chainId], - ], + args: [sender, tokenIn.token.address, BALANCER_ROUTER[chainId]], }); + // Set up details for Permit2 signature const details: PermitDetails[] = [ { token: tokenIn.token.address, @@ -84,14 +85,29 @@ const swap = async () => { }, ]; - const signedPermit2Batch = await signPermit2(account, details); + // Sign Permit2 batch + const signedPermit2Batch = await signPermit2( + client, + sender, + chainId, + details, + ); + + const buildCallInput = { + sender, + recipient, + slippage, + deadline, + wethIsEth, + queryOutput, + }; + // Build call data with Permit2 signature const callData = swap.buildCallWithPermit2( buildCallInput, signedPermit2Batch, ) as SwapBuildOutputExactOut | SwapBuildOutputExactIn; - // Construct transaction to make swap if ('minAmountOut' in callData && 'expectedAmountOut' in queryOutput) { console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); console.log( @@ -105,4 +121,4 @@ const swap = async () => { } }; -export default swap; +export default swapV3; From 177482954310e8640b76fdd3a2b56b2d79a5cb34 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Sun, 24 Nov 2024 16:19:10 -0800 Subject: [PATCH 5/8] layout plan for simple fork setup --- examples/lib/setupFork.ts | 34 ++++++++++++ examples/swaps/swap.ts | 111 ------------------------------------- examples/swaps/swapV2.ts | 114 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 111 deletions(-) create mode 100644 examples/lib/setupFork.ts delete mode 100644 examples/swaps/swap.ts create mode 100644 examples/swaps/swapV2.ts diff --git a/examples/lib/setupFork.ts b/examples/lib/setupFork.ts new file mode 100644 index 00000000..2837cf6c --- /dev/null +++ b/examples/lib/setupFork.ts @@ -0,0 +1,34 @@ +import { startFork } from '../../test/anvil/anvil-global-setup'; +import { ANVIL_NETWORKS } from '../../test/anvil/anvil-global-setup'; +import { createTestClient, http, publicActions, walletActions } from 'viem'; +import { CHAINS, ChainId } from '../../src'; +import { Address } from 'viem'; + +/** + * Default fork setup for all SDK examples + * 1. use default anvil account to wrap ETH into wETH (allows for swap example) + * 2. swap wETH for BAL (allows for proportional add example) + * 3. adds liquidity to 80BAL-20WETH pool (allows for remove example) + */ + +export const setupFork = async () => { + const chainId = ChainId.SEPOLIA; + const { rpcUrl } = await startFork(ANVIL_NETWORKS[ChainId[chainId]]); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + const userAccount = (await client.getAddresses())[0]; + const balance = await client.getBalance({ address: userAccount }); + + console.log('balance', balance); + + const wethAddress = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' as Address; + + return { client, userAccount }; +}; diff --git a/examples/swaps/swap.ts b/examples/swaps/swap.ts deleted file mode 100644 index 5ec8733e..00000000 --- a/examples/swaps/swap.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Example showing how to find swap information for a token pair. - * - * Run with: - * pnpm example ./examples/swaps/swap.ts - */ -import { config } from 'dotenv'; -config(); - -import { - BalancerApi, - API_ENDPOINT, - ChainId, - Slippage, - SwapKind, - Token, - TokenAmount, - Swap, - SwapBuildOutputExactIn, - SwapBuildOutputExactOut, -} from '../../src'; - -const swap = async () => { - // User defined - const rpcUrl = process.env.POLYGON_RPC_URL; - const chainId = ChainId.POLYGON; - const swapKind = SwapKind.GivenIn; - const tokenIn = new Token( - chainId, - '0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6', - 18, - 'MaticX', - ); - const tokenOut = new Token( - chainId, - '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', - 18, - 'WMATIC', - ); - const wethIsEth = false; - const slippage = Slippage.fromPercentage('0.1'); - const swapAmount = - swapKind === SwapKind.GivenIn - ? TokenAmount.fromHumanAmount(tokenIn, '1.2345678910') - : TokenAmount.fromHumanAmount(tokenOut, '1.2345678910'); - const sender = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; - const recipient = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; - const deadline = 999999999999999999n; // Infinity - - // API is used to fetch best path from available liquidity - const balancerApi = new BalancerApi(API_ENDPOINT, chainId); - - const sorPaths = await balancerApi.sorSwapPaths.fetchSorSwapPaths({ - chainId, - tokenIn: tokenIn.address, - tokenOut: tokenOut.address, - swapKind, - swapAmount, - }); - - const swapInput = { - chainId, - paths: sorPaths, - swapKind, - userData: '0x', - }; - - // Swap object provides useful helpers for re-querying, building call, etc - const swap = new Swap(swapInput); - - console.log( - `Input token: ${swap.inputAmount.token.address}, Amount: ${swap.inputAmount.amount}`, - ); - console.log( - `Output token: ${swap.outputAmount.token.address}, Amount: ${swap.outputAmount.amount}`, - ); - - // Get up to date swap result by querying onchain - const queryOutput = await swap.query(rpcUrl); - - // Construct transaction to make swap - if (queryOutput.swapKind === SwapKind.GivenIn) { - console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); - const callData = swap.buildCall({ - slippage, - deadline, - queryOutput, - sender, - recipient, - wethIsEth, - }) as SwapBuildOutputExactIn; - console.log( - `Min Amount Out: ${callData.minAmountOut.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, - ); - } else { - console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); - const callData = swap.buildCall({ - slippage, - deadline, - queryOutput, - sender, - recipient, - wethIsEth, - }) as SwapBuildOutputExactOut; - console.log( - `Max Amount In: ${callData.maxAmountIn.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, - ); - } -}; - -export default swap; diff --git a/examples/swaps/swapV2.ts b/examples/swaps/swapV2.ts new file mode 100644 index 00000000..2ce24816 --- /dev/null +++ b/examples/swaps/swapV2.ts @@ -0,0 +1,114 @@ +/** + * Example showing + * + * Run with: + * pnpm example ./examples/swaps/swap.ts + */ +import { config } from 'dotenv'; +config(); + +import { setupFork } from '../lib/setupFork'; +import { + BalancerApi, + API_ENDPOINT, + ChainId, + Slippage, + SwapKind, + Token, + TokenAmount, + Swap, + SwapBuildOutputExactIn, + SwapBuildOutputExactOut, +} from '../../src'; + +const swap = async () => { + await setupFork(); + + // User defined + // const rpcUrl = process.env.POLYGON_RPC_URL; + // const chainId = ChainId.POLYGON; + // const swapKind = SwapKind.GivenIn; + // const tokenIn = new Token( + // chainId, + // '0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6', + // 18, + // 'MaticX', + // ); + // const tokenOut = new Token( + // chainId, + // '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', + // 18, + // 'WMATIC', + // ); + // const wethIsEth = false; + // const slippage = Slippage.fromPercentage('0.1'); + // const swapAmount = + // swapKind === SwapKind.GivenIn + // ? TokenAmount.fromHumanAmount(tokenIn, '1.2345678910') + // : TokenAmount.fromHumanAmount(tokenOut, '1.2345678910'); + // const sender = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + // const recipient = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + // const deadline = 999999999999999999n; // Infinity + + // // API is used to fetch best path from available liquidity + // const balancerApi = new BalancerApi(API_ENDPOINT, chainId); + + // const sorPaths = await balancerApi.sorSwapPaths.fetchSorSwapPaths({ + // chainId, + // tokenIn: tokenIn.address, + // tokenOut: tokenOut.address, + // swapKind, + // swapAmount, + // }); + + // const swapInput = { + // chainId, + // paths: sorPaths, + // swapKind, + // userData: '0x', + // }; + + // // Swap object provides useful helpers for re-querying, building call, etc + // const swap = new Swap(swapInput); + + // console.log( + // `Input token: ${swap.inputAmount.token.address}, Amount: ${swap.inputAmount.amount}`, + // ); + // console.log( + // `Output token: ${swap.outputAmount.token.address}, Amount: ${swap.outputAmount.amount}`, + // ); + + // // Get up to date swap result by querying onchain + // const queryOutput = await swap.query(rpcUrl); + + // // Construct transaction to make swap + // if (queryOutput.swapKind === SwapKind.GivenIn) { + // console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); + // const callData = swap.buildCall({ + // slippage, + // deadline, + // queryOutput, + // sender, + // recipient, + // wethIsEth, + // }) as SwapBuildOutputExactIn; + // console.log( + // `Min Amount Out: ${callData.minAmountOut.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, + // ); + // } else { + // console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); + // const callData = swap.buildCall({ + // slippage, + // deadline, + // queryOutput, + // sender, + // recipient, + // wethIsEth, + // }) as SwapBuildOutputExactOut; + // console.log( + // `Max Amount In: ${callData.maxAmountIn.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, + // ); + // } +}; + +export default swap; From c7c9fc7835b8be7aa5621fff1122df114133af2d Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Sun, 1 Dec 2024 14:12:51 -0800 Subject: [PATCH 6/8] begin example fork setup --- examples/approvals/approveSpenderOnToken.ts | 19 --- examples/approvals/index.ts | 2 - examples/approvals/signPermit2.ts | 44 ----- examples/lib/setupExampleFork.ts | 48 ++++++ examples/lib/setupFork.ts | 34 ---- examples/swaps/queryCustomPath.ts | 72 ++++---- examples/swaps/querySmartPath.ts | 50 +++--- examples/swaps/swap.ts | 112 ------------- examples/swaps/swapV2.ts | 173 ++++++++++---------- examples/swaps/swapV3.ts | 142 ++++++++-------- 10 files changed, 259 insertions(+), 437 deletions(-) delete mode 100644 examples/approvals/approveSpenderOnToken.ts delete mode 100644 examples/approvals/index.ts delete mode 100644 examples/approvals/signPermit2.ts create mode 100644 examples/lib/setupExampleFork.ts delete mode 100644 examples/lib/setupFork.ts delete mode 100644 examples/swaps/swap.ts diff --git a/examples/approvals/approveSpenderOnToken.ts b/examples/approvals/approveSpenderOnToken.ts deleted file mode 100644 index 08075e8f..00000000 --- a/examples/approvals/approveSpenderOnToken.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { erc20Abi, MaxUint256 } from '../../src'; - -import { Address } from 'viem'; - -export const approveSpenderOnToken = async ( - client: any, - account: Address, - token: Address, - spender: Address, -) => { - await client.writeContract({ - account, - address: token, - abi: erc20Abi, - functionName: 'approve', - args: [spender, MaxUint256], - }); - console.log('Approved spender on token'); -}; diff --git a/examples/approvals/index.ts b/examples/approvals/index.ts deleted file mode 100644 index f52cb84e..00000000 --- a/examples/approvals/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './signPermit2'; -export * from './approveSpenderOnToken'; diff --git a/examples/approvals/signPermit2.ts b/examples/approvals/signPermit2.ts deleted file mode 100644 index 7b873bc8..00000000 --- a/examples/approvals/signPermit2.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - AllowanceTransfer, - Permit2Batch, - PermitDetails, - PERMIT2, - BALANCER_ROUTER, - MaxSigDeadline, - ChainId, -} from '../../src'; - -import { Address } from 'viem'; - -export const signPermit2 = async ( - client: any, - account: Address, - chainId: ChainId, - details: PermitDetails[], -) => { - const spender = BALANCER_ROUTER[chainId]; - - const batch: Permit2Batch = { - details, - spender, - sigDeadline: MaxSigDeadline, - }; - - const { domain, types, values } = AllowanceTransfer.getPermitData( - batch, - PERMIT2[chainId], - chainId, - ); - - await client.impersonateAccount({ address: account }); - - const signature = await client.signTypedData({ - account: account, - message: { ...values }, - domain, - primaryType: 'PermitBatch', - types, - }); - - return { signature, batch }; -}; diff --git a/examples/lib/setupExampleFork.ts b/examples/lib/setupExampleFork.ts new file mode 100644 index 00000000..e2a912bf --- /dev/null +++ b/examples/lib/setupExampleFork.ts @@ -0,0 +1,48 @@ +import { ANVIL_NETWORKS, startFork } from '../../test/anvil/anvil-global-setup'; +import { + createTestClient, + http, + publicActions, + walletActions, + parseEther, + parseAbi, +} from 'viem'; +import { CHAINS, ChainId } from '../../src'; +import { TOKENS } from 'test/lib/utils'; + +/** + * V2: All pool operations happen on Mainnet + * V3: All pool operations happen on Sepolia (pre-launch) + */ + +export const setupExampleFork = async ({ chainId }: { chainId: ChainId }) => { + const { rpcUrl } = await startFork(ANVIL_NETWORKS[ChainId[chainId]]); + + const client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + const userAccount = (await client.getAddresses())[0]; + + // Wrap ETH into WETH for default anvil account #0 + const WETH = TOKENS[chainId].WETH; + const { request } = await client.simulateContract({ + account: userAccount, + address: WETH.address, + abi: parseAbi(['function deposit() payable']), + functionName: 'deposit', + args: [], + value: parseEther('10'), + }); + await client.writeContract(request); + + // TODO: swap wETH for BAL to prepare for add liquidity example + + // TODO: add liquidity to a pool to prepare for remove liquidity example + + return { client, rpcUrl, userAccount }; +}; diff --git a/examples/lib/setupFork.ts b/examples/lib/setupFork.ts deleted file mode 100644 index 2837cf6c..00000000 --- a/examples/lib/setupFork.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { startFork } from '../../test/anvil/anvil-global-setup'; -import { ANVIL_NETWORKS } from '../../test/anvil/anvil-global-setup'; -import { createTestClient, http, publicActions, walletActions } from 'viem'; -import { CHAINS, ChainId } from '../../src'; -import { Address } from 'viem'; - -/** - * Default fork setup for all SDK examples - * 1. use default anvil account to wrap ETH into wETH (allows for swap example) - * 2. swap wETH for BAL (allows for proportional add example) - * 3. adds liquidity to 80BAL-20WETH pool (allows for remove example) - */ - -export const setupFork = async () => { - const chainId = ChainId.SEPOLIA; - const { rpcUrl } = await startFork(ANVIL_NETWORKS[ChainId[chainId]]); - - const client = createTestClient({ - mode: 'anvil', - chain: CHAINS[chainId], - transport: http(rpcUrl), - }) - .extend(publicActions) - .extend(walletActions); - - const userAccount = (await client.getAddresses())[0]; - const balance = await client.getBalance({ address: userAccount }); - - console.log('balance', balance); - - const wethAddress = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' as Address; - - return { client, userAccount }; -}; diff --git a/examples/swaps/queryCustomPath.ts b/examples/swaps/queryCustomPath.ts index f05d0d12..9ed861db 100644 --- a/examples/swaps/queryCustomPath.ts +++ b/examples/swaps/queryCustomPath.ts @@ -1,12 +1,9 @@ -// use custom path to query and return the queryOutput /** - * Example showing how to query swap using paths from the SOR + * Example showing how to query custom path, which explicitly chooses the pool(s) to swap through * * Run with: * pnpm example ./examples/swaps/queryCustomPath.ts */ -import { config } from 'dotenv'; -config(); import { ChainId, @@ -14,39 +11,50 @@ import { Swap, ExactInQueryOutput, ExactOutQueryOutput, + SwapInput, + Path, + TokenApi, } from '../../src'; +import { Address } from 'viem'; -import { Address, parseUnits } from 'viem'; +interface QueryCustomPath { + rpcUrl: string; + chainId: ChainId; + pools: Address[]; + tokenIn: TokenApi; + tokenOut: TokenApi; + swapKind: SwapKind; + protocolVersion: 2 | 3; + inputAmountRaw: bigint; + outputAmountRaw: bigint; +} -const queryCustomPath = async () => { - // User defined - const rpcUrl = process.env.SEPOLIA_RPC_URL; - const chainId = ChainId.SEPOLIA; - const pool = '0xb27aC1DD8192163CFbD6F977C91D31A07E941B87' as Address; // Constant Product Pool from scaffold balancer v3 - const tokenIn = { - address: '0x83f953D2461C6352120E06f5f8EcCD3e4d66d042' as Address, // MockToken1 from scaffold balancer v3 - decimals: 18, - }; - const tokenOut = { - address: '0x9d57eDCe10b7BdDA98997613c33ff7f3e34F4eAd' as Address, - decimals: 18, - }; - const swapKind = SwapKind.GivenIn as SwapKind; - const tokens = [tokenIn, tokenOut]; - const protocolVersion = 3 as const; - const inputAmountRaw = parseUnits('1', 18); - const outputAmountRaw = parseUnits('1', 18); - - const customPaths = [ +export const queryCustomPath = async ({ + rpcUrl, + chainId, + pools, + tokenIn, + tokenOut, + swapKind, + protocolVersion, + inputAmountRaw, + outputAmountRaw, +}: QueryCustomPath): Promise<{ + swap: Swap; + queryOutput: ExactInQueryOutput | ExactOutQueryOutput; +}> => { + // User defines custom paths + const customPaths: Path[] = [ { - pools: [pool], - tokens, + pools, + tokens: [tokenIn, tokenOut], protocolVersion, inputAmountRaw, outputAmountRaw, }, ]; - const swapInput = { chainId, swapKind, paths: customPaths }; + + const swapInput: SwapInput = { chainId, swapKind, paths: customPaths }; // Swap object provides useful helpers for re-querying, building call, etc const swap = new Swap(swapInput); @@ -64,9 +72,7 @@ const queryCustomPath = async () => { } // Get up to date swap result by querying onchain - const queryOutput = (await swap.query(rpcUrl)) as - | ExactInQueryOutput - | ExactOutQueryOutput; + const queryOutput = await swap.query(rpcUrl); // Construct transaction to make swap if (queryOutput.swapKind === SwapKind.GivenIn) { @@ -81,7 +87,5 @@ const queryCustomPath = async () => { }); } - return { swap, chainId, queryOutput }; + return { swap, queryOutput }; }; - -export default queryCustomPath; diff --git a/examples/swaps/querySmartPath.ts b/examples/swaps/querySmartPath.ts index f22e9a5d..146cc6e3 100644 --- a/examples/swaps/querySmartPath.ts +++ b/examples/swaps/querySmartPath.ts @@ -1,11 +1,9 @@ /** - * Example showing how to query swap using paths from the SOR + * Example showing how to query swap using the Smart Order Router (SOR) * * Run with: * pnpm example ./examples/swaps/querySmartPath.ts */ -import { config } from 'dotenv'; -config(); import { BalancerApi, @@ -15,30 +13,30 @@ import { Token, TokenAmount, Swap, + ExactInQueryOutput, + ExactOutQueryOutput, } from '../../src'; -const querySmartPath = async () => { - // User defined - const rpcUrl = process.env.MAINNET_RPC_URL; - const chainId = ChainId.MAINNET; - const swapKind = SwapKind.GivenIn as SwapKind; - const tokenIn = new Token( - chainId, - '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - 6, - 'USDC', - ); - const tokenOut = new Token( - chainId, - '0xba100000625a3754423978a60c9317c58a424e3D', - 18, - 'BAL', - ); - const swapAmount = - swapKind === SwapKind.GivenIn - ? TokenAmount.fromHumanAmount(tokenIn, '100') - : TokenAmount.fromHumanAmount(tokenOut, '100'); +interface QuerySmartPath { + rpcUrl: string; + chainId: ChainId; + swapKind: SwapKind; + tokenIn: Token; + tokenOut: Token; + swapAmount: TokenAmount; +} +export const querySmartPath = async ({ + rpcUrl, + chainId, + swapKind, + tokenIn, + tokenOut, + swapAmount, +}: QuerySmartPath): Promise<{ + swap: Swap; + queryOutput: ExactInQueryOutput | ExactOutQueryOutput; +}> => { // API is used to fetch best path from available liquidity const balancerApi = new BalancerApi(API_ENDPOINT, chainId); @@ -83,7 +81,5 @@ const querySmartPath = async () => { console.log('Expected Amount In:', queryOutput.expectedAmountIn.amount); } - return { swap, chainId, queryOutput }; + return { swap, queryOutput }; }; - -export default querySmartPath; diff --git a/examples/swaps/swap.ts b/examples/swaps/swap.ts deleted file mode 100644 index 28fd2064..00000000 --- a/examples/swaps/swap.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Example showing how to find swap information for a token pair. - * - * Run with: - * pnpm example ./examples/swaps/swap.ts - */ -import { config } from 'dotenv'; -config(); - -import { - BalancerApi, - API_ENDPOINT, - ChainId, - Slippage, - SwapKind, - Token, - TokenAmount, - Swap, - SwapBuildOutputExactIn, - SwapBuildOutputExactOut, - SwapInput, -} from '../../src'; - -const swap = async () => { - // User defined - const rpcUrl = process.env.POLYGON_RPC_URL; - const chainId = ChainId.POLYGON; - const swapKind = SwapKind.GivenIn; - const tokenIn = new Token( - chainId, - '0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6', - 18, - 'MaticX', - ); - const tokenOut = new Token( - chainId, - '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', - 18, - 'WMATIC', - ); - const wethIsEth = false; - const slippage = Slippage.fromPercentage('0.1'); - const swapAmount = - swapKind === SwapKind.GivenIn - ? TokenAmount.fromHumanAmount(tokenIn, '1.2345678910') - : TokenAmount.fromHumanAmount(tokenOut, '1.2345678910'); - const sender = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; - const recipient = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; - const deadline = 999999999999999999n; // Infinity - - // API is used to fetch best path from available liquidity - const balancerApi = new BalancerApi(API_ENDPOINT, chainId); - - const sorPaths = await balancerApi.sorSwapPaths.fetchSorSwapPaths({ - chainId, - tokenIn: tokenIn.address, - tokenOut: tokenOut.address, - swapKind, - swapAmount, - }); - - const swapInput: SwapInput = { - chainId, - paths: sorPaths, - swapKind, - userData: '0x', - }; - - // Swap object provides useful helpers for re-querying, building call, etc - const swap = new Swap(swapInput); - - console.log( - `Input token: ${swap.inputAmount.token.address}, Amount: ${swap.inputAmount.amount}`, - ); - console.log( - `Output token: ${swap.outputAmount.token.address}, Amount: ${swap.outputAmount.amount}`, - ); - - // Get up to date swap result by querying onchain - const queryOutput = await swap.query(rpcUrl); - - // Construct transaction to make swap - if (queryOutput.swapKind === SwapKind.GivenIn) { - console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); - const callData = swap.buildCall({ - slippage, - deadline, - queryOutput, - sender, - recipient, - wethIsEth, - }) as SwapBuildOutputExactIn; - console.log( - `Min Amount Out: ${callData.minAmountOut.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, - ); - } else { - console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); - const callData = swap.buildCall({ - slippage, - deadline, - queryOutput, - sender, - recipient, - wethIsEth, - }) as SwapBuildOutputExactOut; - console.log( - `Max Amount In: ${callData.maxAmountIn.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, - ); - } -}; - -export default swap; diff --git a/examples/swaps/swapV2.ts b/examples/swaps/swapV2.ts index 2ce24816..786a8212 100644 --- a/examples/swaps/swapV2.ts +++ b/examples/swaps/swapV2.ts @@ -1,114 +1,105 @@ /** - * Example showing + * Example showing how to use Smart Order Router (SOR) to query and execute a swap * * Run with: - * pnpm example ./examples/swaps/swap.ts + * pnpm example ./examples/swaps/swapV2.ts */ -import { config } from 'dotenv'; -config(); -import { setupFork } from '../lib/setupFork'; import { - BalancerApi, - API_ENDPOINT, ChainId, Slippage, SwapKind, Token, TokenAmount, - Swap, - SwapBuildOutputExactIn, - SwapBuildOutputExactOut, + SwapBuildCallInput, + VAULT, } from '../../src'; +import { querySmartPath } from './querySmartPath'; +import { setupExampleFork } from '../lib/setupExampleFork'; +import { TOKENS, approveSpenderOnToken } from 'test/lib/utils'; -const swap = async () => { - await setupFork(); +const swapV2 = async () => { + const chainId = ChainId.MAINNET; - // User defined - // const rpcUrl = process.env.POLYGON_RPC_URL; - // const chainId = ChainId.POLYGON; - // const swapKind = SwapKind.GivenIn; - // const tokenIn = new Token( - // chainId, - // '0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6', - // 18, - // 'MaticX', - // ); - // const tokenOut = new Token( - // chainId, - // '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', - // 18, - // 'WMATIC', - // ); - // const wethIsEth = false; - // const slippage = Slippage.fromPercentage('0.1'); - // const swapAmount = - // swapKind === SwapKind.GivenIn - // ? TokenAmount.fromHumanAmount(tokenIn, '1.2345678910') - // : TokenAmount.fromHumanAmount(tokenOut, '1.2345678910'); - // const sender = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; - // const recipient = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; - // const deadline = 999999999999999999n; // Infinity + // Start fork with desired chainId + const { client, rpcUrl, userAccount } = await setupExampleFork({ chainId }); - // // API is used to fetch best path from available liquidity - // const balancerApi = new BalancerApi(API_ENDPOINT, chainId); + // User defines these params for querying swap with SOR + const swapKind = SwapKind.GivenIn; + const tokenIn = new Token( + chainId, + TOKENS[chainId].WETH.address, + TOKENS[chainId].WETH.decimals, + 'WETH', + ); + const tokenOut = new Token( + chainId, + TOKENS[chainId].BAL.address, + TOKENS[chainId].BAL.decimals, + 'BAL', + ); + const swapAmount = + swapKind === SwapKind.GivenIn + ? TokenAmount.fromHumanAmount(tokenIn, '1') + : TokenAmount.fromHumanAmount(tokenOut, '1'); - // const sorPaths = await balancerApi.sorSwapPaths.fetchSorSwapPaths({ - // chainId, - // tokenIn: tokenIn.address, - // tokenOut: tokenOut.address, - // swapKind, - // swapAmount, - // }); + const { swap, queryOutput } = await querySmartPath({ + rpcUrl, + chainId, + swapKind, + tokenIn, + tokenOut, + swapAmount, + }); - // const swapInput = { - // chainId, - // paths: sorPaths, - // swapKind, - // userData: '0x', - // }; + // User defines these params for sending transaction + const sender = userAccount; + const recipient = userAccount; + const slippage = Slippage.fromPercentage('0.1'); + const deadline = 999999999999999999n; // Infinity + const wethIsEth = false; - // // Swap object provides useful helpers for re-querying, building call, etc - // const swap = new Swap(swapInput); + const swapBuildCallInput: SwapBuildCallInput = { + sender, + recipient, + slippage, + deadline, + wethIsEth, + queryOutput, + }; - // console.log( - // `Input token: ${swap.inputAmount.token.address}, Amount: ${swap.inputAmount.amount}`, - // ); - // console.log( - // `Output token: ${swap.outputAmount.token.address}, Amount: ${swap.outputAmount.amount}`, - // ); + // TODO: Figure out why this approval is not working? (related to txReceipt TODO) + await approveSpenderOnToken( + client, + sender, + tokenIn.address, + VAULT[chainId], + ); - // // Get up to date swap result by querying onchain - // const queryOutput = await swap.query(rpcUrl); + // Build call to make swap transaction + const swapCall = swap.buildCall(swapBuildCallInput); - // // Construct transaction to make swap - // if (queryOutput.swapKind === SwapKind.GivenIn) { - // console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); - // const callData = swap.buildCall({ - // slippage, - // deadline, - // queryOutput, - // sender, - // recipient, - // wethIsEth, - // }) as SwapBuildOutputExactIn; - // console.log( - // `Min Amount Out: ${callData.minAmountOut.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, - // ); - // } else { - // console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); - // const callData = swap.buildCall({ - // slippage, - // deadline, - // queryOutput, - // sender, - // recipient, - // wethIsEth, - // }) as SwapBuildOutputExactOut; - // console.log( - // `Max Amount In: ${callData.maxAmountIn.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, - // ); - // } + if ('minAmountOut' in swapCall && 'expectedAmountOut' in queryOutput) { + console.log( + `Min Amount Out: ${swapCall.minAmountOut.amount}\n\nTx Data:\nTo: ${swapCall.to}\nCallData: ${swapCall.callData}\nValue: ${swapCall.value}`, + ); + } else if ('maxAmountIn' in swapCall && 'expectedAmountIn' in queryOutput) { + console.log( + `Max Amount In: ${swapCall.maxAmountIn.amount}\n\nTx Data:\nTo: ${swapCall.to}\nCallData: ${swapCall.callData}\nValue: ${swapCall.value}`, + ); + } + + // Send swap transaction + const hash = await client.sendTransaction({ + account: userAccount, + data: swapCall.callData, + to: swapCall.to, + value: swapCall.value, + }); + + // TODO: parse txReceipt logs for relevent data to display + const txReceipt = await client.getTransactionReceipt({ hash }); + console.log('txReceipt', txReceipt); }; -export default swap; +export default swapV2; diff --git a/examples/swaps/swapV3.ts b/examples/swaps/swapV3.ts index dbb7df09..316b26e3 100644 --- a/examples/swaps/swapV3.ts +++ b/examples/swaps/swapV3.ts @@ -1,99 +1,74 @@ /** - * Example showing how to find swap information for a token pair + * Example showing how to query and execute a v3 swap using a custom path + * Note that all v3 swaps require permit2 approvals * * Run with: * pnpm example ./examples/swaps/swapV3.ts */ -import { config } from 'dotenv'; -config(); import { Slippage, SwapKind, - SwapBuildOutputExactIn, - SwapBuildOutputExactOut, - MaxAllowanceExpiration, - BALANCER_ROUTER, PERMIT2, - permit2Abi, - PermitDetails, TokenAmount, - CHAINS, + ChainId, + Permit2Helper, + SwapBuildCallInput, } from '../../src'; - -import queryCustomPath from './queryCustomPath'; -import { approveSpenderOnToken, signPermit2 } from '../approvals'; - -import { createTestClient, http, publicActions, walletActions } from 'viem'; -import { ANVIL_NETWORKS, startFork } from '../../test/anvil/anvil-global-setup'; +import { TOKENS, approveSpenderOnToken } from 'test/lib/utils'; +import { setupExampleFork } from '../lib/setupExampleFork'; +import { queryCustomPath } from './queryCustomPath'; +import { parseUnits, Address } from 'viem'; const swapV3 = async () => { - const { rpcUrl } = await startFork(ANVIL_NETWORKS.SEPOLIA); - // User defined; - const sender = '0x5036388C540994Ed7b74b82F71175a441F85BdA1'; - const recipient = '0x5036388C540994Ed7b74b82F71175a441F85BdA1'; - const slippage = Slippage.fromPercentage('0.1'); - const deadline = 999999999999999999n; // Infinity - const wethIsEth = false; - - // Get up to date swap result by querying onchain - const { chainId, swap, queryOutput } = await queryCustomPath(); + // Choose chain id to start fork + const chainId = ChainId.SEPOLIA; + const { client, rpcUrl, userAccount } = await setupExampleFork({ chainId }); + + // TODO: Fix query revert, see tenderly simulation: https://www.tdly.co/shared/simulation/f0b0a1de-4e88-4b8d-bf90-c25c4d2145ec + // Query swap results before sending transaction + const { swap, queryOutput } = await queryCustomPath({ + rpcUrl, + chainId, + pools: ['0xA8c18CE5E987d7D82ccAccB93223e9bb5Df4A3c0'] as Address[], // https://test.balancer.fi/pools/sepolia/v3/0xa8c18ce5e987d7d82ccaccb93223e9bb5df4a3c0 + tokenIn: { + address: TOKENS[chainId].WETH.address, + decimals: TOKENS[chainId].WETH.decimals, + }, + tokenOut: { + address: TOKENS[chainId].BAL.address, + decimals: TOKENS[chainId].BAL.decimals, + }, + swapKind: SwapKind.GivenIn, + protocolVersion: 3, + inputAmountRaw: parseUnits('1', TOKENS[chainId].WETH.decimals), + outputAmountRaw: parseUnits('1', TOKENS[chainId].BAL.decimals), + }); + // Amount of tokenIn depends on swapKind let tokenIn: TokenAmount; - if (queryOutput.swapKind === SwapKind.GivenIn) { tokenIn = queryOutput.amountIn; } else { tokenIn = queryOutput.expectedAmountIn; } - const client = createTestClient({ - // account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`), - chain: CHAINS[chainId], - mode: 'anvil', - transport: http(rpcUrl), - }) - .extend(publicActions) - .extend(walletActions); - - // Impersonate sender so we don't need private key - await client.impersonateAccount({ address: sender }); - // Approve Permit2 contract as spender of tokenIn await approveSpenderOnToken( client, - sender, + userAccount, tokenIn.token.address, PERMIT2[chainId], ); - // Get Permit2 nonce for PermitDetails - const [, , nonce] = await client.readContract({ - address: PERMIT2[chainId], - abi: permit2Abi, - functionName: 'allowance', - args: [sender, tokenIn.token.address, BALANCER_ROUTER[chainId]], - }); - - // Set up details for Permit2 signature - const details: PermitDetails[] = [ - { - token: tokenIn.token.address, - amount: tokenIn.amount, - expiration: Number(MaxAllowanceExpiration), - nonce, - }, - ]; - - // Sign Permit2 batch - const signedPermit2Batch = await signPermit2( - client, - sender, - chainId, - details, - ); + // User defines the following params for sending swap transaction + const sender = userAccount; + const recipient = userAccount; + const slippage = Slippage.fromPercentage('0.1'); + const deadline = 999999999999999999n; // Infinity + const wethIsEth = false; - const buildCallInput = { + const swapBuildCallInput: SwapBuildCallInput = { sender, recipient, slippage, @@ -102,23 +77,42 @@ const swapV3 = async () => { queryOutput, }; - // Build call data with Permit2 signature - const callData = swap.buildCallWithPermit2( - buildCallInput, + // Use signature to permit2 approve transfer of tokens to Balancer's cannonical Router + const signedPermit2Batch = await Permit2Helper.signSwapApproval({ + ...swapBuildCallInput, + client, + owner: sender, + }); + + // Build call with Permit2 signature + const swapCall = swap.buildCallWithPermit2( + swapBuildCallInput, signedPermit2Batch, - ) as SwapBuildOutputExactOut | SwapBuildOutputExactIn; + ); - if ('minAmountOut' in callData && 'expectedAmountOut' in queryOutput) { + if ('minAmountOut' in swapCall && 'expectedAmountOut' in queryOutput) { console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); console.log( - `Min Amount Out: ${callData.minAmountOut.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, + `Min Amount Out: ${swapCall.minAmountOut.amount}\n\nTx Data:\nTo: ${swapCall.to}\nCallData: ${swapCall.callData}\nValue: ${swapCall.value}`, ); - } else if ('maxAmountIn' in callData && 'expectedAmountIn' in queryOutput) { + } else if ('maxAmountIn' in swapCall && 'expectedAmountIn' in queryOutput) { console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); console.log( - `Max Amount In: ${callData.maxAmountIn.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`, + `Max Amount In: ${swapCall.maxAmountIn.amount}\n\nTx Data:\nTo: ${swapCall.to}\nCallData: ${swapCall.callData}\nValue: ${swapCall.value}`, ); } + + // Send swap transaction + const hash = await client.sendTransaction({ + account: userAccount, + data: swapCall.callData, + to: swapCall.to, + value: swapCall.value, + }); + + // TODO parse tx receipt logs to display results + const txReceipt = await client.waitForTransactionReceipt({ hash }); + console.log('txReceipt', txReceipt); }; export default swapV3; From 4d8cddcf3722156f80fdfe7fdf84efec0869d8a1 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Sun, 1 Dec 2024 14:31:17 -0800 Subject: [PATCH 7/8] fix comments --- examples/swaps/swapV2.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/swaps/swapV2.ts b/examples/swaps/swapV2.ts index 786a8212..fbb92818 100644 --- a/examples/swaps/swapV2.ts +++ b/examples/swaps/swapV2.ts @@ -19,9 +19,8 @@ import { setupExampleFork } from '../lib/setupExampleFork'; import { TOKENS, approveSpenderOnToken } from 'test/lib/utils'; const swapV2 = async () => { + // Choose chain id to start fork const chainId = ChainId.MAINNET; - - // Start fork with desired chainId const { client, rpcUrl, userAccount } = await setupExampleFork({ chainId }); // User defines these params for querying swap with SOR @@ -68,7 +67,7 @@ const swapV2 = async () => { queryOutput, }; - // TODO: Figure out why this approval is not working? (related to txReceipt TODO) + // Approve V2 Vault contract as spender of tokenIn await approveSpenderOnToken( client, sender, From 53e8f92a1dac10d9d58db96bcd45e371a82a98a5 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Thu, 5 Dec 2024 12:32:55 -0800 Subject: [PATCH 8/8] Improve console logs for swap query and results --- examples/swaps/queryCustomPath.ts | 36 +++++++++------------ examples/swaps/querySmartPath.ts | 30 ++++++++--------- examples/swaps/swapV2.ts | 44 +++++++++++++++++++------ examples/swaps/swapV3.ts | 53 +++++++++++++++++++++---------- 4 files changed, 100 insertions(+), 63 deletions(-) diff --git a/examples/swaps/queryCustomPath.ts b/examples/swaps/queryCustomPath.ts index 9ed861db..180c5f11 100644 --- a/examples/swaps/queryCustomPath.ts +++ b/examples/swaps/queryCustomPath.ts @@ -60,32 +60,26 @@ export const queryCustomPath = async ({ const swap = new Swap(swapInput); if (swapKind === SwapKind.GivenIn) { - console.log('Given tokenIn:', { - address: tokenIn.address, - amount: inputAmountRaw, - }); + console.table([ + { + Type: 'Given Token In', + Address: tokenIn.address, + Amount: inputAmountRaw, + }, + ]); } else { - console.log('Given tokenOut:', { - address: tokenOut.address, - amount: outputAmountRaw, - }); + console.log('Given Token Out:'); + console.table([ + { + Type: 'Given Token Out', + Address: tokenOut.address, + Amount: outputAmountRaw, + }, + ]); } // Get up to date swap result by querying onchain const queryOutput = await swap.query(rpcUrl); - // Construct transaction to make swap - if (queryOutput.swapKind === SwapKind.GivenIn) { - console.log('tokenOut:', { - address: tokenOut.address, - expectedAmount: queryOutput.expectedAmountOut.amount, - }); - } else { - console.log('tokenIn:', { - address: tokenIn.address, - expectedAmount: queryOutput.expectedAmountIn.amount, - }); - } - return { swap, queryOutput }; }; diff --git a/examples/swaps/querySmartPath.ts b/examples/swaps/querySmartPath.ts index 146cc6e3..69ab54dc 100644 --- a/examples/swaps/querySmartPath.ts +++ b/examples/swaps/querySmartPath.ts @@ -57,28 +57,26 @@ export const querySmartPath = async ({ // Swap object provides useful helpers for re-querying, building call, etc const swap = new Swap(swapInput); - console.table({ - Address: { - tokenIn: swap.inputAmount.token.address, - tokenOut: swap.outputAmount.token.address, - }, - Amount: { - tokenIn: swap.inputAmount.amount, - tokenOut: swap.outputAmount.amount, - }, - }); - // Get up to date swap result by querying onchain const queryOutput = await swap.query(rpcUrl); // Construct transaction to make swap if (queryOutput.swapKind === SwapKind.GivenIn) { - console.log( - 'Expected Amount Out:', - queryOutput.expectedAmountOut.amount, - ); + console.table([ + { + Type: 'Given Token In', + Address: swap.inputAmount.token.address, + Amount: swap.inputAmount.amount, + }, + ]); } else { - console.log('Expected Amount In:', queryOutput.expectedAmountIn.amount); + console.table([ + { + Type: 'Expected Amount In', + Address: swap.outputAmount.token.address, + Amount: swap.outputAmount.amount, + }, + ]); } return { swap, queryOutput }; diff --git a/examples/swaps/swapV2.ts b/examples/swaps/swapV2.ts index fbb92818..d899e6ef 100644 --- a/examples/swaps/swapV2.ts +++ b/examples/swaps/swapV2.ts @@ -13,11 +13,12 @@ import { TokenAmount, SwapBuildCallInput, VAULT, + erc20Abi, } from '../../src'; import { querySmartPath } from './querySmartPath'; import { setupExampleFork } from '../lib/setupExampleFork'; import { TOKENS, approveSpenderOnToken } from 'test/lib/utils'; - +import { parseEventLogs } from 'viem'; const swapV2 = async () => { // Choose chain id to start fork const chainId = ChainId.MAINNET; @@ -79,16 +80,26 @@ const swapV2 = async () => { const swapCall = swap.buildCall(swapBuildCallInput); if ('minAmountOut' in swapCall && 'expectedAmountOut' in queryOutput) { - console.log( - `Min Amount Out: ${swapCall.minAmountOut.amount}\n\nTx Data:\nTo: ${swapCall.to}\nCallData: ${swapCall.callData}\nValue: ${swapCall.value}`, - ); + console.table([ + { + Type: 'Query Token Out', + Address: queryOutput.expectedAmountOut.token.address, + Expected: queryOutput.expectedAmountOut.amount, + Minimum: swapCall.minAmountOut.amount, + }, + ]); } else if ('maxAmountIn' in swapCall && 'expectedAmountIn' in queryOutput) { - console.log( - `Max Amount In: ${swapCall.maxAmountIn.amount}\n\nTx Data:\nTo: ${swapCall.to}\nCallData: ${swapCall.callData}\nValue: ${swapCall.value}`, - ); + console.table([ + { + Type: 'Query Token In', + Address: queryOutput.expectedAmountIn.token.address, + Expected: queryOutput.expectedAmountIn.amount, + Maximum: swapCall.maxAmountIn.amount, + }, + ]); } - // Send swap transaction + console.log('Sending swap transaction...'); const hash = await client.sendTransaction({ account: userAccount, data: swapCall.callData, @@ -96,9 +107,22 @@ const swapV2 = async () => { value: swapCall.value, }); - // TODO: parse txReceipt logs for relevent data to display const txReceipt = await client.getTransactionReceipt({ hash }); - console.log('txReceipt', txReceipt); + + const logs = parseEventLogs({ + abi: erc20Abi, + eventName: 'Transfer', + logs: txReceipt.logs, + }); + + console.log('Swap Results:'); + console.table( + logs.map((log, index) => ({ + Type: index === 0 ? 'Token In' : 'Token Out', + Address: log.address, + Amount: log.args.value, + })), + ); }; export default swapV2; diff --git a/examples/swaps/swapV3.ts b/examples/swaps/swapV3.ts index 316b26e3..e0f22f67 100644 --- a/examples/swaps/swapV3.ts +++ b/examples/swaps/swapV3.ts @@ -14,23 +14,23 @@ import { ChainId, Permit2Helper, SwapBuildCallInput, + erc20Abi, } from '../../src'; -import { TOKENS, approveSpenderOnToken } from 'test/lib/utils'; +import { TOKENS, POOLS, approveSpenderOnToken } from 'test/lib/utils'; import { setupExampleFork } from '../lib/setupExampleFork'; import { queryCustomPath } from './queryCustomPath'; -import { parseUnits, Address } from 'viem'; +import { parseUnits, parseEventLogs } from 'viem'; const swapV3 = async () => { // Choose chain id to start fork const chainId = ChainId.SEPOLIA; const { client, rpcUrl, userAccount } = await setupExampleFork({ chainId }); - // TODO: Fix query revert, see tenderly simulation: https://www.tdly.co/shared/simulation/f0b0a1de-4e88-4b8d-bf90-c25c4d2145ec // Query swap results before sending transaction const { swap, queryOutput } = await queryCustomPath({ rpcUrl, chainId, - pools: ['0xA8c18CE5E987d7D82ccAccB93223e9bb5Df4A3c0'] as Address[], // https://test.balancer.fi/pools/sepolia/v3/0xa8c18ce5e987d7d82ccaccb93223e9bb5df4a3c0 + pools: [POOLS[chainId].MOCK_WETH_BAL_POOL.id], tokenIn: { address: TOKENS[chainId].WETH.address, decimals: TOKENS[chainId].WETH.decimals, @@ -41,7 +41,7 @@ const swapV3 = async () => { }, swapKind: SwapKind.GivenIn, protocolVersion: 3, - inputAmountRaw: parseUnits('1', TOKENS[chainId].WETH.decimals), + inputAmountRaw: parseUnits('0.01', TOKENS[chainId].WETH.decimals), outputAmountRaw: parseUnits('1', TOKENS[chainId].BAL.decimals), }); @@ -91,18 +91,26 @@ const swapV3 = async () => { ); if ('minAmountOut' in swapCall && 'expectedAmountOut' in queryOutput) { - console.log(`Updated amount: ${queryOutput.expectedAmountOut.amount}`); - console.log( - `Min Amount Out: ${swapCall.minAmountOut.amount}\n\nTx Data:\nTo: ${swapCall.to}\nCallData: ${swapCall.callData}\nValue: ${swapCall.value}`, - ); + console.table([ + { + Type: 'Query Token Out', + Address: queryOutput.expectedAmountOut.token.address, + Expected: queryOutput.expectedAmountOut.amount, + Minimum: swapCall.minAmountOut.amount, + }, + ]); } else if ('maxAmountIn' in swapCall && 'expectedAmountIn' in queryOutput) { - console.log(`Updated amount: ${queryOutput.expectedAmountIn.amount}`); - console.log( - `Max Amount In: ${swapCall.maxAmountIn.amount}\n\nTx Data:\nTo: ${swapCall.to}\nCallData: ${swapCall.callData}\nValue: ${swapCall.value}`, - ); + console.table([ + { + Type: 'Query Token In', + Address: queryOutput.expectedAmountIn.token.address, + Expected: queryOutput.expectedAmountIn.amount, + Maximum: swapCall.maxAmountIn.amount, + }, + ]); } - // Send swap transaction + console.log('Sending swap transaction...'); const hash = await client.sendTransaction({ account: userAccount, data: swapCall.callData, @@ -110,9 +118,22 @@ const swapV3 = async () => { value: swapCall.value, }); - // TODO parse tx receipt logs to display results const txReceipt = await client.waitForTransactionReceipt({ hash }); - console.log('txReceipt', txReceipt); + + const logs = parseEventLogs({ + abi: erc20Abi, + eventName: 'Transfer', + logs: txReceipt.logs, + }); + + console.log('Swap Results:'); + console.table( + logs.map((log, index) => ({ + Type: index === 0 ? 'Token In' : 'Token Out', + Address: log.address, + Amount: log.args.value, + })), + ); }; export default swapV3;