From d7bb432c54b81e6dd63b570b8826c5e9a9cf6cd4 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Mon, 30 Mar 2026 17:51:00 -0500 Subject: [PATCH 01/10] add support for TON to Solana --- ccip-sdk/src/ton/send.ts | 93 +++++++++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/ccip-sdk/src/ton/send.ts b/ccip-sdk/src/ton/send.ts index 9da0544a..82172b67 100644 --- a/ccip-sdk/src/ton/send.ts +++ b/ccip-sdk/src/ton/send.ts @@ -4,9 +4,9 @@ import { zeroPadValue } from 'ethers' import type { UnsignedTONTx } from './types.ts' import { CCIPError, CCIPErrorCode, CCIPExtraArgsInvalidError } from '../errors/index.ts' -import { type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts' +import { type ExtraArgs, type SVMExtraArgsV1, EVMExtraArgsV2Tag, SVMExtraArgsV1Tag } from '../extra-args.ts' import { type AnyMessage, type WithLogger, ChainFamily } from '../types.ts' -import { bigIntReplacer, bytesToBuffer, getDataBytes } from '../utils.ts' +import { bigIntReplacer, bytesToBuffer, getAddressBytes, getDataBytes } from '../utils.ts' /** Opcode for Router ccipSend operation */ export const CCIP_SEND_OPCODE = 0x31768d95 @@ -45,6 +45,31 @@ function encodeTokenAmounts( return builder.endCell() } +/** + * Checks if extraArgs is SVMExtraArgsV1 format. + */ +function isSVMExtraArgs(extraArgs: ExtraArgs): extraArgs is SVMExtraArgsV1 { + return 'computeUnits' in extraArgs +} + +/** + * Encodes extraArgs as a Cell. + * + * Supports two formats based on the destination chain: + * - GenericExtraArgsV2 (EVMExtraArgsV2) for EVM/TON/Aptos destinations + * - SVMExtraArgsV1 for Solana destinations + * + * @param extraArgs - Extra arguments for CCIP message + * @returns Cell encoding the extra arguments + * @throws {@link CCIPExtraArgsInvalidError} if extraArgs format is invalid + */ +export function encodeExtraArgsCell(extraArgs: ExtraArgs): Cell { + if (isSVMExtraArgs(extraArgs)) { + return encodeSVMExtraArgsCell(extraArgs) + } + return encodeEVMExtraArgsCell(extraArgs) +} + /** * Encodes extraArgs as a Cell using the GenericExtraArgsV2 (EVMExtraArgsV2) format. * @@ -52,11 +77,8 @@ function encodeTokenAmounts( * - tag: 32-bit opcode (0x181dcf10) * - gasLimit: Maybe (1 bit flag + 256 bits if present) * - allowOutOfOrderExecution: 1 bit - * @param extraArgs - Extra arguments for CCIP message - * @returns Cell encoding the extra arguments - * @throws {@link CCIPExtraArgsInvalidError} if `extraArgs` contains fields other than `gasLimit` and `allowOutOfOrderExecution` */ -export function encodeExtraArgsCell(extraArgs: ExtraArgs): Cell { +function encodeEVMExtraArgsCell(extraArgs: ExtraArgs): Cell { if ( Object.keys(extraArgs).filter((k) => k !== '_tag').length !== 2 || !('gasLimit' in extraArgs && 'allowOutOfOrderExecution' in extraArgs) @@ -75,6 +97,51 @@ export function encodeExtraArgsCell(extraArgs: ExtraArgs): Cell { return builder.endCell() } +/** + * Encodes extraArgs as a Cell using the SVMExtraArgsV1 format. + * + * Format per chainlink-ton TL-B: + * - tag: 32-bit opcode (0x1f3b3aba) + * - computeUnits: uint32 + * - accountIsWritableBitmap: uint64 + * - allowOutOfOrderExecution: bool + * - tokenReceiver: uint256 + * - accounts: SnakedCell + */ +function addressToUint256(addr: string): bigint { + const bytes = getAddressBytes(addr) + // zeroPadValue returns a hex string, use it directly + const hex = bytes.length <= 32 ? zeroPadValue(bytes, 32) : '0x' + Buffer.from(bytes).toString('hex') + return BigInt(hex) +} + +function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { + // Encode accounts as a snaked cell of uint256 values + let accountsCell = beginCell().endCell() + if (extraArgs.accounts && extraArgs.accounts.length > 0) { + const accountBuilder = beginCell() + for (const account of extraArgs.accounts) { + accountBuilder.storeUint(addressToUint256(account), 256) + } + accountsCell = accountBuilder.endCell() + } + + // Encode tokenReceiver as uint256 + const tokenReceiver = extraArgs.tokenReceiver + ? addressToUint256(extraArgs.tokenReceiver) + : 0n + + const builder = beginCell() + .storeUint(Number(SVMExtraArgsV1Tag), 32) // 0x1f3b3aba + .storeUint(Number(extraArgs.computeUnits), 32) + .storeUint(Number(extraArgs.accountIsWritableBitmap ?? 0n), 64) + .storeBit(extraArgs.allowOutOfOrderExecution) + .storeUint(tokenReceiver, 256) // uint256 + .storeRef(accountsCell) // SnakedCell + + return builder.endCell() +} + /** * Builds the Router ccipSend message cell. * @@ -92,8 +159,11 @@ export function buildCcipSendCell( feeTokenAddress: Address | null = null, queryId = 0n, ): Cell { - // Get receiver bytes and pad to 32 bytes for cross-chain encoding - const paddedReceiver = bytesToBuffer(zeroPadValue(getDataBytes(message.receiver), 32)) + // Get receiver bytes — use getAddressBytes to handle hex, base58 (Solana), TON raw formats + const receiverBytes = getAddressBytes(message.receiver) + const paddedReceiver = bytesToBuffer( + receiverBytes.length <= 32 ? zeroPadValue(receiverBytes, 32) : receiverBytes, + ) // Data cell (ref 0) const dataCell = beginCell() @@ -162,8 +232,11 @@ export async function getFee( } // Build stack parameters for validatedFee call - const paddedReceiver = bytesToBuffer(zeroPadValue(getDataBytes(message.receiver), 32)) - const receiverSlice = beginCell().storeBuffer(paddedReceiver).endCell() + const feeReceiverBytes = getAddressBytes(message.receiver) + const paddedFeeReceiver = bytesToBuffer( + feeReceiverBytes.length <= 32 ? zeroPadValue(feeReceiverBytes, 32) : feeReceiverBytes, + ) + const receiverSlice = beginCell().storeBuffer(paddedFeeReceiver).endCell() const dataCell = beginCell() .storeBuffer(bytesToBuffer(message.data || '0x')) .endCell() From 7fceedd3d54aeff36f7f028e78c086df45b559b7 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Mon, 30 Mar 2026 18:21:30 -0500 Subject: [PATCH 02/10] fix lint --- ccip-sdk/src/ton/send.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ccip-sdk/src/ton/send.ts b/ccip-sdk/src/ton/send.ts index 82172b67..06ba8c93 100644 --- a/ccip-sdk/src/ton/send.ts +++ b/ccip-sdk/src/ton/send.ts @@ -4,9 +4,14 @@ import { zeroPadValue } from 'ethers' import type { UnsignedTONTx } from './types.ts' import { CCIPError, CCIPErrorCode, CCIPExtraArgsInvalidError } from '../errors/index.ts' -import { type ExtraArgs, type SVMExtraArgsV1, EVMExtraArgsV2Tag, SVMExtraArgsV1Tag } from '../extra-args.ts' +import { + type ExtraArgs, + type SVMExtraArgsV1, + EVMExtraArgsV2Tag, + SVMExtraArgsV1Tag, +} from '../extra-args.ts' import { type AnyMessage, type WithLogger, ChainFamily } from '../types.ts' -import { bigIntReplacer, bytesToBuffer, getAddressBytes, getDataBytes } from '../utils.ts' +import { bigIntReplacer, bytesToBuffer, getAddressBytes } from '../utils.ts' /** Opcode for Router ccipSend operation */ export const CCIP_SEND_OPCODE = 0x31768d95 @@ -111,14 +116,15 @@ function encodeEVMExtraArgsCell(extraArgs: ExtraArgs): Cell { function addressToUint256(addr: string): bigint { const bytes = getAddressBytes(addr) // zeroPadValue returns a hex string, use it directly - const hex = bytes.length <= 32 ? zeroPadValue(bytes, 32) : '0x' + Buffer.from(bytes).toString('hex') + const hex = + bytes.length <= 32 ? zeroPadValue(bytes, 32) : '0x' + Buffer.from(bytes).toString('hex') return BigInt(hex) } function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { // Encode accounts as a snaked cell of uint256 values let accountsCell = beginCell().endCell() - if (extraArgs.accounts && extraArgs.accounts.length > 0) { + if (extraArgs.accounts.length > 0) { const accountBuilder = beginCell() for (const account of extraArgs.accounts) { accountBuilder.storeUint(addressToUint256(account), 256) @@ -127,14 +133,12 @@ function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { } // Encode tokenReceiver as uint256 - const tokenReceiver = extraArgs.tokenReceiver - ? addressToUint256(extraArgs.tokenReceiver) - : 0n + const tokenReceiver = extraArgs.tokenReceiver ? addressToUint256(extraArgs.tokenReceiver) : 0n const builder = beginCell() .storeUint(Number(SVMExtraArgsV1Tag), 32) // 0x1f3b3aba .storeUint(Number(extraArgs.computeUnits), 32) - .storeUint(Number(extraArgs.accountIsWritableBitmap ?? 0n), 64) + .storeUint(Number(extraArgs.accountIsWritableBitmap), 64) .storeBit(extraArgs.allowOutOfOrderExecution) .storeUint(tokenReceiver, 256) // uint256 .storeRef(accountsCell) // SnakedCell From eb45a9a59fb659e12a8efd89db6e6d681a2908f9 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Tue, 31 Mar 2026 14:22:59 -0500 Subject: [PATCH 03/10] address comments and support SUI --- ccip-sdk/src/ton/send.ts | 68 +++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/ccip-sdk/src/ton/send.ts b/ccip-sdk/src/ton/send.ts index 06ba8c93..20444b61 100644 --- a/ccip-sdk/src/ton/send.ts +++ b/ccip-sdk/src/ton/send.ts @@ -1,14 +1,16 @@ import { type Cell, beginCell, toNano } from '@ton/core' import { type TonClient, Address } from '@ton/ton' -import { zeroPadValue } from 'ethers' +import { toBigInt, zeroPadValue } from 'ethers' import type { UnsignedTONTx } from './types.ts' import { CCIPError, CCIPErrorCode, CCIPExtraArgsInvalidError } from '../errors/index.ts' import { type ExtraArgs, type SVMExtraArgsV1, + type SuiExtraArgsV1, EVMExtraArgsV2Tag, SVMExtraArgsV1Tag, + SuiExtraArgsV1Tag, } from '../extra-args.ts' import { type AnyMessage, type WithLogger, ChainFamily } from '../types.ts' import { bigIntReplacer, bytesToBuffer, getAddressBytes } from '../utils.ts' @@ -57,12 +59,20 @@ function isSVMExtraArgs(extraArgs: ExtraArgs): extraArgs is SVMExtraArgsV1 { return 'computeUnits' in extraArgs } +/** + * Checks if extraArgs is SuiExtraArgsV1 format. + */ +function isSuiExtraArgs(extraArgs: ExtraArgs): extraArgs is SuiExtraArgsV1 { + return 'receiverObjectIds' in extraArgs +} + /** * Encodes extraArgs as a Cell. * - * Supports two formats based on the destination chain: + * Supports three formats based on the destination chain: * - GenericExtraArgsV2 (EVMExtraArgsV2) for EVM/TON/Aptos destinations * - SVMExtraArgsV1 for Solana destinations + * - SuiExtraArgsV1 for Sui destinations * * @param extraArgs - Extra arguments for CCIP message * @returns Cell encoding the extra arguments @@ -72,6 +82,9 @@ export function encodeExtraArgsCell(extraArgs: ExtraArgs): Cell { if (isSVMExtraArgs(extraArgs)) { return encodeSVMExtraArgsCell(extraArgs) } + if (isSuiExtraArgs(extraArgs)) { + return encodeSuiExtraArgsCell(extraArgs) + } return encodeEVMExtraArgsCell(extraArgs) } @@ -113,13 +126,6 @@ function encodeEVMExtraArgsCell(extraArgs: ExtraArgs): Cell { * - tokenReceiver: uint256 * - accounts: SnakedCell */ -function addressToUint256(addr: string): bigint { - const bytes = getAddressBytes(addr) - // zeroPadValue returns a hex string, use it directly - const hex = - bytes.length <= 32 ? zeroPadValue(bytes, 32) : '0x' + Buffer.from(bytes).toString('hex') - return BigInt(hex) -} function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { // Encode accounts as a snaked cell of uint256 values @@ -127,18 +133,20 @@ function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { if (extraArgs.accounts.length > 0) { const accountBuilder = beginCell() for (const account of extraArgs.accounts) { - accountBuilder.storeUint(addressToUint256(account), 256) + accountBuilder.storeUint(toBigInt(getAddressBytes(account)), 256) } accountsCell = accountBuilder.endCell() } // Encode tokenReceiver as uint256 - const tokenReceiver = extraArgs.tokenReceiver ? addressToUint256(extraArgs.tokenReceiver) : 0n + const tokenReceiver = extraArgs.tokenReceiver + ? toBigInt(getAddressBytes(extraArgs.tokenReceiver)) + : 0n const builder = beginCell() .storeUint(Number(SVMExtraArgsV1Tag), 32) // 0x1f3b3aba .storeUint(Number(extraArgs.computeUnits), 32) - .storeUint(Number(extraArgs.accountIsWritableBitmap), 64) + .storeUint(extraArgs.accountIsWritableBitmap, 64) .storeBit(extraArgs.allowOutOfOrderExecution) .storeUint(tokenReceiver, 256) // uint256 .storeRef(accountsCell) // SnakedCell @@ -146,6 +154,42 @@ function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { return builder.endCell() } +/** + * Encodes extraArgs as a Cell using the SuiExtraArgsV1 format. + * + * Format per chainlink-ton TL-B: + * - tag: 32-bit opcode (0x21ea4ca9) + * - gasLimit: uint256 + * - allowOutOfOrderExecution: bool + * - tokenReceiver: uint256 + * - receiverObjectIds: SnakedCell + */ +function encodeSuiExtraArgsCell(extraArgs: SuiExtraArgsV1): Cell { + // Encode receiverObjectIds as a snaked cell of uint256 values + let objectIdsCell = beginCell().endCell() + if (extraArgs.receiverObjectIds.length > 0) { + const objectIdsBuilder = beginCell() + for (const objectId of extraArgs.receiverObjectIds) { + objectIdsBuilder.storeUint(toBigInt(getAddressBytes(objectId)), 256) + } + objectIdsCell = objectIdsBuilder.endCell() + } + + // Encode tokenReceiver as uint256 + const tokenReceiver = extraArgs.tokenReceiver + ? toBigInt(getAddressBytes(extraArgs.tokenReceiver)) + : 0n + + const builder = beginCell() + .storeUint(Number(SuiExtraArgsV1Tag), 32) // 0x21ea4ca9 + .storeUint(extraArgs.gasLimit, 256) + .storeBit(extraArgs.allowOutOfOrderExecution) + .storeUint(tokenReceiver, 256) // uint256 + .storeRef(objectIdsCell) // SnakedCell + + return builder.endCell() +} + /** * Builds the Router ccipSend message cell. * From 89482ba297d5ba91c08e7c90b18588ce219ba60e Mon Sep 17 00:00:00 2001 From: Kristijan Date: Fri, 17 Apr 2026 11:45:52 +0200 Subject: [PATCH 04/10] Fix anyToSVM msg.receiver decoding --- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/api/index.ts | 2 +- ccip-sdk/src/evm/index.ts | 5 ++--- ccip-sdk/src/solana/send.ts | 5 ++--- ccip-sdk/src/ton/send.ts | 14 ++++---------- ccip-sdk/src/utils.ts | 12 ++++++++++++ package-lock.json | 28 ---------------------------- 7 files changed, 22 insertions(+), 46 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 9e0129d2..261a0aa3 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -25,7 +25,7 @@ import { Format } from './commands/index.ts' util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests // generate:nofail // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -const VERSION = '1.4.2-36cc294' +const VERSION = '1.4.2-257335e' // generate:end const require = createRequire(import.meta.url) diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index c857a326..7b873a43 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -60,7 +60,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.4.2-36cc294' +export const SDK_VERSION = '1.4.2-257335e' // generate:end /** SDK telemetry header name */ diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index bb896eeb..0967115e 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -21,7 +21,6 @@ import { keccak256, toBeHex, toBigInt, - zeroPadValue, } from 'ethers' import type { TypedContract } from 'ethers-abitype' import { memoize } from 'micro-memoize' @@ -85,6 +84,7 @@ import { import { decodeAddress, decodeOnRampAddress, + encodeAddressToAny, getAddressBytes, getDataBytes, getSomeBlockNumberBefore, @@ -144,8 +144,7 @@ function toRateLimiterState(b: RateLimiterBucket): RateLimiterState { // Addresses <32 bytes (EVM 20B, Aptos/Solana/Sui 32B) are zero-padded to 32 bytes; // Addresses >32 bytes (e.g., TON 4+32=36B) are used as raw bytes without padding function encodeAddressToEvm(address: BytesLike): string { - const bytes = getAddressBytes(address) - return bytes.length < 32 ? zeroPadValue(bytes, 32) : hexlify(bytes) + return hexlify(encodeAddressToAny(address)) } /** typeguard for ethers Signer interface (used for `wallet`s) */ diff --git a/ccip-sdk/src/solana/send.ts b/ccip-sdk/src/solana/send.ts index 8c14e1f9..5ac9e786 100644 --- a/ccip-sdk/src/solana/send.ts +++ b/ccip-sdk/src/solana/send.ts @@ -10,7 +10,6 @@ import { PublicKey, } from '@solana/web3.js' import BN from 'bn.js' -import { zeroPadValue } from 'ethers' import { SolanaChain } from './index.ts' import { @@ -20,7 +19,7 @@ import { CCIPTokenAmountInvalidError, } from '../errors/index.ts' import { type AnyMessage, type WithLogger, ChainFamily } from '../types.ts' -import { bytesToBuffer, toLeArray, util } from '../utils.ts' +import { bytesToBuffer, encodeAddressToAny, toLeArray, util } from '../utils.ts' import { IDL as CCIP_ROUTER_IDL } from './idl/1.6.0/CCIP_ROUTER.ts' import type { UnsignedSolanaTx } from './types.ts' import { resolveATA, simulationProvider } from './utils.ts' @@ -29,7 +28,7 @@ function anyToSvmMessage(message: AnyMessage): IdlTypes[ const feeTokenPubkey = message.feeToken ? new PublicKey(message.feeToken) : PublicKey.default const svmMessage: IdlTypes['SVM2AnyMessage'] = { - receiver: bytesToBuffer(zeroPadValue(message.receiver, 32)), + receiver: encodeAddressToAny(message.receiver), data: bytesToBuffer(message.data || '0x'), tokenAmounts: (message.tokenAmounts || []).map((ta) => { if (!ta.token || ta.amount < 0n) { diff --git a/ccip-sdk/src/ton/send.ts b/ccip-sdk/src/ton/send.ts index 20444b61..70c39834 100644 --- a/ccip-sdk/src/ton/send.ts +++ b/ccip-sdk/src/ton/send.ts @@ -1,6 +1,6 @@ import { type Cell, beginCell, toNano } from '@ton/core' import { type TonClient, Address } from '@ton/ton' -import { toBigInt, zeroPadValue } from 'ethers' +import { toBigInt } from 'ethers' import type { UnsignedTONTx } from './types.ts' import { CCIPError, CCIPErrorCode, CCIPExtraArgsInvalidError } from '../errors/index.ts' @@ -13,7 +13,7 @@ import { SuiExtraArgsV1Tag, } from '../extra-args.ts' import { type AnyMessage, type WithLogger, ChainFamily } from '../types.ts' -import { bigIntReplacer, bytesToBuffer, getAddressBytes } from '../utils.ts' +import { bigIntReplacer, bytesToBuffer, encodeAddressToAny, getAddressBytes } from '../utils.ts' /** Opcode for Router ccipSend operation */ export const CCIP_SEND_OPCODE = 0x31768d95 @@ -208,10 +208,7 @@ export function buildCcipSendCell( queryId = 0n, ): Cell { // Get receiver bytes — use getAddressBytes to handle hex, base58 (Solana), TON raw formats - const receiverBytes = getAddressBytes(message.receiver) - const paddedReceiver = bytesToBuffer( - receiverBytes.length <= 32 ? zeroPadValue(receiverBytes, 32) : receiverBytes, - ) + const paddedReceiver = encodeAddressToAny(message.receiver) // Data cell (ref 0) const dataCell = beginCell() @@ -280,10 +277,7 @@ export async function getFee( } // Build stack parameters for validatedFee call - const feeReceiverBytes = getAddressBytes(message.receiver) - const paddedFeeReceiver = bytesToBuffer( - feeReceiverBytes.length <= 32 ? zeroPadValue(feeReceiverBytes, 32) : feeReceiverBytes, - ) + const paddedFeeReceiver = encodeAddressToAny(message.receiver) const receiverSlice = beginCell().storeBuffer(paddedFeeReceiver).endCell() const dataCell = beginCell() .storeBuffer(bytesToBuffer(message.data || '0x')) diff --git a/ccip-sdk/src/utils.ts b/ccip-sdk/src/utils.ts index 8e1085c9..f1630634 100644 --- a/ccip-sdk/src/utils.ts +++ b/ccip-sdk/src/utils.ts @@ -10,6 +10,7 @@ import { isBytesLike, toBeArray, toBigInt, + zeroPadValue, } from 'ethers' import { memoize } from 'micro-memoize' import yaml from 'yaml' @@ -460,6 +461,17 @@ export function getAddressBytes(address: BytesLike | readonly number[]): Uint8Ar return bytes } +/** + * Encodes remote/alien addresses for Any SRC + * + * Addresses less than 32 bytes (EVM 20B, Aptos/Solana/Sui 32B) are zero-padded to 32 bytes + * Addresses greater than 32 bytes (e.g., TON 4+32=36B) are used as raw bytes without padding + */ +export function encodeAddressToAny(address: BytesLike): Buffer { + const bytes = getAddressBytes(address) + return bytesToBuffer(bytes.length < 32 ? zeroPadValue(bytes, 32) : bytes) +} + /** * Converts snake_case strings to camelCase */ diff --git a/package-lock.json b/package-lock.json index 50d27ce0..081f87b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19045,20 +19045,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, - "node_modules/jayson/node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "extraneous": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/jayson/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -29921,20 +29907,6 @@ "node": ">= 10" } }, - "node_modules/webpack-bundle-analyzer/node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "extraneous": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/webpack-bundle-analyzer/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", From 2f3e794ca616c582376021546ca470adaff57a93 Mon Sep 17 00:00:00 2001 From: Kristijan Date: Fri, 17 Apr 2026 15:15:12 +0200 Subject: [PATCH 05/10] Add asSnakedCell/fromSnakeData utils --- ccip-sdk/src/ton/send.ts | 25 ++++++----------- ccip-sdk/src/ton/utils.ts | 59 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/ccip-sdk/src/ton/send.ts b/ccip-sdk/src/ton/send.ts index 70c39834..7992aa1f 100644 --- a/ccip-sdk/src/ton/send.ts +++ b/ccip-sdk/src/ton/send.ts @@ -1,8 +1,9 @@ -import { type Cell, beginCell, toNano } from '@ton/core' +import { Builder, Cell, beginCell, toNano } from '@ton/core' import { type TonClient, Address } from '@ton/ton' import { toBigInt } from 'ethers' import type { UnsignedTONTx } from './types.ts' +import { asSnakedCell } from './utils.ts' import { CCIPError, CCIPErrorCode, CCIPExtraArgsInvalidError } from '../errors/index.ts' import { type ExtraArgs, @@ -129,14 +130,9 @@ function encodeEVMExtraArgsCell(extraArgs: ExtraArgs): Cell { function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { // Encode accounts as a snaked cell of uint256 values - let accountsCell = beginCell().endCell() - if (extraArgs.accounts.length > 0) { - const accountBuilder = beginCell() - for (const account of extraArgs.accounts) { - accountBuilder.storeUint(toBigInt(getAddressBytes(account)), 256) - } - accountsCell = accountBuilder.endCell() - } + let accountsCell = asSnakedCell(extraArgs.accounts, (account: string) => + new Builder().storeUint(toBigInt(getAddressBytes(account)), 256) + ) // Encode tokenReceiver as uint256 const tokenReceiver = extraArgs.tokenReceiver @@ -166,14 +162,9 @@ function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { */ function encodeSuiExtraArgsCell(extraArgs: SuiExtraArgsV1): Cell { // Encode receiverObjectIds as a snaked cell of uint256 values - let objectIdsCell = beginCell().endCell() - if (extraArgs.receiverObjectIds.length > 0) { - const objectIdsBuilder = beginCell() - for (const objectId of extraArgs.receiverObjectIds) { - objectIdsBuilder.storeUint(toBigInt(getAddressBytes(objectId)), 256) - } - objectIdsCell = objectIdsBuilder.endCell() - } + let objectIdsCell = asSnakedCell(extraArgs.receiverObjectIds, (objectId: string) => + new Builder().storeUint(toBigInt(getAddressBytes(objectId)), 256) + ) // Encode tokenReceiver as uint256 const tokenReceiver = extraArgs.tokenReceiver diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts index c069c561..a7b86a05 100644 --- a/ccip-sdk/src/ton/utils.ts +++ b/ccip-sdk/src/ton/utils.ts @@ -1,4 +1,4 @@ -import { Cell, Dictionary, beginCell } from '@ton/core' +import { Builder, Cell, Slice, Dictionary, beginCell } from '@ton/core' import { hexlify, toBeHex } from 'ethers' import { CCIPTransactionNotFoundError } from '../errors/specialized.ts' @@ -32,6 +32,63 @@ export function extractMagicTag(cell: string | Cell): string { return hexlify(tag) } +// TODO: duplicate from https://github.com/smartcontractkit/chainlink-ton/blob/main/contracts/src/utils/types.ts +export function asSnakedCell(array: T[], builderFn: (item: T) => Builder): Cell { + const cells: Builder[] = [] + let builder = beginCell() + + for (const value of array) { + let itemBuilder = builderFn(value) + if (itemBuilder.refs > 3) { + throw 'Cannot pack more than 3 refs per item, use storeRef to a cell containing the item' + } + if (builder.availableBits < itemBuilder.bits || builder.availableRefs <= 1) { + cells.push(builder) + builder = beginCell() + } + builder.storeBuilder(itemBuilder) + } + cells.push(builder) + + if (cells.length === 0) { + return beginCell().endCell() + } + + // Build the linked structure from the end + let current = cells[cells.length - 1]!.endCell() + for (let i = cells.length - 2; i >= 0; i--) { + const b = cells[i]! + b.storeRef(current) + current = b.endCell() + } + return current +} + +// TODO: duplicate from https://github.com/smartcontractkit/chainlink-ton/blob/main/contracts/src/utils/types.ts +export function fromSnakeData(data: Cell, readerFn: (cs: Slice) => T): T[] { + const array: T[] = [] + let cs = data.beginParse() + while (!isEmpty(cs)) { + if (cs.remainingBits > 0) { + const item = readerFn(cs) + array.push(item) + } else { + cs = cs.loadRef().beginParse() + } + } + return array +} + +// TODO: duplicate from https://github.com/smartcontractkit/chainlink-ton/blob/main/contracts/src/utils/types.ts +export function isEmpty(slice: Slice): boolean { + const remainingBits = slice.remainingBits + const remainingRefs = slice.remainingRefs + if (remainingBits > 0 || remainingRefs > 0) { + return false + } + return true +} + /** * Parses snake format data from a cell. * Snake format: first byte indicates format (0x00), followed by string data that may span multiple cells. From 4072c7f30c044c8e23c7061587c7d853326a8a77 Mon Sep 17 00:00:00 2001 From: Kristijan Date: Fri, 17 Apr 2026 16:04:09 +0200 Subject: [PATCH 06/10] Fix lint --- ccip-sdk/src/ton/send.ts | 10 +++++----- ccip-sdk/src/ton/utils.ts | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/ccip-sdk/src/ton/send.ts b/ccip-sdk/src/ton/send.ts index 7992aa1f..75a71f49 100644 --- a/ccip-sdk/src/ton/send.ts +++ b/ccip-sdk/src/ton/send.ts @@ -1,4 +1,4 @@ -import { Builder, Cell, beginCell, toNano } from '@ton/core' +import { type Cell, Builder, beginCell, toNano } from '@ton/core' import { type TonClient, Address } from '@ton/ton' import { toBigInt } from 'ethers' @@ -130,8 +130,8 @@ function encodeEVMExtraArgsCell(extraArgs: ExtraArgs): Cell { function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { // Encode accounts as a snaked cell of uint256 values - let accountsCell = asSnakedCell(extraArgs.accounts, (account: string) => - new Builder().storeUint(toBigInt(getAddressBytes(account)), 256) + const accountsCell = asSnakedCell(extraArgs.accounts, (account: string) => + new Builder().storeUint(toBigInt(getAddressBytes(account)), 256), ) // Encode tokenReceiver as uint256 @@ -162,8 +162,8 @@ function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { */ function encodeSuiExtraArgsCell(extraArgs: SuiExtraArgsV1): Cell { // Encode receiverObjectIds as a snaked cell of uint256 values - let objectIdsCell = asSnakedCell(extraArgs.receiverObjectIds, (objectId: string) => - new Builder().storeUint(toBigInt(getAddressBytes(objectId)), 256) + const objectIdsCell = asSnakedCell(extraArgs.receiverObjectIds, (objectId: string) => + new Builder().storeUint(toBigInt(getAddressBytes(objectId)), 256), ) // Encode tokenReceiver as uint256 diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts index a7b86a05..ade0723e 100644 --- a/ccip-sdk/src/ton/utils.ts +++ b/ccip-sdk/src/ton/utils.ts @@ -1,4 +1,4 @@ -import { Builder, Cell, Slice, Dictionary, beginCell } from '@ton/core' +import { type Builder, type Slice, Cell, Dictionary, beginCell } from '@ton/core' import { hexlify, toBeHex } from 'ethers' import { CCIPTransactionNotFoundError } from '../errors/specialized.ts' @@ -32,15 +32,17 @@ export function extractMagicTag(cell: string | Cell): string { return hexlify(tag) } -// TODO: duplicate from https://github.com/smartcontractkit/chainlink-ton/blob/main/contracts/src/utils/types.ts +/** + * TODO: duplicate from https://github.com/smartcontractkit/chainlink-ton/blob/main/contracts/src/utils/types.ts + */ export function asSnakedCell(array: T[], builderFn: (item: T) => Builder): Cell { const cells: Builder[] = [] let builder = beginCell() for (const value of array) { - let itemBuilder = builderFn(value) + const itemBuilder = builderFn(value) if (itemBuilder.refs > 3) { - throw 'Cannot pack more than 3 refs per item, use storeRef to a cell containing the item' + throw new Error('Cannot pack more than 3 refs per item, use storeRef to a cell containing the item') } if (builder.availableBits < itemBuilder.bits || builder.availableRefs <= 1) { cells.push(builder) @@ -64,7 +66,9 @@ export function asSnakedCell(array: T[], builderFn: (item: T) => Builder): Ce return current } -// TODO: duplicate from https://github.com/smartcontractkit/chainlink-ton/blob/main/contracts/src/utils/types.ts +/** + * TODO: duplicate from https://github.com/smartcontractkit/chainlink-ton/blob/main/contracts/src/utils/types.ts + */ export function fromSnakeData(data: Cell, readerFn: (cs: Slice) => T): T[] { const array: T[] = [] let cs = data.beginParse() @@ -79,7 +83,9 @@ export function fromSnakeData(data: Cell, readerFn: (cs: Slice) => T): T[] { return array } -// TODO: duplicate from https://github.com/smartcontractkit/chainlink-ton/blob/main/contracts/src/utils/types.ts +/** + * TODO: duplicate from https://github.com/smartcontractkit/chainlink-ton/blob/main/contracts/src/utils/types.ts + */ export function isEmpty(slice: Slice): boolean { const remainingBits = slice.remainingBits const remainingRefs = slice.remainingRefs From eeef3e021e76cf2484f8a08fa1b4742b778486f2 Mon Sep 17 00:00:00 2001 From: Kristijan Date: Mon, 20 Apr 2026 15:26:39 +0200 Subject: [PATCH 07/10] Add TON encode/decodeExtraArgs SVM/SUI support + fix hasher (addr encoding) --- ccip-sdk/src/execution.test.ts | 40 +++++++++ ccip-sdk/src/extra-args.test.ts | 59 +++++++++++--- ccip-sdk/src/ton/hasher.test.ts | 35 ++++++++ ccip-sdk/src/ton/hasher.ts | 8 +- ccip-sdk/src/ton/index.test.ts | 50 ++++++++++++ ccip-sdk/src/ton/index.ts | 139 ++++++++++++++++++++++---------- 6 files changed, 276 insertions(+), 55 deletions(-) diff --git a/ccip-sdk/src/execution.test.ts b/ccip-sdk/src/execution.test.ts index 08bd7257..a919844e 100644 --- a/ccip-sdk/src/execution.test.ts +++ b/ccip-sdk/src/execution.test.ts @@ -347,6 +347,46 @@ describe('calculateManualExecProof', () => { assert.equal(result.proofFlagBits, 0n) }) + // https://api.ccip.chain.link/v2/messages/0xc9d521e2b4be8d995d7f9ffbde183e12d88ec93794d6b4329c23cb354db406a8/execution-inputs + it('should calculate manual execution proof for v1.6 Solana->TON', () => { + const merkleRoot = '0x050adeaa0cfe792abbd5e33a3ba6f2d9204052952d091f7624d1a2d23b771ad1' + const messageId = '0xc9d521e2b4be8d995d7f9ffbde183e12d88ec93794d6b4329c23cb354db406a8' + const messagesInBatch: CCIPMessage[] = [ + { + data: '0x48656c6c6f', + nonce: 0n, + messageId, + sequenceNumber: 4n, + destChainSelector: 1399300952838017768n, + sourceChainSelector: 16423721717087811551n, + sender: '9NhaY2AXejCX3c4tXufzWuv52ZG7rjTJDeb1qSo9UV7S', + feeToken: 'So11111111111111111111111111111111111111112', + receiver: 'EQD4w5mxY0V7Szh2NsZ_BfWuMY6biF42HEjBz1-8_wRO-6gC', + extraArgs: '0x181dcf1040787d0100000000000000000000000001', + tokenAmounts: [], + feeValueJuels: 14388425000000000n, + feeTokenAmount: 1547524n, + allowOutOfOrderExecution: true, + gasLimit: 25000000n, + } as any, + ] + + const lane: Lane = { + sourceChainSelector: 16423721717087811551n, + destChainSelector: 1399300952838017768n, + onRamp: 'Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C', + version: CCIPVersion.V1_6, + } + + const result = calculateManualExecProof(messagesInBatch, lane, messageId, merkleRoot, { + logger: console, + }) + + assert.equal(result.merkleRoot, merkleRoot) + assert.equal(result.proofs.length, 0) + assert.equal(result.proofFlagBits, 0n) + }) + it('should calculate Aptos root correctly', () => { // Test with actual Aptos message structure from requests.test.ts const msgInfoString = diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index 2ee620c7..511eb520 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -86,10 +86,12 @@ describe('encodeExtraArgs', () => { ChainFamily.TON, ) - assert.equal( - encoded, - EVMExtraArgsV2Tag + '8000000000000000000000000000000000000000000000000000000000030d4060', - ) + assert.match(encoded, /^0xb5ee9c72/) + assert.deepEqual(decodeExtraArgs(encoded, ChainFamily.TON), { + _tag: 'EVMExtraArgsV2', + gasLimit: 400000n, + allowOutOfOrderExecution: true, + }) }) it('should encode EVMExtraArgsV2 (GenericExtraArgsV2) with allowOutOfOrderExecution false', () => { @@ -98,10 +100,12 @@ describe('encodeExtraArgs', () => { ChainFamily.TON, ) - assert.equal( - encoded, - EVMExtraArgsV2Tag + '800000000000000000000000000000000000000000000000000000000003d09020', - ) + assert.match(encoded, /^0xb5ee9c72/) + assert.deepEqual(decodeExtraArgs(encoded, ChainFamily.TON), { + _tag: 'EVMExtraArgsV2', + gasLimit: 500000n, + allowOutOfOrderExecution: false, + }) }) it('should parse real Sepolia->TON message extraArgs', () => { @@ -271,8 +275,45 @@ describe('parseExtraArgs', () => { const original = { gasLimit: 400_000n, allowOutOfOrderExecution: true } const encoded = encodeExtraArgs(original, ChainFamily.TON) const decoded = decodeExtraArgs(encoded, ChainFamily.TON) + assert.match(encoded, /^0xb5ee9c72/) assert.deepEqual(decoded, { ...original, _tag: 'EVMExtraArgsV2' }) }) + + it('should round-trip TON SVMExtraArgsV1', () => { + const original = { + computeUnits: 250_000n, + accountIsWritableBitmap: 3n, + allowOutOfOrderExecution: true, + tokenReceiver: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + accounts: [ + '11111111111111111111111111111111', + 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', + ], + } + const encoded = encodeExtraArgs(original, ChainFamily.TON) + const decoded = decodeExtraArgs(encoded, ChainFamily.TON) + + assert.match(encoded, /^0xb5ee9c72/) + assert.deepEqual(decoded, { ...original, _tag: 'SVMExtraArgsV1' }) + }) + + it('should round-trip TON SuiExtraArgsV1', () => { + const original = { + gasLimit: 350_000n, + allowOutOfOrderExecution: false, + tokenReceiver: + '0x1111111111111111111111111111111111111111111111111111111111111111', + receiverObjectIds: [ + '0x2222222222222222222222222222222222222222222222222222222222222222', + '0x3333333333333333333333333333333333333333333333333333333333333333', + ], + } + const encoded = encodeExtraArgs(original, ChainFamily.TON) + const decoded = decodeExtraArgs(encoded, ChainFamily.TON) + + assert.match(encoded, /^0xb5ee9c72/) + assert.deepEqual(decoded, { ...original, _tag: 'SuiExtraArgsV1' }) + }) }) describe('encoding format differences', () => { @@ -304,7 +345,7 @@ describe('parseExtraArgs', () => { const tonEncoded = encodeExtraArgs(args, ChainFamily.TON) assert.equal(evmEncoded.substring(0, 10), EVMExtraArgsV2Tag) - assert.equal(tonEncoded.substring(0, 10), EVMExtraArgsV2Tag) + assert.equal(tonEncoded.substring(0, 10), '0xb5ee9c72') assert.notEqual(evmEncoded, tonEncoded) }) }) diff --git a/ccip-sdk/src/ton/hasher.test.ts b/ccip-sdk/src/ton/hasher.test.ts index a6eb0dd9..e03c21c8 100644 --- a/ccip-sdk/src/ton/hasher.test.ts +++ b/ccip-sdk/src/ton/hasher.test.ts @@ -102,5 +102,40 @@ describe('TON hasher unit tests', () => { assert.equal(computedHash, expectedHash) }) + + // https://api.ccip.chain.link/v2/messages/0xc9d521e2b4be8d995d7f9ffbde183e12d88ec93794d6b4329c23cb354db406a8/execution-inputs + it('should hash the live stuck Solana->TON message to the committed single-leaf merkle root', () => { + const sourceChainSelector = 16423721717087811551n + const destChainSelector = 1399300952838017768n + const hasher = getTONLeafHasher({ + sourceChainSelector, + destChainSelector, + onRamp: 'Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C', + version: CCIPVersion.V1_6, + }) + + const message: CCIPMessage_V1_6 & EVMExtraArgsV2 = { + messageId: '0xc9d521e2b4be8d995d7f9ffbde183e12d88ec93794d6b4329c23cb354db406a8', + sourceChainSelector, + destChainSelector, + sequenceNumber: 4n, + nonce: 0n, + sender: '9NhaY2AXejCX3c4tXufzWuv52ZG7rjTJDeb1qSo9UV7S', + receiver: 'EQD4w5mxY0V7Szh2NsZ_BfWuMY6biF42HEjBz1-8_wRO-6gC', + data: '0x48656c6c6f', + extraArgs: '0x181dcf1040787d0100000000000000000000000001', + tokenAmounts: [], + feeToken: 'So11111111111111111111111111111111111111112', + feeTokenAmount: 1547524n, + feeValueJuels: 14388425000000000n, + gasLimit: 25_000_000n, + allowOutOfOrderExecution: true, + } + + assert.equal( + hasher(message), + '0x050adeaa0cfe792abbd5e33a3ba6f2d9204052952d091f7624d1a2d23b771ad1', + ) + }) }) }) diff --git a/ccip-sdk/src/ton/hasher.ts b/ccip-sdk/src/ton/hasher.ts index 7d3ced78..f268bf42 100644 --- a/ccip-sdk/src/ton/hasher.ts +++ b/ccip-sdk/src/ton/hasher.ts @@ -10,7 +10,7 @@ import { import { decodeExtraArgs } from '../extra-args.ts' import type { LeafHasher } from '../hasher/common.ts' import { type CCIPMessage, type CCIPMessage_V1_6, CCIPVersion } from '../types.ts' -import { bytesToBuffer, networkInfo } from '../utils.ts' +import { getAddressBytes, networkInfo } from '../utils.ts' import { tryParseCell } from './utils.ts' // TON uses 256 bits (32 bytes) of zeros as leaf domain separator @@ -64,7 +64,7 @@ export const hashTONMetadata = ( ): string => { // Domain separator for TON messages const versionHash = BigInt(sha256(Buffer.from('Any2TVMMessageHashV1'))) - const onRampBytes = bytesToBuffer(onRamp) + const onRampBytes = Buffer.from(getAddressBytes(onRamp)) // Build metadata cell const metadataCell = beginCell() @@ -116,7 +116,7 @@ function hashV16TONMessage(message: CCIPMessage_V1_6, metadataHash: string): str .endCell() // Build sender cell with address bytes - const senderBytes = bytesToBuffer(message.sender) + const senderBytes = Buffer.from(getAddressBytes(message.sender)) const senderCell = beginCell() .storeUint(BigInt(senderBytes.length), 8) .storeBuffer(senderBytes) @@ -157,7 +157,7 @@ function buildTokenAmountsCell(tokenAmounts: readonly TokenAmount[]): Cell { // Process each token transfer for (const ta of tokenAmounts) { - const sourcePoolBytes = bytesToBuffer(ta.sourcePoolAddress) + const sourcePoolBytes = Buffer.from(getAddressBytes(ta.sourcePoolAddress)) // Extract amount const amountSource = diff --git a/ccip-sdk/src/ton/index.test.ts b/ccip-sdk/src/ton/index.test.ts index ee3b98d3..85209d73 100644 --- a/ccip-sdk/src/ton/index.test.ts +++ b/ccip-sdk/src/ton/index.test.ts @@ -47,6 +47,56 @@ describe('TON index unit tests', () => { const mockNetworkInfo = networkInfo('ton-testnet') + describe('extra args codec', () => { + it('should round-trip EVM extra args through TONChain static codec', () => { + const original = { gasLimit: 400_000n, allowOutOfOrderExecution: true } + + const encoded = TONChain.encodeExtraArgs(original) + const decoded = TONChain.decodeExtraArgs(encoded) + + assert.match(encoded, /^0xb5ee9c72/) + assert.deepEqual(decoded, { ...original, _tag: 'EVMExtraArgsV2' }) + }) + + it('should round-trip SVM extra args through TONChain static codec', () => { + const original = { + computeUnits: 250_000n, + accountIsWritableBitmap: 5n, + allowOutOfOrderExecution: true, + tokenReceiver: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + accounts: [ + '11111111111111111111111111111111', + 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', + ], + } + + const encoded = TONChain.encodeExtraArgs(original) + const decoded = TONChain.decodeExtraArgs(encoded) + + assert.match(encoded, /^0xb5ee9c72/) + assert.deepEqual(decoded, { ...original, _tag: 'SVMExtraArgsV1' }) + }) + + it('should round-trip Sui extra args through TONChain static codec', () => { + const original = { + gasLimit: 350_000n, + allowOutOfOrderExecution: false, + tokenReceiver: + '0x1111111111111111111111111111111111111111111111111111111111111111', + receiverObjectIds: [ + '0x2222222222222222222222222222222222222222222222222222222222222222', + '0x3333333333333333333333333333333333333333333333333333333333333333', + ], + } + + const encoded = TONChain.encodeExtraArgs(original) + const decoded = TONChain.decodeExtraArgs(encoded) + + assert.match(encoded, /^0xb5ee9c72/) + assert.deepEqual(decoded, { ...original, _tag: 'SuiExtraArgsV1' }) + }) + }) + describe('execute', { timeout: 10e3 }, () => { const mockWalletAddress = Address.parse('EQCVYafY2dq6dxpJXxm0ugndeoCi1uohtNthyotzpcGVmaoa') diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 2aca1eac..ed70ce1e 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -46,12 +46,13 @@ import { CCIPTransactionNotFoundError, CCIPWalletInvalidError, } from '../errors/index.ts' -import { type EVMExtraArgsV2, type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts' +import { type EVMExtraArgsV1, type EVMExtraArgsV2, type ExtraArgs, type GenericExtraArgsV3, type SuiExtraArgsV1, type SVMExtraArgsV1, EVMExtraArgsV1Tag, EVMExtraArgsV2Tag, GenericExtraArgsV3Tag, SuiExtraArgsV1Tag, SVMExtraArgsV1Tag } from '../extra-args.ts' import type { LeafHasher } from '../hasher/common.ts' import { buildMessageForDest, getMessagesInBatch } from '../requests.ts' import { supportedChains } from '../supported-chains.ts' import { type AnyMessage, + type CCIPMessage, type CCIPExecution, type CCIPRequest, type ChainLog, @@ -77,7 +78,7 @@ import { import { generateUnsignedExecuteReport } from './exec.ts' import { getTONLeafHasher } from './hasher.ts' import { type UnsignedTONTx, isTONWallet } from './types.ts' -import { crc32, lookupTxByRawHash, parseJettonContent } from './utils.ts' +import { crc32, fromSnakeData, lookupTxByRawHash, parseJettonContent } from './utils.ts' import type { CCIPMessage_V1_6_EVM } from '../evm/messages.ts' export type { TONWallet, UnsignedTONTx } from './types.ts' @@ -89,6 +90,80 @@ function isTvmError(error: unknown): error is Error & { exitCode: number } { return error instanceof Error && 'exitCode' in error && typeof error.exitCode === 'number' } +function decodeLegacyEVMTONExtraArgs( + extraArgs: BytesLike, +): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | undefined { + let bytes + try { + bytes = bytesToBuffer(extraArgs) + if (dataSlice(bytes, 0, 4) !== EVMExtraArgsV2Tag) return + } catch { + return + } + + const slice = new Slice(new BitReader(new BitString(bytes, 32, bytes.length * 8)), []) + const gasLimit = slice.loadMaybeUintBig(256) ?? 0n + const allowOutOfOrderExecution = slice.loadBit() + + return { + _tag: 'EVMExtraArgsV2', + gasLimit, + allowOutOfOrderExecution, + } +} + +function decodeTONExtraArgsCell( + cell: Cell, +): + | (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) + | (SVMExtraArgsV1 & { _tag: 'SVMExtraArgsV1' }) + | (SuiExtraArgsV1 & { _tag: 'SuiExtraArgsV1' }) + | undefined { + const slice = cell.beginParse() + const tag = hexlify(slice.loadBuffer(4)) + + switch (tag) { + case EVMExtraArgsV2Tag: + return { + _tag: 'EVMExtraArgsV2', + gasLimit: slice.loadMaybeUintBig(256) ?? 0n, + allowOutOfOrderExecution: slice.loadBit(), + } + + case SVMExtraArgsV1Tag: + return { + _tag: 'SVMExtraArgsV1', + computeUnits: BigInt(slice.loadUint(32)), + accountIsWritableBitmap: slice.loadUintBig(64), + allowOutOfOrderExecution: slice.loadBit(), + tokenReceiver: decodeAddress(toBeHex(slice.loadUintBig(256), 32), ChainFamily.Solana), + accounts: + slice.remainingRefs > 0 + ? fromSnakeData(slice.loadRef(), (accountSlice) => + decodeAddress(toBeHex(accountSlice.loadUintBig(256), 32), ChainFamily.Solana), + ) + : [], + } + + case SuiExtraArgsV1Tag: + return { + _tag: 'SuiExtraArgsV1', + gasLimit: slice.loadUintBig(256), + allowOutOfOrderExecution: slice.loadBit(), + tokenReceiver: decodeAddress(toBeHex(slice.loadUintBig(256), 32), ChainFamily.Sui), + receiverObjectIds: + slice.remainingRefs > 0 + ? fromSnakeData(slice.loadRef(), (objectSlice) => + decodeAddress(toBeHex(objectSlice.loadUintBig(256), 32), ChainFamily.Sui), + ) + : [], + } + + default: + return + } +} + /** * TON chain implementation supporting TON networks. * @@ -617,7 +692,7 @@ export class TONChain extends Chain { }: { data: unknown topics?: readonly string[] - }): CCIPMessage_V1_6_EVM | undefined { + }): CCIPMessage | undefined { if (!data || typeof data !== 'string') return if (topics?.length && topics[0] !== crc32('CCIPMessageSent')) return @@ -673,8 +748,8 @@ export class TONChain extends Chain { // Load extraArgs from ref 2 const extraArgsCell = bodySlice.loadRef() - // Build extraArgs as raw hex matching reference format - const extraArgs = '0x' + extraArgsCell.bits.toString().toLowerCase().replace('_', '0') + // Serialize full cell graph so nested refs are preserved for SVM/Sui extraArgs. + const extraArgs = hexlify(extraArgsCell.toBoc()) const parsed = this.decodeExtraArgs(extraArgs) if (!parsed) return const { _tag, ...extraArgsObj } = parsed @@ -706,59 +781,39 @@ export class TONChain extends Chain { } /** - * Encodes extra args from TON messages into BOC serialization format. + * Encodes TON extra args as a BOC-serialized cell. * - * Currently only supports GenericExtraArgsV2 (EVMExtraArgsV2) encoding since TON - * lanes are only connected to EVM chains. When new lanes are planned to be added, - * this should be extended to support them (eg. Solana and SVMExtraArgsV1) + * BOC serialization preserves nested refs, which is required for SVM and Sui + * extra args that use snaked cells. * * @param args - Extra arguments containing gas limit and execution flags * @returns Hex string of BOC-encoded extra args (0x-prefixed) - * @throws {@link CCIPExtraArgsInvalidError} if args contains fields other than `gasLimit` and `allowOutOfOrderExecution` */ static encodeExtraArgs(args: ExtraArgs): string { const cell = encodeExtraArgsCell(args) - return '0x' + cell.bits.toString().toLowerCase().replace('_', '0') + return hexlify(cell.toBoc()) } /** - * Decodes extra arguments from TON messages. - * Handles both raw bit-packed data (starts with EVMExtraArgsV2 tag directly) - * and BOC-wrapped data (starts with TON BOC magic `0xb5ee9c72`, unwrapped first). - * Returns undefined if parsing fails or the tag doesn't match. - * - * Currently only supports EVMExtraArgsV2 (GenericExtraArgsV2) encoding since TON - * lanes are only connected to EVM chains. When new lanes are planned to be added, - * this should be extended to support them (eg. Solana and SVMExtraArgsV1) + * Decodes TON extra arguments. + * Accepts BOC-serialized cells for all supported variants and legacy raw + * GenericExtraArgsV2 bits for backward compatibility. * - * @param extraArgs - Extra args as hex string or bytes (raw bit-packed or BOC-wrapped) - * @returns Decoded EVMExtraArgsV2 (GenericExtraArgsV2) object or undefined if invalid + * @param extraArgs - Extra args as hex string or bytes + * @returns Decoded TON extra args object or undefined if invalid */ static decodeExtraArgs( extraArgs: BytesLike, - ): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | undefined { - let bytes + ): + | (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) + | (SVMExtraArgsV1 & { _tag: 'SVMExtraArgsV1' }) + | (SuiExtraArgsV1 & { _tag: 'SuiExtraArgsV1' }) + | undefined { try { - bytes = bytesToBuffer(extraArgs) - - // If the data is BOC-wrapped (starts with TON BOC magic 0xb5ee9c72), extract bits - if (dataSlice(bytes, 0, 4) !== EVMExtraArgsV2Tag) { - const cell = Cell.fromBoc(bytes)[0]! - bytes = bytesToBuffer('0x' + cell.bits.toString().toLowerCase().replace('_', '0')) - } - if (dataSlice(bytes, 0, 4) !== EVMExtraArgsV2Tag) return + const cell = Cell.fromBoc(bytesToBuffer(extraArgs))[0]! + return decodeTONExtraArgsCell(cell) } catch { - return - } - - const slice = new Slice(new BitReader(new BitString(bytes, 32, bytes.length * 8)), []) - const gasLimit = slice.loadMaybeUintBig(256) ?? 0n - const allowOutOfOrderExecution = slice.loadBit() - - return { - _tag: 'EVMExtraArgsV2', - gasLimit, - allowOutOfOrderExecution, + return decodeLegacyEVMTONExtraArgs(extraArgs) } } From 7645c87c167f7add16c50455a574cbcf0b2fd79d Mon Sep 17 00:00:00 2001 From: Kristijan Date: Mon, 20 Apr 2026 16:29:42 +0200 Subject: [PATCH 08/10] Fix lint --- ccip-sdk/src/extra-args.test.ts | 3 +-- ccip-sdk/src/ton/index.test.ts | 3 +-- ccip-sdk/src/ton/index.ts | 24 ++++++++++++++++-------- ccip-sdk/src/ton/utils.ts | 4 +++- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index 511eb520..352d847c 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -301,8 +301,7 @@ describe('parseExtraArgs', () => { const original = { gasLimit: 350_000n, allowOutOfOrderExecution: false, - tokenReceiver: - '0x1111111111111111111111111111111111111111111111111111111111111111', + tokenReceiver: '0x1111111111111111111111111111111111111111111111111111111111111111', receiverObjectIds: [ '0x2222222222222222222222222222222222222222222222222222222222222222', '0x3333333333333333333333333333333333333333333333333333333333333333', diff --git a/ccip-sdk/src/ton/index.test.ts b/ccip-sdk/src/ton/index.test.ts index 85209d73..d98043c8 100644 --- a/ccip-sdk/src/ton/index.test.ts +++ b/ccip-sdk/src/ton/index.test.ts @@ -81,8 +81,7 @@ describe('TON index unit tests', () => { const original = { gasLimit: 350_000n, allowOutOfOrderExecution: false, - tokenReceiver: - '0x1111111111111111111111111111111111111111111111111111111111111111', + tokenReceiver: '0x1111111111111111111111111111111111111111111111111111111111111111', receiverObjectIds: [ '0x2222222222222222222222222222222222222222222222222222222222222222', '0x3333333333333333333333333333333333333333333333333333333333333333', diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index ed70ce1e..e2423837 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -46,14 +46,22 @@ import { CCIPTransactionNotFoundError, CCIPWalletInvalidError, } from '../errors/index.ts' -import { type EVMExtraArgsV1, type EVMExtraArgsV2, type ExtraArgs, type GenericExtraArgsV3, type SuiExtraArgsV1, type SVMExtraArgsV1, EVMExtraArgsV1Tag, EVMExtraArgsV2Tag, GenericExtraArgsV3Tag, SuiExtraArgsV1Tag, SVMExtraArgsV1Tag } from '../extra-args.ts' +import { + type EVMExtraArgsV2, + type ExtraArgs, + type SVMExtraArgsV1, + type SuiExtraArgsV1, + EVMExtraArgsV2Tag, + SVMExtraArgsV1Tag, + SuiExtraArgsV1Tag, +} from '../extra-args.ts' import type { LeafHasher } from '../hasher/common.ts' import { buildMessageForDest, getMessagesInBatch } from '../requests.ts' import { supportedChains } from '../supported-chains.ts' import { type AnyMessage, - type CCIPMessage, type CCIPExecution, + type CCIPMessage, type CCIPRequest, type ChainLog, type ChainTransaction, @@ -781,10 +789,10 @@ export class TONChain extends Chain { } /** - * Encodes TON extra args as a BOC-serialized cell. + * Encodes TON extra args as a BOC-serialized cell. * - * BOC serialization preserves nested refs, which is required for SVM and Sui - * extra args that use snaked cells. + * BOC serialization preserves nested refs, which is required for SVM and Sui + * extra args that use snaked cells. * * @param args - Extra arguments containing gas limit and execution flags * @returns Hex string of BOC-encoded extra args (0x-prefixed) @@ -795,9 +803,9 @@ export class TONChain extends Chain { } /** - * Decodes TON extra arguments. - * Accepts BOC-serialized cells for all supported variants and legacy raw - * GenericExtraArgsV2 bits for backward compatibility. + * Decodes TON extra arguments. + * Accepts BOC-serialized cells for all supported variants and legacy raw + * GenericExtraArgsV2 bits for backward compatibility. * * @param extraArgs - Extra args as hex string or bytes * @returns Decoded TON extra args object or undefined if invalid diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts index ade0723e..7e3fcd9f 100644 --- a/ccip-sdk/src/ton/utils.ts +++ b/ccip-sdk/src/ton/utils.ts @@ -42,7 +42,9 @@ export function asSnakedCell(array: T[], builderFn: (item: T) => Builder): Ce for (const value of array) { const itemBuilder = builderFn(value) if (itemBuilder.refs > 3) { - throw new Error('Cannot pack more than 3 refs per item, use storeRef to a cell containing the item') + throw new Error( + 'Cannot pack more than 3 refs per item, use storeRef to a cell containing the item', + ) } if (builder.availableBits < itemBuilder.bits || builder.availableRefs <= 1) { cells.push(builder) From 2a4c3113786aae5e1e712836c547b51d86d25b3d Mon Sep 17 00:00:00 2001 From: Kristijan Date: Wed, 22 Apr 2026 14:14:44 +0200 Subject: [PATCH 09/10] Fix test, extract extra-args.ts, simplify encodeAddressToAny func --- ccip-sdk/src/extra-args.test.ts | 2 +- ccip-sdk/src/ton/extra-args.ts | 225 +++++++++++++++++++++ ccip-sdk/src/ton/index.integration.test.ts | 10 +- ccip-sdk/src/ton/index.ts | 115 +---------- ccip-sdk/src/ton/send.ts | 147 +------------- ccip-sdk/src/utils.ts | 5 +- 6 files changed, 250 insertions(+), 254 deletions(-) create mode 100644 ccip-sdk/src/ton/extra-args.ts diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index 352d847c..abe5c400 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -344,7 +344,7 @@ describe('parseExtraArgs', () => { const tonEncoded = encodeExtraArgs(args, ChainFamily.TON) assert.equal(evmEncoded.substring(0, 10), EVMExtraArgsV2Tag) - assert.equal(tonEncoded.substring(0, 10), '0xb5ee9c72') + assert.equal(tonEncoded.substring(0, 10), '0xb5ee9c72') // TON's BOC encoding marker assert.notEqual(evmEncoded, tonEncoded) }) }) diff --git a/ccip-sdk/src/ton/extra-args.ts b/ccip-sdk/src/ton/extra-args.ts new file mode 100644 index 00000000..91a0eac0 --- /dev/null +++ b/ccip-sdk/src/ton/extra-args.ts @@ -0,0 +1,225 @@ +import { type Cell, BitReader, BitString, Builder, Slice, beginCell } from '@ton/core' +import { type BytesLike, dataSlice, hexlify, toBeHex, toBigInt } from 'ethers' + +import { CCIPExtraArgsInvalidError } from '../errors/index.ts' +import { + type EVMExtraArgsV2, + type ExtraArgs, + type SVMExtraArgsV1, + type SuiExtraArgsV1, + EVMExtraArgsV2Tag, + SVMExtraArgsV1Tag, + SuiExtraArgsV1Tag, +} from '../extra-args.ts' +import { ChainFamily } from '../types.ts' +import { bigIntReplacer, bytesToBuffer, decodeAddress, getAddressBytes } from '../utils.ts' +import { asSnakedCell, fromSnakeData } from './utils.ts' + +/** + * Checks if extraArgs is SVMExtraArgsV1 format. + */ +function isSVMExtraArgs(extraArgs: ExtraArgs): extraArgs is SVMExtraArgsV1 { + return 'computeUnits' in extraArgs +} + +/** + * Checks if extraArgs is SuiExtraArgsV1 format. + */ +function isSuiExtraArgs(extraArgs: ExtraArgs): extraArgs is SuiExtraArgsV1 { + return 'receiverObjectIds' in extraArgs +} + +/** + * Encodes extraArgs as a Cell. + * + * Supports three formats based on the destination chain: + * - GenericExtraArgsV2 (EVMExtraArgsV2) for EVM/TON/Aptos destinations + * - SVMExtraArgsV1 for Solana destinations + * - SuiExtraArgsV1 for Sui destinations + * + * @param extraArgs - Extra arguments for CCIP message + * @returns Cell encoding the extra arguments + * @throws {@link CCIPExtraArgsInvalidError} if extraArgs format is invalid + */ +export function encodeExtraArgsCell(extraArgs: ExtraArgs): Cell { + if (isSVMExtraArgs(extraArgs)) { + return encodeSVMExtraArgsCell(extraArgs) + } + if (isSuiExtraArgs(extraArgs)) { + return encodeSuiExtraArgsCell(extraArgs) + } + return encodeEVMExtraArgsCell(extraArgs) +} + +/** + * Encodes extraArgs as a Cell using the GenericExtraArgsV2 (EVMExtraArgsV2) format. + * + * Format per chainlink-ton TL-B: + * - tag: 32-bit opcode (0x181dcf10) + * - gasLimit: Maybe (1 bit flag + 256 bits if present) + * - allowOutOfOrderExecution: 1 bit + */ +function encodeEVMExtraArgsCell(extraArgs: ExtraArgs): Cell { + if ( + Object.keys(extraArgs).filter((k) => k !== '_tag').length !== 2 || + !('gasLimit' in extraArgs && 'allowOutOfOrderExecution' in extraArgs) + ) + throw new CCIPExtraArgsInvalidError(ChainFamily.TON, JSON.stringify(extraArgs, bigIntReplacer)) + + let gasLimit: bigint | null = null + if (extraArgs.gasLimit > 0n) { + gasLimit = extraArgs.gasLimit + } + + const builder = beginCell().storeUint(Number(EVMExtraArgsV2Tag), 32) // 0x181dcf10 + builder.storeMaybeUint(gasLimit, 256) + builder.storeBit(extraArgs.allowOutOfOrderExecution) + + return builder.endCell() +} + +/** + * Encodes extraArgs as a Cell using the SVMExtraArgsV1 format. + * + * Format per chainlink-ton TL-B: + * - tag: 32-bit opcode (0x1f3b3aba) + * - computeUnits: uint32 + * - accountIsWritableBitmap: uint64 + * - allowOutOfOrderExecution: bool + * - tokenReceiver: uint256 + * - accounts: SnakedCell + */ +function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { + // Encode accounts as a snaked cell of uint256 values + const accountsCell = asSnakedCell(extraArgs.accounts, (account: string) => + new Builder().storeUint(toBigInt(getAddressBytes(account)), 256), + ) + + // Encode tokenReceiver as uint256 + const tokenReceiver = extraArgs.tokenReceiver + ? toBigInt(getAddressBytes(extraArgs.tokenReceiver)) + : 0n + + const builder = beginCell() + .storeUint(Number(SVMExtraArgsV1Tag), 32) // 0x1f3b3aba + .storeUint(Number(extraArgs.computeUnits), 32) + .storeUint(extraArgs.accountIsWritableBitmap, 64) + .storeBit(extraArgs.allowOutOfOrderExecution) + .storeUint(tokenReceiver, 256) // uint256 + .storeRef(accountsCell) // SnakedCell + + return builder.endCell() +} + +/** + * Encodes extraArgs as a Cell using the SuiExtraArgsV1 format. + * + * Format per chainlink-ton TL-B: + * - tag: 32-bit opcode (0x21ea4ca9) + * - gasLimit: uint256 + * - allowOutOfOrderExecution: bool + * - tokenReceiver: uint256 + * - receiverObjectIds: SnakedCell + */ +function encodeSuiExtraArgsCell(extraArgs: SuiExtraArgsV1): Cell { + // Encode receiverObjectIds as a snaked cell of uint256 values + const objectIdsCell = asSnakedCell(extraArgs.receiverObjectIds, (objectId: string) => + new Builder().storeUint(toBigInt(getAddressBytes(objectId)), 256), + ) + + // Encode tokenReceiver as uint256 + const tokenReceiver = extraArgs.tokenReceiver + ? toBigInt(getAddressBytes(extraArgs.tokenReceiver)) + : 0n + + const builder = beginCell() + .storeUint(Number(SuiExtraArgsV1Tag), 32) // 0x21ea4ca9 + .storeUint(extraArgs.gasLimit, 256) + .storeBit(extraArgs.allowOutOfOrderExecution) + .storeUint(tokenReceiver, 256) // uint256 + .storeRef(objectIdsCell) // SnakedCell + + return builder.endCell() +} + +/** + * Decodes extraArgs from a BytesLike value in the legacy EVM/TON format (EVMExtraArgsV2). + * Returns undefined if the format is invalid or does not match EVMExtraArgsV2. + */ +export function decodeLegacyEVMTONExtraArgs( + extraArgs: BytesLike, +): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | undefined { + let bytes + try { + bytes = bytesToBuffer(extraArgs) + if (dataSlice(bytes, 0, 4) !== EVMExtraArgsV2Tag) return + } catch { + return + } + + const slice = new Slice(new BitReader(new BitString(bytes, 32, bytes.length * 8)), []) + const gasLimit = slice.loadMaybeUintBig(256) ?? 0n + const allowOutOfOrderExecution = slice.loadBit() + + return { + _tag: 'EVMExtraArgsV2', + gasLimit, + allowOutOfOrderExecution, + } +} + +/** + * Decodes extraArgs from a TON Cell. + * Returns undefined if the format is invalid or does not match any known extraArgs format. + */ +export function decodeTONExtraArgsCell( + cell: Cell, +): + | (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) + | (SVMExtraArgsV1 & { _tag: 'SVMExtraArgsV1' }) + | (SuiExtraArgsV1 & { _tag: 'SuiExtraArgsV1' }) + | undefined { + const slice = cell.beginParse() + const tag = hexlify(slice.loadBuffer(4)) + + switch (tag) { + case EVMExtraArgsV2Tag: + return { + _tag: 'EVMExtraArgsV2', + gasLimit: slice.loadMaybeUintBig(256) ?? 0n, + allowOutOfOrderExecution: slice.loadBit(), + } + + case SVMExtraArgsV1Tag: + return { + _tag: 'SVMExtraArgsV1', + computeUnits: BigInt(slice.loadUint(32)), + accountIsWritableBitmap: slice.loadUintBig(64), + allowOutOfOrderExecution: slice.loadBit(), + tokenReceiver: decodeAddress(toBeHex(slice.loadUintBig(256), 32), ChainFamily.Solana), + accounts: + slice.remainingRefs > 0 + ? fromSnakeData(slice.loadRef(), (accountSlice) => + decodeAddress(toBeHex(accountSlice.loadUintBig(256), 32), ChainFamily.Solana), + ) + : [], + } + + case SuiExtraArgsV1Tag: + return { + _tag: 'SuiExtraArgsV1', + gasLimit: slice.loadUintBig(256), + allowOutOfOrderExecution: slice.loadBit(), + tokenReceiver: decodeAddress(toBeHex(slice.loadUintBig(256), 32), ChainFamily.Sui), + receiverObjectIds: + slice.remainingRefs > 0 + ? fromSnakeData(slice.loadRef(), (objectSlice) => + decodeAddress(toBeHex(objectSlice.loadUintBig(256), 32), ChainFamily.Sui), + ) + : [], + } + + default: + return + } +} diff --git a/ccip-sdk/src/ton/index.integration.test.ts b/ccip-sdk/src/ton/index.integration.test.ts index ccf5c024..8ba18675 100644 --- a/ccip-sdk/src/ton/index.integration.test.ts +++ b/ccip-sdk/src/ton/index.integration.test.ts @@ -7,6 +7,7 @@ import '../index.ts' import { TONChain } from './index.ts' import { crc32 } from './utils.ts' import type { CCIPMessage_V1_6_EVM } from '../evm/messages.ts' +import type { CCIPMessage, CCIPVersion } from '../types.ts' // CRC32 hex values for TON external message topics const CCIP_MESSAGE_SENT_TOPIC = crc32('CCIPMessageSent') // 0xa45d293c @@ -250,7 +251,7 @@ describe.skip('TON index integration tests', () => { }) describe('decodeMessage', () => { - let message: CCIPMessage_V1_6_EVM | undefined + let message: CCIPMessage | undefined let ccipTxHash: string | undefined let messageLog: any @@ -389,10 +390,11 @@ describe.skip('TON index integration tests', () => { it('should decode gasLimit as positive bigint', () => { assert.ok(message) - assert.equal(typeof message.gasLimit, 'bigint') - assert.ok(message.gasLimit > 0n, 'gasLimit should be positive') + assert.ok('gasLimit' in message, 'message should have gasLimit property') + assert.equal(typeof (message as CCIPMessage_V1_6_EVM).gasLimit, 'bigint') + assert.ok((message as CCIPMessage_V1_6_EVM).gasLimit > 0n, 'gasLimit should be positive') assert.ok( - message.gasLimit >= 100_000n, + (message as CCIPMessage_V1_6_EVM).gasLimit >= 100_000n, 'gasLimit should be at least 100k for cross-chain calls', ) }) diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index e2423837..3922162a 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -1,32 +1,14 @@ import { Buffer } from 'buffer' -import { - type Transaction, - Address, - BitReader, - BitString, - Cell, - Slice, - beginCell, - fromNano, - toNano, -} from '@ton/core' +import { type Transaction, Address, Cell, beginCell, fromNano, toNano } from '@ton/core' import { TonClient } from '@ton/ton' import { type AxiosAdapter, getAdapter } from 'axios' -import { - type BytesLike, - dataSlice, - hexlify, - isBytesLike, - isHexString, - toBeArray, - toBeHex, -} from 'ethers' +import { type BytesLike, hexlify, isBytesLike, isHexString, toBeArray, toBeHex } from 'ethers' import { type Memoized, memoize } from 'micro-memoize' import type { PickDeep } from 'type-fest' import { streamTransactionsForAddress } from './logs.ts' -import { encodeExtraArgsCell, generateUnsignedCcipSend, getFee as getFeeImpl } from './send.ts' +import { generateUnsignedCcipSend, getFee as getFeeImpl } from './send.ts' import { type ChainContext, type ChainStatic, @@ -46,15 +28,7 @@ import { CCIPTransactionNotFoundError, CCIPWalletInvalidError, } from '../errors/index.ts' -import { - type EVMExtraArgsV2, - type ExtraArgs, - type SVMExtraArgsV1, - type SuiExtraArgsV1, - EVMExtraArgsV2Tag, - SVMExtraArgsV1Tag, - SuiExtraArgsV1Tag, -} from '../extra-args.ts' +import type { EVMExtraArgsV2, ExtraArgs, SVMExtraArgsV1, SuiExtraArgsV1 } from '../extra-args.ts' import type { LeafHasher } from '../hasher/common.ts' import { buildMessageForDest, getMessagesInBatch } from '../requests.ts' import { supportedChains } from '../supported-chains.ts' @@ -84,9 +58,14 @@ import { sleep, } from '../utils.ts' import { generateUnsignedExecuteReport } from './exec.ts' +import { + decodeLegacyEVMTONExtraArgs, + decodeTONExtraArgsCell, + encodeExtraArgsCell, +} from './extra-args.ts' import { getTONLeafHasher } from './hasher.ts' import { type UnsignedTONTx, isTONWallet } from './types.ts' -import { crc32, fromSnakeData, lookupTxByRawHash, parseJettonContent } from './utils.ts' +import { crc32, lookupTxByRawHash, parseJettonContent } from './utils.ts' import type { CCIPMessage_V1_6_EVM } from '../evm/messages.ts' export type { TONWallet, UnsignedTONTx } from './types.ts' @@ -98,80 +77,6 @@ function isTvmError(error: unknown): error is Error & { exitCode: number } { return error instanceof Error && 'exitCode' in error && typeof error.exitCode === 'number' } -function decodeLegacyEVMTONExtraArgs( - extraArgs: BytesLike, -): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | undefined { - let bytes - try { - bytes = bytesToBuffer(extraArgs) - if (dataSlice(bytes, 0, 4) !== EVMExtraArgsV2Tag) return - } catch { - return - } - - const slice = new Slice(new BitReader(new BitString(bytes, 32, bytes.length * 8)), []) - const gasLimit = slice.loadMaybeUintBig(256) ?? 0n - const allowOutOfOrderExecution = slice.loadBit() - - return { - _tag: 'EVMExtraArgsV2', - gasLimit, - allowOutOfOrderExecution, - } -} - -function decodeTONExtraArgsCell( - cell: Cell, -): - | (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) - | (SVMExtraArgsV1 & { _tag: 'SVMExtraArgsV1' }) - | (SuiExtraArgsV1 & { _tag: 'SuiExtraArgsV1' }) - | undefined { - const slice = cell.beginParse() - const tag = hexlify(slice.loadBuffer(4)) - - switch (tag) { - case EVMExtraArgsV2Tag: - return { - _tag: 'EVMExtraArgsV2', - gasLimit: slice.loadMaybeUintBig(256) ?? 0n, - allowOutOfOrderExecution: slice.loadBit(), - } - - case SVMExtraArgsV1Tag: - return { - _tag: 'SVMExtraArgsV1', - computeUnits: BigInt(slice.loadUint(32)), - accountIsWritableBitmap: slice.loadUintBig(64), - allowOutOfOrderExecution: slice.loadBit(), - tokenReceiver: decodeAddress(toBeHex(slice.loadUintBig(256), 32), ChainFamily.Solana), - accounts: - slice.remainingRefs > 0 - ? fromSnakeData(slice.loadRef(), (accountSlice) => - decodeAddress(toBeHex(accountSlice.loadUintBig(256), 32), ChainFamily.Solana), - ) - : [], - } - - case SuiExtraArgsV1Tag: - return { - _tag: 'SuiExtraArgsV1', - gasLimit: slice.loadUintBig(256), - allowOutOfOrderExecution: slice.loadBit(), - tokenReceiver: decodeAddress(toBeHex(slice.loadUintBig(256), 32), ChainFamily.Sui), - receiverObjectIds: - slice.remainingRefs > 0 - ? fromSnakeData(slice.loadRef(), (objectSlice) => - decodeAddress(toBeHex(objectSlice.loadUintBig(256), 32), ChainFamily.Sui), - ) - : [], - } - - default: - return - } -} - /** * TON chain implementation supporting TON networks. * diff --git a/ccip-sdk/src/ton/send.ts b/ccip-sdk/src/ton/send.ts index 75a71f49..855f8ddf 100644 --- a/ccip-sdk/src/ton/send.ts +++ b/ccip-sdk/src/ton/send.ts @@ -1,20 +1,11 @@ -import { type Cell, Builder, beginCell, toNano } from '@ton/core' +import { type Cell, beginCell, toNano } from '@ton/core' import { type TonClient, Address } from '@ton/ton' -import { toBigInt } from 'ethers' +import { CCIPError, CCIPErrorCode } from '../errors/index.ts' +import type { AnyMessage, WithLogger } from '../types.ts' +import { bytesToBuffer, encodeAddressToAny } from '../utils.ts' +import { encodeExtraArgsCell } from './extra-args.ts' import type { UnsignedTONTx } from './types.ts' -import { asSnakedCell } from './utils.ts' -import { CCIPError, CCIPErrorCode, CCIPExtraArgsInvalidError } from '../errors/index.ts' -import { - type ExtraArgs, - type SVMExtraArgsV1, - type SuiExtraArgsV1, - EVMExtraArgsV2Tag, - SVMExtraArgsV1Tag, - SuiExtraArgsV1Tag, -} from '../extra-args.ts' -import { type AnyMessage, type WithLogger, ChainFamily } from '../types.ts' -import { bigIntReplacer, bytesToBuffer, encodeAddressToAny, getAddressBytes } from '../utils.ts' /** Opcode for Router ccipSend operation */ export const CCIP_SEND_OPCODE = 0x31768d95 @@ -53,134 +44,6 @@ function encodeTokenAmounts( return builder.endCell() } -/** - * Checks if extraArgs is SVMExtraArgsV1 format. - */ -function isSVMExtraArgs(extraArgs: ExtraArgs): extraArgs is SVMExtraArgsV1 { - return 'computeUnits' in extraArgs -} - -/** - * Checks if extraArgs is SuiExtraArgsV1 format. - */ -function isSuiExtraArgs(extraArgs: ExtraArgs): extraArgs is SuiExtraArgsV1 { - return 'receiverObjectIds' in extraArgs -} - -/** - * Encodes extraArgs as a Cell. - * - * Supports three formats based on the destination chain: - * - GenericExtraArgsV2 (EVMExtraArgsV2) for EVM/TON/Aptos destinations - * - SVMExtraArgsV1 for Solana destinations - * - SuiExtraArgsV1 for Sui destinations - * - * @param extraArgs - Extra arguments for CCIP message - * @returns Cell encoding the extra arguments - * @throws {@link CCIPExtraArgsInvalidError} if extraArgs format is invalid - */ -export function encodeExtraArgsCell(extraArgs: ExtraArgs): Cell { - if (isSVMExtraArgs(extraArgs)) { - return encodeSVMExtraArgsCell(extraArgs) - } - if (isSuiExtraArgs(extraArgs)) { - return encodeSuiExtraArgsCell(extraArgs) - } - return encodeEVMExtraArgsCell(extraArgs) -} - -/** - * Encodes extraArgs as a Cell using the GenericExtraArgsV2 (EVMExtraArgsV2) format. - * - * Format per chainlink-ton TL-B: - * - tag: 32-bit opcode (0x181dcf10) - * - gasLimit: Maybe (1 bit flag + 256 bits if present) - * - allowOutOfOrderExecution: 1 bit - */ -function encodeEVMExtraArgsCell(extraArgs: ExtraArgs): Cell { - if ( - Object.keys(extraArgs).filter((k) => k !== '_tag').length !== 2 || - !('gasLimit' in extraArgs && 'allowOutOfOrderExecution' in extraArgs) - ) - throw new CCIPExtraArgsInvalidError(ChainFamily.TON, JSON.stringify(extraArgs, bigIntReplacer)) - - let gasLimit: bigint | null = null - if (extraArgs.gasLimit > 0n) { - gasLimit = extraArgs.gasLimit - } - - const builder = beginCell().storeUint(Number(EVMExtraArgsV2Tag), 32) // 0x181dcf10 - builder.storeMaybeUint(gasLimit, 256) - builder.storeBit(extraArgs.allowOutOfOrderExecution) - - return builder.endCell() -} - -/** - * Encodes extraArgs as a Cell using the SVMExtraArgsV1 format. - * - * Format per chainlink-ton TL-B: - * - tag: 32-bit opcode (0x1f3b3aba) - * - computeUnits: uint32 - * - accountIsWritableBitmap: uint64 - * - allowOutOfOrderExecution: bool - * - tokenReceiver: uint256 - * - accounts: SnakedCell - */ - -function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { - // Encode accounts as a snaked cell of uint256 values - const accountsCell = asSnakedCell(extraArgs.accounts, (account: string) => - new Builder().storeUint(toBigInt(getAddressBytes(account)), 256), - ) - - // Encode tokenReceiver as uint256 - const tokenReceiver = extraArgs.tokenReceiver - ? toBigInt(getAddressBytes(extraArgs.tokenReceiver)) - : 0n - - const builder = beginCell() - .storeUint(Number(SVMExtraArgsV1Tag), 32) // 0x1f3b3aba - .storeUint(Number(extraArgs.computeUnits), 32) - .storeUint(extraArgs.accountIsWritableBitmap, 64) - .storeBit(extraArgs.allowOutOfOrderExecution) - .storeUint(tokenReceiver, 256) // uint256 - .storeRef(accountsCell) // SnakedCell - - return builder.endCell() -} - -/** - * Encodes extraArgs as a Cell using the SuiExtraArgsV1 format. - * - * Format per chainlink-ton TL-B: - * - tag: 32-bit opcode (0x21ea4ca9) - * - gasLimit: uint256 - * - allowOutOfOrderExecution: bool - * - tokenReceiver: uint256 - * - receiverObjectIds: SnakedCell - */ -function encodeSuiExtraArgsCell(extraArgs: SuiExtraArgsV1): Cell { - // Encode receiverObjectIds as a snaked cell of uint256 values - const objectIdsCell = asSnakedCell(extraArgs.receiverObjectIds, (objectId: string) => - new Builder().storeUint(toBigInt(getAddressBytes(objectId)), 256), - ) - - // Encode tokenReceiver as uint256 - const tokenReceiver = extraArgs.tokenReceiver - ? toBigInt(getAddressBytes(extraArgs.tokenReceiver)) - : 0n - - const builder = beginCell() - .storeUint(Number(SuiExtraArgsV1Tag), 32) // 0x21ea4ca9 - .storeUint(extraArgs.gasLimit, 256) - .storeBit(extraArgs.allowOutOfOrderExecution) - .storeUint(tokenReceiver, 256) // uint256 - .storeRef(objectIdsCell) // SnakedCell - - return builder.endCell() -} - /** * Builds the Router ccipSend message cell. * diff --git a/ccip-sdk/src/utils.ts b/ccip-sdk/src/utils.ts index f1630634..d9c55b9a 100644 --- a/ccip-sdk/src/utils.ts +++ b/ccip-sdk/src/utils.ts @@ -10,7 +10,6 @@ import { isBytesLike, toBeArray, toBigInt, - zeroPadValue, } from 'ethers' import { memoize } from 'micro-memoize' import yaml from 'yaml' @@ -469,7 +468,9 @@ export function getAddressBytes(address: BytesLike | readonly number[]): Uint8Ar */ export function encodeAddressToAny(address: BytesLike): Buffer { const bytes = getAddressBytes(address) - return bytesToBuffer(bytes.length < 32 ? zeroPadValue(bytes, 32) : bytes) + return bytes.length < 32 + ? Buffer.concat([Buffer.alloc(32 - bytes.length), Buffer.from(bytes)]) // pad to 32 bytes + : Buffer.from(bytes) } /** From aa712bf3cf0ea22e410a2b46ada7804873cc2329 Mon Sep 17 00:00:00 2001 From: Kristijan Date: Wed, 22 Apr 2026 14:26:12 +0200 Subject: [PATCH 10/10] extra-args.ts polish --- ccip-sdk/src/ton/extra-args.ts | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/ccip-sdk/src/ton/extra-args.ts b/ccip-sdk/src/ton/extra-args.ts index 91a0eac0..8dcebddb 100644 --- a/ccip-sdk/src/ton/extra-args.ts +++ b/ccip-sdk/src/ton/extra-args.ts @@ -71,11 +71,12 @@ function encodeEVMExtraArgsCell(extraArgs: ExtraArgs): Cell { gasLimit = extraArgs.gasLimit } - const builder = beginCell().storeUint(Number(EVMExtraArgsV2Tag), 32) // 0x181dcf10 - builder.storeMaybeUint(gasLimit, 256) - builder.storeBit(extraArgs.allowOutOfOrderExecution) - - return builder.endCell() + // 0x181dcf10 + return beginCell() + .storeUint(Number(EVMExtraArgsV2Tag), 32) + .storeMaybeUint(gasLimit, 256) + .storeBit(extraArgs.allowOutOfOrderExecution) + .endCell() } /** @@ -91,24 +92,23 @@ function encodeEVMExtraArgsCell(extraArgs: ExtraArgs): Cell { */ function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { // Encode accounts as a snaked cell of uint256 values - const accountsCell = asSnakedCell(extraArgs.accounts, (account: string) => - new Builder().storeUint(toBigInt(getAddressBytes(account)), 256), - ) + const builderFn = (account: string) => + new Builder().storeUint(toBigInt(getAddressBytes(account)), 256) + const accountsCell = asSnakedCell(extraArgs.accounts, builderFn) // Encode tokenReceiver as uint256 const tokenReceiver = extraArgs.tokenReceiver ? toBigInt(getAddressBytes(extraArgs.tokenReceiver)) : 0n - const builder = beginCell() + return beginCell() .storeUint(Number(SVMExtraArgsV1Tag), 32) // 0x1f3b3aba .storeUint(Number(extraArgs.computeUnits), 32) .storeUint(extraArgs.accountIsWritableBitmap, 64) .storeBit(extraArgs.allowOutOfOrderExecution) .storeUint(tokenReceiver, 256) // uint256 .storeRef(accountsCell) // SnakedCell - - return builder.endCell() + .endCell() } /** @@ -123,23 +123,22 @@ function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { */ function encodeSuiExtraArgsCell(extraArgs: SuiExtraArgsV1): Cell { // Encode receiverObjectIds as a snaked cell of uint256 values - const objectIdsCell = asSnakedCell(extraArgs.receiverObjectIds, (objectId: string) => - new Builder().storeUint(toBigInt(getAddressBytes(objectId)), 256), - ) + const builderFn = (objectId: string) => + new Builder().storeUint(toBigInt(getAddressBytes(objectId)), 256) + const objectIdsCell = asSnakedCell(extraArgs.receiverObjectIds, builderFn) // Encode tokenReceiver as uint256 const tokenReceiver = extraArgs.tokenReceiver ? toBigInt(getAddressBytes(extraArgs.tokenReceiver)) : 0n - const builder = beginCell() + return beginCell() .storeUint(Number(SuiExtraArgsV1Tag), 32) // 0x21ea4ca9 .storeUint(extraArgs.gasLimit, 256) .storeBit(extraArgs.allowOutOfOrderExecution) .storeUint(tokenReceiver, 256) // uint256 .storeRef(objectIdsCell) // SnakedCell - - return builder.endCell() + .endCell() } /**