UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

161 lines 6.86 kB
import { zeroAddress } from 'viem'; import { assert } from '@hyperlane-xyz/utils'; import { encodeExecuteCalldata, encodePermit2PermitInput, encodePermit2TransferFromInput, encodeQuoteExecuteCalldata, encodeSubmitQuoteInput, encodeSweepInput, encodeTransferFromInput, encodeTransferRemoteInput, encodeTransferRemoteToInput, extractQuoteTotals, } from './codec.js'; import { QuotedCallsCommand, TokenPullMode } from './types.js'; /** type(uint256).max — standing approval so subsequent calls skip the SSTORE */ const MAX_UINT256 = 2n ** 256n - 1n; // ============ Shared: build quote command sequence ============ /** Build the command sequence for quoteExecute (no token pull/sweep) */ function buildQuoteCommands(params) { const commands = []; const inputs = []; // Submit quotes for (const cmd of params.quotes) { commands.push(QuotedCallsCommand.SUBMIT_QUOTE); inputs.push(encodeSubmitQuoteInput(cmd, params.clientSalt)); } // Transfer command (for quoting — amount matters, value/approval don't) if (params.targetRouter) { commands.push(QuotedCallsCommand.TRANSFER_REMOTE_TO); inputs.push(encodeTransferRemoteToInput({ router: params.warpRoute, destination: params.destination, recipient: params.recipient, amount: params.amount, targetRouter: params.targetRouter, value: 0n, token: params.token, approval: 0n, })); } else { commands.push(QuotedCallsCommand.TRANSFER_REMOTE); inputs.push(encodeTransferRemoteInput({ warpRoute: params.warpRoute, destination: params.destination, recipient: params.recipient, amount: params.amount, value: 0n, token: params.token, approval: 0n, })); } return { commands, inputs }; } // ============ Step 1: Build quoteExecute calldata ============ /** * Build calldata for QuotedCalls.quoteExecute() — used via eth_call * to discover fee amounts before building the real execute tx. * * Commands: [SUBMIT_QUOTE×N, TRANSFER_REMOTE/TRANSFER_REMOTE_TO] */ export function buildQuoteCalldata(params) { const { commands, inputs } = buildQuoteCommands(params); return { to: params.quotedCallsAddress, data: encodeQuoteExecuteCalldata(commands, inputs), value: 0n, }; } // ============ Step 2: Build execute calldata using quote results ============ /** * Build calldata for QuotedCalls.execute() using fee amounts * from a prior quoteExecute call. * * Commands: [SUBMIT_QUOTE×N, TRANSFER_FROM/PERMIT2, TRANSFER_REMOTE, SWEEP] */ export function buildExecuteCalldata(params) { const commands = []; const inputs = []; const isNativeRoute = params.token === zeroAddress; // The transfer command is the last command in feeQuotes // (index = number of quotes, since quoteExecute had [SUBMIT_QUOTE×N, TRANSFER]) const transferCommandIndex = params.quotes.length; assert(params.feeQuotes.length > transferCommandIndex, `feeQuotes has ${params.feeQuotes.length} entries but transfer command is at index ${transferCommandIndex}`); const transferQuotes = params.feeQuotes[transferCommandIndex]; // Extract per-command native value from the transfer quotes let transferNativeValue = 0n; let transferTokenFee = 0n; for (const q of transferQuotes) { if (q.token.toLowerCase() === zeroAddress) { transferNativeValue += q.amount; } else { transferTokenFee += q.amount; } } // Total ERC20 to pull = sum of all token fees across all commands // sumQuotesByToken normalizes keys to lowercase const { nativeValue: totalNativeValue, tokenTotals } = extractQuoteTotals(params.feeQuotes); const tokenKey = params.token.toLowerCase(); const totalTokenNeeded = isNativeRoute ? 0n : (tokenTotals.get(tokenKey) ?? 0n); // 1. Submit quotes for (const cmd of params.quotes) { commands.push(QuotedCallsCommand.SUBMIT_QUOTE); inputs.push(encodeSubmitQuoteInput(cmd, params.clientSalt)); } // 2. Token pull (skip for native routes) if (!isNativeRoute && totalTokenNeeded > 0n) { if (params.tokenPullMode === TokenPullMode.Permit2) { assert(params.permit2Data != null, 'permit2Data required when tokenPullMode is Permit2'); commands.push(QuotedCallsCommand.PERMIT2_PERMIT); inputs.push(encodePermit2PermitInput(params.permit2Data)); commands.push(QuotedCallsCommand.PERMIT2_TRANSFER_FROM); inputs.push(encodePermit2TransferFromInput(params.token, totalTokenNeeded)); } else { commands.push(QuotedCallsCommand.TRANSFER_FROM); inputs.push(encodeTransferFromInput(params.token, totalTokenNeeded)); } } // 3. Transfer remote — use max approval for standing allowance (skips SSTORE after first call) const approval = isNativeRoute ? 0n : MAX_UINT256; if (params.targetRouter) { commands.push(QuotedCallsCommand.TRANSFER_REMOTE_TO); inputs.push(encodeTransferRemoteToInput({ router: params.warpRoute, destination: params.destination, recipient: params.recipient, amount: params.amount, targetRouter: params.targetRouter, value: transferNativeValue, token: params.token, approval, })); } else { commands.push(QuotedCallsCommand.TRANSFER_REMOTE); inputs.push(encodeTransferRemoteInput({ warpRoute: params.warpRoute, destination: params.destination, recipient: params.recipient, amount: params.amount, value: transferNativeValue, token: params.token, approval, })); } // 4. Sweep leftover tokens + ETH back to caller. // Skip when using TransferFrom (exact pull) and no token fees — nothing to sweep. const hasTokenFees = transferTokenFee > 0n; const needsSweep = isNativeRoute || params.tokenPullMode === TokenPullMode.Permit2 || hasTokenFees; if (needsSweep) { commands.push(QuotedCallsCommand.SWEEP); inputs.push(encodeSweepInput(params.token)); } // msg.value = total native value from quotes. // For native routes, quoteTransferRemote already includes the transfer amount // in the native quotes (Quote({token: address(0), amount: _amount + feeAmount})), // so we don't add params.amount again. const value = totalNativeValue; return { to: params.quotedCallsAddress, data: encodeExecuteCalldata(commands, inputs), value, }; } //# sourceMappingURL=builder.js.map