@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
307 lines • 13.5 kB
JavaScript
import { expect } from 'chai';
import { decodeAbiParameters, decodeFunctionData, encodeAbiParameters, encodePacked, keccak256, zeroAddress, } from 'viem';
import { CONTRACT_BALANCE, computeScopedSalt, decodeQuoteExecuteResult, encodeExecuteCalldata, encodePermit2PermitInput, encodePermit2TransferFromInput, encodeQuoteExecuteCalldata, encodeSubmitQuoteInput, encodeSweepInput, encodeTransferFromInput, encodeTransferRemoteInput, encodeTransferRemoteToInput, extractQuoteTotals, quotedCallsAbi, } from './codec.js';
import { QuotedCallsCommand } from './types.js';
const QUOTER = '0x1111111111111111111111111111111111111111';
const TOKEN = '0x2222222222222222222222222222222222222222';
const WARP_ROUTE = '0x3333333333333333333333333333333333333333';
const RECIPIENT = '0x0000000000000000000000004444444444444444444444444444444444444444';
const CLIENT_SALT = '0x5555555555555555555555555555555555555555555555555555555555555555';
const SIGNATURE = '0xaabbccdd';
const MOCK_QUOTE = {
quoter: QUOTER,
quote: {
context: '0xdeadbeef',
data: '0xcafebabe',
issuedAt: 1000,
expiry: 1000,
salt: CLIENT_SALT,
submitter: '0x6666666666666666666666666666666666666666',
},
signature: SIGNATURE,
};
describe('QuotedCalls codec', () => {
describe('encodeSubmitQuoteInput', () => {
it('round-trips through ABI decode', () => {
const encoded = encodeSubmitQuoteInput(MOCK_QUOTE, CLIENT_SALT);
const [quoter, quote, signature, clientSalt] = decodeAbiParameters([
{ type: 'address' },
{
type: 'tuple',
components: [
{ name: 'context', type: 'bytes' },
{ name: 'data', type: 'bytes' },
{ name: 'issuedAt', type: 'uint48' },
{ name: 'expiry', type: 'uint48' },
{ name: 'salt', type: 'bytes32' },
{ name: 'submitter', type: 'address' },
],
},
{ type: 'bytes' },
{ type: 'bytes32' },
], encoded);
expect(quoter.toLowerCase()).to.equal(QUOTER);
expect(quote.context).to.equal(MOCK_QUOTE.quote.context);
expect(quote.data).to.equal(MOCK_QUOTE.quote.data);
expect(Number(quote.issuedAt)).to.equal(MOCK_QUOTE.quote.issuedAt);
expect(Number(quote.expiry)).to.equal(MOCK_QUOTE.quote.expiry);
expect(quote.salt).to.equal(MOCK_QUOTE.quote.salt);
expect(quote.submitter.toLowerCase()).to.equal(MOCK_QUOTE.quote.submitter);
expect(signature).to.equal(SIGNATURE);
expect(clientSalt).to.equal(CLIENT_SALT);
});
});
describe('encodeTransferFromInput', () => {
it('encodes token and amount', () => {
const encoded = encodeTransferFromInput(TOKEN, 1000n);
const [token, amount] = decodeAbiParameters([{ type: 'address' }, { type: 'uint256' }], encoded);
expect(token.toLowerCase()).to.equal(TOKEN);
expect(amount).to.equal(1000n);
});
});
describe('encodePermit2TransferFromInput', () => {
it('encodes token and uint160 amount', () => {
const encoded = encodePermit2TransferFromInput(TOKEN, 500n);
const [token, amount] = decodeAbiParameters([{ type: 'address' }, { type: 'uint160' }], encoded);
expect(token.toLowerCase()).to.equal(TOKEN);
expect(amount).to.equal(500n);
});
});
describe('encodePermit2PermitInput', () => {
it('encodes PermitSingle and signature', () => {
const permit2Data = {
permitSingle: {
details: {
token: TOKEN,
amount: 1000n,
expiration: 9999,
nonce: 0,
},
spender: WARP_ROUTE,
sigDeadline: 9999,
},
signature: SIGNATURE,
};
const encoded = encodePermit2PermitInput(permit2Data);
// Should encode without error and produce valid hex
expect(encoded).to.match(/^0x/);
expect(encoded.length).to.be.greaterThan(10);
});
});
describe('encodeTransferRemoteInput', () => {
it('encodes all 7 fields', () => {
const encoded = encodeTransferRemoteInput({
warpRoute: WARP_ROUTE,
destination: 42161,
recipient: RECIPIENT,
amount: CONTRACT_BALANCE,
value: CONTRACT_BALANCE,
token: TOKEN,
approval: CONTRACT_BALANCE,
});
const decoded = decodeAbiParameters([
{ type: 'address' },
{ type: 'uint32' },
{ type: 'bytes32' },
{ type: 'uint256' },
{ type: 'uint256' },
{ type: 'address' },
{ type: 'uint256' },
], encoded);
expect(decoded[0].toLowerCase()).to.equal(WARP_ROUTE);
expect(Number(decoded[1])).to.equal(42161);
expect(decoded[2]).to.equal(RECIPIENT);
expect(decoded[3]).to.equal(CONTRACT_BALANCE);
expect(decoded[4]).to.equal(CONTRACT_BALANCE);
expect(decoded[5].toLowerCase()).to.equal(TOKEN);
expect(decoded[6]).to.equal(CONTRACT_BALANCE);
});
});
describe('encodeTransferRemoteToInput', () => {
it('encodes all 8 fields with targetRouter', () => {
const targetRouter = '0x0000000000000000000000007777777777777777777777777777777777777777';
const encoded = encodeTransferRemoteToInput({
router: WARP_ROUTE,
destination: 10,
recipient: RECIPIENT,
amount: 1000n,
targetRouter,
value: 500n,
token: TOKEN,
approval: 1000n,
});
const decoded = decodeAbiParameters([
{ type: 'address' },
{ type: 'uint32' },
{ type: 'bytes32' },
{ type: 'uint256' },
{ type: 'bytes32' },
{ type: 'uint256' },
{ type: 'address' },
{ type: 'uint256' },
], encoded);
expect(Number(decoded[1])).to.equal(10);
expect(decoded[4]).to.equal(targetRouter);
});
});
describe('encodeSweepInput', () => {
it('encodes token address', () => {
const encoded = encodeSweepInput(TOKEN);
const [token] = decodeAbiParameters([{ type: 'address' }], encoded);
expect(token.toLowerCase()).to.equal(TOKEN);
});
it('encodes zero address for ETH-only sweep', () => {
const encoded = encodeSweepInput(zeroAddress);
const [token] = decodeAbiParameters([{ type: 'address' }], encoded);
expect(token).to.equal(zeroAddress);
});
});
describe('encodeExecuteCalldata', () => {
it('encodes commands and inputs into execute calldata', () => {
const commands = [
QuotedCallsCommand.TRANSFER_FROM,
QuotedCallsCommand.SWEEP,
];
const inputs = [
encodeTransferFromInput(TOKEN, 1000n),
encodeSweepInput(TOKEN),
];
const calldata = encodeExecuteCalldata(commands, inputs);
// Decode using the execute ABI
const decoded = decodeFunctionData({
abi: [
{
name: 'execute',
type: 'function',
inputs: [
{ name: 'commands', type: 'bytes' },
{ name: 'inputs', type: 'bytes[]' },
],
outputs: [],
stateMutability: 'payable',
},
],
data: calldata,
});
expect(decoded.functionName).to.equal('execute');
// commands should be 2 bytes: 0x0308 (TRANSFER_FROM=0x03, SWEEP=0x08)
expect(decoded.args[0]).to.equal('0x0308');
expect(decoded.args[1]).to.have.length(2);
});
});
describe('encodeQuoteExecuteCalldata', () => {
it('encodes quoteExecute function selector', () => {
const commands = [QuotedCallsCommand.TRANSFER_REMOTE];
const inputs = [
encodeTransferRemoteInput({
warpRoute: TOKEN,
destination: 42161,
recipient: RECIPIENT,
amount: 1000n,
value: 0n,
token: TOKEN,
approval: 0n,
}),
];
const calldata = encodeQuoteExecuteCalldata(commands, inputs);
const decoded = decodeFunctionData({
abi: quotedCallsAbi,
data: calldata,
});
expect(decoded.functionName).to.equal('quoteExecute');
});
});
describe('decodeQuoteExecuteResult', () => {
it('round-trips Quote[][] through encode/decode', () => {
// Encode a Quote[][] as ABI return data (simulating quoteExecute return)
const input = [
[], // SUBMIT_QUOTE → empty
[
{ token: zeroAddress, amount: 100n },
{ token: TOKEN, amount: 1050n },
{ token: TOKEN, amount: 10n },
],
];
const encoded = encodeAbiParameters([
{
type: 'tuple[][]',
components: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
},
], [
input.map((perCmd) => perCmd.map((q) => ({ token: q.token, amount: q.amount }))),
]);
const decoded = decodeQuoteExecuteResult(encoded);
expect(decoded).to.have.length(2);
expect(decoded[0]).to.have.length(0);
expect(decoded[1]).to.have.length(3);
expect(decoded[1][0].token).to.equal(zeroAddress);
expect(decoded[1][0].amount).to.equal(100n);
expect(decoded[1][1].token.toLowerCase()).to.equal(TOKEN);
expect(decoded[1][1].amount).to.equal(1050n);
expect(decoded[1][2].amount).to.equal(10n);
});
it('decodes empty results', () => {
const encoded = encodeAbiParameters([
{
type: 'tuple[][]',
components: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
},
], [[]]);
const decoded = decodeQuoteExecuteResult(encoded);
expect(decoded).to.have.length(0);
});
});
describe('extractQuoteTotals', () => {
it('sums native and token fees separately', () => {
const results = [
[],
[
{ token: zeroAddress, amount: 100n },
{ token: TOKEN, amount: 1050n },
{ token: TOKEN, amount: 10n },
],
];
const { nativeValue, tokenTotals } = extractQuoteTotals(results);
expect(nativeValue).to.equal(100n);
expect(tokenTotals.get(TOKEN)).to.equal(1060n);
expect(tokenTotals.has(zeroAddress)).to.be.false;
});
it('returns zero for empty quotes', () => {
const { nativeValue, tokenTotals } = extractQuoteTotals([[], []]);
expect(nativeValue).to.equal(0n);
expect(tokenTotals.size).to.equal(0);
});
});
describe('CONTRACT_BALANCE', () => {
it('equals 2^255', () => {
expect(CONTRACT_BALANCE).to.equal(2n ** 255n);
});
});
describe('computeScopedSalt', () => {
it('matches QuotedCalls._scopeSalt: keccak256(abi.encodePacked(address, bytes32))', () => {
const caller = QUOTER;
const salt = computeScopedSalt(caller, CLIENT_SALT);
// encodePacked(address, bytes32) = 20 + 32 = 52 bytes
const expected = keccak256(encodePacked(['address', 'bytes32'], [caller, CLIENT_SALT]));
expect(salt).to.equal(expected);
});
it('produces different salts for different callers', () => {
const salt1 = computeScopedSalt(QUOTER, CLIENT_SALT);
const salt2 = computeScopedSalt(TOKEN, CLIENT_SALT);
expect(salt1).to.not.equal(salt2);
});
it('produces different salts for different clientSalts', () => {
const otherSalt = '0x7777777777777777777777777777777777777777777777777777777777777777';
const salt1 = computeScopedSalt(QUOTER, CLIENT_SALT);
const salt2 = computeScopedSalt(QUOTER, otherSalt);
expect(salt1).to.not.equal(salt2);
});
});
});
//# sourceMappingURL=codec.test.js.map