Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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