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/swaps/queryCustomPath.ts b/examples/swaps/queryCustomPath.ts new file mode 100644 index 00000000..180c5f11 --- /dev/null +++ b/examples/swaps/queryCustomPath.ts @@ -0,0 +1,85 @@ +/** + * 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 { + ChainId, + SwapKind, + Swap, + ExactInQueryOutput, + ExactOutQueryOutput, + SwapInput, + Path, + TokenApi, +} from '../../src'; +import { Address } from 'viem'; + +interface QueryCustomPath { + rpcUrl: string; + chainId: ChainId; + pools: Address[]; + tokenIn: TokenApi; + tokenOut: TokenApi; + swapKind: SwapKind; + protocolVersion: 2 | 3; + inputAmountRaw: bigint; + outputAmountRaw: bigint; +} + +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, + tokens: [tokenIn, tokenOut], + protocolVersion, + inputAmountRaw, + outputAmountRaw, + }, + ]; + + const swapInput: SwapInput = { chainId, swapKind, paths: customPaths }; + + // Swap object provides useful helpers for re-querying, building call, etc + const swap = new Swap(swapInput); + + if (swapKind === SwapKind.GivenIn) { + console.table([ + { + Type: 'Given Token In', + Address: tokenIn.address, + Amount: inputAmountRaw, + }, + ]); + } else { + 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); + + return { swap, queryOutput }; +}; diff --git a/examples/swaps/querySmartPath.ts b/examples/swaps/querySmartPath.ts new file mode 100644 index 00000000..69ab54dc --- /dev/null +++ b/examples/swaps/querySmartPath.ts @@ -0,0 +1,83 @@ +/** + * Example showing how to query swap using the Smart Order Router (SOR) + * + * Run with: + * pnpm example ./examples/swaps/querySmartPath.ts + */ + +import { + BalancerApi, + API_ENDPOINT, + ChainId, + SwapKind, + Token, + TokenAmount, + Swap, + ExactInQueryOutput, + ExactOutQueryOutput, +} from '../../src'; + +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); + + 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); + + // 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.table([ + { + Type: 'Given Token In', + Address: swap.inputAmount.token.address, + Amount: swap.inputAmount.amount, + }, + ]); + } else { + console.table([ + { + Type: 'Expected Amount In', + Address: swap.outputAmount.token.address, + Amount: swap.outputAmount.amount, + }, + ]); + } + + return { swap, queryOutput }; +}; 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 new file mode 100644 index 00000000..d899e6ef --- /dev/null +++ b/examples/swaps/swapV2.ts @@ -0,0 +1,128 @@ +/** + * Example showing how to use Smart Order Router (SOR) to query and execute a swap + * + * Run with: + * pnpm example ./examples/swaps/swapV2.ts + */ + +import { + ChainId, + Slippage, + SwapKind, + Token, + 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; + const { client, rpcUrl, userAccount } = await setupExampleFork({ 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 { swap, queryOutput } = await querySmartPath({ + rpcUrl, + chainId, + swapKind, + tokenIn, + tokenOut, + swapAmount, + }); + + // 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; + + const swapBuildCallInput: SwapBuildCallInput = { + sender, + recipient, + slippage, + deadline, + wethIsEth, + queryOutput, + }; + + // Approve V2 Vault contract as spender of tokenIn + await approveSpenderOnToken( + client, + sender, + tokenIn.address, + VAULT[chainId], + ); + + // Build call to make swap transaction + const swapCall = swap.buildCall(swapBuildCallInput); + + if ('minAmountOut' in swapCall && 'expectedAmountOut' in queryOutput) { + 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.table([ + { + Type: 'Query Token In', + Address: queryOutput.expectedAmountIn.token.address, + Expected: queryOutput.expectedAmountIn.amount, + Maximum: swapCall.maxAmountIn.amount, + }, + ]); + } + + console.log('Sending swap transaction...'); + const hash = await client.sendTransaction({ + account: userAccount, + data: swapCall.callData, + to: swapCall.to, + value: swapCall.value, + }); + + const txReceipt = await client.getTransactionReceipt({ hash }); + + 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 new file mode 100644 index 00000000..e0f22f67 --- /dev/null +++ b/examples/swaps/swapV3.ts @@ -0,0 +1,139 @@ +/** + * 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 { + Slippage, + SwapKind, + PERMIT2, + TokenAmount, + ChainId, + Permit2Helper, + SwapBuildCallInput, + erc20Abi, +} from '../../src'; +import { TOKENS, POOLS, approveSpenderOnToken } from 'test/lib/utils'; +import { setupExampleFork } from '../lib/setupExampleFork'; +import { queryCustomPath } from './queryCustomPath'; +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 }); + + // Query swap results before sending transaction + const { swap, queryOutput } = await queryCustomPath({ + rpcUrl, + chainId, + pools: [POOLS[chainId].MOCK_WETH_BAL_POOL.id], + 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('0.01', 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; + } + + // Approve Permit2 contract as spender of tokenIn + await approveSpenderOnToken( + client, + userAccount, + tokenIn.token.address, + PERMIT2[chainId], + ); + + // 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 swapBuildCallInput: SwapBuildCallInput = { + sender, + recipient, + slippage, + deadline, + wethIsEth, + queryOutput, + }; + + // 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, + ); + + if ('minAmountOut' in swapCall && 'expectedAmountOut' in queryOutput) { + 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.table([ + { + Type: 'Query Token In', + Address: queryOutput.expectedAmountIn.token.address, + Expected: queryOutput.expectedAmountIn.amount, + Maximum: swapCall.maxAmountIn.amount, + }, + ]); + } + + console.log('Sending swap transaction...'); + const hash = await client.sendTransaction({ + account: userAccount, + data: swapCall.callData, + to: swapCall.to, + value: swapCall.value, + }); + + const txReceipt = await client.waitForTransactionReceipt({ hash }); + + 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;