@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
161 lines • 6.86 kB
JavaScript
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