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 new file mode 100644 index 00000000..f52cb84e --- /dev/null +++ b/examples/approvals/index.ts @@ -0,0 +1,2 @@ +export * from './signPermit2'; +export * from './approveSpenderOnToken'; diff --git a/examples/approvals/signPermit2.ts b/examples/approvals/signPermit2.ts new file mode 100644 index 00000000..7b873bc8 --- /dev/null +++ b/examples/approvals/signPermit2.ts @@ -0,0 +1,44 @@ +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/swaps/queryCustomPath.ts b/examples/swaps/queryCustomPath.ts new file mode 100644 index 00000000..f05d0d12 --- /dev/null +++ b/examples/swaps/queryCustomPath.ts @@ -0,0 +1,87 @@ +// 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 as SwapKind; + 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); + + 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 + | ExactOutQueryOutput; + + // 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, chainId, queryOutput }; +}; + +export default queryCustomPath; diff --git a/examples/swaps/querySmartPath.ts b/examples/swaps/querySmartPath.ts new file mode 100644 index 00000000..f22e9a5d --- /dev/null +++ b/examples/swaps/querySmartPath.ts @@ -0,0 +1,89 @@ +/** + * 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.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'); + + // 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.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, + ); + } else { + console.log('Expected Amount In:', queryOutput.expectedAmountIn.amount); + } + + return { swap, chainId, queryOutput }; +}; + +export default querySmartPath; diff --git a/examples/swaps/swap.ts b/examples/swaps/swap.ts deleted file mode 100644 index e8638392..00000000 --- a/examples/swaps/swap.ts +++ /dev/null @@ -1,110 +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, - }; - - // 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/swapV3.ts b/examples/swaps/swapV3.ts new file mode 100644 index 00000000..dbb7df09 --- /dev/null +++ b/examples/swaps/swapV3.ts @@ -0,0 +1,124 @@ +/** + * 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 { + Slippage, + SwapKind, + SwapBuildOutputExactIn, + SwapBuildOutputExactOut, + MaxAllowanceExpiration, + BALANCER_ROUTER, + PERMIT2, + permit2Abi, + PermitDetails, + TokenAmount, + CHAINS, +} 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'; + +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(); + + 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, + 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, + ); + + const buildCallInput = { + sender, + recipient, + slippage, + deadline, + wethIsEth, + queryOutput, + }; + + // Build call data with Permit2 signature + const callData = swap.buildCallWithPermit2( + buildCallInput, + signedPermit2Batch, + ) as SwapBuildOutputExactOut | SwapBuildOutputExactIn; + + 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 swapV3;