Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2ab536e
package json changes
Farber98 Dec 3, 2025
eb2e28c
ton hasher
Farber98 Dec 4, 2025
3358322
prettier
Farber98 Dec 4, 2025
e02ff2d
fix eslint
Farber98 Dec 4, 2025
e98b408
hasher and util tests
Farber98 Dec 5, 2025
1a15df8
Merge branch 'main' into juan/ton-hasher
Farber98 Dec 5, 2025
cb84a79
enable ton leaf hasher
Farber98 Dec 5, 2025
dacf34a
push report serialization
Farber98 Dec 5, 2025
4f5a327
Merge branch 'main' into juan/ton-hasher
Farber98 Dec 7, 2025
25e7a4b
fix
Farber98 Dec 7, 2025
50d49e8
add sdk exec
Farber98 Dec 8, 2025
4bae2ee
prettier + eslint
Farber98 Dec 9, 2025
b6f51c5
ton client + ton wallet for manualExec
Farber98 Dec 9, 2025
332c574
remove comment
Farber98 Dec 9, 2025
6d76a31
some andre feedback
Farber98 Dec 9, 2025
743ec7c
ton provider
Farber98 Dec 9, 2025
679a60c
Merge branch 'main' into juan/ton-hasher
Farber98 Dec 10, 2025
7f53e09
fix tests
Farber98 Dec 10, 2025
ec8cfbf
lint
Farber98 Dec 10, 2025
317713a
fix eslint
Farber98 Dec 10, 2025
977ac9e
put unref back with new version
Farber98 Dec 10, 2025
2acfbf4
lint
Farber98 Dec 10, 2025
40fbfbb
package lock
Farber98 Dec 10, 2025
1c42b41
change cli imports
Farber98 Dec 10, 2025
fdadd8d
fix lint
Farber98 Dec 10, 2025
89ac031
fix lint
Farber98 Dec 10, 2025
e76ce2d
ton provider remove comm
Farber98 Dec 10, 2025
1f218ea
unsigned report impl and provider fix
Farber98 Dec 10, 2025
19f3ab1
use EvmExtraArgs instead of generic
Farber98 Dec 10, 2025
cfd789f
use evmExtraArgsV2
Farber98 Dec 10, 2025
e89612b
remove genericextraargs
Farber98 Dec 10, 2025
ffcc0e9
fix dist
Farber98 Dec 10, 2025
e811609
adjust selector
andrevmatos Dec 11, 2025
e393329
address andre feedback
Farber98 Dec 11, 2025
c0290f0
fix hasher leaf domain separator and improve tests
Farber98 Dec 11, 2025
de87e6f
remove random hashes tests
Farber98 Dec 11, 2025
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
4 changes: 4 additions & 0 deletions ccip-cli/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { loadAptosWallet } from './aptos.ts'
import { loadEvmWallet } from './evm.ts'
import { loadSolanaWallet } from './solana.ts'
import { loadTonWallet } from './ton.ts'
import type { Ctx } from '../commands/index.ts'

const RPCS_RE = /\b(?:http|ws)s?:\/\/[\w/\\@&?%~#.,;:=+-]+/
Expand Down Expand Up @@ -187,6 +188,9 @@ export async function loadChainWallet(chain: Chain, opts: { wallet?: unknown })
case ChainFamily.Aptos:
wallet = await loadAptosWallet(opts)
return [wallet.accountAddress.toString(), wallet] as const
case ChainFamily.TON:
wallet = await loadTonWallet(opts)
return [wallet.contract.address.toString(), wallet] as const
default:
throw new Error(`Unsupported chain family: ${chain.network.family}`)
}
Expand Down
67 changes: 67 additions & 0 deletions ccip-cli/src/providers/ton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { existsSync, readFileSync } from 'node:fs'
import util from 'node:util'

import type { TONWallet } from '@chainlink/ccip-sdk/src/ton/types.ts'
import { keyPairFromSecretKey, mnemonicToPrivateKey } from '@ton/crypto'
import { WalletContractV4 } from '@ton/ton'

/**
* Loads a TON wallet from the provided options.
* @param wallet - wallet options (as passed from yargs argv)
* @returns Promise to TONWallet instance
*/
export async function loadTonWallet({
wallet: walletOpt,
}: { wallet?: unknown } = {}): Promise<TONWallet> {
if (!walletOpt) walletOpt = process.env['USER_KEY'] || process.env['OWNER_KEY']

if (typeof walletOpt !== 'string')
throw new Error(`Invalid wallet option: ${util.inspect(walletOpt)}`)

// Handle mnemonic phrase
if (walletOpt.includes(' ')) {
const mnemonic = walletOpt.trim().split(' ')
const keyPair = await mnemonicToPrivateKey(mnemonic)
const contract = WalletContractV4.create({
workchain: 0,
publicKey: keyPair.publicKey,
})
return { contract, keyPair }
}

// Handle hex private key
if (walletOpt.startsWith('0x')) {
const secretKey = Buffer.from(walletOpt.slice(2), 'hex')
if (secretKey.length === 32) {
throw new Error(
'Invalid private key: 32-byte seeds not supported. Use 64-byte secret key or mnemonic.',
)
}
if (secretKey.length !== 64) {
throw new Error('Invalid private key: must be 64 bytes (or use mnemonic)')
}
const keyPair = keyPairFromSecretKey(secretKey)
const contract = WalletContractV4.create({
workchain: 0,
publicKey: keyPair.publicKey,
})
return { contract, keyPair }
}

// Handle file path
if (existsSync(walletOpt)) {
const content = readFileSync(walletOpt, 'utf8').trim()
const secretKey = Buffer.from(content.startsWith('0x') ? content.slice(2) : content, 'hex')
if (secretKey.length !== 64) {
throw new Error('Invalid private key in file: must be 64 bytes')
}
const keyPair = keyPairFromSecretKey(secretKey)
const contract = WalletContractV4.create({
workchain: 0,
publicKey: keyPair.publicKey,
})
return { contract, keyPair }
}

throw new Error('Wallet not specified')
}
4 changes: 3 additions & 1 deletion ccip-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
"@mysten/sui": "^1.45.2",
"@solana/spl-token": "0.4.14",
"@solana/web3.js": "^1.98.4",
"@ton/core": "0.62.0",
"@ton/ton": "^16.1.0",
"abitype": "1.2.1",
"bn.js": "^5.2.2",
"borsh": "^2.0.0",
Expand All @@ -68,4 +70,4 @@
"bigint-buffer": "npm:@trufflesuite/[email protected]",
"axios": "^1.13.2"
}
}
}
2 changes: 2 additions & 0 deletions ccip-sdk/src/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
} from './extra-args.ts'
import type { LeafHasher } from './hasher/common.ts'
import type { UnsignedSolanaTx } from './solana/types.ts'
import type { UnsignedTONTx } from './ton/types.ts'
import {
type AnyMessage,
type CCIPCommit,
Expand Down Expand Up @@ -100,6 +101,7 @@ export type UnsignedTx = {
[ChainFamily.EVM]: UnsignedEVMTx
[ChainFamily.Solana]: UnsignedSolanaTx
[ChainFamily.Aptos]: UnsignedAptosTx
[ChainFamily.TON]: UnsignedTONTx
[ChainFamily.Sui]: never // TODO
}

Expand Down
81 changes: 80 additions & 1 deletion ccip-sdk/src/extra-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { dataSlice, getNumber } from 'ethers'

// Import index.ts to ensure all Chain classes are loaded and registered
import './index.ts'
import { decodeExtraArgs, encodeExtraArgs } from './extra-args.ts'
import { EVMExtraArgsV2Tag, decodeExtraArgs, encodeExtraArgs } from './extra-args.ts'
import { extractMagicTag } from './ton/utils.ts'
import { ChainFamily } from './types.ts'

describe('encodeExtraArgs', () => {
Expand Down Expand Up @@ -74,6 +75,40 @@ describe('encodeExtraArgs', () => {
assert.equal(encoded.length, 2 + 2 * (4 + 32 + 1)) // Much shorter than EVM encoding
})
})
describe('TON extra args', () => {
it('should encode EVMExtraArgsV2 (GenericExtraArgsV2)', () => {
const encoded = encodeExtraArgs(
{ gasLimit: 400_000n, allowOutOfOrderExecution: true },
ChainFamily.TON,
)

assert.equal(extractMagicTag(encoded), EVMExtraArgsV2Tag)
assert.ok(encoded.length > 10)
})

it('should encode EVMExtraArgsV2 (GenericExtraArgsV2) with allowOutOfOrderExecution false', () => {
const encoded = encodeExtraArgs(
{ gasLimit: 500_000n, allowOutOfOrderExecution: false },
ChainFamily.TON,
)

assert.equal(extractMagicTag(encoded), EVMExtraArgsV2Tag)
assert.ok(encoded.length > 10)
})

it('should parse real Sepolia->TON message extraArgs', () => {
// https://sepolia.etherscan.io/tx/0x6bdfcce8def68f19f40d340bc38d01866c10a4c92685df1c3d08180280a4ccac
const res = decodeExtraArgs(
'0x181dcf100000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000001',
ChainFamily.EVM,
)
assert.deepEqual(res, {
_tag: 'EVMExtraArgsV2',
gasLimit: 100_000_000n,
allowOutOfOrderExecution: true,
})
})
})
})

describe('parseExtraArgs', () => {
Expand Down Expand Up @@ -138,6 +173,33 @@ describe('parseExtraArgs', () => {
})
})
})
describe('TON extra args (TLB encoding)', () => {
it('should parse EVMExtraArgsV2 (GenericExtraArgsV2)', () => {
const encoded = encodeExtraArgs(
{ gasLimit: 400_000n, allowOutOfOrderExecution: true },
ChainFamily.TON,
)
const res = decodeExtraArgs(encoded, ChainFamily.TON)
assert.deepEqual(res, {
_tag: 'EVMExtraArgsV2',
gasLimit: 400000n,
allowOutOfOrderExecution: true,
})
})

it('should parse EVMExtraArgsV2 (GenericExtraArgsV2) with allowOutOfOrderExecution false', () => {
const encoded = encodeExtraArgs(
{ gasLimit: 500_000n, allowOutOfOrderExecution: false },
ChainFamily.TON,
)
const res = decodeExtraArgs(encoded, ChainFamily.TON)
assert.deepEqual(res, {
_tag: 'EVMExtraArgsV2',
gasLimit: 500000n,
allowOutOfOrderExecution: false,
})
})
})

describe('auto-detect chain family', () => {
it('should auto-detect EVM v1 args', () => {
Expand Down Expand Up @@ -196,6 +258,13 @@ describe('parseExtraArgs', () => {
const decoded = decodeExtraArgs(encoded, ChainFamily.Aptos)
assert.deepEqual(decoded, { ...original, _tag: 'EVMExtraArgsV2' })
})

it('should round-trip TON EVMExtraArgsV2', () => {
const original = { gasLimit: 400_000n, allowOutOfOrderExecution: true }
const encoded = encodeExtraArgs(original, ChainFamily.TON)
const decoded = decodeExtraArgs(encoded, ChainFamily.TON)
assert.deepEqual(decoded, { ...original, _tag: 'EVMExtraArgsV2' })
})
})

describe('encoding format differences', () => {
Expand All @@ -220,5 +289,15 @@ describe('parseExtraArgs', () => {
// But different lengths
assert.ok(evmEncoded.length > aptosEncoded.length)
})

it('should produce different encodings for EVM vs TON', () => {
const args = { gasLimit: 300_000n, allowOutOfOrderExecution: false }
const evmEncoded = encodeExtraArgs(args, ChainFamily.EVM)
const tonEncoded = encodeExtraArgs(args, ChainFamily.TON)

assert.equal(evmEncoded.substring(0, 10), EVMExtraArgsV2Tag)
assert.equal(extractMagicTag(tonEncoded), EVMExtraArgsV2Tag)
assert.notEqual(evmEncoded, tonEncoded)
})
})
})
4 changes: 3 additions & 1 deletion ccip-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ import { AptosChain } from './aptos/index.ts'
import { EVMChain } from './evm/index.ts'
import { SolanaChain } from './solana/index.ts'
import { SuiChain } from './sui/index.ts'
import { TONChain } from './ton/index.ts'
import { ChainFamily } from './types.ts'
export { AptosChain, ChainFamily, EVMChain, SolanaChain, SuiChain }
export { AptosChain, ChainFamily, EVMChain, SolanaChain, SuiChain, TONChain }
// use `supportedChains` to override/register derived classes, if needed
export { supportedChains } from './supported-chains.ts'
// import `allSupportedChains` to get them all registered, in tree-shaken environments
Expand All @@ -54,4 +55,5 @@ export const allSupportedChains = {
[ChainFamily.Solana]: SolanaChain,
[ChainFamily.Aptos]: AptosChain,
[ChainFamily.Sui]: SuiChain,
[ChainFamily.TON]: TONChain,
}
23 changes: 23 additions & 0 deletions ccip-sdk/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1335,6 +1335,29 @@ const selectors: Selectors = {
family: 'sui',
},
// end:generate

// generate:
// fetch('https://github.com/smartcontractkit/chain-selectors/raw/main/selectors_ton.yml')
// .then((res) => res.text())
// .then((body) => require('yaml').parse(body, { intAsBigInt: true }).selectors)
// .then((obj) => Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, { ...v, family: 'ton' }])))
// .then((obj) => [...require('util').inspect(obj).split('\n').slice(1, -1), ','])
'-239': {
name: 'ton-mainnet',
selector: 16448340667252469081n,
family: 'ton',
},
'-3': {
name: 'ton-testnet',
selector: 1399300952838017768n,
family: 'ton',
},
'-217': {
name: 'ton-localnet',
selector: 13879075125137744094n,
family: 'ton',
},
// end:generate
}

export default selectors
3 changes: 1 addition & 2 deletions ccip-sdk/src/solana/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import { type ExecutionReport, type WithLogger, ChainFamily } from '../types.ts'
import { IDL as CCIP_OFFRAMP_IDL } from './idl/1.6.0/CCIP_OFFRAMP.ts'
import { encodeSolanaOffchainTokenData } from './offchain.ts'
import type { CCIPMessage_V1_6_Solana, UnsignedSolanaTx } from './types.ts'
import { getDataBytes, toLeArray } from '../utils.ts'
import { bytesToBuffer } from './utils.ts'
import { bytesToBuffer, getDataBytes, toLeArray } from '../utils.ts'

type ExecAlt = {
initialIxs: TransactionInstruction[]
Expand Down
2 changes: 1 addition & 1 deletion ccip-sdk/src/solana/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
ExecutionState,
} from '../types.ts'
import {
bytesToBuffer,
createRateLimitedFetch,
decodeAddress,
decodeOnRampAddress,
Expand All @@ -83,7 +84,6 @@ import { fetchSolanaOffchainTokenData } from './offchain.ts'
import { generateUnsignedCcipSend, getFee } from './send.ts'
import { type CCIPMessage_V1_6_Solana, type UnsignedSolanaTx, isWallet } from './types.ts'
import {
bytesToBuffer,
getErrorFromLogs,
hexDiscriminator,
parseSolanaLogs,
Expand Down
4 changes: 2 additions & 2 deletions ccip-sdk/src/solana/offchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { hexlify } from 'ethers'

import { getUsdcAttestation } from '../offchain.ts'
import type { CCIPMessage, CCIPRequest, OffchainTokenData, WithLogger } from '../types.ts'
import { networkInfo, util } from '../utils.ts'
import { bytesToBuffer, networkInfo, util } from '../utils.ts'
import { IDL as BASE_TOKEN_POOL } from './idl/1.6.0/BASE_TOKEN_POOL.ts'
import { IDL as CCTP_TOKEN_POOL } from './idl/1.6.0/CCIP_CCTP_TOKEN_POOL.ts'
import type { SolanaLog, SolanaTransaction } from './index.ts'
import { bytesToBuffer, hexDiscriminator } from './utils.ts'
import { hexDiscriminator } from './utils.ts'

interface CcipCctpMessageSentEvent {
originalSender: PublicKey
Expand Down
4 changes: 2 additions & 2 deletions ccip-sdk/src/solana/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import { zeroPadValue } from 'ethers'

import { SolanaChain } from './index.ts'
import { type AnyMessage, type WithLogger, ChainFamily } from '../types.ts'
import { toLeArray, util } from '../utils.ts'
import { bytesToBuffer, 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 { bytesToBuffer, simulationProvider } from './utils.ts'
import { simulationProvider } from './utils.ts'

function anyToSvmMessage(message: AnyMessage): IdlTypes<typeof CCIP_ROUTER_IDL>['SVM2AnyMessage'] {
const feeTokenPubkey = message.feeToken ? new PublicKey(message.feeToken) : PublicKey.default
Expand Down
13 changes: 1 addition & 12 deletions ccip-sdk/src/solana/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Buffer } from 'buffer'

import { eventDiscriminator } from '@coral-xyz/anchor'
import {
type AddressLookupTableAccount,
Expand All @@ -14,7 +12,7 @@ import {
TransactionMessage,
VersionedTransaction,
} from '@solana/web3.js'
import { type BytesLike, dataLength, dataSlice, hexlify } from 'ethers'
import { dataLength, dataSlice, hexlify } from 'ethers'

import type { Log_, WithLogger } from '../types.ts'
import { getDataBytes, sleep } from '../utils.ts'
Expand All @@ -29,15 +27,6 @@ export function hexDiscriminator(eventName: string): string {
return hexlify(eventDiscriminator(eventName))
}

/**
* Converts bytes to a Node.js Buffer.
* @param bytes - Bytes to convert.
* @returns Node.js Buffer.
*/
export function bytesToBuffer(bytes: BytesLike): Buffer {
return Buffer.from(getDataBytes(bytes).buffer)
}

/**
* Waits for a Solana transaction to reach finalized status.
* @param connection - Solana connection instance.
Expand Down
Loading
Loading