@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
193 lines • 7.12 kB
JavaScript
import { expect } from 'chai';
import { decodeFunctionData, zeroAddress } from 'viem';
import { buildExecuteCalldata, buildQuoteCalldata } from './builder.js';
import { QuotedCallsCommand, TokenPullMode } from './types.js';
const QUOTED_CALLS = '0x1111111111111111111111111111111111111111';
const WARP_ROUTE = '0x2222222222222222222222222222222222222222';
const TOKEN = '0x3333333333333333333333333333333333333333';
const RECIPIENT = '0x0000000000000000000000004444444444444444444444444444444444444444';
const CLIENT_SALT = '0x5555555555555555555555555555555555555555555555555555555555555555';
const MOCK_QUOTE = {
quoter: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
quote: {
context: '0xdeadbeef',
data: '0xcafebabe',
issuedAt: 1000,
expiry: 1000,
salt: CLIENT_SALT,
submitter: QUOTED_CALLS,
},
signature: '0xaabb',
};
const BASE_PARAMS = {
quotedCallsAddress: QUOTED_CALLS,
warpRoute: WARP_ROUTE,
destination: 42161,
recipient: RECIPIENT,
amount: 1000n,
token: TOKEN,
quotes: [MOCK_QUOTE],
clientSalt: CLIENT_SALT,
};
const executeAbi = [
{
name: 'execute',
type: 'function',
inputs: [
{ name: 'commands', type: 'bytes' },
{ name: 'inputs', type: 'bytes[]' },
],
outputs: [],
stateMutability: 'payable',
},
{
name: 'quoteExecute',
type: 'function',
inputs: [
{ name: 'commands', type: 'bytes' },
{ name: 'inputs', type: 'bytes[]' },
],
outputs: [],
stateMutability: 'nonpayable',
},
];
function decodeCommands(data) {
const decoded = decodeFunctionData({ abi: executeAbi, data });
const commandsHex = decoded.args[0].slice(2);
const commands = [];
for (let i = 0; i < commandsHex.length; i += 2) {
commands.push(parseInt(commandsHex.slice(i, i + 2), 16));
}
return { fn: decoded.functionName, commands };
}
// Mock fee quotes from quoteExecute
const MOCK_FEE_QUOTES = [
[], // SUBMIT_QUOTE → empty
[
// TRANSFER_REMOTE → 3 quotes
{ token: zeroAddress, amount: 100n }, // IGP native fee
{ token: TOKEN, amount: 1050n }, // amount + internal fee
{ token: TOKEN, amount: 10n }, // external fee
],
];
describe('buildQuoteCalldata', () => {
it('builds quoteExecute with SUBMIT_QUOTE + TRANSFER_REMOTE', () => {
const result = buildQuoteCalldata(BASE_PARAMS);
expect(result.to).to.equal(QUOTED_CALLS);
expect(result.value).to.equal(0n);
const { fn, commands } = decodeCommands(result.data);
expect(fn).to.equal('quoteExecute');
expect(commands).to.deep.equal([
QuotedCallsCommand.SUBMIT_QUOTE,
QuotedCallsCommand.TRANSFER_REMOTE,
]);
});
it('builds quoteExecute with TRANSFER_REMOTE_TO for cross-collateral', () => {
const targetRouter = '0x0000000000000000000000007777777777777777777777777777777777777777';
const result = buildQuoteCalldata({ ...BASE_PARAMS, targetRouter });
const { commands } = decodeCommands(result.data);
expect(commands).to.deep.equal([
QuotedCallsCommand.SUBMIT_QUOTE,
QuotedCallsCommand.TRANSFER_REMOTE_TO,
]);
});
});
describe('buildExecuteCalldata', () => {
it('builds ERC20 TransferFrom execute from fee quotes', () => {
const result = buildExecuteCalldata({
...BASE_PARAMS,
feeQuotes: MOCK_FEE_QUOTES,
tokenPullMode: TokenPullMode.TransferFrom,
});
expect(result.to).to.equal(QUOTED_CALLS);
// native value = IGP fee (100)
expect(result.value).to.equal(100n);
const { fn, commands } = decodeCommands(result.data);
expect(fn).to.equal('execute');
expect(commands).to.deep.equal([
QuotedCallsCommand.SUBMIT_QUOTE,
QuotedCallsCommand.TRANSFER_FROM,
QuotedCallsCommand.TRANSFER_REMOTE,
QuotedCallsCommand.SWEEP,
]);
});
it('builds Permit2 execute from fee quotes', () => {
const result = buildExecuteCalldata({
...BASE_PARAMS,
feeQuotes: MOCK_FEE_QUOTES,
tokenPullMode: TokenPullMode.Permit2,
permit2Data: {
permitSingle: {
details: {
token: TOKEN,
amount: 2000n,
expiration: 9999,
nonce: 0,
},
spender: QUOTED_CALLS,
sigDeadline: 9999,
},
signature: '0xaabb',
},
});
const { commands } = decodeCommands(result.data);
expect(commands).to.deep.equal([
QuotedCallsCommand.SUBMIT_QUOTE,
QuotedCallsCommand.PERMIT2_PERMIT,
QuotedCallsCommand.PERMIT2_TRANSFER_FROM,
QuotedCallsCommand.TRANSFER_REMOTE,
QuotedCallsCommand.SWEEP,
]);
});
it('builds native route execute (no TRANSFER_FROM)', () => {
const nativeFeeQuotes = [
[],
[{ token: zeroAddress, amount: 5100n }],
];
const result = buildExecuteCalldata({
...BASE_PARAMS,
token: zeroAddress,
amount: 5000n,
feeQuotes: nativeFeeQuotes,
tokenPullMode: TokenPullMode.TransferFrom,
});
// msg.value = native total from quotes (5100 already includes transfer amount)
expect(result.value).to.equal(5100n);
const { commands } = decodeCommands(result.data);
// No TRANSFER_FROM for native route
expect(commands).to.deep.equal([
QuotedCallsCommand.SUBMIT_QUOTE,
QuotedCallsCommand.TRANSFER_REMOTE,
QuotedCallsCommand.SWEEP,
]);
});
it('skips SWEEP for TransferFrom with zero token fees', () => {
const zeroTokenFeeQuotes = [
[], // SUBMIT_QUOTE → empty
[
// TRANSFER_REMOTE → native fee only, no token fee
{ token: zeroAddress, amount: 100n },
],
];
const result = buildExecuteCalldata({
...BASE_PARAMS,
feeQuotes: zeroTokenFeeQuotes,
tokenPullMode: TokenPullMode.TransferFrom,
});
const { commands } = decodeCommands(result.data);
// No TRANSFER_FROM (totalTokenNeeded=0) and no SWEEP (TransferFrom + zero token fees)
expect(commands).to.deep.equal([
QuotedCallsCommand.SUBMIT_QUOTE,
QuotedCallsCommand.TRANSFER_REMOTE,
]);
expect(result.value).to.equal(100n);
});
it('throws when Permit2 mode without permit2Data', () => {
expect(() => buildExecuteCalldata({
...BASE_PARAMS,
feeQuotes: MOCK_FEE_QUOTES,
tokenPullMode: TokenPullMode.Permit2,
})).to.throw('permit2Data required');
});
});
//# sourceMappingURL=builder.test.js.map