Skip to content
2 changes: 1 addition & 1 deletion ccip-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion ccip-sdk/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
5 changes: 2 additions & 3 deletions ccip-sdk/src/evm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
keccak256,
toBeHex,
toBigInt,
zeroPadValue,
} from 'ethers'
import type { TypedContract } from 'ethers-abitype'
import { memoize } from 'micro-memoize'
Expand Down Expand Up @@ -85,6 +84,7 @@ import {
import {
decodeAddress,
decodeOnRampAddress,
encodeAddressToAny,
getAddressBytes,
getDataBytes,
getSomeBlockNumberBefore,
Expand Down Expand Up @@ -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) */
Expand Down
40 changes: 40 additions & 0 deletions ccip-sdk/src/execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
58 changes: 49 additions & 9 deletions ccip-sdk/src/extra-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -271,8 +275,44 @@ 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', () => {
Expand Down Expand Up @@ -304,7 +344,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)
})
})
Expand Down
5 changes: 2 additions & 3 deletions ccip-sdk/src/solana/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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'
Expand All @@ -29,7 +28,7 @@ function anyToSvmMessage(message: AnyMessage): IdlTypes<typeof CCIP_ROUTER_IDL>[
const feeTokenPubkey = message.feeToken ? new PublicKey(message.feeToken) : PublicKey.default

const svmMessage: IdlTypes<typeof CCIP_ROUTER_IDL>['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) {
Expand Down
35 changes: 35 additions & 0 deletions ccip-sdk/src/ton/hasher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
})
})
})
8 changes: 4 additions & 4 deletions ccip-sdk/src/ton/hasher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 =
Expand Down
49 changes: 49 additions & 0 deletions ccip-sdk/src/ton/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,55 @@ 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')

Expand Down
Loading
Loading