From 2ab536e0b311d8cc96067d1d81ad6966a70e7744 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 3 Dec 2025 17:37:06 -0300 Subject: [PATCH 01/33] package json changes --- ccip-cli/src/index.ts | 2 +- package-lock.json | 38 +++++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 0a7adf3c..c4cc315a 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -9,7 +9,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 = '0.91.0-87e59c2' +const VERSION = '0.91.0-a488616' // generate:end const globalOpts = { diff --git a/package-lock.json b/package-lock.json index 6a191969..702341dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2244,6 +2244,7 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -2616,7 +2617,6 @@ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -2682,6 +2682,7 @@ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "license": "MIT", + "peer": true, "dependencies": { "defer-to-connect": "^2.0.0" }, @@ -2715,6 +2716,7 @@ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "license": "MIT", + "peer": true, "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", @@ -2742,7 +2744,8 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -2770,6 +2773,7 @@ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -2788,6 +2792,7 @@ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -2876,7 +2881,6 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -3362,7 +3366,6 @@ "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.0.tgz", "integrity": "sha512-fD3ROjckUrWsybaSor2AdWxzA0e/DSyV2dA4aYd7bd8orHsoJjl09fOgKfUkTDfk0BsDGBf4NBgu/c7JoS2Npw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/wevm" }, @@ -3385,7 +3388,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3872,6 +3874,7 @@ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.6.0" } @@ -3881,6 +3884,7 @@ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "license": "MIT", + "peer": true, "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -4017,6 +4021,7 @@ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "license": "MIT", + "peer": true, "dependencies": { "mimic-response": "^1.0.0" }, @@ -4241,6 +4246,7 @@ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -5037,7 +5043,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", @@ -5473,6 +5478,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "license": "MIT", + "peer": true, "dependencies": { "pump": "^3.0.0" }, @@ -5666,7 +5672,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -5794,13 +5799,15 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "license": "MIT", + "peer": true, "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" @@ -6638,6 +6645,7 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -6713,6 +6721,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -6899,6 +6908,7 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -7053,6 +7063,7 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7165,7 +7176,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7231,7 +7241,6 @@ "integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7286,6 +7295,7 @@ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -7322,7 +7332,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7436,7 +7445,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/resolve-from": { "version": "4.0.0", @@ -7463,6 +7473,7 @@ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "license": "MIT", + "peer": true, "dependencies": { "lowercase-keys": "^2.0.0" }, @@ -8443,7 +8454,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8508,7 +8518,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -8851,7 +8860,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, From eb2e28c78b2e5c4d24f20808b592ae5e1e740ff4 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Thu, 4 Dec 2025 16:47:06 -0300 Subject: [PATCH 02/33] ton hasher --- ccip-cli/src/index.ts | 2 +- ccip-sdk/package.json | 3 +- ccip-sdk/src/chain.ts | 2 + ccip-sdk/src/extra-args.test.ts | 71 ++++++++- ccip-sdk/src/extra-args.ts | 7 +- ccip-sdk/src/index.ts | 5 +- ccip-sdk/src/ton/hasher.ts | 185 +++++++++++++++++++++++ ccip-sdk/src/ton/index.ts | 260 ++++++++++++++++++++++++++++++++ ccip-sdk/src/ton/utils.ts | 57 +++++++ ccip-sdk/src/types.ts | 1 + package-lock.json | 58 +++++++ 11 files changed, 644 insertions(+), 7 deletions(-) create mode 100644 ccip-sdk/src/ton/hasher.ts create mode 100644 ccip-sdk/src/ton/index.ts create mode 100644 ccip-sdk/src/ton/utils.ts diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index c4cc315a..4f219eb9 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -9,7 +9,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 = '0.91.0-a488616' +const VERSION = '0.91.0-2ab536e' // generate:end const globalOpts = { diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index 481d7fea..32d84a08 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -59,10 +59,11 @@ "bs58": "^6.0.0", "ethers": "6.15.0", "micro-memoize": "^5.1.1", + "@ton/core": "^0.62.0", "type-fest": "^5.3.0", "yaml": "2.8.2" }, "overrides": { "axios": "^1.13.2" } -} +} \ No newline at end of file diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index 5a14d008..dbdfeac2 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -10,6 +10,7 @@ import type { ExtraArgs, SVMExtraArgsV1, SuiExtraArgsV1, + GenericExtraArgsV2, } from './extra-args.ts' import type { LeafHasher } from './hasher/common.ts' import { @@ -393,6 +394,7 @@ export type ChainStatic = Function & { | (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | (SVMExtraArgsV1 & { _tag: 'SVMExtraArgsV1' }) | (SuiExtraArgsV1 & { _tag: 'SuiExtraArgsV1' }) + | (GenericExtraArgsV2 & { _tag: 'GenericExtraArgsV2' }) | undefined encodeExtraArgs(extraArgs: ExtraArgs): string /** diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index b308dad0..9f560340 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -1,11 +1,10 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' - +import { extractMagicTag } from './ton/utils.ts' 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, GenericExtraArgsV2, decodeExtraArgs, encodeExtraArgs } from './extra-args.ts' import { ChainFamily } from './types.ts' describe('encodeExtraArgs', () => { @@ -74,6 +73,27 @@ describe('encodeExtraArgs', () => { assert.equal(encoded.length, 2 + 2 * (4 + 32 + 1)) // Much shorter than EVM encoding }) }) + describe('TON extra args', () => { + it('should encode GenericExtraArgsV2', () => { + const encoded = encodeExtraArgs( + { gasLimit: 400_000n, allowOutOfOrderExecution: true }, + ChainFamily.TON, + ) + + assert.equal(extractMagicTag(encoded), GenericExtraArgsV2) + assert.ok(encoded.length > 10) + }) + + it('should encode GenericExtraArgsV2 with allowOutOfOrderExecution false', () => { + const encoded = encodeExtraArgs( + { gasLimit: 500_000n, allowOutOfOrderExecution: false }, + ChainFamily.TON, + ) + + assert.equal(extractMagicTag(encoded), GenericExtraArgsV2) + assert.ok(encoded.length > 10) + }) + }) }) describe('parseExtraArgs', () => { @@ -138,6 +158,33 @@ describe('parseExtraArgs', () => { }) }) }) + describe('TON extra args (TLB encoding)', () => { + it('should parse GenericExtraArgsV2', () => { + const encoded = encodeExtraArgs( + { gasLimit: 400_000n, allowOutOfOrderExecution: true }, + ChainFamily.TON, + ) + const res = decodeExtraArgs(encoded, ChainFamily.TON) + assert.deepEqual(res, { + _tag: 'GenericExtraArgsV2', + gasLimit: 400000n, + allowOutOfOrderExecution: true, + }) + }) + + it('should parse GenericExtraArgsV2 with allowOutOfOrderExecution false', () => { + const encoded = encodeExtraArgs( + { gasLimit: 500_000n, allowOutOfOrderExecution: false }, + ChainFamily.TON, + ) + const res = decodeExtraArgs(encoded, ChainFamily.TON) + assert.deepEqual(res, { + _tag: 'GenericExtraArgsV2', + gasLimit: 500000n, + allowOutOfOrderExecution: false, + }) + }) + }) describe('auto-detect chain family', () => { it('should auto-detect EVM v1 args', () => { @@ -196,6 +243,13 @@ describe('parseExtraArgs', () => { const decoded = decodeExtraArgs(encoded, ChainFamily.Aptos) assert.deepEqual(decoded, { ...original, _tag: 'EVMExtraArgsV2' }) }) + + it('should round-trip TON GenericExtraArgsV2', () => { + const original = { gasLimit: 400_000n, allowOutOfOrderExecution: true } + const encoded = encodeExtraArgs(original, ChainFamily.TON) + const decoded = decodeExtraArgs(encoded, ChainFamily.TON) + assert.deepEqual(decoded, { ...original, _tag: 'GenericExtraArgsV2' }) + }) }) describe('encoding format differences', () => { @@ -220,5 +274,16 @@ 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), GenericExtraArgsV2) + assert.notEqual(evmEncoded, tonEncoded) + }) }) + }) diff --git a/ccip-sdk/src/extra-args.ts b/ccip-sdk/src/extra-args.ts index ee1ee480..79c89f63 100644 --- a/ccip-sdk/src/extra-args.ts +++ b/ccip-sdk/src/extra-args.ts @@ -7,6 +7,7 @@ export const EVMExtraArgsV1Tag = id('CCIP EVMExtraArgsV1').substring(0, 10) as ' export const EVMExtraArgsV2Tag = id('CCIP EVMExtraArgsV2').substring(0, 10) as '0x181dcf10' export const SVMExtraArgsV1Tag = id('CCIP SVMExtraArgsV1').substring(0, 10) as '0x1f3b3aba' export const SuiExtraArgsV1Tag = id('CCIP SuiExtraArgsV1').substring(0, 10) as '0x21ea4ca9' +export const GenericExtraArgsV2 = EVMExtraArgsV2Tag export type EVMExtraArgsV1 = { gasLimit: bigint @@ -27,7 +28,10 @@ export type SuiExtraArgsV1 = EVMExtraArgsV2 & { receiverObjectIds: string[] } -export type ExtraArgs = EVMExtraArgsV1 | EVMExtraArgsV2 | SVMExtraArgsV1 | SuiExtraArgsV1 +// Same structure as EVMExtraArgsV2. TON calls it GenericExtraArgsV2 +export type GenericExtraArgsV2 = EVMExtraArgsV2 + +export type ExtraArgs = EVMExtraArgsV1 | EVMExtraArgsV2 | SVMExtraArgsV1 | SuiExtraArgsV1 | GenericExtraArgsV2 /** * Encodes extra arguments for CCIP messages. @@ -53,6 +57,7 @@ export function decodeExtraArgs( | (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | (SVMExtraArgsV1 & { _tag: 'SVMExtraArgsV1' }) | (SuiExtraArgsV1 & { _tag: 'SuiExtraArgsV1' }) + | (GenericExtraArgsV2 & { _tag: 'GenericExtraArgsV2' }) | undefined { if (!data || data === '') return let chains diff --git a/ccip-sdk/src/index.ts b/ccip-sdk/src/index.ts index 9c83db44..637f5a80 100644 --- a/ccip-sdk/src/index.ts +++ b/ccip-sdk/src/index.ts @@ -14,6 +14,7 @@ export { type ExtraArgs, type SVMExtraArgsV1, type SuiExtraArgsV1, + type GenericExtraArgsV2, decodeExtraArgs, encodeExtraArgs, } from './extra-args.ts' @@ -42,8 +43,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 @@ -52,4 +54,5 @@ export const allSupportedChains = { [ChainFamily.Solana]: SolanaChain, [ChainFamily.Aptos]: AptosChain, [ChainFamily.Sui]: SuiChain, + [ChainFamily.TON]: TONChain, } diff --git a/ccip-sdk/src/ton/hasher.ts b/ccip-sdk/src/ton/hasher.ts new file mode 100644 index 00000000..1cd15fa4 --- /dev/null +++ b/ccip-sdk/src/ton/hasher.ts @@ -0,0 +1,185 @@ +import { Address, beginCell, Cell } from '@ton/core' +import { decodeExtraArgs } from '../extra-args.ts' +import { type LeafHasher, LEAF_DOMAIN_SEPARATOR } from '../hasher/common.ts' +import { type CCIPMessage, type CCIPMessage_V1_6, CCIPVersion } from '../types.ts' +import { networkInfo } from '../utils.ts' +import { hexToBuffer, toBigInt, tryParseCell, sha256 } from './utils.ts' + +// Convert LEAF_DOMAIN_SEPARATOR from hex string to Buffer for cell storage +const LEAF_DOMAIN_BUFFER = Buffer.from(LEAF_DOMAIN_SEPARATOR.slice(2).padStart(64, '0'), 'hex') + +/** + * Creates a leaf hasher for TON messages + * + * @param sourceChainSelector + * @param destChainSelector + * @param onRamp - as hex string + * @param version - CCIP version (only v1.6 supported for TON) + * @returns A LeafHasher function that computes message hashes for TON + */ +export function getTONLeafHasher({ + sourceChainSelector, + destChainSelector, + onRamp, + version, +}: { + sourceChainSelector: bigint + destChainSelector: bigint + onRamp: string + version: V +}): LeafHasher { + if (version !== CCIPVersion.V1_6) { + throw new Error(`TON only supports CCIP v1.6, got: ${version}`) + } + + // Pre-compute metadata hash once for all messages using this hasher + const metadataHash = hashTONMetadata(sourceChainSelector, destChainSelector, onRamp) + + // Return the actual hashing function that will be called for each message + return ((message: CCIPMessage): string => { + return hashV16TONMessage(message, metadataHash) + }) as LeafHasher +} + +/** + * Creates a hash that uniquely identifies the message lane configuration + * (source chain, destination chain, and onRamp address). + * Following the TON implementation from chainlink-ton repo. + * + * @param sourceChainSelector + * @param destChainSelector + * @param onRamp + * @returns SHA256 hash of the metadata as hex string + */ +export const hashTONMetadata = ( + sourceChainSelector: bigint, + destChainSelector: bigint, + onRamp: string, +): string => { + // Domain separator for TON messages + const versionHash = BigInt(sha256(Buffer.from('Any2TVMMessageHashV1'))) + const onRampBytes = hexToBuffer(onRamp) + + // Build metadata cell + const metadataCell = beginCell() + .storeUint(versionHash, 256) + .storeUint(sourceChainSelector, 64) + .storeUint(destChainSelector, 64) + .storeRef( + beginCell() + .storeUint(BigInt(onRampBytes.length), 8) + .storeBuffer(onRampBytes) + .endCell(), + ) + .endCell() + + // Return cell hash as hex string (excludes BOC headers) + return '0x' + metadataCell.hash().toString('hex') +} + +/** + * Computes the full message hash for a CCIP v1.6 TON message + * Follows the chainlink-ton's Any2TVMRampMessage.generateMessageId() + * + * @param message - CCIP message to hash + * @param metadataHash - Pre-computed metadata hash from hashTONMetadata() + * @returns SHA256 hash of the complete message as hex string + */ +function hashV16TONMessage( + message: CCIPMessage_V1_6, + metadataHash: string, +): string { + // Extract gas limit from message + let gasLimit: bigint + const embeddedGasLimit = (message as Partial<{ gasLimit: bigint }>).gasLimit + + if (typeof embeddedGasLimit === 'bigint') { + gasLimit = embeddedGasLimit + } else { + const parsedArgs = decodeExtraArgs( + message.extraArgs, + networkInfo(message.header.sourceChainSelector).family, + ) + if (!parsedArgs || parsedArgs._tag !== 'GenericExtraArgsV2') { + throw new Error('Invalid extraArgs for TON message, must be GenericExtraArgsV2') + } + gasLimit = parsedArgs.gasLimit || 0n + } + + // Build header cell containing header routing information + const headerCell = beginCell() + .storeUint(toBigInt(message.header.messageId), 256) + .storeAddress(Address.parse(message.receiver)) + .storeUint(toBigInt(message.header.sequenceNumber), 64) + .storeCoins(gasLimit) + .storeUint(toBigInt(message.header.nonce), 64) + .endCell() + + // Build sender cell with address bytes + const senderBytes = hexToBuffer(message.sender) + const senderCell = beginCell() + .storeUint(BigInt(senderBytes.length), 8) + .storeBuffer(senderBytes) + .endCell() + + // Build token amounts cell if tokens are being transferred + const tokenAmountsCell = + message.tokenAmounts.length > 0 ? buildTokenAmountsCell(message.tokenAmounts) : null + + // Assemble the complete message cell + const messageCell = beginCell() + .storeBuffer(LEAF_DOMAIN_BUFFER) + .storeUint(toBigInt(metadataHash), 256) + .storeRef(headerCell) + .storeRef(senderCell) + .storeRef(tryParseCell(message.data)) + .storeMaybeRef(tokenAmountsCell) + .endCell() + + // Return cell hash as hex string + return '0x' + messageCell.hash().toString('hex') +} + +// Type alias for token amount entries in CCIP messages +type TokenAmount = CCIPMessage_V1_6['tokenAmounts'][number] + +/** + * Creates a nested cell structure for token amounts, where each token + * transfer is stored as a reference cell containing source pool, destination, + * amount, and extra data. + * + * @param tokenAmounts - Array of token transfer details + * @returns Cell containing all token transfer information + */ +function buildTokenAmountsCell(tokenAmounts: readonly TokenAmount[]): Cell { + const builder = beginCell() + + // Process each token transfer + for (const ta of tokenAmounts) { + const sourcePoolBytes = hexToBuffer(ta.sourcePoolAddress) + + // Extract amount + const amountSource = + (ta as { amount?: bigint | number | string }).amount ?? + (ta as { destGasAmount?: bigint | number | string }).destGasAmount ?? + 0n + const amount = toBigInt(amountSource) + + // Store each token transfer as a reference cell + builder.storeRef( + beginCell() + .storeRef( + beginCell() + .storeUint(BigInt(sourcePoolBytes.length), 8) + .storeBuffer(sourcePoolBytes) + .endCell(), + ) + .storeAddress(Address.parse(ta.destTokenAddress)) + .storeUint(amount, 256) + .storeRef(tryParseCell(ta.extraData)) + .endCell(), + ) + } + + return builder.endCell() +} \ No newline at end of file diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts new file mode 100644 index 00000000..8a0f8aa1 --- /dev/null +++ b/ccip-sdk/src/ton/index.ts @@ -0,0 +1,260 @@ +import { type BytesLike, isBytesLike } from 'ethers' +import type { PickDeep } from 'type-fest' +import { beginCell, Cell } from '@ton/core' +import { GenericExtraArgsV2, } from '../extra-args.ts' +import type { ExtraArgs } from '../extra-args.ts' +import { type LogFilter, Chain } from '../chain.ts' +import type { LeafHasher } from '../hasher/common.ts' +import { supportedChains } from '../supported-chains.ts' +import { + type AnyMessage, + type CCIPRequest, + type ChainTransaction, + type CommitReport, + type ExecutionReceipt, + type ExecutionReport, + type Lane, + type Log_, + type NetworkInfo, + type OffchainTokenData, + type CCIPMessage_V1_6, + ChainFamily, +} from '../types.ts' +import { getDataBytes } from '../utils.ts' + +type CCIPMessage_V1_6_TON = CCIPMessage_V1_6 & GenericExtraArgsV2 +const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2, 16) + +export class TONChain extends Chain { + static { + supportedChains[ChainFamily.TON] = TONChain + } + static readonly family = ChainFamily.TON + static readonly decimals = 8 + + readonly network: NetworkInfo + + constructor() { + super() + throw new Error('Not implemented') + } + + static async fromUrl(_url: string): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async getBlockTimestamp(_version: number | 'finalized'): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async getTransaction(_hash: string | number): Promise { + return Promise.reject(new Error('Not implemented')) + } + + // eslint-disable-next-line require-yield + async *getLogs(_opts: LogFilter & { versionAsHash?: boolean }) { + await Promise.resolve() + throw new Error('Not implemented') + } + + override async fetchRequestsInTx(_tx: string | ChainTransaction): Promise { + return Promise.reject(new Error('Not implemented')) + } + + override async fetchAllMessagesInBatch< + R extends PickDeep< + CCIPRequest, + 'lane' | `log.${'topics' | 'address' | 'blockNumber'}` | 'message.header.sequenceNumber' + >, + >( + _request: R, + _commit: Pick, + _opts?: { page?: number }, + ): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async typeAndVersion( + _address: string, + ): Promise< + | [type_: string, version: string, typeAndVersion: string] + | [type_: string, version: string, typeAndVersion: string, suffix: string] + > { + return Promise.reject(new Error('Not implemented')) + } + + getRouterForOnRamp(_onRamp: string, _destChainSelector: bigint): Promise { + return Promise.reject(new Error('Not implemented')) + } + + getRouterForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { + return Promise.reject(new Error('Not implemented')) + } + + getNativeTokenForRouter(_router: string): Promise { + return Promise.reject(new Error('Not implemented')) + } + + getOffRampsForRouter(_router: string, _sourceChainSelector: bigint): Promise { + return Promise.reject(new Error('Not implemented')) + } + + getOnRampForRouter(_router: string, _destChainSelector: bigint): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async getOnRampForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { + return Promise.reject(new Error('Not implemented')) + } + + getCommitStoreForOffRamp(_offRamp: string): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async getTokenForTokenPool(_tokenPool: string): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async getTokenInfo(_token: string): Promise<{ symbol: string; decimals: number }> { + return Promise.reject(new Error('Not implemented')) + } + + getTokenAdminRegistryFor(_address: string): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async getWalletAddress(_opts?: { wallet?: unknown }): Promise { + return Promise.reject(new Error('Not implemented')) + } + + // Static methods for decoding + static decodeMessage(_log: Log_): CCIPMessage_V1_6_TON | undefined { + throw new Error('Not implemented') + } + + /** + * Encodes extra args from TON messages into BOC serialization format. + * + * @param args - Extra arguments containing gas limit and execution flags + * @returns Hex string of BOC-encoded extra args (0x-prefixed) + */ + static encodeExtraArgs(args: ExtraArgs): string { + if (!args) return '0x' + if ('gasLimit' in args && 'allowOutOfOrderExecution' in args) { + const cell = beginCell() + .storeUint(GENERIC_V2_EXTRA_ARGS_TAG, 32) // magic tag + .storeUint(args.gasLimit, 256) // gasLimit + .storeBit(args.allowOutOfOrderExecution) // bool + .endCell() + + // Return full BOC including headers + return '0x' + cell.toBoc().toString('hex') + } + return '0x' + } + + /** + * Decodes BOC-encoded extra arguments from TON messages + * Parses the BOC format and extracts extra args, validating the magic tag + * to ensure correct type. Returns undefined if parsing fails or tag doesn't match. + * + * @param extraArgs - BOC-encoded extra args as hex string or bytes + * @returns Decoded GenericExtraArgsV2 object or undefined if invalid + */ + static decodeExtraArgs( + extraArgs: BytesLike, + ): (GenericExtraArgsV2 & { _tag: 'GenericExtraArgsV2' }) | undefined { + const data = Buffer.from(getDataBytes(extraArgs)) + + try { + // Parse BOC format to extract cell data + const cell = Cell.fromBoc(data)[0] + const slice = cell.beginParse() + + // Load and verify magic tag to ensure correct extra args type + const magicTag = slice.loadUint(32) + if (magicTag !== GENERIC_V2_EXTRA_ARGS_TAG) return undefined + + return { + _tag: 'GenericExtraArgsV2', + gasLimit: slice.loadUintBig(256), + allowOutOfOrderExecution: slice.loadBit(), + } + } catch { + // Return undefined for any parsing errors (invalid BOC, malformed data, etc.) + return undefined + } + } + + static decodeCommits(_log: Log_, _lane?: Lane): CommitReport[] | undefined { + throw new Error('Not implemented') + } + + static decodeReceipt(_log: Log_): ExecutionReceipt | undefined { + throw new Error('Not implemented') + } + + static getAddress(bytes: BytesLike): string { + throw new Error('Not implemented') + } + + static getDestLeafHasher(_lane: Lane): LeafHasher { + throw new Error('Not implemented') + } + + async getFee(_router: string, _destChainSelector: bigint, _message: AnyMessage): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async sendMessage( + _router: string, + _destChainSelector: bigint, + _message: AnyMessage & { fee: bigint }, + _opts?: { wallet?: unknown; approveMax?: boolean }, + ): Promise { + return Promise.reject(new Error('Not implemented')) + } + + fetchOffchainTokenData(request: CCIPRequest): Promise { + if (!('receiverObjectIds' in request.message)) { + throw new Error('Invalid message, not v1.6 TON') + } + // default offchain token data + return Promise.resolve(request.message.tokenAmounts.map(() => undefined)) + } + + async executeReport( + _offRamp: string, + _execReport: ExecutionReport, + _opts?: { wallet?: unknown; gasLimit?: number }, + ): Promise { + return Promise.reject(new Error('Not implemented')) + } + + static parse(data: unknown) { + if (isBytesLike(data)) { + const parsedExtraArgs = this.decodeExtraArgs(data) + if (parsedExtraArgs) return parsedExtraArgs + } + } + + async getSupportedTokens(_address: string): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async getRegistryTokenConfig(_address: string, _tokenName: string): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async getTokenPoolConfigs(_tokenPool: string): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async getTokenPoolRemotes(_tokenPool: string): Promise { + return Promise.reject(new Error('Not implemented')) + } + + async getFeeTokens(_router: string): Promise { + return Promise.reject(new Error('Not implemented')) + } +} diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts new file mode 100644 index 00000000..53d57de9 --- /dev/null +++ b/ccip-sdk/src/ton/utils.ts @@ -0,0 +1,57 @@ +import { beginCell, Cell } from '@ton/core' +import { createHash } from 'crypto' + +/** + * Computes SHA256 hash of data and returns as hex string + * Used throughout TON hasher for domain separation and message hashing + */ +export const sha256 = (data: Uint8Array): string => { + return '0x' + createHash('sha256').update(data).digest('hex') +} + +/** + * Converts hex string to Buffer, handling 0x prefix normalization + * Returns empty buffer for empty input + */ +export const hexToBuffer = (value: string): Buffer => { + const normalized = value.startsWith('0x') || value.startsWith('0X') ? value.slice(2) : value + return normalized.length === 0 ? Buffer.alloc(0) : Buffer.from(normalized, 'hex') +} + +/** + * Converts various numeric types to BigInt for TON's big integer operations + * Used throughout the hasher for chain selectors, amounts, and sequence numbers + */ +export const toBigInt = (value: bigint | number | string): bigint => { + if (typeof value === 'bigint') return value + if (typeof value === 'number') return BigInt(value) + return BigInt(value) +} + +/** + * Attempts to parse hex string as TON BOC (Bag of Cells) format + * Falls back to storing raw bytes as cell data if BOC parsing fails + * Used for parsing message data, extra data, and other hex-encoded fields + */ +export const tryParseCell = (hex: string): Cell => { + const bytes = hexToBuffer(hex) + if (bytes.length === 0) return beginCell().endCell() + try { + return Cell.fromBoc(bytes)[0] + } catch { + return beginCell().storeBuffer(bytes).endCell() + } +} + + +/** + * Extracts the 32-bit magic tag from a BOC-encoded cell + * Magic tags identify the type of TON structures (e.g., extra args types) + * Used for type detection and validation when decoding CCIP extra args + * Returns tag as 0x-prefixed hex string for easy comparison + */ +export function extractMagicTag(bocHex: string): string { + const cell = Cell.fromBoc(hexToBuffer(bocHex))[0] + const tag = cell.beginParse().loadUint(32) + return `0x${tag.toString(16).padStart(8, '0')}` +} diff --git a/ccip-sdk/src/types.ts b/ccip-sdk/src/types.ts index b8181c69..918a3368 100644 --- a/ccip-sdk/src/types.ts +++ b/ccip-sdk/src/types.ts @@ -43,6 +43,7 @@ export const ChainFamily = { Solana: 'solana', Aptos: 'aptos', Sui: 'sui', + TON: 'ton', } as const export type ChainFamily = (typeof ChainFamily)[keyof typeof ChainFamily] diff --git a/package-lock.json b/package-lock.json index 702341dc..bb4a308f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,6 +101,7 @@ "@mysten/sui": "^1.45.2", "@solana/spl-token": "0.4.14", "@solana/web3.js": "^1.98.4", + "@ton/core": "^0.62.0", "abitype": "1.2.0", "bn.js": "^5.2.2", "borsh": "^2.0.0", @@ -2690,6 +2691,40 @@ "node": ">=10" } }, + "node_modules/@ton/core": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.62.0.tgz", + "integrity": "sha512-GCYlzzx11rSESKkiHvNy9tL8zWth+ZtUbvV29WH478FvBp8xTw24AyoigwXWNV+OLCAcnwlGhZpTpxjD3wzCwA==", + "license": "MIT", + "dependencies": { + "symbol.inspect": "1.0.1" + }, + "peerDependencies": { + "@ton/crypto": ">=3.2.0" + } + }, + "node_modules/@ton/crypto": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz", + "integrity": "sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@ton/crypto-primitives": "2.1.0", + "jssha": "3.2.0", + "tweetnacl": "1.0.3" + } + }, + "node_modules/@ton/crypto-primitives": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz", + "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==", + "license": "MIT", + "peer": true, + "dependencies": { + "jssha": "3.2.0" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -6543,6 +6578,16 @@ "json5": "lib/cli.js" } }, + "node_modules/jssha": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", + "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/jwt-decode": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", @@ -8103,6 +8148,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol.inspect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol.inspect/-/symbol.inspect-1.0.1.tgz", + "integrity": "sha512-YQSL4duoHmLhsTD1Pw8RW6TZ5MaTX5rXJnqacJottr2P2LZBF/Yvrc3ku4NUpMOm8aM0KOCqM+UAkMA5HWQCzQ==", + "license": "ISC" + }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", @@ -8343,6 +8394,13 @@ "node": "*" } }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense", + "peer": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", From 3358322c38e7c1371f8cb895f02593800198238a Mon Sep 17 00:00:00 2001 From: Farber98 Date: Thu, 4 Dec 2025 16:52:01 -0300 Subject: [PATCH 03/33] prettier --- ccip-sdk/src/extra-args.test.ts | 8 ++- ccip-sdk/src/extra-args.ts | 7 ++- ccip-sdk/src/ton/hasher.ts | 94 +++++++++++++++------------------ ccip-sdk/src/ton/index.ts | 38 ++++++------- ccip-sdk/src/ton/utils.ts | 67 ++++++++++++----------- 5 files changed, 108 insertions(+), 106 deletions(-) diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index 9f560340..9aaa2739 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -4,7 +4,12 @@ import { extractMagicTag } from './ton/utils.ts' import { dataSlice, getNumber } from 'ethers' // Import index.ts to ensure all Chain classes are loaded and registered import './index.ts' -import { EVMExtraArgsV2Tag, GenericExtraArgsV2, decodeExtraArgs, encodeExtraArgs } from './extra-args.ts' +import { + EVMExtraArgsV2Tag, + GenericExtraArgsV2, + decodeExtraArgs, + encodeExtraArgs, +} from './extra-args.ts' import { ChainFamily } from './types.ts' describe('encodeExtraArgs', () => { @@ -285,5 +290,4 @@ describe('parseExtraArgs', () => { assert.notEqual(evmEncoded, tonEncoded) }) }) - }) diff --git a/ccip-sdk/src/extra-args.ts b/ccip-sdk/src/extra-args.ts index 79c89f63..fb87f582 100644 --- a/ccip-sdk/src/extra-args.ts +++ b/ccip-sdk/src/extra-args.ts @@ -31,7 +31,12 @@ export type SuiExtraArgsV1 = EVMExtraArgsV2 & { // Same structure as EVMExtraArgsV2. TON calls it GenericExtraArgsV2 export type GenericExtraArgsV2 = EVMExtraArgsV2 -export type ExtraArgs = EVMExtraArgsV1 | EVMExtraArgsV2 | SVMExtraArgsV1 | SuiExtraArgsV1 | GenericExtraArgsV2 +export type ExtraArgs = + | EVMExtraArgsV1 + | EVMExtraArgsV2 + | SVMExtraArgsV1 + | SuiExtraArgsV1 + | GenericExtraArgsV2 /** * Encodes extra arguments for CCIP messages. diff --git a/ccip-sdk/src/ton/hasher.ts b/ccip-sdk/src/ton/hasher.ts index 1cd15fa4..596887ed 100644 --- a/ccip-sdk/src/ton/hasher.ts +++ b/ccip-sdk/src/ton/hasher.ts @@ -5,17 +5,17 @@ import { type CCIPMessage, type CCIPMessage_V1_6, CCIPVersion } from '../types.t import { networkInfo } from '../utils.ts' import { hexToBuffer, toBigInt, tryParseCell, sha256 } from './utils.ts' -// Convert LEAF_DOMAIN_SEPARATOR from hex string to Buffer for cell storage +// Convert LEAF_DOMAIN_SEPARATOR from hex string to Buffer for cell storage const LEAF_DOMAIN_BUFFER = Buffer.from(LEAF_DOMAIN_SEPARATOR.slice(2).padStart(64, '0'), 'hex') -/** - * Creates a leaf hasher for TON messages - * +/** + * Creates a leaf hasher for TON messages + * * @param sourceChainSelector * @param destChainSelector - * @param onRamp - as hex string - * @param version - CCIP version (only v1.6 supported for TON) - * @returns A LeafHasher function that computes message hashes for TON + * @param onRamp - as hex string + * @param version - CCIP version (only v1.6 supported for TON) + * @returns A LeafHasher function that computes message hashes for TON */ export function getTONLeafHasher({ sourceChainSelector, @@ -32,31 +32,31 @@ export function getTONLeafHasher({ throw new Error(`TON only supports CCIP v1.6, got: ${version}`) } - // Pre-compute metadata hash once for all messages using this hasher + // Pre-compute metadata hash once for all messages using this hasher const metadataHash = hashTONMetadata(sourceChainSelector, destChainSelector, onRamp) - // Return the actual hashing function that will be called for each message + // Return the actual hashing function that will be called for each message return ((message: CCIPMessage): string => { return hashV16TONMessage(message, metadataHash) }) as LeafHasher } -/** - * Creates a hash that uniquely identifies the message lane configuration - * (source chain, destination chain, and onRamp address). - * Following the TON implementation from chainlink-ton repo. - * - * @param sourceChainSelector - * @param destChainSelector - * @param onRamp - * @returns SHA256 hash of the metadata as hex string +/** + * Creates a hash that uniquely identifies the message lane configuration + * (source chain, destination chain, and onRamp address). + * Following the TON implementation from chainlink-ton repo. + * + * @param sourceChainSelector + * @param destChainSelector + * @param onRamp + * @returns SHA256 hash of the metadata as hex string */ export const hashTONMetadata = ( sourceChainSelector: bigint, destChainSelector: bigint, onRamp: string, ): string => { - // Domain separator for TON messages + // Domain separator for TON messages const versionHash = BigInt(sha256(Buffer.from('Any2TVMMessageHashV1'))) const onRampBytes = hexToBuffer(onRamp) @@ -66,29 +66,23 @@ export const hashTONMetadata = ( .storeUint(sourceChainSelector, 64) .storeUint(destChainSelector, 64) .storeRef( - beginCell() - .storeUint(BigInt(onRampBytes.length), 8) - .storeBuffer(onRampBytes) - .endCell(), + beginCell().storeUint(BigInt(onRampBytes.length), 8).storeBuffer(onRampBytes).endCell(), ) .endCell() - // Return cell hash as hex string (excludes BOC headers) + // Return cell hash as hex string (excludes BOC headers) return '0x' + metadataCell.hash().toString('hex') } -/** - * Computes the full message hash for a CCIP v1.6 TON message - * Follows the chainlink-ton's Any2TVMRampMessage.generateMessageId() - * - * @param message - CCIP message to hash - * @param metadataHash - Pre-computed metadata hash from hashTONMetadata() - * @returns SHA256 hash of the complete message as hex string +/** + * Computes the full message hash for a CCIP v1.6 TON message + * Follows the chainlink-ton's Any2TVMRampMessage.generateMessageId() + * + * @param message - CCIP message to hash + * @param metadataHash - Pre-computed metadata hash from hashTONMetadata() + * @returns SHA256 hash of the complete message as hex string */ -function hashV16TONMessage( - message: CCIPMessage_V1_6, - metadataHash: string, -): string { +function hashV16TONMessage(message: CCIPMessage_V1_6, metadataHash: string): string { // Extract gas limit from message let gasLimit: bigint const embeddedGasLimit = (message as Partial<{ gasLimit: bigint }>).gasLimit @@ -106,7 +100,7 @@ function hashV16TONMessage( gasLimit = parsedArgs.gasLimit || 0n } - // Build header cell containing header routing information + // Build header cell containing header routing information const headerCell = beginCell() .storeUint(toBigInt(message.header.messageId), 256) .storeAddress(Address.parse(message.receiver)) @@ -115,14 +109,14 @@ function hashV16TONMessage( .storeUint(toBigInt(message.header.nonce), 64) .endCell() - // Build sender cell with address bytes + // Build sender cell with address bytes const senderBytes = hexToBuffer(message.sender) const senderCell = beginCell() .storeUint(BigInt(senderBytes.length), 8) .storeBuffer(senderBytes) .endCell() - // Build token amounts cell if tokens are being transferred + // Build token amounts cell if tokens are being transferred const tokenAmountsCell = message.tokenAmounts.length > 0 ? buildTokenAmountsCell(message.tokenAmounts) : null @@ -136,25 +130,25 @@ function hashV16TONMessage( .storeMaybeRef(tokenAmountsCell) .endCell() - // Return cell hash as hex string + // Return cell hash as hex string return '0x' + messageCell.hash().toString('hex') } -// Type alias for token amount entries in CCIP messages +// Type alias for token amount entries in CCIP messages type TokenAmount = CCIPMessage_V1_6['tokenAmounts'][number] -/** - * Creates a nested cell structure for token amounts, where each token - * transfer is stored as a reference cell containing source pool, destination, - * amount, and extra data. - * - * @param tokenAmounts - Array of token transfer details - * @returns Cell containing all token transfer information +/** + * Creates a nested cell structure for token amounts, where each token + * transfer is stored as a reference cell containing source pool, destination, + * amount, and extra data. + * + * @param tokenAmounts - Array of token transfer details + * @returns Cell containing all token transfer information */ function buildTokenAmountsCell(tokenAmounts: readonly TokenAmount[]): Cell { const builder = beginCell() - // Process each token transfer + // Process each token transfer for (const ta of tokenAmounts) { const sourcePoolBytes = hexToBuffer(ta.sourcePoolAddress) @@ -165,7 +159,7 @@ function buildTokenAmountsCell(tokenAmounts: readonly TokenAmount[]): Cell { 0n const amount = toBigInt(amountSource) - // Store each token transfer as a reference cell + // Store each token transfer as a reference cell builder.storeRef( beginCell() .storeRef( @@ -182,4 +176,4 @@ function buildTokenAmountsCell(tokenAmounts: readonly TokenAmount[]): Cell { } return builder.endCell() -} \ No newline at end of file +} diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 8a0f8aa1..b6612987 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -1,7 +1,7 @@ import { type BytesLike, isBytesLike } from 'ethers' import type { PickDeep } from 'type-fest' import { beginCell, Cell } from '@ton/core' -import { GenericExtraArgsV2, } from '../extra-args.ts' +import { GenericExtraArgsV2 } from '../extra-args.ts' import type { ExtraArgs } from '../extra-args.ts' import { type LogFilter, Chain } from '../chain.ts' import type { LeafHasher } from '../hasher/common.ts' @@ -132,19 +132,19 @@ export class TONChain extends Chain { throw new Error('Not implemented') } - /** - * Encodes extra args from TON messages into BOC serialization format. - * - * @param args - Extra arguments containing gas limit and execution flags - * @returns Hex string of BOC-encoded extra args (0x-prefixed) + /** + * Encodes extra args from TON messages into BOC serialization format. + * + * @param args - Extra arguments containing gas limit and execution flags + * @returns Hex string of BOC-encoded extra args (0x-prefixed) */ static encodeExtraArgs(args: ExtraArgs): string { if (!args) return '0x' if ('gasLimit' in args && 'allowOutOfOrderExecution' in args) { const cell = beginCell() - .storeUint(GENERIC_V2_EXTRA_ARGS_TAG, 32) // magic tag - .storeUint(args.gasLimit, 256) // gasLimit - .storeBit(args.allowOutOfOrderExecution) // bool + .storeUint(GENERIC_V2_EXTRA_ARGS_TAG, 32) // magic tag + .storeUint(args.gasLimit, 256) // gasLimit + .storeBit(args.allowOutOfOrderExecution) // bool .endCell() // Return full BOC including headers @@ -153,13 +153,13 @@ export class TONChain extends Chain { return '0x' } - /** - * Decodes BOC-encoded extra arguments from TON messages - * Parses the BOC format and extracts extra args, validating the magic tag - * to ensure correct type. Returns undefined if parsing fails or tag doesn't match. - * - * @param extraArgs - BOC-encoded extra args as hex string or bytes - * @returns Decoded GenericExtraArgsV2 object or undefined if invalid + /** + * Decodes BOC-encoded extra arguments from TON messages + * Parses the BOC format and extracts extra args, validating the magic tag + * to ensure correct type. Returns undefined if parsing fails or tag doesn't match. + * + * @param extraArgs - BOC-encoded extra args as hex string or bytes + * @returns Decoded GenericExtraArgsV2 object or undefined if invalid */ static decodeExtraArgs( extraArgs: BytesLike, @@ -167,11 +167,11 @@ export class TONChain extends Chain { const data = Buffer.from(getDataBytes(extraArgs)) try { - // Parse BOC format to extract cell data + // Parse BOC format to extract cell data const cell = Cell.fromBoc(data)[0] const slice = cell.beginParse() - // Load and verify magic tag to ensure correct extra args type + // Load and verify magic tag to ensure correct extra args type const magicTag = slice.loadUint(32) if (magicTag !== GENERIC_V2_EXTRA_ARGS_TAG) return undefined @@ -181,7 +181,7 @@ export class TONChain extends Chain { allowOutOfOrderExecution: slice.loadBit(), } } catch { - // Return undefined for any parsing errors (invalid BOC, malformed data, etc.) + // Return undefined for any parsing errors (invalid BOC, malformed data, etc.) return undefined } } diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts index 53d57de9..6b6dd704 100644 --- a/ccip-sdk/src/ton/utils.ts +++ b/ccip-sdk/src/ton/utils.ts @@ -1,57 +1,56 @@ import { beginCell, Cell } from '@ton/core' import { createHash } from 'crypto' -/** - * Computes SHA256 hash of data and returns as hex string - * Used throughout TON hasher for domain separation and message hashing +/** + * Computes SHA256 hash of data and returns as hex string + * Used throughout TON hasher for domain separation and message hashing */ export const sha256 = (data: Uint8Array): string => { - return '0x' + createHash('sha256').update(data).digest('hex') + return '0x' + createHash('sha256').update(data).digest('hex') } -/** - * Converts hex string to Buffer, handling 0x prefix normalization +/** + * Converts hex string to Buffer, handling 0x prefix normalization * Returns empty buffer for empty input */ export const hexToBuffer = (value: string): Buffer => { - const normalized = value.startsWith('0x') || value.startsWith('0X') ? value.slice(2) : value - return normalized.length === 0 ? Buffer.alloc(0) : Buffer.from(normalized, 'hex') + const normalized = value.startsWith('0x') || value.startsWith('0X') ? value.slice(2) : value + return normalized.length === 0 ? Buffer.alloc(0) : Buffer.from(normalized, 'hex') } -/** - * Converts various numeric types to BigInt for TON's big integer operations - * Used throughout the hasher for chain selectors, amounts, and sequence numbers +/** + * Converts various numeric types to BigInt for TON's big integer operations + * Used throughout the hasher for chain selectors, amounts, and sequence numbers */ export const toBigInt = (value: bigint | number | string): bigint => { - if (typeof value === 'bigint') return value - if (typeof value === 'number') return BigInt(value) - return BigInt(value) + if (typeof value === 'bigint') return value + if (typeof value === 'number') return BigInt(value) + return BigInt(value) } -/** - * Attempts to parse hex string as TON BOC (Bag of Cells) format - * Falls back to storing raw bytes as cell data if BOC parsing fails - * Used for parsing message data, extra data, and other hex-encoded fields +/** + * Attempts to parse hex string as TON BOC (Bag of Cells) format + * Falls back to storing raw bytes as cell data if BOC parsing fails + * Used for parsing message data, extra data, and other hex-encoded fields */ export const tryParseCell = (hex: string): Cell => { - const bytes = hexToBuffer(hex) - if (bytes.length === 0) return beginCell().endCell() - try { - return Cell.fromBoc(bytes)[0] - } catch { - return beginCell().storeBuffer(bytes).endCell() - } + const bytes = hexToBuffer(hex) + if (bytes.length === 0) return beginCell().endCell() + try { + return Cell.fromBoc(bytes)[0] + } catch { + return beginCell().storeBuffer(bytes).endCell() + } } - -/** - * Extracts the 32-bit magic tag from a BOC-encoded cell - * Magic tags identify the type of TON structures (e.g., extra args types) - * Used for type detection and validation when decoding CCIP extra args - * Returns tag as 0x-prefixed hex string for easy comparison +/** + * Extracts the 32-bit magic tag from a BOC-encoded cell + * Magic tags identify the type of TON structures (e.g., extra args types) + * Used for type detection and validation when decoding CCIP extra args + * Returns tag as 0x-prefixed hex string for easy comparison */ export function extractMagicTag(bocHex: string): string { - const cell = Cell.fromBoc(hexToBuffer(bocHex))[0] - const tag = cell.beginParse().loadUint(32) - return `0x${tag.toString(16).padStart(8, '0')}` + const cell = Cell.fromBoc(hexToBuffer(bocHex))[0] + const tag = cell.beginParse().loadUint(32) + return `0x${tag.toString(16).padStart(8, '0')}` } From e02ff2def147d96befc45855300765879e65e6b9 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Thu, 4 Dec 2025 17:18:19 -0300 Subject: [PATCH 04/33] fix eslint --- ccip-sdk/src/chain.ts | 2 +- ccip-sdk/src/extra-args.test.ts | 4 +++- ccip-sdk/src/extra-args.ts | 7 +------ ccip-sdk/src/index.ts | 2 +- ccip-sdk/src/ton/hasher.ts | 5 +++-- ccip-sdk/src/ton/index.ts | 10 +++++----- ccip-sdk/src/ton/utils.ts | 3 ++- 7 files changed, 16 insertions(+), 17 deletions(-) diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index dbdfeac2..c438c679 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -8,9 +8,9 @@ import type { EVMExtraArgsV1, EVMExtraArgsV2, ExtraArgs, + GenericExtraArgsV2, SVMExtraArgsV1, SuiExtraArgsV1, - GenericExtraArgsV2, } from './extra-args.ts' import type { LeafHasher } from './hasher/common.ts' import { diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index 9aaa2739..aea54db2 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -1,7 +1,8 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' -import { extractMagicTag } from './ton/utils.ts' + import { dataSlice, getNumber } from 'ethers' + // Import index.ts to ensure all Chain classes are loaded and registered import './index.ts' import { @@ -10,6 +11,7 @@ import { decodeExtraArgs, encodeExtraArgs, } from './extra-args.ts' +import { extractMagicTag } from './ton/utils.ts' import { ChainFamily } from './types.ts' describe('encodeExtraArgs', () => { diff --git a/ccip-sdk/src/extra-args.ts b/ccip-sdk/src/extra-args.ts index fb87f582..9a53bbaf 100644 --- a/ccip-sdk/src/extra-args.ts +++ b/ccip-sdk/src/extra-args.ts @@ -31,12 +31,7 @@ export type SuiExtraArgsV1 = EVMExtraArgsV2 & { // Same structure as EVMExtraArgsV2. TON calls it GenericExtraArgsV2 export type GenericExtraArgsV2 = EVMExtraArgsV2 -export type ExtraArgs = - | EVMExtraArgsV1 - | EVMExtraArgsV2 - | SVMExtraArgsV1 - | SuiExtraArgsV1 - | GenericExtraArgsV2 +export type ExtraArgs = EVMExtraArgsV1 | EVMExtraArgsV2 | SVMExtraArgsV1 | SuiExtraArgsV1 /** * Encodes extra arguments for CCIP messages. diff --git a/ccip-sdk/src/index.ts b/ccip-sdk/src/index.ts index 637f5a80..a378c339 100644 --- a/ccip-sdk/src/index.ts +++ b/ccip-sdk/src/index.ts @@ -12,9 +12,9 @@ export { type EVMExtraArgsV1, type EVMExtraArgsV2, type ExtraArgs, + type GenericExtraArgsV2, type SVMExtraArgsV1, type SuiExtraArgsV1, - type GenericExtraArgsV2, decodeExtraArgs, encodeExtraArgs, } from './extra-args.ts' diff --git a/ccip-sdk/src/ton/hasher.ts b/ccip-sdk/src/ton/hasher.ts index 596887ed..a4e44bb0 100644 --- a/ccip-sdk/src/ton/hasher.ts +++ b/ccip-sdk/src/ton/hasher.ts @@ -1,9 +1,10 @@ -import { Address, beginCell, Cell } from '@ton/core' +import { type Cell, Address, beginCell } from '@ton/core' + import { decodeExtraArgs } from '../extra-args.ts' import { type LeafHasher, LEAF_DOMAIN_SEPARATOR } from '../hasher/common.ts' import { type CCIPMessage, type CCIPMessage_V1_6, CCIPVersion } from '../types.ts' import { networkInfo } from '../utils.ts' -import { hexToBuffer, toBigInt, tryParseCell, sha256 } from './utils.ts' +import { hexToBuffer, sha256, toBigInt, tryParseCell } from './utils.ts' // Convert LEAF_DOMAIN_SEPARATOR from hex string to Buffer for cell storage const LEAF_DOMAIN_BUFFER = Buffer.from(LEAF_DOMAIN_SEPARATOR.slice(2).padStart(64, '0'), 'hex') diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index b6612987..c43d57b8 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -1,13 +1,14 @@ +import { Cell, beginCell } from '@ton/core' import { type BytesLike, isBytesLike } from 'ethers' import type { PickDeep } from 'type-fest' -import { beginCell, Cell } from '@ton/core' -import { GenericExtraArgsV2 } from '../extra-args.ts' -import type { ExtraArgs } from '../extra-args.ts' + import { type LogFilter, Chain } from '../chain.ts' +import { type ExtraArgs, GenericExtraArgsV2 } from '../extra-args.ts' import type { LeafHasher } from '../hasher/common.ts' import { supportedChains } from '../supported-chains.ts' import { type AnyMessage, + type CCIPMessage_V1_6, type CCIPRequest, type ChainTransaction, type CommitReport, @@ -17,7 +18,6 @@ import { type Log_, type NetworkInfo, type OffchainTokenData, - type CCIPMessage_V1_6, ChainFamily, } from '../types.ts' import { getDataBytes } from '../utils.ts' @@ -194,7 +194,7 @@ export class TONChain extends Chain { throw new Error('Not implemented') } - static getAddress(bytes: BytesLike): string { + static getAddress(_bytes: BytesLike): string { throw new Error('Not implemented') } diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts index 6b6dd704..52524942 100644 --- a/ccip-sdk/src/ton/utils.ts +++ b/ccip-sdk/src/ton/utils.ts @@ -1,6 +1,7 @@ -import { beginCell, Cell } from '@ton/core' import { createHash } from 'crypto' +import { Cell, beginCell } from '@ton/core' + /** * Computes SHA256 hash of data and returns as hex string * Used throughout TON hasher for domain separation and message hashing From e98b4080ecca74d7e0a06d2fb592c1b2923076ec Mon Sep 17 00:00:00 2001 From: Farber98 Date: Fri, 5 Dec 2025 15:36:19 -0300 Subject: [PATCH 05/33] hasher and util tests --- ccip-sdk/src/ton/hasher.test.ts | 159 ++++++++++++++++++++++++++++++++ ccip-sdk/src/ton/utils.test.ts | 143 ++++++++++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 ccip-sdk/src/ton/hasher.test.ts create mode 100644 ccip-sdk/src/ton/utils.test.ts diff --git a/ccip-sdk/src/ton/hasher.test.ts b/ccip-sdk/src/ton/hasher.test.ts new file mode 100644 index 00000000..1323e6dc --- /dev/null +++ b/ccip-sdk/src/ton/hasher.test.ts @@ -0,0 +1,159 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { type CCIPMessage_V1_6, CCIPVersion } from '../types.ts' +import { getTONLeafHasher, hashTONMetadata } from './hasher.ts' + +const ZERO_ADDRESS = '0x' + '0'.repeat(40) +const TON_RECEIVER = '0:' + '3'.repeat(64) + +describe('TON hasher', () => { + const sourceChainSelector = 743186221051783445n + const destChainSelector = 16015286601757825753n + const onRamp = '0x1234567890123456789012345678901234567890' + + describe('hashTONMetadata', () => { + it('should create consistent metadata hash', () => { + const hash1 = hashTONMetadata(sourceChainSelector, destChainSelector, onRamp) + const hash2 = hashTONMetadata(sourceChainSelector, destChainSelector, onRamp) + + assert.equal(hash1, hash2) + assert.match(hash1, /^0x[a-f0-9]{64}$/) + }) + + it('should create different hashes for different parameters', () => { + const hash1 = hashTONMetadata(sourceChainSelector, destChainSelector, onRamp) + const hash2 = hashTONMetadata(sourceChainSelector + 1n, destChainSelector, onRamp) + + assert.notEqual(hash1, hash2) + }) + }) + + describe('getTONLeafHasher', () => { + it('should throw error for unsupported version', () => { + assert.throws(() => { + getTONLeafHasher({ + sourceChainSelector, + destChainSelector, + onRamp, + version: CCIPVersion.V1_2, + }) + }, /TON only supports CCIP v1.6/) + }) + + it('should create hasher for v1.6', () => { + const hasher = getTONLeafHasher({ + sourceChainSelector, + destChainSelector, + onRamp, + version: CCIPVersion.V1_6, + }) + + assert.equal(typeof hasher, 'function') + }) + }) + + describe('message hashing', () => { + const hasher = getTONLeafHasher({ + sourceChainSelector, + destChainSelector, + onRamp, + version: CCIPVersion.V1_6, + }) + + it('should hash basic message', () => { + const message: CCIPMessage_V1_6 & { + gasLimit: bigint + allowOutOfOrderExecution: boolean + } = { + header: { + messageId: '0x' + '1'.repeat(64), + sequenceNumber: 123n, + nonce: 456n, + sourceChainSelector, + destChainSelector, + }, + sender: '0x' + '2'.repeat(40), + receiver: TON_RECEIVER, + data: '0x1234', + extraArgs: '0x181dcf10000000000000000000000000000000000000000000000000000000000000000001', + gasLimit: 0n, + allowOutOfOrderExecution: true, + tokenAmounts: [] as CCIPMessage_V1_6['tokenAmounts'], + feeToken: ZERO_ADDRESS, + feeTokenAmount: 0n, + feeValueJuels: 0n, + } + + const hash = hasher(message) + assert.match(hash, /^0x[a-f0-9]{64}$/) + }) + + it('should hash message with tokens', () => { + const tokenAmounts: CCIPMessage_V1_6['tokenAmounts'] = [ + { + sourcePoolAddress: '0:' + '4'.repeat(64), + destTokenAddress: '0:' + '5'.repeat(64), + extraData: '0x', + destGasAmount: 1000n, + amount: 1000n, + destExecData: '0x', + }, + ] + + const message: CCIPMessage_V1_6 & { + gasLimit: bigint + allowOutOfOrderExecution: boolean + } = { + header: { + messageId: '0x' + '1'.repeat(64), + sequenceNumber: 123n, + nonce: 456n, + sourceChainSelector, + destChainSelector, + }, + sender: '0x' + '2'.repeat(40), + receiver: TON_RECEIVER, + data: '0x1234', + extraArgs: '0x181dcf10000000000000000000000000000000000000000000000000000000000000000001', + gasLimit: 0n, + allowOutOfOrderExecution: true, + tokenAmounts, + feeToken: ZERO_ADDRESS, + feeTokenAmount: 0n, + feeValueJuels: 0n, + } + + const hash = hasher(message) + assert.match(hash, /^0x[a-f0-9]{64}$/) + }) + + it('should handle embedded gasLimit', () => { + const message: CCIPMessage_V1_6 & { + gasLimit: bigint + allowOutOfOrderExecution: boolean + } = { + header: { + messageId: '0x' + '1'.repeat(64), + sequenceNumber: 123n, + nonce: 456n, + sourceChainSelector, + destChainSelector, + }, + sender: '0x' + '2'.repeat(40), + receiver: TON_RECEIVER, + data: '0x1234', + extraArgs: '0x', + gasLimit: 500000n, + allowOutOfOrderExecution: false, + tokenAmounts: [] as CCIPMessage_V1_6['tokenAmounts'], + feeToken: ZERO_ADDRESS, + feeTokenAmount: 0n, + feeValueJuels: 0n, + } + + const hash = hasher(message) + assert.match(hash, /^0x[a-f0-9]{64}$/) + }) + }) +}) diff --git a/ccip-sdk/src/ton/utils.test.ts b/ccip-sdk/src/ton/utils.test.ts new file mode 100644 index 00000000..9ff3e8e1 --- /dev/null +++ b/ccip-sdk/src/ton/utils.test.ts @@ -0,0 +1,143 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { beginCell } from '@ton/core' + +import { extractMagicTag, hexToBuffer, sha256, toBigInt, tryParseCell } from './utils.ts' +import { + EVMExtraArgsV1Tag, + GenericExtraArgsV2, + SVMExtraArgsV1Tag, + SuiExtraArgsV1Tag, +} from '../extra-args.ts' + +describe('TON utils', () => { + describe('sha256', () => { + it('should compute SHA256 hash of data', () => { + const data = new Uint8Array([1, 2, 3, 4]) + const hash = sha256(data) + assert.match(hash, /^0x[a-f0-9]{64}$/) + assert.equal(hash, '0x9f64a747e1b97f131fabb6b447296c9b6f0201e79fb3c5356e6c77e89b6a806a') + }) + + it('should handle empty data', () => { + const hash = sha256(new Uint8Array()) + assert.equal(hash, '0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') + }) + }) + + describe('hexToBuffer', () => { + it('should convert hex string with 0x prefix', () => { + const buffer = hexToBuffer('0x48656c6c6f') + assert.deepEqual(buffer, Buffer.from('Hello')) + }) + + it('should convert hex string without 0x prefix', () => { + const buffer = hexToBuffer('48656c6c6f') + assert.deepEqual(buffer, Buffer.from('Hello')) + }) + + it('should handle uppercase 0X prefix', () => { + const buffer = hexToBuffer('0X48656c6c6f') + assert.deepEqual(buffer, Buffer.from('Hello')) + }) + + it('should return empty buffer for empty input', () => { + const buffer = hexToBuffer('') + assert.deepEqual(buffer, Buffer.alloc(0)) + }) + + it('should return empty buffer for just 0x', () => { + const buffer = hexToBuffer('0x') + assert.deepEqual(buffer, Buffer.alloc(0)) + }) + }) + + describe('toBigInt', () => { + it('should return bigint unchanged', () => { + const result = toBigInt(123n) + assert.equal(result, 123n) + }) + + it('should convert number to bigint', () => { + const result = toBigInt(123) + assert.equal(result, 123n) + }) + + it('should convert string to bigint', () => { + const result = toBigInt('123') + assert.equal(result, 123n) + }) + + it('should convert hex string to bigint', () => { + const result = toBigInt('0x7b') + assert.equal(result, 123n) + }) + }) + + describe('tryParseCell', () => { + it('should parse valid BOC format', () => { + const cell = beginCell().storeUint(0x12345678, 32).endCell() + + const bocHex = '0x' + cell.toBoc().toString('hex') + const parsed = tryParseCell(bocHex) + + assert.equal(parsed.beginParse().loadUint(32), 0x12345678) + }) + + it('should fall back to raw bytes for invalid BOC', () => { + const rawHex = '0x48656c6c6f' // "Hello" in hex + const cell = tryParseCell(rawHex) + + assert.deepEqual(cell.beginParse().loadBuffer(5), Buffer.from('Hello')) + }) + + it('should return empty cell for empty input', () => { + const cell = tryParseCell('') + const slice = cell.beginParse() + assert.equal(slice.remainingBits, 0) + assert.equal(slice.remainingRefs, 0) + }) + }) + + describe('extractMagicTag', () => { + it('should extract magic tag from BOC', () => { + const cell = beginCell() + .storeUint(Number(GenericExtraArgsV2), 32) + .storeUint(123456, 256) + .storeBit(true) + .endCell() + + const bocHex = '0x' + cell.toBoc().toString('hex') + const tag = extractMagicTag(bocHex) + + assert.equal(tag, '0x181dcf10') + }) + + it('should pad tag to 8 hex digits', () => { + const cell = beginCell().storeUint(0x123, 32).endCell() + + const bocHex = '0x' + cell.toBoc().toString('hex') + const tag = extractMagicTag(bocHex) + + assert.equal(tag, '0x00000123') + }) + + it('should handle different tag values', () => { + const testCases = [ + { input: Number(EVMExtraArgsV1Tag), expected: '0x97a657c9' }, + { input: Number(SuiExtraArgsV1Tag), expected: '0x21ea4ca9' }, + { input: Number(SVMExtraArgsV1Tag), expected: '0x1f3b3aba' }, + ] + + for (const testCase of testCases) { + const cell = beginCell().storeUint(testCase.input, 32).endCell() + + const bocHex = '0x' + cell.toBoc().toString('hex') + const tag = extractMagicTag(bocHex) + + assert.equal(tag, testCase.expected) + } + }) + }) +}) From cb84a79917619cf795f851849855e92dacf00237 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Fri, 5 Dec 2025 15:48:50 -0300 Subject: [PATCH 06/33] enable ton leaf hasher --- ccip-sdk/src/ton/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index c43d57b8..fc824e20 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -21,6 +21,7 @@ import { ChainFamily, } from '../types.ts' import { getDataBytes } from '../utils.ts' +import { getTONLeafHasher } from './hasher.ts' type CCIPMessage_V1_6_TON = CCIPMessage_V1_6 & GenericExtraArgsV2 const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2, 16) @@ -34,9 +35,10 @@ export class TONChain extends Chain { readonly network: NetworkInfo - constructor() { + constructor(network: NetworkInfo) { super() - throw new Error('Not implemented') + + this.network = network } static async fromUrl(_url: string): Promise { @@ -198,8 +200,8 @@ export class TONChain extends Chain { throw new Error('Not implemented') } - static getDestLeafHasher(_lane: Lane): LeafHasher { - throw new Error('Not implemented') + static getDestLeafHasher(lane: Lane): LeafHasher { + return getTONLeafHasher(lane) } async getFee(_router: string, _destChainSelector: bigint, _message: AnyMessage): Promise { From dacf34ab09440d299602ff138418efb589bc069a Mon Sep 17 00:00:00 2001 From: Farber98 Date: Fri, 5 Dec 2025 18:53:33 -0300 Subject: [PATCH 07/33] push report serialization --- ccip-sdk/src/ton/index.ts | 3 +- ccip-sdk/src/ton/types.ts | 115 ++++++++++++++++++++++++++++++++++++++ ccip-sdk/src/types.ts | 3 +- 3 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 ccip-sdk/src/ton/types.ts diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index fc824e20..8f271d7e 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -8,7 +8,6 @@ import type { LeafHasher } from '../hasher/common.ts' import { supportedChains } from '../supported-chains.ts' import { type AnyMessage, - type CCIPMessage_V1_6, type CCIPRequest, type ChainTransaction, type CommitReport, @@ -22,8 +21,8 @@ import { } from '../types.ts' import { getDataBytes } from '../utils.ts' import { getTONLeafHasher } from './hasher.ts' +import type { CCIPMessage_V1_6_TON } from './types.ts' -type CCIPMessage_V1_6_TON = CCIPMessage_V1_6 & GenericExtraArgsV2 const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2, 16) export class TONChain extends Chain { diff --git a/ccip-sdk/src/ton/types.ts b/ccip-sdk/src/ton/types.ts new file mode 100644 index 00000000..442f7507 --- /dev/null +++ b/ccip-sdk/src/ton/types.ts @@ -0,0 +1,115 @@ +import { type Builder, Address, Cell, beginCell } from '@ton/core' +import type { BytesLike } from 'ethers' + +import type { GenericExtraArgsV2 } from '../extra-args.ts' +import type { CCIPMessage_V1_6, ExecutionReport } from '../types.ts' + +export type CCIPMessage_V1_6_TON = CCIPMessage_V1_6 & GenericExtraArgsV2 + +// asSnakeData helper for encoding variable-length arrays +function asSnakeData(array: T[], builderFn: (item: T) => Builder): Cell { + const cells: Builder[] = [] + let builder = beginCell() + + for (const value of array) { + const itemBuilder = builderFn(value) + if (itemBuilder.refs > 3) { + throw new Error('Cannot pack more than 3 refs per item; store it in a separate ref cell.') + } + if (builder.availableBits < itemBuilder.bits || builder.availableRefs <= 1) { + cells.push(builder) + builder = beginCell() + } + builder.storeBuilder(itemBuilder) + } + cells.push(builder) + + // 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 +} + +function convertProofsToBigInt(proofs: readonly BytesLike[]): bigint[] { + return proofs.map((proof) => { + if (typeof proof === 'string') { + return BigInt(proof.startsWith('0x') ? proof : '0x' + proof) + } + if (proof instanceof Uint8Array) { + return BigInt('0x' + Buffer.from(proof).toString('hex')) + } + throw new Error(`Unsupported proof type: ${typeof proof}`) + }) +} + +export function serializeExecutionReport(execReport: ExecutionReport): Cell { + return beginCell() + .storeUint(execReport.message.header.sourceChainSelector, 64) + .storeRef(asSnakeData([execReport.message], serializeMessage)) + .storeRef(Cell.EMPTY) + .storeRef( + asSnakeData(convertProofsToBigInt(execReport.proofs), (proof: bigint) => { + return beginCell().storeUint(proof, 256) + }), + ) + .storeUint(execReport.proofFlagBits, 256) + .endCell() +} + +function serializeMessage(message: CCIPMessage_V1_6_TON): Builder { + return beginCell() + .storeRef(serializeHeader(message.header)) + .storeRef(serializeSender(message.sender)) + .storeRef(serializeData(message.data)) + .storeAddress(Address.parse(message.receiver)) + .storeCoins(message.gasLimit) + .storeMaybeRef( + message.tokenAmounts?.length > 0 ? serializeTokenAmounts(message.tokenAmounts) : null, + ) +} + +function serializeHeader(header: CCIPMessage_V1_6['header']): Builder { + return beginCell() + .storeUint(BigInt(header.messageId), 256) + .storeUint(header.sourceChainSelector, 64) + .storeUint(header.destChainSelector, 64) + .storeUint(header.sequenceNumber, 64) + .storeUint(header.nonce, 64) +} + +function serializeSender(sender: string): Builder { + const senderBytes = Buffer.from(sender.slice(2), 'hex') + return beginCell().storeUint(senderBytes.length, 8).storeBuffer(senderBytes) +} + +function serializeData(data: string): Builder { + return beginCell().storeBuffer(Buffer.from(data.slice(2), 'hex')) +} + +function serializeTokenAmounts(tokenAmounts: CCIPMessage_V1_6['tokenAmounts']): Builder { + const builder = beginCell() + for (const ta of tokenAmounts) { + builder.storeRef( + beginCell() + .storeRef(serializeSourcePool(ta.sourcePoolAddress)) + .storeAddress(Address.parse(ta.destTokenAddress)) + .storeUint(BigInt(ta.amount), 256) + .storeRef( + beginCell() + .storeBuffer(Buffer.from(ta.extraData.slice(2), 'hex')) + .endCell(), + ) + .endCell(), + ) + } + return builder +} + +function serializeSourcePool(address: string): Builder { + const bytes = Buffer.from(address.slice(2), 'hex') + return beginCell().storeUint(bytes.length, 8).storeBuffer(bytes) +} diff --git a/ccip-sdk/src/types.ts b/ccip-sdk/src/types.ts index 918a3368..cbc82379 100644 --- a/ccip-sdk/src/types.ts +++ b/ccip-sdk/src/types.ts @@ -6,6 +6,7 @@ import type { CCIPMessage_EVM, CCIPMessage_V1_6_EVM } from './evm/messages.ts' import type { ExtraArgs } from './extra-args.ts' import type { CCIPMessage_V1_6_Solana } from './solana/types.ts' import type { CCIPMessage_V1_6_Sui } from './sui/types.ts' +import type { CCIPMessage_V1_6_TON } from './ton/types.ts' // v1.6 Base type from EVM contains the intersection of all other CCIPMessage v1.6 types export type { CCIPMessage_V1_6 } from './evm/messages.ts' @@ -79,7 +80,7 @@ export type CCIPMessage = V extends | typeof CCIPVersion.V1_2 | typeof CCIPVersion.V1_5 ? CCIPMessage_EVM - : CCIPMessage_V1_6_EVM | CCIPMessage_V1_6_Solana | CCIPMessage_V1_6_Sui + : CCIPMessage_V1_6_EVM | CCIPMessage_V1_6_Solana | CCIPMessage_V1_6_Sui | CCIPMessage_V1_6_TON export type Log_ = Pick & { data: BytesLike | Record From 25e7a4b24547b987edba9d60dad5e2a51d15edaf Mon Sep 17 00:00:00 2001 From: Farber98 Date: Sat, 6 Dec 2025 22:02:10 -0300 Subject: [PATCH 08/33] fix --- ccip-cli/src/index.ts | 6 +++--- ccip-sdk/src/extra-args.ts | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 72a572c5..0834196f 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -9,7 +9,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 = '0.91.0-9e5d987' +const VERSION = '0.91.0-4f5a327' // generate:end const globalOpts = { @@ -53,7 +53,7 @@ async function main() { .options(globalOpts) .middleware((argv) => { if (!argv.verbose) { - console.debug = () => { } + console.debug = () => {} } }) .commandDir('commands', { @@ -69,7 +69,7 @@ async function main() { } if (import.meta.url === `file://${process.argv[1]}`) { - const later = setTimeout(() => { }, 2 ** 31 - 1) // keep event-loop alive + const later = setTimeout(() => {}, 2 ** 31 - 1) // keep event-loop alive await main() .catch((err) => { console.error(err) diff --git a/ccip-sdk/src/extra-args.ts b/ccip-sdk/src/extra-args.ts index e3d4fda3..47791c45 100644 --- a/ccip-sdk/src/extra-args.ts +++ b/ccip-sdk/src/extra-args.ts @@ -62,7 +62,12 @@ export type GenericExtraArgsV2 = EVMExtraArgsV2 /** * Union type of all supported extra arguments formats. */ -export type ExtraArgs = EVMExtraArgsV1 | EVMExtraArgsV2 | SVMExtraArgsV1 | SuiExtraArgsV1 | GenericExtraArgsV2 +export type ExtraArgs = + | EVMExtraArgsV1 + | EVMExtraArgsV2 + | SVMExtraArgsV1 + | SuiExtraArgsV1 + | GenericExtraArgsV2 /** * Encodes extra arguments for CCIP messages. From 50d49e89763299f9ceb3db5bf562520588694d7d Mon Sep 17 00:00:00 2001 From: Farber98 Date: Mon, 8 Dec 2025 19:13:52 -0300 Subject: [PATCH 09/33] add sdk exec --- ccip-cli/src/index.ts | 2 +- ccip-sdk/package.json | 3 +- ccip-sdk/src/ton/exec.test.ts | 195 ++++++++++++++++++++++++++++++++++ ccip-sdk/src/ton/exec.ts | 43 ++++++++ ccip-sdk/src/ton/index.ts | 61 +++++++++-- ccip-sdk/src/ton/utils.ts | 2 +- package-lock.json | 60 ++++++++++- 7 files changed, 354 insertions(+), 12 deletions(-) create mode 100644 ccip-sdk/src/ton/exec.test.ts create mode 100644 ccip-sdk/src/ton/exec.ts diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 0834196f..38c2a2f0 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -9,7 +9,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 = '0.91.0-4f5a327' +const VERSION = '0.91.0-25e7a4b' // generate:end const globalOpts = { diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index 32d84a08..52f225c0 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -59,7 +59,8 @@ "bs58": "^6.0.0", "ethers": "6.15.0", "micro-memoize": "^5.1.1", - "@ton/core": "^0.62.0", + "@ton/core": "0.62.0", + "@tonconnect/sdk": "3.3.1", "type-fest": "^5.3.0", "yaml": "2.8.2" }, diff --git a/ccip-sdk/src/ton/exec.test.ts b/ccip-sdk/src/ton/exec.test.ts new file mode 100644 index 00000000..776e5080 --- /dev/null +++ b/ccip-sdk/src/ton/exec.test.ts @@ -0,0 +1,195 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { Cell, beginCell, toNano } from '@ton/core' +import { executeReport } from './exec.ts' +import type { ExecutionReport } from '../types.ts' +import type { CCIPMessage_V1_6_TON } from './types.ts' + +describe('TON executeReport', () => { + const mockTonConnect = { + sendTransaction: async (_tx: unknown) => ({ boc: '0x123' }), + } + + const offrampAddress = '0:' + '5'.repeat(64) + + const baseExecReport: ExecutionReport = { + message: { + header: { + messageId: '0x' + '1'.repeat(64), + sourceChainSelector: 743186221051783445n, + destChainSelector: 16015286601757825753n, + sequenceNumber: 1n, + nonce: 0n, + }, + sender: '0x' + '2'.repeat(40), + receiver: '0:' + '3'.repeat(64), + data: '0x', + extraArgs: '0x181dcf10000000000000000000000000000000000000000000000000000000000000000001', + feeToken: '0x' + '0'.repeat(40), + feeTokenAmount: 0n, + feeValueJuels: 0n, + tokenAmounts: [], + gasLimit: 200000n, + allowOutOfOrderExecution: true, + }, + proofs: [], + proofFlagBits: 0n, + merkleRoot: '0x' + '4'.repeat(64), + offchainTokenData: [], + } + + it('should construct valid manuallyExecute transaction with correct structure', async () => { + const execReport: ExecutionReport = { + ...baseExecReport, + message: { + ...baseExecReport.message, + data: '0x1234', + gasLimit: 500000n, + }, + proofs: ['0x' + '0'.repeat(63) + '1'], + } + + let capturedTx: any + const tonConnectWithCapture = { + sendTransaction: async (tx: unknown) => { + capturedTx = tx + return { boc: '0x123' } + }, + } + + const result = await executeReport(tonConnectWithCapture as any, offrampAddress, execReport) + + // Verify transaction structure + assert.equal(capturedTx.messages[0].address, offrampAddress) + assert.equal(capturedTx.messages[0].amount, toNano('0.5').toString()) + + // Verify BOC payload contains correct opcode + const payload = capturedTx.messages[0].payload + assert.match(payload, /^0x/) + + // Parse BOC using Cell.fromBoc() instead of storeBuffer + const bocBytes = Buffer.from(payload.slice(2), 'hex') + const [cell] = Cell.fromBoc(bocBytes) + const slice = cell.beginParse() + + // Verify opcode (0xa00785cf for manuallyExecute) + const opcode = slice.loadUint(32) + assert.equal(opcode, 0xa00785cf) + + // Verify queryID is 0 + const queryId = slice.loadUint(64) + assert.equal(queryId, 0) + + assert.match(result.hash, /^0x/) + }) + + it('should handle gas override correctly in transaction', async () => { + let capturedTx: any + const tonConnectWithCapture = { + sendTransaction: async (tx: unknown) => { + capturedTx = tx + return { boc: '0x123' } + }, + } + + const result = await executeReport( + tonConnectWithCapture as any, + offrampAddress, + baseExecReport, + { gasLimit: 1_000_000_000 }, + ) + + // Parse BOC to verify gas override is included + const payload = capturedTx.messages[0].payload + const bocBytes = Buffer.from(payload.slice(2), 'hex') + const [cell] = Cell.fromBoc(bocBytes) + const slice = cell.beginParse() + + slice.loadUint(32) // opcode + slice.loadUint(64) // queryID + slice.loadRef() // execution report reference + + // Verify gas override + const gasOverride = slice.loadCoins() + assert.equal(gasOverride, 1_000_000_000n) + + assert.match(result.hash, /^0x/) + }) + + it('should throw error for invalid execution report', async () => { + const invalidReport = { + message: { + // Missing required fields + header: { + messageId: '0x' + '1'.repeat(64), + }, + }, + proofs: [], + proofFlagBits: 0n, + merkleRoot: '0x' + '4'.repeat(64), + offchainTokenData: [], + } + + await assert.rejects( + executeReport(mockTonConnect as any, offrampAddress, invalidReport as any), + /Cannot convert undefined to a BigInt/, + ) + }) + + it('should handle TonConnect transaction failure', async () => { + const failingTonConnect = { + sendTransaction: async (_tx: unknown) => { + throw new Error('Transaction failed') + }, + } + + await assert.rejects( + executeReport(failingTonConnect as any, offrampAddress, baseExecReport), + /Transaction failed/, + ) + }) + + it('should use correct transaction validity period', async () => { + const execReport: ExecutionReport = { + message: { + header: { + messageId: '0x' + '1'.repeat(64), + sourceChainSelector: 743186221051783445n, + destChainSelector: 16015286601757825753n, + sequenceNumber: 1n, + nonce: 0n, + }, + sender: '0x' + '2'.repeat(40), + receiver: '0:' + '3'.repeat(64), + data: '0x', + extraArgs: '0x181dcf10000000000000000000000000000000000000000000000000000000000000000001', + feeToken: '0x' + '0'.repeat(40), + feeTokenAmount: 0n, + feeValueJuels: 0n, + tokenAmounts: [], + gasLimit: 200000n, + allowOutOfOrderExecution: true, + }, + proofs: [], + proofFlagBits: 0n, + merkleRoot: '0x' + '4'.repeat(64), + offchainTokenData: [], + } + + let capturedTx: any + const tonConnectWithCapture = { + sendTransaction: async (tx: unknown) => { + capturedTx = tx + return { boc: '0x123' } + }, + } + + const beforeTime = Math.floor(Date.now() / 1000) + await executeReport(tonConnectWithCapture as any, offrampAddress, execReport) + const afterTime = Math.floor(Date.now() / 1000) + + // Verify validUntil is set to current time + 300 seconds (5 minutes) + assert.ok(capturedTx.validUntil >= beforeTime + 300) + assert.ok(capturedTx.validUntil <= afterTime + 300) + }) +}) diff --git a/ccip-sdk/src/ton/exec.ts b/ccip-sdk/src/ton/exec.ts new file mode 100644 index 00000000..b9066ed9 --- /dev/null +++ b/ccip-sdk/src/ton/exec.ts @@ -0,0 +1,43 @@ +import { beginCell, toNano } from '@ton/core' +import type { ExecutionReport } from '../types.ts' +import type { CCIPMessage_V1_6_TON } from './types.ts' +import { serializeExecutionReport } from './types.ts' +import { TonConnect } from '@tonconnect/sdk' + +export async function executeReport( + tonConnect: TonConnect, + offRamp: string, + execReport: ExecutionReport, + opts?: { gasLimit?: number }, +): Promise<{ hash: string }> { + // Serialize the execution report + const serializedReport = serializeExecutionReport(execReport) + + // Use provided gasLimit as override, or 0 for no override + const gasOverride = opts?.gasLimit ? BigInt(opts.gasLimit) : 0n + + // Construct the OffRamp_ManuallyExecute message + const payload = beginCell() + .storeUint(0xa00785cf, 32) // Opcode for OffRamp_ManuallyExecute + .storeUint(0, 64) // queryID (default 0) + .storeRef(serializedReport) // ExecutionReport as reference + .storeCoins(gasOverride) // gasOverride (optional, 0 = no override) + .endCell() + + const bocHex = '0x' + payload.toBoc().toString('hex') + + // Send transaction via TonConnect + const transaction = { + validUntil: Math.floor(Date.now() / 1000) + 300, + messages: [ + { + address: offRamp, + amount: toNano('0.5').toString(), // Base fee for manual execution + payload: bocHex, + }, + ], + } + + const result = await tonConnect.sendTransaction(transaction) + return { hash: result.boc } +} diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 8f271d7e..4a502085 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -20,8 +20,11 @@ import { ChainFamily, } from '../types.ts' import { getDataBytes } from '../utils.ts' +// import { parseTONLogs } from './utils.ts' import { getTONLeafHasher } from './hasher.ts' import type { CCIPMessage_V1_6_TON } from './types.ts' +import { TonConnect } from '@tonconnect/sdk' +import { executeReport } from './exec.ts' const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2, 16) @@ -48,8 +51,17 @@ export class TONChain extends Chain { return Promise.reject(new Error('Not implemented')) } - async getTransaction(_hash: string | number): Promise { - return Promise.reject(new Error('Not implemented')) + async getTransaction(hash: string): Promise { + // TODO: Implement full transaction fetching when TON provider is available + // For now, return minimal structure for executeReport flow + // The hash from TonConnect is the BOC of the signed transaction + return { + hash, + logs: [], + blockNumber: 0, + timestamp: Math.floor(Date.now() / 1000), + from: 'unknown', + } } // eslint-disable-next-line require-yield @@ -128,6 +140,29 @@ export class TONChain extends Chain { return Promise.reject(new Error('Not implemented')) } + static getWallet(_opts: { wallet?: unknown } = {}): Promise { + throw new Error('static TON wallet loading not available') + } + + async getWallet(opts: { wallet?: unknown } = {}): Promise { + // Handle TonConnect instance if provided + if (opts.wallet instanceof TonConnect) { + return opts.wallet + } + + // TonConnect doesn't support raw private key wallet creation + // Users must provide a pre-configured TonConnect instance + if (typeof opts.wallet === 'string') { + throw new Error( + 'TON requires a TonConnect instance. Raw private key support is not available. ' + + 'Create a TonConnect instance and pass it as opts.wallet.', + ) + } + + // Delegate to static method (for CLI overrides) + return (this.constructor as typeof TONChain).getWallet(opts) + } + // Static methods for decoding static decodeMessage(_log: Log_): CCIPMessage_V1_6_TON | undefined { throw new Error('Not implemented') @@ -225,11 +260,25 @@ export class TONChain extends Chain { } async executeReport( - _offRamp: string, - _execReport: ExecutionReport, - _opts?: { wallet?: unknown; gasLimit?: number }, + offRamp: string, + execReport: ExecutionReport, + opts?: { wallet?: unknown; gasLimit?: number }, ): Promise { - return Promise.reject(new Error('Not implemented')) + const tonConnect = await this.getWallet(opts) + + // Validate TON message format + if (!('gasLimit' in execReport.message && 'allowOutOfOrderExecution' in execReport.message)) { + throw new Error('TON expects GenericExtraArgsV2 reports') + } + + const result = await executeReport( + tonConnect, + offRamp, + execReport as ExecutionReport, + opts, + ) + + return this.getTransaction(result.hash) } static parse(data: unknown) { diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts index 52524942..498d5066 100644 --- a/ccip-sdk/src/ton/utils.ts +++ b/ccip-sdk/src/ton/utils.ts @@ -1,5 +1,5 @@ import { createHash } from 'crypto' - +// import type { Log_ } from '../types.ts' import { Cell, beginCell } from '@ton/core' /** diff --git a/package-lock.json b/package-lock.json index 30af09aa..5a71b326 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,7 +103,8 @@ "@mysten/sui": "^1.45.2", "@solana/spl-token": "0.4.14", "@solana/web3.js": "^1.98.4", - "@ton/core": "^0.62.0", + "@ton/core": "0.62.0", + "@tonconnect/sdk": "3.3.1", "abitype": "1.2.0", "bn.js": "^5.2.2", "borsh": "^2.0.0", @@ -2849,6 +2850,45 @@ "jssha": "3.2.0" } }, + "node_modules/@tonconnect/isomorphic-eventsource": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@tonconnect/isomorphic-eventsource/-/isomorphic-eventsource-0.0.2.tgz", + "integrity": "sha512-B4UoIjPi0QkvIzZH5fV3BQLWrqSYABdrzZQSI9sJA9aA+iC0ohOzFwVVGXanlxeDAy1bcvPbb29f6sVUk0UnnQ==", + "license": "Apache-2.0", + "dependencies": { + "eventsource": "^2.0.2" + } + }, + "node_modules/@tonconnect/isomorphic-fetch": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@tonconnect/isomorphic-fetch/-/isomorphic-fetch-0.0.3.tgz", + "integrity": "sha512-jIg5nTrDwnite4fXao3dD83eCpTvInTjZon/rZZrIftIegh4XxyVb5G2mpMqXrVGk1e8SVXm3Kj5OtfMplQs0w==", + "license": "Apache-2.0", + "dependencies": { + "node-fetch": "^2.6.9" + } + }, + "node_modules/@tonconnect/protocol": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@tonconnect/protocol/-/protocol-2.3.0.tgz", + "integrity": "sha512-OxrmcXF/EsSdGeASP9VpTRojuMtTV87DKYFLRq4ZJvF/Hirfm2cgcxzzj2uksEGm5IIR010UWo6b38RuokNwFQ==", + "license": "Apache-2.0", + "dependencies": { + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1" + } + }, + "node_modules/@tonconnect/sdk": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@tonconnect/sdk/-/sdk-3.3.1.tgz", + "integrity": "sha512-lhXJu95VvbD36u5mMPg2sg+w4GQwkrYnHeJ8rVveu2N7UPwt0jvrEqKlvf7Ss1gh5RDtzs35SS3GbJlaIOAJNA==", + "license": "Apache-2.0", + "dependencies": { + "@tonconnect/isomorphic-eventsource": "0.0.2", + "@tonconnect/isomorphic-fetch": "0.0.3", + "@tonconnect/protocol": "2.3.0" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -5528,6 +5568,15 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -9056,8 +9105,13 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense", - "peer": true + "license": "Unlicense" + }, + "node_modules/tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", + "license": "Unlicense" }, "node_modules/type-check": { "version": "0.4.0", From 4bae2eed9d1fea83c76631f6787e3084d9b5acc0 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Mon, 8 Dec 2025 22:19:38 -0300 Subject: [PATCH 10/33] prettier + eslint --- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/extra-args.ts | 10 ++- ccip-sdk/src/ton/exec.test.ts | 2 + ccip-sdk/src/ton/exec.ts | 9 ++- ccip-sdk/src/ton/index.ts | 116 +++++++++++++++++++++++++++++++++- ccip-sdk/src/ton/types.ts | 6 ++ ccip-sdk/src/ton/utils.ts | 1 + 7 files changed, 133 insertions(+), 13 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 38c2a2f0..0a1cad83 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -9,7 +9,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 = '0.91.0-25e7a4b' +const VERSION = '0.91.0-50d49e8' // generate:end const globalOpts = { diff --git a/ccip-sdk/src/extra-args.ts b/ccip-sdk/src/extra-args.ts index 47791c45..792171d9 100644 --- a/ccip-sdk/src/extra-args.ts +++ b/ccip-sdk/src/extra-args.ts @@ -57,17 +57,15 @@ export type SuiExtraArgsV1 = EVMExtraArgsV2 & { } // Same structure as EVMExtraArgsV2. TON calls it GenericExtraArgsV2 +/** + * + */ export type GenericExtraArgsV2 = EVMExtraArgsV2 /** * Union type of all supported extra arguments formats. */ -export type ExtraArgs = - | EVMExtraArgsV1 - | EVMExtraArgsV2 - | SVMExtraArgsV1 - | SuiExtraArgsV1 - | GenericExtraArgsV2 +export type ExtraArgs = EVMExtraArgsV1 | EVMExtraArgsV2 | SVMExtraArgsV1 | SuiExtraArgsV1 /** * Encodes extra arguments for CCIP messages. diff --git a/ccip-sdk/src/ton/exec.test.ts b/ccip-sdk/src/ton/exec.test.ts index 776e5080..bfeea67c 100644 --- a/ccip-sdk/src/ton/exec.test.ts +++ b/ccip-sdk/src/ton/exec.test.ts @@ -1,6 +1,8 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' + import { Cell, beginCell, toNano } from '@ton/core' + import { executeReport } from './exec.ts' import type { ExecutionReport } from '../types.ts' import type { CCIPMessage_V1_6_TON } from './types.ts' diff --git a/ccip-sdk/src/ton/exec.ts b/ccip-sdk/src/ton/exec.ts index b9066ed9..b7927d6e 100644 --- a/ccip-sdk/src/ton/exec.ts +++ b/ccip-sdk/src/ton/exec.ts @@ -1,9 +1,12 @@ import { beginCell, toNano } from '@ton/core' +import type { TonConnect } from '@tonconnect/sdk' + import type { ExecutionReport } from '../types.ts' -import type { CCIPMessage_V1_6_TON } from './types.ts' -import { serializeExecutionReport } from './types.ts' -import { TonConnect } from '@tonconnect/sdk' +import { type CCIPMessage_V1_6_TON, serializeExecutionReport } from './types.ts' +/** + * + */ export async function executeReport( tonConnect: TonConnect, offRamp: string, diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 4a502085..22cf8d58 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -1,4 +1,5 @@ import { Cell, beginCell } from '@ton/core' +import { TonConnect } from '@tonconnect/sdk' import { type BytesLike, isBytesLike } from 'ethers' import type { PickDeep } from 'type-fest' @@ -21,13 +22,15 @@ import { } from '../types.ts' import { getDataBytes } from '../utils.ts' // import { parseTONLogs } from './utils.ts' +import { executeReport } from './exec.ts' import { getTONLeafHasher } from './hasher.ts' import type { CCIPMessage_V1_6_TON } from './types.ts' -import { TonConnect } from '@tonconnect/sdk' -import { executeReport } from './exec.ts' const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2, 16) +/** + * + */ export class TONChain extends Chain { static { supportedChains[ChainFamily.TON] = TONChain @@ -37,20 +40,32 @@ export class TONChain extends Chain { readonly network: NetworkInfo + /** + * + */ constructor(network: NetworkInfo) { super() this.network = network } + /** + * + */ static async fromUrl(_url: string): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async getBlockTimestamp(_version: number | 'finalized'): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async getTransaction(hash: string): Promise { // TODO: Implement full transaction fetching when TON provider is available // For now, return minimal structure for executeReport flow @@ -64,16 +79,24 @@ export class TONChain extends Chain { } } - // eslint-disable-next-line require-yield + /** + * + */ async *getLogs(_opts: LogFilter & { versionAsHash?: boolean }) { await Promise.resolve() throw new Error('Not implemented') } + /** + * + */ override async fetchRequestsInTx(_tx: string | ChainTransaction): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ override async fetchAllMessagesInBatch< R extends PickDeep< CCIPRequest, @@ -87,6 +110,9 @@ export class TONChain extends Chain { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async typeAndVersion( _address: string, ): Promise< @@ -96,54 +122,93 @@ export class TONChain extends Chain { return Promise.reject(new Error('Not implemented')) } + /** + * + */ getRouterForOnRamp(_onRamp: string, _destChainSelector: bigint): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ getRouterForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ getNativeTokenForRouter(_router: string): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ getOffRampsForRouter(_router: string, _sourceChainSelector: bigint): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ getOnRampForRouter(_router: string, _destChainSelector: bigint): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async getOnRampForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ getCommitStoreForOffRamp(_offRamp: string): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async getTokenForTokenPool(_tokenPool: string): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async getTokenInfo(_token: string): Promise<{ symbol: string; decimals: number }> { return Promise.reject(new Error('Not implemented')) } + /** + * + */ getTokenAdminRegistryFor(_address: string): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async getWalletAddress(_opts?: { wallet?: unknown }): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ static getWallet(_opts: { wallet?: unknown } = {}): Promise { throw new Error('static TON wallet loading not available') } + /** + * + */ async getWallet(opts: { wallet?: unknown } = {}): Promise { // Handle TonConnect instance if provided if (opts.wallet instanceof TonConnect) { @@ -164,6 +229,9 @@ export class TONChain extends Chain { } // Static methods for decoding + /** + * + */ static decodeMessage(_log: Log_): CCIPMessage_V1_6_TON | undefined { throw new Error('Not implemented') } @@ -222,26 +290,44 @@ export class TONChain extends Chain { } } + /** + * + */ static decodeCommits(_log: Log_, _lane?: Lane): CommitReport[] | undefined { throw new Error('Not implemented') } + /** + * + */ static decodeReceipt(_log: Log_): ExecutionReceipt | undefined { throw new Error('Not implemented') } + /** + * + */ static getAddress(_bytes: BytesLike): string { throw new Error('Not implemented') } + /** + * + */ static getDestLeafHasher(lane: Lane): LeafHasher { return getTONLeafHasher(lane) } + /** + * + */ async getFee(_router: string, _destChainSelector: bigint, _message: AnyMessage): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async sendMessage( _router: string, _destChainSelector: bigint, @@ -251,6 +337,9 @@ export class TONChain extends Chain { return Promise.reject(new Error('Not implemented')) } + /** + * + */ fetchOffchainTokenData(request: CCIPRequest): Promise { if (!('receiverObjectIds' in request.message)) { throw new Error('Invalid message, not v1.6 TON') @@ -259,6 +348,9 @@ export class TONChain extends Chain { return Promise.resolve(request.message.tokenAmounts.map(() => undefined)) } + /** + * + */ async executeReport( offRamp: string, execReport: ExecutionReport, @@ -281,6 +373,9 @@ export class TONChain extends Chain { return this.getTransaction(result.hash) } + /** + * + */ static parse(data: unknown) { if (isBytesLike(data)) { const parsedExtraArgs = this.decodeExtraArgs(data) @@ -288,22 +383,37 @@ export class TONChain extends Chain { } } + /** + * + */ async getSupportedTokens(_address: string): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async getRegistryTokenConfig(_address: string, _tokenName: string): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async getTokenPoolConfigs(_tokenPool: string): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async getTokenPoolRemotes(_tokenPool: string): Promise { return Promise.reject(new Error('Not implemented')) } + /** + * + */ async getFeeTokens(_router: string): Promise { return Promise.reject(new Error('Not implemented')) } diff --git a/ccip-sdk/src/ton/types.ts b/ccip-sdk/src/ton/types.ts index 442f7507..b6129ac8 100644 --- a/ccip-sdk/src/ton/types.ts +++ b/ccip-sdk/src/ton/types.ts @@ -4,6 +4,9 @@ import type { BytesLike } from 'ethers' import type { GenericExtraArgsV2 } from '../extra-args.ts' import type { CCIPMessage_V1_6, ExecutionReport } from '../types.ts' +/** + * + */ export type CCIPMessage_V1_6_TON = CCIPMessage_V1_6 & GenericExtraArgsV2 // asSnakeData helper for encoding variable-length arrays @@ -46,6 +49,9 @@ function convertProofsToBigInt(proofs: readonly BytesLike[]): bigint[] { }) } +/** + * + */ export function serializeExecutionReport(execReport: ExecutionReport): Cell { return beginCell() .storeUint(execReport.message.header.sourceChainSelector, 64) diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts index 498d5066..f3c186ce 100644 --- a/ccip-sdk/src/ton/utils.ts +++ b/ccip-sdk/src/ton/utils.ts @@ -1,4 +1,5 @@ import { createHash } from 'crypto' + // import type { Log_ } from '../types.ts' import { Cell, beginCell } from '@ton/core' From b6f51c595afaebff4c4702d03116325df17e5f7a Mon Sep 17 00:00:00 2001 From: Farber98 Date: Tue, 9 Dec 2025 01:13:04 -0300 Subject: [PATCH 11/33] ton client + ton wallet for manualExec --- ccip-cli/src/index.ts | 2 +- ccip-sdk/package.json | 4 +- ccip-sdk/src/selectors.ts | 23 +++ ccip-sdk/src/ton/exec.test.ts | 268 ++++++++++++++++++++++------------ ccip-sdk/src/ton/exec.ts | 44 ++++-- ccip-sdk/src/ton/index.ts | 144 +++++++++++++----- ccip-sdk/src/ton/types.ts | 14 ++ ccip-sdk/src/ton/utils.ts | 87 ++++++++++- ccip-sdk/src/types.ts | 4 +- package-lock.json | 91 +++++------- 10 files changed, 481 insertions(+), 200 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 0a1cad83..088261b6 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -9,7 +9,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 = '0.91.0-50d49e8' +const VERSION = '0.91.0-4bae2ee' // generate:end const globalOpts = { diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index 52f225c0..135c8067 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -53,14 +53,14 @@ "@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.0", "bn.js": "^5.2.2", "borsh": "^2.0.0", "bs58": "^6.0.0", "ethers": "6.15.0", "micro-memoize": "^5.1.1", - "@ton/core": "0.62.0", - "@tonconnect/sdk": "3.3.1", "type-fest": "^5.3.0", "yaml": "2.8.2" }, diff --git a/ccip-sdk/src/selectors.ts b/ccip-sdk/src/selectors.ts index 76e4f800..1698fd31 100644 --- a/ccip-sdk/src/selectors.ts +++ b/ccip-sdk/src/selectors.ts @@ -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]) => [`ton:${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 diff --git a/ccip-sdk/src/ton/exec.test.ts b/ccip-sdk/src/ton/exec.test.ts index bfeea67c..b0c60358 100644 --- a/ccip-sdk/src/ton/exec.test.ts +++ b/ccip-sdk/src/ton/exec.test.ts @@ -1,18 +1,115 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' -import { Cell, beginCell, toNano } from '@ton/core' +import { type Cell, Address, toNano } from '@ton/core' +import type { KeyPair } from '@ton/crypto' +import type { WalletContractV4 } from '@ton/ton' import { executeReport } from './exec.ts' import type { ExecutionReport } from '../types.ts' -import type { CCIPMessage_V1_6_TON } from './types.ts' +import type { CCIPMessage_V1_6_TON, TONWallet } from './types.ts' describe('TON executeReport', () => { - const mockTonConnect = { - sendTransaction: async (_tx: unknown) => ({ boc: '0x123' }), + const offrampAddress = '0:' + '5'.repeat(64) + + // Mock KeyPair (64-byte secret key = 32 seed + 32 public) + const mockKeyPair: KeyPair = { + publicKey: Buffer.alloc(32, 0x01), + secretKey: Buffer.alloc(64, 0x02), } - const offrampAddress = '0:' + '5'.repeat(64) + // Mock wallet address + const mockWalletAddress = Address.parse('0:' + 'a'.repeat(64)) + + /** + * Creates a mock TonClient and TONWallet that captures sendTransfer calls + * and simulates transaction confirmation + */ + function createMockClientAndWallet(opts?: { + seqno?: number + shouldFail?: boolean + txLt?: string + txHash?: string + }) { + let capturedTransfer: { + seqno: number + secretKey: Buffer + messages: Array<{ to: Address; value: bigint; body: Cell }> + } | null = null + + const mockTxLt = opts?.txLt ?? '12345678' + const mockTxHash = opts?.txHash ?? 'abcdef1234567890' + + const mockOpenedWallet = { + getSeqno: async () => opts?.seqno ?? 0, + sendTransfer: async (params: { + seqno: number + secretKey: Buffer + messages: Array<{ info: { dest: Address; value: { coins: bigint } }; body: Cell }> + }) => { + if (opts?.shouldFail) { + throw new Error('Transaction failed') + } + capturedTransfer = { + seqno: params.seqno, + secretKey: params.secretKey, + messages: params.messages.map((m) => ({ + to: m.info.dest, + value: m.info.value.coins, + body: m.body, + })), + } + }, + } + + // Create mock outgoing message matching the offramp destination + const mockOutMessage = { + info: { + type: 'internal' as const, + dest: Address.parse(offrampAddress), + }, + } + + const mockClient = { + open: (_contract: WalletContractV4) => mockOpenedWallet, + // Mock runMethod for seqno check in waitForTransaction + runMethod: async (_address: Address, method: string) => { + if (method === 'seqno') { + return { + stack: { + // Return seqno + 1 to simulate transaction was confirmed + readNumber: () => (opts?.seqno ?? 0) + 1, + }, + } + } + throw new Error(`Unknown method: ${method}`) + }, + // Mock getTransactions for waitForTransaction + getTransactions: async (_address: Address, _opts: { limit: number }) => [ + { + lt: BigInt(mockTxLt), + hash: () => Buffer.from(mockTxHash, 'hex'), + now: Math.floor(Date.now() / 1000), + outMessages: { + values: () => [mockOutMessage], + }, + }, + ], + } + + const mockWallet: TONWallet = { + contract: { address: mockWalletAddress } as WalletContractV4, + keyPair: mockKeyPair, + } + + return { + client: mockClient as any, + wallet: mockWallet, + getCapturedTransfer: () => capturedTransfer, + mockTxLt, + mockTxHash, + } + } const baseExecReport: ExecutionReport = { message: { @@ -41,6 +138,10 @@ describe('TON executeReport', () => { } it('should construct valid manuallyExecute transaction with correct structure', async () => { + const { client, wallet, getCapturedTransfer, mockTxLt, mockTxHash } = createMockClientAndWallet( + { seqno: 42 }, + ) + const execReport: ExecutionReport = { ...baseExecReport, message: { @@ -51,28 +152,22 @@ describe('TON executeReport', () => { proofs: ['0x' + '0'.repeat(63) + '1'], } - let capturedTx: any - const tonConnectWithCapture = { - sendTransaction: async (tx: unknown) => { - capturedTx = tx - return { boc: '0x123' } - }, - } + const result = await executeReport(client, wallet, offrampAddress, execReport) - const result = await executeReport(tonConnectWithCapture as any, offrampAddress, execReport) + const captured = getCapturedTransfer() + assert.ok(captured, 'Transfer should be captured') - // Verify transaction structure - assert.equal(capturedTx.messages[0].address, offrampAddress) - assert.equal(capturedTx.messages[0].amount, toNano('0.5').toString()) + // Verify seqno was used + assert.equal(captured.seqno, 42) - // Verify BOC payload contains correct opcode - const payload = capturedTx.messages[0].payload - assert.match(payload, /^0x/) + // Verify message destination + assert.equal(captured.messages.length, 1) + assert.equal(captured.messages[0].to.toString(), Address.parse(offrampAddress).toString()) + assert.equal(captured.messages[0].value, toNano('0.5')) - // Parse BOC using Cell.fromBoc() instead of storeBuffer - const bocBytes = Buffer.from(payload.slice(2), 'hex') - const [cell] = Cell.fromBoc(bocBytes) - const slice = cell.beginParse() + // Parse the body Cell to verify opcode + const body = captured.messages[0].body + const slice = body.beginParse() // Verify opcode (0xa00785cf for manuallyExecute) const opcode = slice.loadUint(32) @@ -82,30 +177,27 @@ describe('TON executeReport', () => { const queryId = slice.loadUint(64) assert.equal(queryId, 0) - assert.match(result.hash, /^0x/) + // Verify hash is in format "workchain:address:lt:hash" + const parts = result.hash.split(':') + assert.equal(parts.length, 4, 'Hash should have 4 parts (workchain:address:lt:hash)') + assert.equal(parts[0], '0', 'Workchain should be 0') + assert.equal(parts[2], mockTxLt, 'LT should match') + assert.equal(parts[3], mockTxHash, 'Hash should match') }) it('should handle gas override correctly in transaction', async () => { - let capturedTx: any - const tonConnectWithCapture = { - sendTransaction: async (tx: unknown) => { - capturedTx = tx - return { boc: '0x123' } - }, - } + const { client, wallet, getCapturedTransfer } = createMockClientAndWallet() - const result = await executeReport( - tonConnectWithCapture as any, - offrampAddress, - baseExecReport, - { gasLimit: 1_000_000_000 }, - ) + await executeReport(client, wallet, offrampAddress, baseExecReport, { + gasLimit: 1_000_000_000, + }) + + const captured = getCapturedTransfer() + assert.ok(captured, 'Transfer should be captured') - // Parse BOC to verify gas override is included - const payload = capturedTx.messages[0].payload - const bocBytes = Buffer.from(payload.slice(2), 'hex') - const [cell] = Cell.fromBoc(bocBytes) - const slice = cell.beginParse() + // Parse body to verify gas override is included + const body = captured.messages[0].body + const slice = body.beginParse() slice.loadUint(32) // opcode slice.loadUint(64) // queryID @@ -114,11 +206,32 @@ describe('TON executeReport', () => { // Verify gas override const gasOverride = slice.loadCoins() assert.equal(gasOverride, 1_000_000_000n) + }) + + it('should set gasOverride to 0 when not provided', async () => { + const { client, wallet, getCapturedTransfer } = createMockClientAndWallet() + + await executeReport(client, wallet, offrampAddress, baseExecReport) + + const captured = getCapturedTransfer() + assert.ok(captured, 'Transfer should be captured') + + // Parse body to verify gas override is 0 + const body = captured.messages[0].body + const slice = body.beginParse() + + slice.loadUint(32) // opcode + slice.loadUint(64) // queryID + slice.loadRef() // execution report reference - assert.match(result.hash, /^0x/) + // Verify gas override is 0 + const gasOverride = slice.loadCoins() + assert.equal(gasOverride, 0n) }) it('should throw error for invalid execution report', async () => { + const { client, wallet } = createMockClientAndWallet() + const invalidReport = { message: { // Missing required fields @@ -133,65 +246,38 @@ describe('TON executeReport', () => { } await assert.rejects( - executeReport(mockTonConnect as any, offrampAddress, invalidReport as any), + executeReport(client, wallet, offrampAddress, invalidReport as any), /Cannot convert undefined to a BigInt/, ) }) - it('should handle TonConnect transaction failure', async () => { - const failingTonConnect = { - sendTransaction: async (_tx: unknown) => { - throw new Error('Transaction failed') - }, - } + it('should handle wallet sendTransfer failure', async () => { + const { client, wallet } = createMockClientAndWallet({ shouldFail: true }) await assert.rejects( - executeReport(failingTonConnect as any, offrampAddress, baseExecReport), + executeReport(client, wallet, offrampAddress, baseExecReport), /Transaction failed/, ) }) + it('should return hash in workchain:address:lt:hash format', async () => { + const { client, wallet, mockTxLt, mockTxHash } = createMockClientAndWallet({ + seqno: 123, + txLt: '9999999', + txHash: 'deadbeef12345678', + }) - it('should use correct transaction validity period', async () => { - const execReport: ExecutionReport = { - message: { - header: { - messageId: '0x' + '1'.repeat(64), - sourceChainSelector: 743186221051783445n, - destChainSelector: 16015286601757825753n, - sequenceNumber: 1n, - nonce: 0n, - }, - sender: '0x' + '2'.repeat(40), - receiver: '0:' + '3'.repeat(64), - data: '0x', - extraArgs: '0x181dcf10000000000000000000000000000000000000000000000000000000000000000001', - feeToken: '0x' + '0'.repeat(40), - feeTokenAmount: 0n, - feeValueJuels: 0n, - tokenAmounts: [], - gasLimit: 200000n, - allowOutOfOrderExecution: true, - }, - proofs: [], - proofFlagBits: 0n, - merkleRoot: '0x' + '4'.repeat(64), - offchainTokenData: [], - } - - let capturedTx: any - const tonConnectWithCapture = { - sendTransaction: async (tx: unknown) => { - capturedTx = tx - return { boc: '0x123' } - }, - } + const result = await executeReport(client, wallet, offrampAddress, baseExecReport) - const beforeTime = Math.floor(Date.now() / 1000) - await executeReport(tonConnectWithCapture as any, offrampAddress, execReport) - const afterTime = Math.floor(Date.now() / 1000) + // Verify hash format + const parts = result.hash.split(':') + assert.equal(parts.length, 4, 'Hash should have 4 parts') + assert.equal(parts[0], '0', 'Workchain should be 0') + assert.ok(parts[1].length === 64, 'Address should be 64 hex chars') + assert.equal(parts[2], mockTxLt, 'LT should match') + assert.equal(parts[3], mockTxHash, 'Transaction hash should match') - // Verify validUntil is set to current time + 300 seconds (5 minutes) - assert.ok(capturedTx.validUntil >= beforeTime + 300) - assert.ok(capturedTx.validUntil <= afterTime + 300) + // Verify the full address can be parsed + const fullAddress = `${parts[0]}:${parts[1]}` + assert.doesNotThrow(() => Address.parse(fullAddress), 'Address should be parseable') }) }) diff --git a/ccip-sdk/src/ton/exec.ts b/ccip-sdk/src/ton/exec.ts index b7927d6e..537423e8 100644 --- a/ccip-sdk/src/ton/exec.ts +++ b/ccip-sdk/src/ton/exec.ts @@ -1,14 +1,16 @@ -import { beginCell, toNano } from '@ton/core' -import type { TonConnect } from '@tonconnect/sdk' +import { Address, beginCell, toNano } from '@ton/core' +import { type TonClient, internal } from '@ton/ton' import type { ExecutionReport } from '../types.ts' -import { type CCIPMessage_V1_6_TON, serializeExecutionReport } from './types.ts' +import { type CCIPMessage_V1_6_TON, type TONWallet, serializeExecutionReport } from './types.ts' +import { waitForTransaction } from './utils.ts' /** * */ export async function executeReport( - tonConnect: TonConnect, + client: TonClient, + wallet: TONWallet, offRamp: string, execReport: ExecutionReport, opts?: { gasLimit?: number }, @@ -27,20 +29,30 @@ export async function executeReport( .storeCoins(gasOverride) // gasOverride (optional, 0 = no override) .endCell() - const bocHex = '0x' + payload.toBoc().toString('hex') + // Open wallet and send transaction + const openedWallet = client.open(wallet.contract) + const seqno = await openedWallet.getSeqno() + const walletAddress = wallet.contract.address - // Send transaction via TonConnect - const transaction = { - validUntil: Math.floor(Date.now() / 1000) + 300, + await openedWallet.sendTransfer({ + seqno, + secretKey: wallet.keyPair.secretKey, messages: [ - { - address: offRamp, - amount: toNano('0.5').toString(), // Base fee for manual execution - payload: bocHex, - }, + internal({ + to: offRamp, + value: toNano('0.5'), + body: payload, + }), ], - } + }) + + // Wait for transaction to be confirmed + const offRampAddress = Address.parse(offRamp) + const txInfo = await waitForTransaction(client, walletAddress, seqno, offRampAddress) - const result = await tonConnect.sendTransaction(transaction) - return { hash: result.boc } + // Return composite hash in format "workchain:address:lt:hash" + // we use toRawString() to get "workchain:addr" format + return { + hash: `${walletAddress.toRawString()}:${txInfo.lt}:${txInfo.hash}`, + } } diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 22cf8d58..9e1fed99 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -1,6 +1,8 @@ -import { Cell, beginCell } from '@ton/core' -import { TonConnect } from '@tonconnect/sdk' +import { Address, Cell, beginCell } from '@ton/core' +import { keyPairFromSecretKey, mnemonicToPrivateKey } from '@ton/crypto' +import { TonClient, WalletContractV4 } from '@ton/ton' import { type BytesLike, isBytesLike } from 'ethers' +import { memoize } from 'micro-memoize' import type { PickDeep } from 'type-fest' import { type LogFilter, Chain } from '../chain.ts' @@ -20,11 +22,11 @@ import { type OffchainTokenData, ChainFamily, } from '../types.ts' -import { getDataBytes } from '../utils.ts' +import { getDataBytes, networkInfo } from '../utils.ts' // import { parseTONLogs } from './utils.ts' import { executeReport } from './exec.ts' import { getTONLeafHasher } from './hasher.ts' -import type { CCIPMessage_V1_6_TON } from './types.ts' +import type { CCIPMessage_V1_6_TON, TONWallet } from './types.ts' const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2, 16) @@ -39,21 +41,40 @@ export class TONChain extends Chain { static readonly decimals = 8 readonly network: NetworkInfo + readonly provider: TonClient /** * */ - constructor(network: NetworkInfo) { + constructor(client: TonClient, network: NetworkInfo) { super() - + this.provider = client this.network = network + + this.getTransaction = memoize(this.getTransaction.bind(this), { + maxSize: 100, + }) } /** * */ - static async fromUrl(_url: string): Promise { - return Promise.reject(new Error('Not implemented')) + static async fromUrl(url: string): Promise { + const client = new TonClient({ endpoint: url }) + + // Detect network from URL + let networkId: string + if (url.includes('testnet')) { + networkId = 'ton-testnet' + } else if (url.includes('mainnet') || url.includes('toncenter.com/api')) { + networkId = 'ton-mainnet' + } else { + // Default to mainnet for unknown URLs + networkId = 'ton-mainnet' + } + + const network = networkInfo(networkId) as NetworkInfo + return new TONChain(client, network) } /** @@ -64,18 +85,40 @@ export class TONChain extends Chain { } /** + * Fetches a transaction by its hash. * + * TON transactions are identified by (address, lt, hash). + * Expected format: "workchain:address:lt:hash" + * Example: "0:abc123...def:12345:abc123...def" + * + * @param hash - Transaction identifier in format "workchain:address:lt:hash" + * @returns ChainTransaction with transaction details */ async getTransaction(hash: string): Promise { - // TODO: Implement full transaction fetching when TON provider is available - // For now, return minimal structure for executeReport flow - // The hash from TonConnect is the BOC of the signed transaction + const parts = hash.split(':') + + if (parts.length !== 4) { + throw new Error( + `Invalid TON transaction hash format: "${hash}". Expected "workchain:address:lt:hash"`, + ) + } + + const address = Address.parseRaw(`${parts[0]}:${parts[1]}`) + const lt = parts[2] + const txHash = parts[3] + + const tx = await this.provider.getTransaction(address, lt, txHash) + + if (!tx) { + throw new Error(`Transaction not found: ${hash}`) + } + return { hash, - logs: [], - blockNumber: 0, - timestamp: Math.floor(Date.now() / 1000), - from: 'unknown', + logs: [], // TODO + blockNumber: Number(tx.lt), + timestamp: tx.now, + from: address.toString(), } } @@ -207,21 +250,58 @@ export class TONChain extends Chain { } /** - * + * Loads a TON wallet from various input formats. */ - async getWallet(opts: { wallet?: unknown } = {}): Promise { - // Handle TonConnect instance if provided - if (opts.wallet instanceof TonConnect) { - return opts.wallet + async getWallet(opts: { wallet?: unknown } = {}): Promise { + // Handle private key string (hex or base64) + if (typeof opts.wallet === 'string') { + // Try mnemonic phrase first (space-separated words) + const words = opts.wallet.trim().split(/\s+/) + if (words.length >= 12 && words.length <= 24) { + const keyPair = await mnemonicToPrivateKey(words) + const contract = WalletContractV4.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }) + return { contract, keyPair } + } + + // Try hex or base64 secret key (64 bytes) + let secretKey: Buffer + + if (opts.wallet.startsWith('0x')) { + secretKey = Buffer.from(opts.wallet.slice(2), 'hex') + } else { + try { + secretKey = Buffer.from(opts.wallet, 'base64') + if (secretKey.length !== 64) { + secretKey = Buffer.from(opts.wallet, 'hex') + } + } catch { + secretKey = Buffer.from(opts.wallet, 'hex') + } + } + + if (secretKey.length === 64) { + const keyPair = keyPairFromSecretKey(secretKey) + const contract = WalletContractV4.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }) + return { contract, keyPair } + } + + throw new Error('Invalid key format. Expected 64-byte secret key or mnemonic phrase.') } - // TonConnect doesn't support raw private key wallet creation - // Users must provide a pre-configured TonConnect instance - if (typeof opts.wallet === 'string') { - throw new Error( - 'TON requires a TonConnect instance. Raw private key support is not available. ' + - 'Create a TonConnect instance and pass it as opts.wallet.', - ) + // Handle TONWallet instance directly + if ( + opts.wallet && + typeof opts.wallet === 'object' && + 'contract' in opts.wallet && + 'keyPair' in opts.wallet + ) { + return opts.wallet as TONWallet } // Delegate to static method (for CLI overrides) @@ -356,15 +436,11 @@ export class TONChain extends Chain { execReport: ExecutionReport, opts?: { wallet?: unknown; gasLimit?: number }, ): Promise { - const tonConnect = await this.getWallet(opts) - - // Validate TON message format - if (!('gasLimit' in execReport.message && 'allowOutOfOrderExecution' in execReport.message)) { - throw new Error('TON expects GenericExtraArgsV2 reports') - } + const wallet = await this.getWallet(opts) const result = await executeReport( - tonConnect, + this.provider, + wallet, offRamp, execReport as ExecutionReport, opts, diff --git a/ccip-sdk/src/ton/types.ts b/ccip-sdk/src/ton/types.ts index b6129ac8..b6877f75 100644 --- a/ccip-sdk/src/ton/types.ts +++ b/ccip-sdk/src/ton/types.ts @@ -1,4 +1,6 @@ import { type Builder, Address, Cell, beginCell } from '@ton/core' +import type { KeyPair } from '@ton/crypto' +import type { WalletContractV4 } from '@ton/ton' import type { BytesLike } from 'ethers' import type { GenericExtraArgsV2 } from '../extra-args.ts' @@ -9,6 +11,18 @@ import type { CCIPMessage_V1_6, ExecutionReport } from '../types.ts' */ export type CCIPMessage_V1_6_TON = CCIPMessage_V1_6 & GenericExtraArgsV2 +/** + * TON wallet with keypair for signing transactions + */ +export interface TONWallet { + contract: WalletContractV4 + keyPair: KeyPair +} + +/** + * Result of executeReport containing transaction identification info + */ + // asSnakeData helper for encoding variable-length arrays function asSnakeData(array: T[], builderFn: (item: T) => Builder): Cell { const cells: Builder[] = [] diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts index f3c186ce..77cd70c8 100644 --- a/ccip-sdk/src/ton/utils.ts +++ b/ccip-sdk/src/ton/utils.ts @@ -1,7 +1,9 @@ import { createHash } from 'crypto' -// import type { Log_ } from '../types.ts' -import { Cell, beginCell } from '@ton/core' +import { type Address, Cell, beginCell } from '@ton/core' +import type { TonClient } from '@ton/ton' + +import { sleep } from '../utils.ts' /** * Computes SHA256 hash of data and returns as hex string @@ -56,3 +58,84 @@ export function extractMagicTag(bocHex: string): string { const tag = cell.beginParse().loadUint(32) return `0x${tag.toString(16).padStart(8, '0')}` } + +/** + * Waits for a transaction to be confirmed by polling until the wallet's seqno advances. + * Once seqno advances past expectedSeqno, fetches the latest transaction details. + * + * @param client - TON client + * @param walletAddress - Address of the wallet that sent the transaction + * @param expectedSeqno - The seqno used when sending the transaction + * @param expectedDestination - Optional destination address to verify (e.g., offRamp) + * @param maxAttempts - Maximum polling attempts (default: 25) + * @param intervalMs - Polling interval in ms (default: 1000) + * @returns Transaction info with lt and hash + */ +export async function waitForTransaction( + client: TonClient, + walletAddress: Address, + expectedSeqno: number, + expectedDestination?: Address, + maxAttempts = 25, + intervalMs = 1000, +): Promise<{ lt: string; hash: string; timestamp: number }> { + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + // Check current seqno by calling the wallet's seqno getter + const seqnoResult = await client.runMethod(walletAddress, 'seqno') + const currentSeqno = seqnoResult.stack.readNumber() + + // Check if transaction was processed + const seqnoAdvanced = currentSeqno > expectedSeqno + + if (seqnoAdvanced) { + // Get the most recent transaction (should be ours) + const txs = await client.getTransactions(walletAddress, { limit: 5 }) + + for (const tx of txs) { + // If destination verification requested, check outgoing messages + if (expectedDestination) { + const outMessages = tx.outMessages.values() + let destinationMatch = false + + for (const msg of outMessages) { + if (msg.info.type === 'internal' && msg.info.dest.equals(expectedDestination)) { + destinationMatch = true + break + } + } + + if (!destinationMatch) continue + } + + return { + lt: tx.lt.toString(), + hash: tx.hash().toString('hex'), + timestamp: tx.now, + } + } + } + + // Handle case where contract was just deployed (seqno 0 -> 1) + if (expectedSeqno === 0 && attempt > 0) { + const txs = await client.getTransactions(walletAddress, { limit: 1 }) + if (txs.length > 0) { + const tx = txs[0] + return { + lt: tx.lt.toString(), + hash: tx.hash().toString('hex'), + timestamp: tx.now, + } + } + } + } catch { + // Contract might not be initialized yet, or network error - retry + } + + await sleep(intervalMs) + } + + throw new Error( + `Transaction with seqno ${expectedSeqno} not confirmed after ${maxAttempts} attempts`, + ) +} diff --git a/ccip-sdk/src/types.ts b/ccip-sdk/src/types.ts index 450e45c1..7d835739 100644 --- a/ccip-sdk/src/types.ts +++ b/ccip-sdk/src/types.ts @@ -70,7 +70,9 @@ type ChainFamilyWithId = F extends typeof ChainFamily.EVM ? { readonly family: F; readonly chainId: string } : F extends typeof ChainFamily.Aptos | typeof ChainFamily.Sui ? { readonly family: F; readonly chainId: `${F}:${number}` } - : never + : F extends typeof ChainFamily.TON + ? { readonly family: F; readonly chainId: number } + : never /** * Network information including chain selector and metadata. diff --git a/package-lock.json b/package-lock.json index 5a71b326..f4ad1f85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,7 +104,7 @@ "@solana/spl-token": "0.4.14", "@solana/web3.js": "^1.98.4", "@ton/core": "0.62.0", - "@tonconnect/sdk": "3.3.1", + "@ton/ton": "^16.1.0", "abitype": "1.2.0", "bn.js": "^5.2.2", "borsh": "^2.0.0", @@ -2850,43 +2850,21 @@ "jssha": "3.2.0" } }, - "node_modules/@tonconnect/isomorphic-eventsource": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@tonconnect/isomorphic-eventsource/-/isomorphic-eventsource-0.0.2.tgz", - "integrity": "sha512-B4UoIjPi0QkvIzZH5fV3BQLWrqSYABdrzZQSI9sJA9aA+iC0ohOzFwVVGXanlxeDAy1bcvPbb29f6sVUk0UnnQ==", - "license": "Apache-2.0", - "dependencies": { - "eventsource": "^2.0.2" - } - }, - "node_modules/@tonconnect/isomorphic-fetch": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@tonconnect/isomorphic-fetch/-/isomorphic-fetch-0.0.3.tgz", - "integrity": "sha512-jIg5nTrDwnite4fXao3dD83eCpTvInTjZon/rZZrIftIegh4XxyVb5G2mpMqXrVGk1e8SVXm3Kj5OtfMplQs0w==", - "license": "Apache-2.0", - "dependencies": { - "node-fetch": "^2.6.9" - } - }, - "node_modules/@tonconnect/protocol": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@tonconnect/protocol/-/protocol-2.3.0.tgz", - "integrity": "sha512-OxrmcXF/EsSdGeASP9VpTRojuMtTV87DKYFLRq4ZJvF/Hirfm2cgcxzzj2uksEGm5IIR010UWo6b38RuokNwFQ==", - "license": "Apache-2.0", - "dependencies": { - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "node_modules/@tonconnect/sdk": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@tonconnect/sdk/-/sdk-3.3.1.tgz", - "integrity": "sha512-lhXJu95VvbD36u5mMPg2sg+w4GQwkrYnHeJ8rVveu2N7UPwt0jvrEqKlvf7Ss1gh5RDtzs35SS3GbJlaIOAJNA==", - "license": "Apache-2.0", + "node_modules/@ton/ton": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@ton/ton/-/ton-16.1.0.tgz", + "integrity": "sha512-vRlMZVJ0/JABFDTFInyLh3C4LRP6AF3VtOl2iwCEcPfqRxdPcHW4r+bJLkKvo5fCknaGS8CEVdBeu6ziXHv2Ig==", + "license": "MIT", "dependencies": { - "@tonconnect/isomorphic-eventsource": "0.0.2", - "@tonconnect/isomorphic-fetch": "0.0.3", - "@tonconnect/protocol": "2.3.0" + "axios": "^1.6.7", + "dataloader": "^2.0.0", + "symbol.inspect": "1.0.1", + "teslabot": "^1.3.0", + "zod": "^3.21.4" + }, + "peerDependencies": { + "@ton/core": ">=0.62.0 <1.0.0", + "@ton/crypto": ">=3.2.0" } }, "node_modules/@tybys/wasm-util": { @@ -4412,6 +4390,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -5568,15 +5552,6 @@ "node": ">=0.8.x" } }, - "node_modules/eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -8887,6 +8862,12 @@ "node": ">=6" } }, + "node_modules/teslabot": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/teslabot/-/teslabot-1.5.0.tgz", + "integrity": "sha512-e2MmELhCgrgZEGo7PQu/6bmYG36IDH+YrBI1iGm6jovXkeDIGa3pZ2WSqRjzkuw2vt1EqfkZoV5GpXgqL8QJVg==", + "license": "MIT" + }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -9105,13 +9086,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense" - }, - "node_modules/tweetnacl-util": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", - "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", - "license": "Unlicense" + "license": "Unlicense", + "peer": true }, "node_modules/type-check": { "version": "0.4.0", @@ -9805,6 +9781,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } From 332c57471243fa7313ee33aba92061244ceb163b Mon Sep 17 00:00:00 2001 From: Farber98 Date: Tue, 9 Dec 2025 01:14:41 -0300 Subject: [PATCH 12/33] remove comment --- ccip-sdk/src/ton/types.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ccip-sdk/src/ton/types.ts b/ccip-sdk/src/ton/types.ts index b6877f75..660c2301 100644 --- a/ccip-sdk/src/ton/types.ts +++ b/ccip-sdk/src/ton/types.ts @@ -19,10 +19,6 @@ export interface TONWallet { keyPair: KeyPair } -/** - * Result of executeReport containing transaction identification info - */ - // asSnakeData helper for encoding variable-length arrays function asSnakeData(array: T[], builderFn: (item: T) => Builder): Cell { const cells: Builder[] = [] From 6d76a31754ce77f7c4be2ceb9dbf5a410aba16d3 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Tue, 9 Dec 2025 11:52:00 -0300 Subject: [PATCH 13/33] some andre feedback --- ccip-sdk/src/extra-args.test.ts | 8 ++++---- ccip-sdk/src/extra-args.ts | 2 +- ccip-sdk/src/solana/exec.ts | 4 ++-- ccip-sdk/src/solana/index.ts | 9 ++------- ccip-sdk/src/solana/offchain.ts | 4 ++-- ccip-sdk/src/solana/send.ts | 4 ++-- ccip-sdk/src/solana/utils.ts | 11 +---------- ccip-sdk/src/ton/hasher.test.ts | 2 +- ccip-sdk/src/ton/hasher.ts | 3 ++- ccip-sdk/src/ton/index.ts | 4 ++-- ccip-sdk/src/ton/types.ts | 29 +++++++-------------------- ccip-sdk/src/ton/utils.test.ts | 7 ++++--- ccip-sdk/src/ton/utils.ts | 35 +++++++++++---------------------- ccip-sdk/src/utils.ts | 11 ++++++++++- 14 files changed, 52 insertions(+), 81 deletions(-) diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index aea54db2..fbc2b562 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -7,7 +7,7 @@ import { dataSlice, getNumber } from 'ethers' import './index.ts' import { EVMExtraArgsV2Tag, - GenericExtraArgsV2, + GenericExtraArgsV2Tag, decodeExtraArgs, encodeExtraArgs, } from './extra-args.ts' @@ -87,7 +87,7 @@ describe('encodeExtraArgs', () => { ChainFamily.TON, ) - assert.equal(extractMagicTag(encoded), GenericExtraArgsV2) + assert.equal(extractMagicTag(encoded), GenericExtraArgsV2Tag) assert.ok(encoded.length > 10) }) @@ -97,7 +97,7 @@ describe('encodeExtraArgs', () => { ChainFamily.TON, ) - assert.equal(extractMagicTag(encoded), GenericExtraArgsV2) + assert.equal(extractMagicTag(encoded), GenericExtraArgsV2Tag) assert.ok(encoded.length > 10) }) }) @@ -288,7 +288,7 @@ describe('parseExtraArgs', () => { const tonEncoded = encodeExtraArgs(args, ChainFamily.TON) assert.equal(evmEncoded.substring(0, 10), EVMExtraArgsV2Tag) - assert.equal(extractMagicTag(tonEncoded), GenericExtraArgsV2) + assert.equal(extractMagicTag(tonEncoded), GenericExtraArgsV2Tag) assert.notEqual(evmEncoded, tonEncoded) }) }) diff --git a/ccip-sdk/src/extra-args.ts b/ccip-sdk/src/extra-args.ts index 792171d9..85d1b933 100644 --- a/ccip-sdk/src/extra-args.ts +++ b/ccip-sdk/src/extra-args.ts @@ -11,7 +11,7 @@ export const EVMExtraArgsV2Tag = id('CCIP EVMExtraArgsV2').substring(0, 10) as ' export const SVMExtraArgsV1Tag = id('CCIP SVMExtraArgsV1').substring(0, 10) as '0x1f3b3aba' /** Tag identifier for SuiExtraArgsV1 encoding. */ export const SuiExtraArgsV1Tag = id('CCIP SuiExtraArgsV1').substring(0, 10) as '0x21ea4ca9' -export const GenericExtraArgsV2 = EVMExtraArgsV2Tag +export const GenericExtraArgsV2Tag = EVMExtraArgsV2Tag /** * EVM extra arguments version 1 with gas limit only. diff --git a/ccip-sdk/src/solana/exec.ts b/ccip-sdk/src/solana/exec.ts index 87a8e1a3..f9815399 100644 --- a/ccip-sdk/src/solana/exec.ts +++ b/ccip-sdk/src/solana/exec.ts @@ -20,8 +20,8 @@ import type { ChainTransaction, ExecutionReport } 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 } from './types.ts' -import { getDataBytes, sleep, toLeArray } from '../utils.ts' -import { bytesToBuffer, simulateTransaction, simulationProvider } from './utils.ts' +import { bytesToBuffer, getDataBytes, sleep, toLeArray } from '../utils.ts' +import { simulateTransaction, simulationProvider } from './utils.ts' type ExecStepTx = readonly [reason: string, transactions: VersionedTransaction] diff --git a/ccip-sdk/src/solana/index.ts b/ccip-sdk/src/solana/index.ts index a33aa0df..21b6c858 100644 --- a/ccip-sdk/src/solana/index.ts +++ b/ccip-sdk/src/solana/index.ts @@ -69,6 +69,7 @@ import { ExecutionState, } from '../types.ts' import { + bytesToBuffer, createRateLimitedFetch, decodeAddress, decodeOnRampAddress, @@ -89,13 +90,7 @@ import { IDL as CCIP_ROUTER_IDL } from './idl/1.6.0/CCIP_ROUTER.ts' import { fetchSolanaOffchainTokenData } from './offchain.ts' import { ccipSend, getFee } from './send.ts' import type { CCIPMessage_V1_6_Solana } from './types.ts' -import { - bytesToBuffer, - getErrorFromLogs, - hexDiscriminator, - parseSolanaLogs, - simulationProvider, -} from './utils.ts' +import { getErrorFromLogs, hexDiscriminator, parseSolanaLogs, simulationProvider } from './utils.ts' import { fetchAllMessagesInBatch, fetchCCIPRequestById, diff --git a/ccip-sdk/src/solana/offchain.ts b/ccip-sdk/src/solana/offchain.ts index cf6450bf..ec7b0035 100644 --- a/ccip-sdk/src/solana/offchain.ts +++ b/ccip-sdk/src/solana/offchain.ts @@ -6,11 +6,11 @@ import { hexlify } from 'ethers' import { getUsdcAttestation } from '../offchain.ts' import type { CCIPMessage, CCIPRequest, OffchainTokenData } from '../types.ts' -import { networkInfo } from '../utils.ts' +import { bytesToBuffer, networkInfo } 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 diff --git a/ccip-sdk/src/solana/send.ts b/ccip-sdk/src/solana/send.ts index 06e62cb0..425112ee 100644 --- a/ccip-sdk/src/solana/send.ts +++ b/ccip-sdk/src/solana/send.ts @@ -22,9 +22,9 @@ import { zeroPadValue } from 'ethers' import { SolanaChain } from './index.ts' import type { AnyMessage } from '../types.ts' -import { toLeArray } from '../utils.ts' +import { bytesToBuffer, toLeArray } from '../utils.ts' import { IDL as CCIP_ROUTER_IDL } from './idl/1.6.0/CCIP_ROUTER.ts' -import { bytesToBuffer, simulateTransaction, simulationProvider } from './utils.ts' +import { simulateTransaction, simulationProvider } from './utils.ts' function anyToSvmMessage(message: AnyMessage): IdlTypes['SVM2AnyMessage'] { const feeTokenPubkey = message.feeToken ? new PublicKey(message.feeToken) : PublicKey.default diff --git a/ccip-sdk/src/solana/utils.ts b/ccip-sdk/src/solana/utils.ts index 4a513063..557fbeb5 100644 --- a/ccip-sdk/src/solana/utils.ts +++ b/ccip-sdk/src/solana/utils.ts @@ -12,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_ } from '../types.ts' import { getDataBytes, sleep } from '../utils.ts' @@ -26,15 +26,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. diff --git a/ccip-sdk/src/ton/hasher.test.ts b/ccip-sdk/src/ton/hasher.test.ts index 1323e6dc..5093a4b0 100644 --- a/ccip-sdk/src/ton/hasher.test.ts +++ b/ccip-sdk/src/ton/hasher.test.ts @@ -92,7 +92,7 @@ describe('TON hasher', () => { it('should hash message with tokens', () => { const tokenAmounts: CCIPMessage_V1_6['tokenAmounts'] = [ { - sourcePoolAddress: '0:' + '4'.repeat(64), + sourcePoolAddress: '0x123456789abcdef123456789abcdef123456789a', destTokenAddress: '0:' + '5'.repeat(64), extraData: '0x', destGasAmount: 1000n, diff --git a/ccip-sdk/src/ton/hasher.ts b/ccip-sdk/src/ton/hasher.ts index a4e44bb0..fa4b5b26 100644 --- a/ccip-sdk/src/ton/hasher.ts +++ b/ccip-sdk/src/ton/hasher.ts @@ -1,10 +1,11 @@ import { type Cell, Address, beginCell } from '@ton/core' +import { sha256, toBigInt } from 'ethers' import { decodeExtraArgs } from '../extra-args.ts' import { type LeafHasher, LEAF_DOMAIN_SEPARATOR } from '../hasher/common.ts' import { type CCIPMessage, type CCIPMessage_V1_6, CCIPVersion } from '../types.ts' import { networkInfo } from '../utils.ts' -import { hexToBuffer, sha256, toBigInt, tryParseCell } from './utils.ts' +import { hexToBuffer, tryParseCell } from './utils.ts' // Convert LEAF_DOMAIN_SEPARATOR from hex string to Buffer for cell storage const LEAF_DOMAIN_BUFFER = Buffer.from(LEAF_DOMAIN_SEPARATOR.slice(2).padStart(64, '0'), 'hex') diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 9e1fed99..5470842a 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -6,7 +6,7 @@ import { memoize } from 'micro-memoize' import type { PickDeep } from 'type-fest' import { type LogFilter, Chain } from '../chain.ts' -import { type ExtraArgs, GenericExtraArgsV2 } from '../extra-args.ts' +import { type ExtraArgs, type GenericExtraArgsV2, GenericExtraArgsV2Tag } from '../extra-args.ts' import type { LeafHasher } from '../hasher/common.ts' import { supportedChains } from '../supported-chains.ts' import { @@ -28,7 +28,7 @@ import { executeReport } from './exec.ts' import { getTONLeafHasher } from './hasher.ts' import type { CCIPMessage_V1_6_TON, TONWallet } from './types.ts' -const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2, 16) +const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2Tag, 16) /** * diff --git a/ccip-sdk/src/ton/types.ts b/ccip-sdk/src/ton/types.ts index 660c2301..477b1143 100644 --- a/ccip-sdk/src/ton/types.ts +++ b/ccip-sdk/src/ton/types.ts @@ -1,10 +1,11 @@ import { type Builder, Address, Cell, beginCell } from '@ton/core' import type { KeyPair } from '@ton/crypto' import type { WalletContractV4 } from '@ton/ton' -import type { BytesLike } from 'ethers' +import { toBigInt } from 'ethers' import type { GenericExtraArgsV2 } from '../extra-args.ts' import type { CCIPMessage_V1_6, ExecutionReport } from '../types.ts' +import { bytesToBuffer } from '../utils.ts' /** * @@ -47,18 +48,6 @@ function asSnakeData(array: T[], builderFn: (item: T) => Builder): Cell { return current } -function convertProofsToBigInt(proofs: readonly BytesLike[]): bigint[] { - return proofs.map((proof) => { - if (typeof proof === 'string') { - return BigInt(proof.startsWith('0x') ? proof : '0x' + proof) - } - if (proof instanceof Uint8Array) { - return BigInt('0x' + Buffer.from(proof).toString('hex')) - } - throw new Error(`Unsupported proof type: ${typeof proof}`) - }) -} - /** * */ @@ -68,7 +57,7 @@ export function serializeExecutionReport(execReport: ExecutionReport { + asSnakeData(execReport.proofs.map(toBigInt), (proof: bigint) => { return beginCell().storeUint(proof, 256) }), ) @@ -98,12 +87,12 @@ function serializeHeader(header: CCIPMessage_V1_6['header']): Builder { } function serializeSender(sender: string): Builder { - const senderBytes = Buffer.from(sender.slice(2), 'hex') + const senderBytes = bytesToBuffer(sender) return beginCell().storeUint(senderBytes.length, 8).storeBuffer(senderBytes) } function serializeData(data: string): Builder { - return beginCell().storeBuffer(Buffer.from(data.slice(2), 'hex')) + return beginCell().storeBuffer(bytesToBuffer(data)) } function serializeTokenAmounts(tokenAmounts: CCIPMessage_V1_6['tokenAmounts']): Builder { @@ -114,11 +103,7 @@ function serializeTokenAmounts(tokenAmounts: CCIPMessage_V1_6['tokenAmounts']): .storeRef(serializeSourcePool(ta.sourcePoolAddress)) .storeAddress(Address.parse(ta.destTokenAddress)) .storeUint(BigInt(ta.amount), 256) - .storeRef( - beginCell() - .storeBuffer(Buffer.from(ta.extraData.slice(2), 'hex')) - .endCell(), - ) + .storeRef(beginCell().storeBuffer(bytesToBuffer(ta.extraData)).endCell()) .endCell(), ) } @@ -126,6 +111,6 @@ function serializeTokenAmounts(tokenAmounts: CCIPMessage_V1_6['tokenAmounts']): } function serializeSourcePool(address: string): Builder { - const bytes = Buffer.from(address.slice(2), 'hex') + const bytes = bytesToBuffer(address) return beginCell().storeUint(bytes.length, 8).storeBuffer(bytes) } diff --git a/ccip-sdk/src/ton/utils.test.ts b/ccip-sdk/src/ton/utils.test.ts index 9ff3e8e1..965e5c7b 100644 --- a/ccip-sdk/src/ton/utils.test.ts +++ b/ccip-sdk/src/ton/utils.test.ts @@ -2,11 +2,12 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' import { beginCell } from '@ton/core' +import { sha256, toBigInt } from 'ethers' -import { extractMagicTag, hexToBuffer, sha256, toBigInt, tryParseCell } from './utils.ts' +import { extractMagicTag, hexToBuffer, tryParseCell } from './utils.ts' import { EVMExtraArgsV1Tag, - GenericExtraArgsV2, + GenericExtraArgsV2Tag, SVMExtraArgsV1Tag, SuiExtraArgsV1Tag, } from '../extra-args.ts' @@ -103,7 +104,7 @@ describe('TON utils', () => { describe('extractMagicTag', () => { it('should extract magic tag from BOC', () => { const cell = beginCell() - .storeUint(Number(GenericExtraArgsV2), 32) + .storeUint(Number(GenericExtraArgsV2Tag), 32) .storeUint(123456, 256) .storeBit(true) .endCell() diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts index 77cd70c8..2fb9f78a 100644 --- a/ccip-sdk/src/ton/utils.ts +++ b/ccip-sdk/src/ton/utils.ts @@ -1,35 +1,24 @@ -import { createHash } from 'crypto' - import { type Address, Cell, beginCell } from '@ton/core' import type { TonClient } from '@ton/ton' -import { sleep } from '../utils.ts' - -/** - * Computes SHA256 hash of data and returns as hex string - * Used throughout TON hasher for domain separation and message hashing - */ -export const sha256 = (data: Uint8Array): string => { - return '0x' + createHash('sha256').update(data).digest('hex') -} +import { bytesToBuffer, sleep } from '../utils.ts' /** * Converts hex string to Buffer, handling 0x prefix normalization * Returns empty buffer for empty input */ export const hexToBuffer = (value: string): Buffer => { - const normalized = value.startsWith('0x') || value.startsWith('0X') ? value.slice(2) : value - return normalized.length === 0 ? Buffer.alloc(0) : Buffer.from(normalized, 'hex') -} - -/** - * Converts various numeric types to BigInt for TON's big integer operations - * Used throughout the hasher for chain selectors, amounts, and sequence numbers - */ -export const toBigInt = (value: bigint | number | string): bigint => { - if (typeof value === 'bigint') return value - if (typeof value === 'number') return BigInt(value) - return BigInt(value) + if (!value || value === '0x' || value === '0X') return Buffer.alloc(0) + // Normalize to lowercase 0x prefix for bytesToBuffer/getDataBytes + let normalized: string + if (value.startsWith('0x')) { + normalized = value + } else if (value.startsWith('0X')) { + normalized = `0x${value.slice(2)}` + } else { + normalized = `0x${value}` + } + return bytesToBuffer(normalized) } /** diff --git a/ccip-sdk/src/utils.ts b/ccip-sdk/src/utils.ts index 3e806b1c..840f27fc 100644 --- a/ccip-sdk/src/utils.ts +++ b/ccip-sdk/src/utils.ts @@ -278,6 +278,15 @@ export function getDataBytes(data: BytesLike | readonly number[]): Uint8Array { } } +/** + * Converts bytes to a Node.js Buffer. + * @param bytes - Bytes to convert (hex string, Uint8Array, Base64, etc). + * @returns Node.js Buffer. + */ +export function bytesToBuffer(bytes: BytesLike | readonly number[]): Buffer { + return Buffer.from(getDataBytes(bytes)) +} + /** * Extracts address bytes, handling both hex and Base58 formats. * @param address - Address in hex or Base58 format. @@ -338,7 +347,7 @@ export function convertKeysToCamelCase( * @param ms - Duration in milliseconds. * @returns Promise that resolves after the specified duration. */ -export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms).unref()) +export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) /** * Parses a typeAndVersion string into its components. From 743ec7c4d412932877cb2a41032068d343ad7b9a Mon Sep 17 00:00:00 2001 From: Farber98 Date: Tue, 9 Dec 2025 13:30:02 -0300 Subject: [PATCH 14/33] ton provider --- ccip-cli/src/providers/index.ts | 5 +-- ccip-cli/src/providers/ton.ts | 62 +++++++++++++++++++++++++++++++++ ccip-sdk/src/ton/index.ts | 28 ++++++++++++--- 3 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 ccip-cli/src/providers/ton.ts diff --git a/ccip-cli/src/providers/index.ts b/ccip-cli/src/providers/index.ts index 38574f8e..78908aee 100644 --- a/ccip-cli/src/providers/index.ts +++ b/ccip-cli/src/providers/index.ts @@ -11,6 +11,7 @@ import { import './aptos.ts' import './evm.ts' import './solana.ts' +import './ton.ts' const RPCS_RE = /\b(?:http|ws)s?:\/\/[\w/\\@&?%~#.,;:=+-]+/ @@ -89,7 +90,7 @@ export function fetchChainsFromRpcs( chains[chain.network.name] = chain$ delete chainsCbs[chain.network.name] }, - () => {}, + () => { }, ) txs.push(tx$) } @@ -137,7 +138,7 @@ export function fetchChainsFromRpcs( if (txHash) { return [chainGetter, init$] } else { - void init$.catch(() => {}) + void init$.catch(() => { }) return chainGetter } } diff --git a/ccip-cli/src/providers/ton.ts b/ccip-cli/src/providers/ton.ts new file mode 100644 index 00000000..68bb22b3 --- /dev/null +++ b/ccip-cli/src/providers/ton.ts @@ -0,0 +1,62 @@ +import { existsSync, readFileSync } from 'node:fs' +import { TONChain } from '@chainlink/ccip-sdk/src/index.ts' +import { keyPairFromSecretKey, mnemonicToPrivateKey } from '@ton/crypto' +import { WalletContractV4 } from '@ton/ton' +import type { TONWallet } from '../../../ccip-sdk/src/ton/types.ts' + +TONChain.getWallet = async function loadTONWallet({ + wallet +}: { wallet?: unknown } = {}): Promise { + if (!wallet) wallet = process.env['USER_KEY'] || process.env['OWNER_KEY'] + + if (typeof wallet !== 'string') + throw new Error(`Invalid wallet option: ${wallet}`) + + // Handle mnemonic phrase + if (wallet.includes(' ')) { + const mnemonic = wallet.trim().split(' ') + const keyPair = await mnemonicToPrivateKey(mnemonic) + const contract = WalletContractV4.create({ + workchain: 0, + publicKey: keyPair.publicKey + }) + return { contract, keyPair } // Now properly typed as TONWallet + } + + // Handle hex private key + if (wallet.startsWith('0x')) { + const secretKey = Buffer.from(wallet.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(wallet)) { + const content = readFileSync(wallet, '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 } as TONWallet + } + + throw new Error('Wallet not specified') +} diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 5470842a..58b7f196 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -57,19 +57,39 @@ export class TONChain extends Chain { } /** - * + * Creates a TONChain instance from an RPC URL. + * Verifies the connection and detects the network. */ static async fromUrl(url: string): Promise { + // Validate URL format for TON endpoints + if ( + !url.includes('toncenter') && + !url.includes('ton') && + !url.includes('localhost') && + !url.includes('127.0.0.1') + ) { + throw new Error(`Invalid TON RPC URL: ${url}`) + } + const client = new TonClient({ endpoint: url }) + // Verify connection by making an actual RPC call + try { + await client.getMasterchainInfo() + } catch (error) { + throw new Error( + `Failed to connect to TON endpoint ${url}: ${error instanceof Error ? error.message : error}`, + ) + } + // Detect network from URL let networkId: string if (url.includes('testnet')) { networkId = 'ton-testnet' - } else if (url.includes('mainnet') || url.includes('toncenter.com/api')) { - networkId = 'ton-mainnet' + } else if (url.includes('sandbox') || url.includes('localhost') || url.includes('127.0.0.1')) { + networkId = 'ton-localnet' } else { - // Default to mainnet for unknown URLs + // Default to mainnet for production endpoints networkId = 'ton-mainnet' } From 7f53e094c300f54ff19d1455a52314b4f1a859e9 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 11:26:21 -0300 Subject: [PATCH 15/33] fix tests --- ccip-sdk/src/solana/exec.ts | 3 +-- ccip-sdk/src/solana/index.ts | 1 - ccip-sdk/src/solana/offchain.ts | 2 +- ccip-sdk/src/solana/send.ts | 4 ++-- ccip-sdk/src/utils.test.ts | 2 +- ccip-sdk/src/utils.ts | 34 ++++++++++++++++----------------- 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/ccip-sdk/src/solana/exec.ts b/ccip-sdk/src/solana/exec.ts index 058b6d04..73557def 100644 --- a/ccip-sdk/src/solana/exec.ts +++ b/ccip-sdk/src/solana/exec.ts @@ -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 { getDataBytes, toLeArray, bytesToBuffer } from '../utils.ts' type ExecAlt = { initialIxs: TransactionInstruction[] diff --git a/ccip-sdk/src/solana/index.ts b/ccip-sdk/src/solana/index.ts index 87f58981..5bac45ba 100644 --- a/ccip-sdk/src/solana/index.ts +++ b/ccip-sdk/src/solana/index.ts @@ -84,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, diff --git a/ccip-sdk/src/solana/offchain.ts b/ccip-sdk/src/solana/offchain.ts index 9cc621cf..80a438db 100644 --- a/ccip-sdk/src/solana/offchain.ts +++ b/ccip-sdk/src/solana/offchain.ts @@ -4,7 +4,7 @@ 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 { networkInfo, util, bytesToBuffer } 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' diff --git a/ccip-sdk/src/solana/send.ts b/ccip-sdk/src/solana/send.ts index 32f7baef..f4f409b2 100644 --- a/ccip-sdk/src/solana/send.ts +++ b/ccip-sdk/src/solana/send.ts @@ -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 { toLeArray, util, bytesToBuffer, } 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['SVM2AnyMessage'] { const feeTokenPubkey = message.feeToken ? new PublicKey(message.feeToken) : PublicKey.default diff --git a/ccip-sdk/src/utils.test.ts b/ccip-sdk/src/utils.test.ts index ce7c20a1..5a6dd112 100644 --- a/ccip-sdk/src/utils.test.ts +++ b/ccip-sdk/src/utils.test.ts @@ -814,7 +814,7 @@ describe('toLeArray', () => { it('should handle zero', () => { const result = toLeArray(0n) - assert.deepEqual(result, new Uint8Array([])) + assert.deepEqual(result, new Uint8Array([0x00])) }) it('should handle custom width', () => { diff --git a/ccip-sdk/src/utils.ts b/ccip-sdk/src/utils.ts index 7d6e1b06..af3ea811 100644 --- a/ccip-sdk/src/utils.ts +++ b/ccip-sdk/src/utils.ts @@ -8,7 +8,7 @@ import { decodeBase64, getBytes, isBytesLike, - toBeArray, + toBeHex, toBigInt, } from 'ethers' import { memoize } from 'micro-memoize' @@ -48,7 +48,7 @@ export async function getSomeBlockNumberBefore( beforeBlockNumber = Math.max( 1, Math.trunc(beforeBlockNumber - (beforeTimestamp - timestamp) / estimatedBlockTime) - - 10 ** iter, + 10 ** iter, ) beforeTimestamp = await getBlockTimestamp(beforeBlockNumber) estimatedBlockTime = (now - beforeTimestamp) / (recentBlockNumber - beforeBlockNumber) @@ -245,7 +245,7 @@ export function leToBigInt(data: BytesLike | readonly number[]): bigint { * @returns Little-endian Uint8Array. */ export function toLeArray(value: BigNumberish, width?: Numeric): Uint8Array { - return toBeArray(value, width).reverse() + return getBytes(toBeHex(value, width)).reverse() } /** @@ -536,21 +536,21 @@ export function createRateLimitedFetch( const util = 'util' in globalThis ? ( - globalThis as unknown as { - util: { - inspect: ((v: unknown) => string) & { - custom: symbol - defaultOptions: Record - } + globalThis as unknown as { + util: { + inspect: ((v: unknown) => string) & { + custom: symbol + defaultOptions: Record } } - ).util - : { - inspect: Object.assign((v: unknown) => JSON.stringify(v), { - custom: Symbol('custom'), - defaultOptions: { - depth: 2, - } as Record, - }), } + ).util + : { + inspect: Object.assign((v: unknown) => JSON.stringify(v), { + custom: Symbol('custom'), + defaultOptions: { + depth: 2, + } as Record, + }), + } export { util } From ec8cfbff74c51d4b279684f5aad6f77d5241d32e Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 11:31:38 -0300 Subject: [PATCH 16/33] lint --- ccip-sdk/src/solana/exec.ts | 2 +- ccip-sdk/src/solana/index.ts | 18 +++++++++--------- ccip-sdk/src/solana/offchain.ts | 2 +- ccip-sdk/src/solana/send.ts | 2 +- ccip-sdk/src/utils.ts | 30 +++++++++++++++--------------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/ccip-sdk/src/solana/exec.ts b/ccip-sdk/src/solana/exec.ts index 73557def..7beccd3e 100644 --- a/ccip-sdk/src/solana/exec.ts +++ b/ccip-sdk/src/solana/exec.ts @@ -17,7 +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, bytesToBuffer } from '../utils.ts' +import { bytesToBuffer, getDataBytes, toLeArray } from '../utils.ts' type ExecAlt = { initialIxs: TransactionInstruction[] diff --git a/ccip-sdk/src/solana/index.ts b/ccip-sdk/src/solana/index.ts index 5bac45ba..29cd3b33 100644 --- a/ccip-sdk/src/solana/index.ts +++ b/ccip-sdk/src/solana/index.ts @@ -283,7 +283,7 @@ export class SolanaChain extends Chain { }) if (!tx) throw new Error(`Transaction not found: ${hash}`) if (tx.blockTime) { - ; ( + ;( this.getBlockTimestamp as Memoized ).cache.set([tx.slot], Promise.resolve(tx.blockTime)) } else { @@ -293,10 +293,10 @@ export class SolanaChain extends Chain { // Parse logs from transaction using helper function const logs_ = tx.meta?.logMessages?.length ? parseSolanaLogs(tx.meta?.logMessages).map((l) => ({ - ...l, - transactionHash: hash, - blockNumber: tx.slot, - })) + ...l, + transactionHash: hash, + blockNumber: tx.slot, + })) : [] const chainTx: SolanaTransaction = { @@ -1434,9 +1434,9 @@ export class SolanaChain extends Chain { } } try { - ; ({ base } = tokenPoolCoder.accounts.decode('chainConfig', acc.account.data)) + ;({ base } = tokenPoolCoder.accounts.decode('chainConfig', acc.account.data)) } catch (_) { - ; ({ base } = cctpTokenPoolCoder.accounts.decode('chainConfig', acc.account.data)) + ;({ base } = cctpTokenPoolCoder.accounts.decode('chainConfig', acc.account.data)) } let remoteChainSelector @@ -1475,7 +1475,7 @@ export class SolanaChain extends Chain { const cur = inboundRateLimiterState.tokens + inboundRateLimiterState.rate * - BigInt(Math.floor(Date.now() / 1000) - base.inboundRateLimit.lastUpdated.toNumber()) + BigInt(Math.floor(Date.now() / 1000) - base.inboundRateLimit.lastUpdated.toNumber()) if (cur < inboundRateLimiterState.capacity) inboundRateLimiterState.tokens = cur else inboundRateLimiterState.tokens = inboundRateLimiterState.capacity } @@ -1490,7 +1490,7 @@ export class SolanaChain extends Chain { const cur = outboundRateLimiterState.tokens + outboundRateLimiterState.rate * - BigInt(Math.floor(Date.now() / 1000) - base.outboundRateLimit.lastUpdated.toNumber()) + BigInt(Math.floor(Date.now() / 1000) - base.outboundRateLimit.lastUpdated.toNumber()) if (cur < outboundRateLimiterState.capacity) outboundRateLimiterState.tokens = cur else outboundRateLimiterState.tokens = outboundRateLimiterState.capacity } diff --git a/ccip-sdk/src/solana/offchain.ts b/ccip-sdk/src/solana/offchain.ts index 80a438db..ed64ed6c 100644 --- a/ccip-sdk/src/solana/offchain.ts +++ b/ccip-sdk/src/solana/offchain.ts @@ -4,7 +4,7 @@ import { hexlify } from 'ethers' import { getUsdcAttestation } from '../offchain.ts' import type { CCIPMessage, CCIPRequest, OffchainTokenData, WithLogger } from '../types.ts' -import { networkInfo, util, bytesToBuffer } 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' diff --git a/ccip-sdk/src/solana/send.ts b/ccip-sdk/src/solana/send.ts index f4f409b2..23dda2ba 100644 --- a/ccip-sdk/src/solana/send.ts +++ b/ccip-sdk/src/solana/send.ts @@ -19,7 +19,7 @@ import { zeroPadValue } from 'ethers' import { SolanaChain } from './index.ts' import { type AnyMessage, type WithLogger, ChainFamily } from '../types.ts' -import { toLeArray, util, bytesToBuffer, } 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 { simulationProvider } from './utils.ts' diff --git a/ccip-sdk/src/utils.ts b/ccip-sdk/src/utils.ts index af3ea811..05e0b4a3 100644 --- a/ccip-sdk/src/utils.ts +++ b/ccip-sdk/src/utils.ts @@ -48,7 +48,7 @@ export async function getSomeBlockNumberBefore( beforeBlockNumber = Math.max( 1, Math.trunc(beforeBlockNumber - (beforeTimestamp - timestamp) / estimatedBlockTime) - - 10 ** iter, + 10 ** iter, ) beforeTimestamp = await getBlockTimestamp(beforeBlockNumber) estimatedBlockTime = (now - beforeTimestamp) / (recentBlockNumber - beforeBlockNumber) @@ -536,21 +536,21 @@ export function createRateLimitedFetch( const util = 'util' in globalThis ? ( - globalThis as unknown as { - util: { - inspect: ((v: unknown) => string) & { - custom: symbol - defaultOptions: Record + globalThis as unknown as { + util: { + inspect: ((v: unknown) => string) & { + custom: symbol + defaultOptions: Record + } } } - } - ).util + ).util : { - inspect: Object.assign((v: unknown) => JSON.stringify(v), { - custom: Symbol('custom'), - defaultOptions: { - depth: 2, - } as Record, - }), - } + inspect: Object.assign((v: unknown) => JSON.stringify(v), { + custom: Symbol('custom'), + defaultOptions: { + depth: 2, + } as Record, + }), + } export { util } From 317713aaf67703550521bf459d7aa0cd885b4456 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 13:16:42 -0300 Subject: [PATCH 17/33] fix eslint --- ccip-sdk/src/chain.ts | 1 + ccip-sdk/src/extra-args.ts | 4 +- ccip-sdk/src/solana/utils.ts | 2 - ccip-sdk/src/ton/exec.ts | 9 ++ ccip-sdk/src/ton/hasher.ts | 18 ++-- ccip-sdk/src/ton/index.ts | 192 ++++++++++++++++------------------- ccip-sdk/src/ton/types.ts | 8 +- 7 files changed, 114 insertions(+), 120 deletions(-) diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index 70124cf4..fe50fb44 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -102,6 +102,7 @@ export type UnsignedTx = { [ChainFamily.Solana]: UnsignedSolanaTx [ChainFamily.Aptos]: UnsignedAptosTx [ChainFamily.Sui]: never // TODO + [ChainFamily.TON]: never // TODO } /** diff --git a/ccip-sdk/src/extra-args.ts b/ccip-sdk/src/extra-args.ts index 85d1b933..acff3c3d 100644 --- a/ccip-sdk/src/extra-args.ts +++ b/ccip-sdk/src/extra-args.ts @@ -56,9 +56,9 @@ export type SuiExtraArgsV1 = EVMExtraArgsV2 & { receiverObjectIds: string[] } -// Same structure as EVMExtraArgsV2. TON calls it GenericExtraArgsV2 /** - * + * Tag identifier for GenericExtraArgsV2 encoding. + * Uses the same tag as EVMExtraArgsV2 since they share the same structure. */ export type GenericExtraArgsV2 = EVMExtraArgsV2 diff --git a/ccip-sdk/src/solana/utils.ts b/ccip-sdk/src/solana/utils.ts index a688bc76..e4a2ec7c 100644 --- a/ccip-sdk/src/solana/utils.ts +++ b/ccip-sdk/src/solana/utils.ts @@ -1,5 +1,3 @@ -import { Buffer } from 'buffer' - import { eventDiscriminator } from '@coral-xyz/anchor' import { type AddressLookupTableAccount, diff --git a/ccip-sdk/src/ton/exec.ts b/ccip-sdk/src/ton/exec.ts index 537423e8..d5d14062 100644 --- a/ccip-sdk/src/ton/exec.ts +++ b/ccip-sdk/src/ton/exec.ts @@ -6,7 +6,16 @@ import { type CCIPMessage_V1_6_TON, type TONWallet, serializeExecutionReport } f import { waitForTransaction } from './utils.ts' /** + * Executes a CCIP message on the TON OffRamp contract. + * Serializes the execution report, constructs the OffRamp_ManuallyExecute message, + * sends the transaction via the wallet, and waits for confirmation. * + * @param client - TonClient instance for RPC calls. + * @param wallet - TON wallet with contract and keypair for signing. + * @param offRamp - OffRamp contract address. + * @param execReport - Execution report containing the CCIP message and proofs. + * @param opts - Optional execution options. Gas limit override for execution (0 = no override). + * @returns Transaction hash in format "workchain:address:lt:hash". */ export async function executeReport( client: TonClient, diff --git a/ccip-sdk/src/ton/hasher.ts b/ccip-sdk/src/ton/hasher.ts index fa4b5b26..190aaeb0 100644 --- a/ccip-sdk/src/ton/hasher.ts +++ b/ccip-sdk/src/ton/hasher.ts @@ -11,13 +11,11 @@ import { hexToBuffer, tryParseCell } from './utils.ts' const LEAF_DOMAIN_BUFFER = Buffer.from(LEAF_DOMAIN_SEPARATOR.slice(2).padStart(64, '0'), 'hex') /** - * Creates a leaf hasher for TON messages + * Creates a leaf hasher for TON messages. * - * @param sourceChainSelector - * @param destChainSelector - * @param onRamp - as hex string - * @param version - CCIP version (only v1.6 supported for TON) - * @returns A LeafHasher function that computes message hashes for TON + * @param lane - Lane configuration containing sourceChainSelector, destChainSelector, + * onRamp (as hex string), and version (only v1.6 supported for TON). + * @returns A LeafHasher function that computes message hashes for TON. */ export function getTONLeafHasher({ sourceChainSelector, @@ -48,10 +46,10 @@ export function getTONLeafHasher({ * (source chain, destination chain, and onRamp address). * Following the TON implementation from chainlink-ton repo. * - * @param sourceChainSelector - * @param destChainSelector - * @param onRamp - * @returns SHA256 hash of the metadata as hex string + * @param sourceChainSelector - Source chain selector. + * @param destChainSelector - Destination chain selector. + * @param onRamp - OnRamp address as hex string. + * @returns SHA256 hash of the metadata as hex string. */ export const hashTONMetadata = ( sourceChainSelector: bigint, diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 58b7f196..1e8609b1 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -20,6 +20,7 @@ import { type Log_, type NetworkInfo, type OffchainTokenData, + type WithLogger, ChainFamily, } from '../types.ts' import { getDataBytes, networkInfo } from '../utils.ts' @@ -31,25 +32,26 @@ import type { CCIPMessage_V1_6_TON, TONWallet } from './types.ts' const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2Tag, 16) /** - * + * TON chain implementation supporting TON networks. */ export class TONChain extends Chain { static { supportedChains[ChainFamily.TON] = TONChain } static readonly family = ChainFamily.TON - static readonly decimals = 8 + static readonly decimals = 9 // TON uses 9 decimals (nanotons) - readonly network: NetworkInfo readonly provider: TonClient /** - * + * Creates a new TONChain instance. + * @param client - TonClient instance. + * @param network - Network information for this chain. + * @param ctx - Context containing logger. */ - constructor(client: TonClient, network: NetworkInfo) { - super() + constructor(client: TonClient, network: NetworkInfo, ctx?: WithLogger) { + super(network, ctx) this.provider = client - this.network = network this.getTransaction = memoize(this.getTransaction.bind(this), { maxSize: 100, @@ -59,8 +61,11 @@ export class TONChain extends Chain { /** * Creates a TONChain instance from an RPC URL. * Verifies the connection and detects the network. + * @param url - RPC endpoint URL. + * @param ctx - Context containing logger. + * @returns A new TONChain instance. */ - static async fromUrl(url: string): Promise { + static async fromUrl(url: string, ctx?: WithLogger): Promise { // Validate URL format for TON endpoints if ( !url.includes('toncenter') && @@ -77,9 +82,8 @@ export class TONChain extends Chain { try { await client.getMasterchainInfo() } catch (error) { - throw new Error( - `Failed to connect to TON endpoint ${url}: ${error instanceof Error ? error.message : error}`, - ) + const message = error instanceof Error ? error.message : String(error) + throw new Error(`Failed to connect to TON endpoint ${url}: ${message}`) } // Detect network from URL @@ -93,13 +97,10 @@ export class TONChain extends Chain { networkId = 'ton-mainnet' } - const network = networkInfo(networkId) as NetworkInfo - return new TONChain(client, network) + return new TONChain(client, networkInfo(networkId), ctx) } - /** - * - */ + /** {@inheritDoc Chain.getBlockTimestamp} */ async getBlockTimestamp(_version: number | 'finalized'): Promise { return Promise.reject(new Error('Not implemented')) } @@ -142,24 +143,19 @@ export class TONChain extends Chain { } } - /** - * - */ - async *getLogs(_opts: LogFilter & { versionAsHash?: boolean }) { + /** {@inheritDoc Chain.getLogs} */ + async *getLogs(_opts: LogFilter & { versionAsHash?: boolean }): AsyncIterableIterator { await Promise.resolve() throw new Error('Not implemented') + yield undefined as never } - /** - * - */ + /** {@inheritDoc Chain.fetchRequestsInTx} */ override async fetchRequestsInTx(_tx: string | ChainTransaction): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.fetchAllMessagesInBatch} */ override async fetchAllMessagesInBatch< R extends PickDeep< CCIPRequest, @@ -173,9 +169,7 @@ export class TONChain extends Chain { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.typeAndVersion} */ async typeAndVersion( _address: string, ): Promise< @@ -185,92 +179,69 @@ export class TONChain extends Chain { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getRouterForOnRamp} */ getRouterForOnRamp(_onRamp: string, _destChainSelector: bigint): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getRouterForOffRamp} */ getRouterForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getNativeTokenForRouter} */ getNativeTokenForRouter(_router: string): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getOffRampsForRouter} */ getOffRampsForRouter(_router: string, _sourceChainSelector: bigint): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getOnRampForRouter} */ getOnRampForRouter(_router: string, _destChainSelector: bigint): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getOnRampForOffRamp} */ async getOnRampForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getCommitStoreForOffRamp} */ getCommitStoreForOffRamp(_offRamp: string): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getTokenForTokenPool} */ async getTokenForTokenPool(_tokenPool: string): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getTokenInfo} */ async getTokenInfo(_token: string): Promise<{ symbol: string; decimals: number }> { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getTokenAdminRegistryFor} */ getTokenAdminRegistryFor(_address: string): Promise { return Promise.reject(new Error('Not implemented')) } /** - * - */ - async getWalletAddress(_opts?: { wallet?: unknown }): Promise { - return Promise.reject(new Error('Not implemented')) - } - - /** - * + * Static wallet loading not available for TON. + * @param _opts - Wallet options (unused). + * @returns Never resolves, always throws. */ - static getWallet(_opts: { wallet?: unknown } = {}): Promise { + static getWallet(_opts: { wallet?: unknown } = {}): Promise { throw new Error('static TON wallet loading not available') } /** * Loads a TON wallet from various input formats. + * @param opts - Wallet options (mnemonic, secret key, or TONWallet instance). + * @returns TONWallet instance. */ async getWallet(opts: { wallet?: unknown } = {}): Promise { // Handle private key string (hex or base64) @@ -330,7 +301,9 @@ export class TONChain extends Chain { // Static methods for decoding /** - * + * Decodes a CCIP message from a TON log event. + * @param _log - Log with data field. + * @returns Decoded CCIPMessage or undefined if not valid. */ static decodeMessage(_log: Log_): CCIPMessage_V1_6_TON | undefined { throw new Error('Not implemented') @@ -391,43 +364,60 @@ export class TONChain extends Chain { } /** - * + * Decodes commit reports from a TON log event. + * @param _log - Log with data field. + * @param _lane - Lane info for filtering. + * @returns Array of CommitReport or undefined if not valid. */ static decodeCommits(_log: Log_, _lane?: Lane): CommitReport[] | undefined { throw new Error('Not implemented') } /** - * + * Decodes an execution receipt from a TON log event. + * @param _log - Log with data field. + * @returns ExecutionReceipt or undefined if not valid. */ static decodeReceipt(_log: Log_): ExecutionReceipt | undefined { throw new Error('Not implemented') } /** - * + * Converts bytes to a TON address. + * @param _bytes - Bytes to convert. + * @returns TON address string. */ static getAddress(_bytes: BytesLike): string { throw new Error('Not implemented') } /** - * + * Gets the leaf hasher for TON destination chains. + * @param lane - Lane configuration. + * @param _ctx - Context containing logger. + * @returns Leaf hasher function. */ - static getDestLeafHasher(lane: Lane): LeafHasher { + static getDestLeafHasher(lane: Lane, _ctx?: WithLogger): LeafHasher { return getTONLeafHasher(lane) } - /** - * - */ + /** {@inheritDoc Chain.getFee} */ async getFee(_router: string, _destChainSelector: bigint, _message: AnyMessage): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.generateUnsignedSendMessage} */ + generateUnsignedSendMessage( + _sender: string, + _router: string, + _destChainSelector: bigint, + _message: AnyMessage & { fee?: bigint }, + _opts?: { approveMax?: boolean }, + ): Promise { + return Promise.reject(new Error('Not implemented')) + } + + /** {@inheritDoc Chain.sendMessage} */ async sendMessage( _router: string, _destChainSelector: bigint, @@ -437,9 +427,7 @@ export class TONChain extends Chain { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.fetchOffchainTokenData} */ fetchOffchainTokenData(request: CCIPRequest): Promise { if (!('receiverObjectIds' in request.message)) { throw new Error('Invalid message, not v1.6 TON') @@ -448,9 +436,17 @@ export class TONChain extends Chain { return Promise.resolve(request.message.tokenAmounts.map(() => undefined)) } - /** - * - */ + /** {@inheritDoc Chain.generateUnsignedExecuteReport} */ + generateUnsignedExecuteReport( + _payer: string, + _offRamp: string, + _execReport: ExecutionReport, + _opts?: { wallet?: unknown; gasLimit?: number }, + ): Promise { + return Promise.reject(new Error('Not implemented')) + } + + /** {@inheritDoc Chain.executeReport} */ async executeReport( offRamp: string, execReport: ExecutionReport, @@ -470,7 +466,9 @@ export class TONChain extends Chain { } /** - * + * Parses raw TON data into typed structures. + * @param data - Raw data to parse. + * @returns Parsed data or undefined. */ static parse(data: unknown) { if (isBytesLike(data)) { @@ -479,37 +477,27 @@ export class TONChain extends Chain { } } - /** - * - */ + /** {@inheritDoc Chain.getSupportedTokens} */ async getSupportedTokens(_address: string): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getRegistryTokenConfig} */ async getRegistryTokenConfig(_address: string, _tokenName: string): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getTokenPoolConfigs} */ async getTokenPoolConfigs(_tokenPool: string): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getTokenPoolRemotes} */ async getTokenPoolRemotes(_tokenPool: string): Promise { return Promise.reject(new Error('Not implemented')) } - /** - * - */ + /** {@inheritDoc Chain.getFeeTokens} */ async getFeeTokens(_router: string): Promise { return Promise.reject(new Error('Not implemented')) } diff --git a/ccip-sdk/src/ton/types.ts b/ccip-sdk/src/ton/types.ts index 477b1143..34e1b19c 100644 --- a/ccip-sdk/src/ton/types.ts +++ b/ccip-sdk/src/ton/types.ts @@ -7,9 +7,7 @@ import type { GenericExtraArgsV2 } from '../extra-args.ts' import type { CCIPMessage_V1_6, ExecutionReport } from '../types.ts' import { bytesToBuffer } from '../utils.ts' -/** - * - */ +/** TON-specific CCIP v1.6 message type with GenericExtraArgsV2 (gasLimit + allowOutOfOrderExecution). */ export type CCIPMessage_V1_6_TON = CCIPMessage_V1_6 & GenericExtraArgsV2 /** @@ -49,7 +47,9 @@ function asSnakeData(array: T[], builderFn: (item: T) => Builder): Cell { } /** - * + * Serializes an execution report into a TON Cell for OffRamp execution. + * @param execReport - Execution report containing message, proofs, and proof flag bits. + * @returns BOC-serialized Cell containing the execution report. */ export function serializeExecutionReport(execReport: ExecutionReport): Cell { return beginCell() From 977ac9e5638c5ff08c5ecf8fdea624f1de22f1a7 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 14:08:01 -0300 Subject: [PATCH 18/33] put unref back with new version --- ccip-sdk/src/utils.ts | 32 ++++++++++++++++---------------- package-lock.json | 16 +++++++--------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/ccip-sdk/src/utils.ts b/ccip-sdk/src/utils.ts index 05e0b4a3..95dce0be 100644 --- a/ccip-sdk/src/utils.ts +++ b/ccip-sdk/src/utils.ts @@ -48,7 +48,7 @@ export async function getSomeBlockNumberBefore( beforeBlockNumber = Math.max( 1, Math.trunc(beforeBlockNumber - (beforeTimestamp - timestamp) / estimatedBlockTime) - - 10 ** iter, + 10 ** iter, ) beforeTimestamp = await getBlockTimestamp(beforeBlockNumber) estimatedBlockTime = (now - beforeTimestamp) / (recentBlockNumber - beforeBlockNumber) @@ -347,7 +347,7 @@ export function convertKeysToCamelCase( * @param ms - Duration in milliseconds. * @returns Promise that resolves after the specified duration. */ -export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) +export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms).unref) /** * Parses a typeAndVersion string into its components. @@ -536,21 +536,21 @@ export function createRateLimitedFetch( const util = 'util' in globalThis ? ( - globalThis as unknown as { - util: { - inspect: ((v: unknown) => string) & { - custom: symbol - defaultOptions: Record - } + globalThis as unknown as { + util: { + inspect: ((v: unknown) => string) & { + custom: symbol + defaultOptions: Record } } - ).util - : { - inspect: Object.assign((v: unknown) => JSON.stringify(v), { - custom: Symbol('custom'), - defaultOptions: { - depth: 2, - } as Record, - }), } + ).util + : { + inspect: Object.assign((v: unknown) => JSON.stringify(v), { + custom: Symbol('custom'), + defaultOptions: { + depth: 2, + } as Record, + }), + } export { util } diff --git a/package-lock.json b/package-lock.json index 00cfe157..3eb6a592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@chainlink/ccip-tools-ts", - "version": "0.91.0", + "version": "0.91.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@chainlink/ccip-tools-ts", - "version": "0.91.0", + "version": "0.91.1", "license": "MIT", "workspaces": [ "ccip-sdk", @@ -31,7 +31,7 @@ }, "ccip-cli": { "name": "@chainlink/ccip-cli", - "version": "0.91.0", + "version": "0.91.1", "license": "MIT", "dependencies": { "@aptos-labs/ts-sdk": "^5.1.6", @@ -96,7 +96,7 @@ }, "ccip-sdk": { "name": "@chainlink/ccip-sdk", - "version": "0.91.0", + "version": "0.91.1", "license": "MIT", "dependencies": { "@aptos-labs/ts-sdk": "^5.1.6", @@ -2824,6 +2824,7 @@ "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.62.0.tgz", "integrity": "sha512-GCYlzzx11rSESKkiHvNy9tL8zWth+ZtUbvV29WH478FvBp8xTw24AyoigwXWNV+OLCAcnwlGhZpTpxjD3wzCwA==", "license": "MIT", + "peer": true, "dependencies": { "symbol.inspect": "1.0.1" }, @@ -2848,7 +2849,6 @@ "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz", "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==", "license": "MIT", - "peer": true, "dependencies": { "jssha": "3.2.0" } @@ -7068,7 +7068,6 @@ "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": "*" } @@ -9080,8 +9079,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense", - "peer": true + "license": "Unlicense" }, "node_modules/type-check": { "version": "0.4.0", @@ -9800,4 +9798,4 @@ } } } -} \ No newline at end of file +} From 2acfbf4cef3e7722d4f23427531ce61038ab99bc Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 14:09:29 -0300 Subject: [PATCH 19/33] lint --- ccip-sdk/src/utils.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/ccip-sdk/src/utils.ts b/ccip-sdk/src/utils.ts index 95dce0be..d55fca88 100644 --- a/ccip-sdk/src/utils.ts +++ b/ccip-sdk/src/utils.ts @@ -48,7 +48,7 @@ export async function getSomeBlockNumberBefore( beforeBlockNumber = Math.max( 1, Math.trunc(beforeBlockNumber - (beforeTimestamp - timestamp) / estimatedBlockTime) - - 10 ** iter, + 10 ** iter, ) beforeTimestamp = await getBlockTimestamp(beforeBlockNumber) estimatedBlockTime = (now - beforeTimestamp) / (recentBlockNumber - beforeBlockNumber) @@ -347,7 +347,7 @@ export function convertKeysToCamelCase( * @param ms - Duration in milliseconds. * @returns Promise that resolves after the specified duration. */ -export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms).unref) +export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms).unref()) /** * Parses a typeAndVersion string into its components. @@ -536,21 +536,21 @@ export function createRateLimitedFetch( const util = 'util' in globalThis ? ( - globalThis as unknown as { - util: { - inspect: ((v: unknown) => string) & { - custom: symbol - defaultOptions: Record + globalThis as unknown as { + util: { + inspect: ((v: unknown) => string) & { + custom: symbol + defaultOptions: Record + } } } - } - ).util + ).util : { - inspect: Object.assign((v: unknown) => JSON.stringify(v), { - custom: Symbol('custom'), - defaultOptions: { - depth: 2, - } as Record, - }), - } + inspect: Object.assign((v: unknown) => JSON.stringify(v), { + custom: Symbol('custom'), + defaultOptions: { + depth: 2, + } as Record, + }), + } export { util } From 40fbfbb6d36337f8a0a1a28960ba287ff014930c Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 14:27:10 -0300 Subject: [PATCH 20/33] package lock --- package-lock.json | 1150 +++++++++++++++++++++++++++++++++------------ 1 file changed, 856 insertions(+), 294 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3eb6a592..015ef732 100644 --- a/package-lock.json +++ b/package-lock.json @@ -155,9 +155,9 @@ } }, "node_modules/@0no-co/graphqlsp": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.15.1.tgz", - "integrity": "sha512-UBDBuVGpX5Ti0PjGnSAzkMG04psNYxKfJ+1bgF8HFPfHHpKNVl4GULHSNW0GTOngcYCYA70c+InoKw0qjHwmVQ==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.15.2.tgz", + "integrity": "sha512-Ys031WnS3sTQQBtRTkQsYnw372OlW72ais4sp0oh2UMPRNyxxnq85zRfU4PIdoy9kWriysPT5BYAkgIxhbonFA==", "license": "MIT", "dependencies": { "@gql.tada/internal": "^1.0.0", @@ -187,9 +187,9 @@ } }, "node_modules/@aptos-labs/aptos-client": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@aptos-labs/aptos-client/-/aptos-client-2.0.0.tgz", - "integrity": "sha512-A23T3zTCRXEKURodp00dkadVtIrhWjC9uo08dRDBkh69OhCnBAxkENmUy/rcBarfLoFr60nRWt7cBkc8wxr1mg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aptos-labs/aptos-client/-/aptos-client-2.1.0.tgz", + "integrity": "sha512-ttdY0qclRvbYAAwzijkFeipuqTfLFJnoXlNIm58tIw3DKhIlfYdR6iLqTeCpI23oOPghnO99FZecej/0MTrtuA==", "license": "Apache-2.0", "engines": { "node": ">=20.0.0" @@ -1941,14 +1941,14 @@ } }, "node_modules/@ledgerhq/domain-service": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@ledgerhq/domain-service/-/domain-service-1.4.1.tgz", - "integrity": "sha512-ku4Q/d+uiznylCqGTzSfvopzgVeBsGqANkF6CHnIu8tThFwlrU2h4O7D7OAgIUcVo1TXgm8a7p4noprDL7ySvA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@ledgerhq/domain-service/-/domain-service-1.4.2.tgz", + "integrity": "sha512-wVbvBhVXWNJ5Yb0aT23yE9NGGct5FX0kOCHjzEJmmPQt4aSHmHfCEKSO4DgD+51Mesux7se99GLa5+/a/s4tAQ==", "license": "Apache-2.0", "dependencies": { "@ledgerhq/errors": "^6.27.0", "@ledgerhq/logs": "^6.13.0", - "@ledgerhq/types-live": "^6.89.0", + "@ledgerhq/types-live": "^6.90.0", "axios": "1.12.2", "eip55": "^2.1.1", "react": "18.3.1", @@ -2073,9 +2073,9 @@ "license": "Apache-2.0" }, "node_modules/@ledgerhq/types-live": { - "version": "6.89.0", - "resolved": "https://registry.npmjs.org/@ledgerhq/types-live/-/types-live-6.89.0.tgz", - "integrity": "sha512-wz+3HiyTjnzGe8yAfzF3gzu0ftt5gYoDBDJaIqRiYsCOZtVkqJ9PG/mvLDBzOCGvR+Tj7dGTHuzRz0UXqSTjRA==", + "version": "6.90.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/types-live/-/types-live-6.90.0.tgz", + "integrity": "sha512-F+YyfE0HZoqc8HkaJXEHhMjU4JX6eHzCfXKmJH/MHiymrqg8tOa+c07mWIlvspf5jcpswRM3r1k81y2A8q/aWw==", "license": "Apache-2.0", "dependencies": { "bignumber.js": "^9.1.2", @@ -3041,6 +3041,157 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -3051,13 +3202,28 @@ "node": ">= 4" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/parser": { "version": "8.48.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -3077,7 +3243,7 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/project-service": { + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { "version": "8.48.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", @@ -3099,7 +3265,7 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { "version": "8.48.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", @@ -3117,7 +3283,7 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.48.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", @@ -3134,32 +3300,7 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", - "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { "version": "8.48.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", @@ -3173,7 +3314,7 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/typescript-estree": { + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { "version": "8.48.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", @@ -3201,7 +3342,25 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", @@ -3211,7 +3370,7 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", @@ -3227,17 +3386,16 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", - "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1" + "@typescript-eslint/tsconfig-utils": "^8.46.4", + "@typescript-eslint/types": "^8.46.4", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3247,19 +3405,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", - "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3269,22 +3426,398 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], + "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", + "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", "cpu": [ @@ -4038,6 +4571,21 @@ "node": ">=4.5" } }, + "node_modules/bufferutil": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/c8": { "version": "10.1.3", "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", @@ -5086,9 +5634,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "61.4.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-61.4.2.tgz", - "integrity": "sha512-WzZNvefoUaG/JWikVFhNLYqE2BEd6LQD2ZyfJOe1Ld3Cir05csDMMf0AihGwrSbB/e7fHRSfQOZ4F/hik9fQww==", + "version": "61.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-61.5.0.tgz", + "integrity": "sha512-PR81eOGq4S7diVnV9xzFSBE4CDENRQGP0Lckkek8AdHtbj+6Bm0cItwlFnxsLFriJHspiE3mpu8U20eODyToIg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5142,187 +5690,19 @@ }, "eslint-config-prettier": { "optional": true - } - } - }, - "node_modules/eslint-plugin-tsdoc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.5.0.tgz", - "integrity": "sha512-ush8ehCwub2rgE16OIgQPFyj/o0k3T8kL++9IrAI4knsmupNo8gvfO2ERgDHWWgTC5MglbwLVRswU93HyXqNpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@microsoft/tsdoc": "0.16.0", - "@microsoft/tsdoc-config": "0.18.0", - "@typescript-eslint/utils": "~8.46.0" - } - }, - "node_modules/eslint-plugin-tsdoc/node_modules/@typescript-eslint/project-service": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", - "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.4", - "@typescript-eslint/types": "^8.46.4", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/eslint-plugin-tsdoc/node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", - "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-tsdoc/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", - "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/eslint-plugin-tsdoc/node_modules/@typescript-eslint/types": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", - "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-tsdoc/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", - "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.46.4", - "@typescript-eslint/tsconfig-utils": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/eslint-plugin-tsdoc/node_modules/@typescript-eslint/utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", - "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/eslint-plugin-tsdoc/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", - "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.4", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-tsdoc/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + } } }, - "node_modules/eslint-plugin-tsdoc/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/eslint-plugin-tsdoc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.5.0.tgz", + "integrity": "sha512-ush8ehCwub2rgE16OIgQPFyj/o0k3T8kL++9IrAI4knsmupNo8gvfO2ERgDHWWgTC5MglbwLVRswU93HyXqNpw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "@microsoft/tsdoc": "0.16.0", + "@microsoft/tsdoc-config": "0.18.0", + "@typescript-eslint/utils": "~8.46.0" } }, "node_modules/eslint-scope": { @@ -5665,24 +6045,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -7247,19 +7609,6 @@ "node": ">=8.6" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -7757,14 +8106,13 @@ } }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", - "peer": true, "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -8969,6 +9317,38 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9095,9 +9475,9 @@ } }, "node_modules/type-fest": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.0.tgz", - "integrity": "sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz", + "integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -9225,6 +9605,173 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/project-service": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -9328,6 +9875,21 @@ "node-gyp-build-test": "build-test.js" } }, + "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==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", From 1c42b413d5a52bd1f8203bdb6fe1663a893686a3 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 14:40:41 -0300 Subject: [PATCH 21/33] change cli imports --- ccip-cli/src/providers/ton.ts | 2 +- package-lock.json | 31 +------------------------------ 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/ccip-cli/src/providers/ton.ts b/ccip-cli/src/providers/ton.ts index 68bb22b3..0132ce11 100644 --- a/ccip-cli/src/providers/ton.ts +++ b/ccip-cli/src/providers/ton.ts @@ -1,8 +1,8 @@ import { existsSync, readFileSync } from 'node:fs' import { TONChain } from '@chainlink/ccip-sdk/src/index.ts' +import type { TONWallet } from '@chainlink/ccip-sdk/src/ton/types.ts' import { keyPairFromSecretKey, mnemonicToPrivateKey } from '@ton/crypto' import { WalletContractV4 } from '@ton/ton' -import type { TONWallet } from '../../../ccip-sdk/src/ton/types.ts' TONChain.getWallet = async function loadTONWallet({ wallet diff --git a/package-lock.json b/package-lock.json index 015ef732..370d98ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3224,6 +3224,7 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -4571,21 +4572,6 @@ "node": ">=4.5" } }, - "node_modules/bufferutil": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", - "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/c8": { "version": "10.1.3", "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", @@ -9875,21 +9861,6 @@ "node-gyp-build-test": "build-test.js" } }, - "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==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", From fdadd8d821233eea0bb51c95d10c5ea61b2cb086 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 14:44:24 -0300 Subject: [PATCH 22/33] fix lint --- ccip-cli/src/index.ts | 2 +- ccip-cli/src/providers/index.ts | 4 +- ccip-cli/src/providers/ton.ts | 91 ++++++++++++++++----------------- 3 files changed, 48 insertions(+), 49 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index a91aec0b..e01d9194 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -72,7 +72,7 @@ function wasCalledAsScript() { } if (import.meta?.main || wasCalledAsScript()) { - const later = setTimeout(() => { }, 2 ** 31 - 1) // keep event-loop alive + const later = setTimeout(() => {}, 2 ** 31 - 1) // keep event-loop alive await main() .catch((err) => { console.error(err) diff --git a/ccip-cli/src/providers/index.ts b/ccip-cli/src/providers/index.ts index b48abfa6..9f5be52f 100644 --- a/ccip-cli/src/providers/index.ts +++ b/ccip-cli/src/providers/index.ts @@ -116,7 +116,7 @@ export function fetchChainsFromRpcs( chains[chain.network.name] = chain$ delete chainsCbs[chain.network.name] }, - () => { }, + () => {}, ) txs.push(tx$) } @@ -164,7 +164,7 @@ export function fetchChainsFromRpcs( if (txHash) { return [chainGetter, init$] } else { - void init$.catch(() => { }) + void init$.catch(() => {}) return chainGetter } } diff --git a/ccip-cli/src/providers/ton.ts b/ccip-cli/src/providers/ton.ts index 0132ce11..c5e71d0c 100644 --- a/ccip-cli/src/providers/ton.ts +++ b/ccip-cli/src/providers/ton.ts @@ -1,62 +1,61 @@ import { existsSync, readFileSync } from 'node:fs' + import { TONChain } from '@chainlink/ccip-sdk/src/index.ts' import type { TONWallet } from '@chainlink/ccip-sdk/src/ton/types.ts' import { keyPairFromSecretKey, mnemonicToPrivateKey } from '@ton/crypto' import { WalletContractV4 } from '@ton/ton' TONChain.getWallet = async function loadTONWallet({ - wallet + wallet, }: { wallet?: unknown } = {}): Promise { - if (!wallet) wallet = process.env['USER_KEY'] || process.env['OWNER_KEY'] + if (!wallet) wallet = process.env['USER_KEY'] || process.env['OWNER_KEY'] - if (typeof wallet !== 'string') - throw new Error(`Invalid wallet option: ${wallet}`) + if (typeof wallet !== 'string') throw new Error(`Invalid wallet option: ${wallet}`) - // Handle mnemonic phrase - if (wallet.includes(' ')) { - const mnemonic = wallet.trim().split(' ') - const keyPair = await mnemonicToPrivateKey(mnemonic) - const contract = WalletContractV4.create({ - workchain: 0, - publicKey: keyPair.publicKey - }) - return { contract, keyPair } // Now properly typed as TONWallet - } + // Handle mnemonic phrase + if (wallet.includes(' ')) { + const mnemonic = wallet.trim().split(' ') + const keyPair = await mnemonicToPrivateKey(mnemonic) + const contract = WalletContractV4.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }) + return { contract, keyPair } // Now properly typed as TONWallet + } - // Handle hex private key - if (wallet.startsWith('0x')) { - const secretKey = Buffer.from(wallet.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 hex private key + if (wallet.startsWith('0x')) { + const secretKey = Buffer.from(wallet.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(wallet)) { - const content = readFileSync(wallet, '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 } as TONWallet + // Handle file path + if (existsSync(wallet)) { + const content = readFileSync(wallet, '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 } as TONWallet + } - throw new Error('Wallet not specified') + throw new Error('Wallet not specified') } From 89ac0310dbd56120f60918f65d24255253162e3d Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 14:52:34 -0300 Subject: [PATCH 23/33] fix lint --- ccip-cli/src/providers/ton.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccip-cli/src/providers/ton.ts b/ccip-cli/src/providers/ton.ts index c5e71d0c..825d1290 100644 --- a/ccip-cli/src/providers/ton.ts +++ b/ccip-cli/src/providers/ton.ts @@ -10,7 +10,7 @@ TONChain.getWallet = async function loadTONWallet({ }: { wallet?: unknown } = {}): Promise { if (!wallet) wallet = process.env['USER_KEY'] || process.env['OWNER_KEY'] - if (typeof wallet !== 'string') throw new Error(`Invalid wallet option: ${wallet}`) + if (typeof wallet !== 'string') throw new Error(`Invalid wallet option: ${String(wallet)}`) // Handle mnemonic phrase if (wallet.includes(' ')) { From e76ce2dbfdeb3acf5b6e241fbb48cb9babadd872 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 15:19:53 -0300 Subject: [PATCH 24/33] ton provider remove comm --- ccip-cli/src/providers/ton.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccip-cli/src/providers/ton.ts b/ccip-cli/src/providers/ton.ts index 825d1290..16b0037e 100644 --- a/ccip-cli/src/providers/ton.ts +++ b/ccip-cli/src/providers/ton.ts @@ -20,7 +20,7 @@ TONChain.getWallet = async function loadTONWallet({ workchain: 0, publicKey: keyPair.publicKey, }) - return { contract, keyPair } // Now properly typed as TONWallet + return { contract, keyPair } } // Handle hex private key From 1f218ea2975f775d31093b8655ba960919b099e6 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 18:15:05 -0300 Subject: [PATCH 25/33] unsigned report impl and provider fix --- ccip-cli/src/providers/index.ts | 4 + ccip-cli/src/providers/ton.ts | 30 ++++--- ccip-sdk/src/chain.ts | 3 +- ccip-sdk/src/ton/exec.test.ts | 80 ++++++++++++++++- ccip-sdk/src/ton/exec.ts | 61 +++++++++---- ccip-sdk/src/ton/index.ts | 148 +++++++++++++------------------- ccip-sdk/src/ton/types.ts | 35 +++++++- 7 files changed, 242 insertions(+), 119 deletions(-) diff --git a/ccip-cli/src/providers/index.ts b/ccip-cli/src/providers/index.ts index 9f5be52f..383303fb 100644 --- a/ccip-cli/src/providers/index.ts +++ b/ccip-cli/src/providers/index.ts @@ -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/\\@&?%~#.,;:=+-]+/ @@ -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}`) } diff --git a/ccip-cli/src/providers/ton.ts b/ccip-cli/src/providers/ton.ts index 16b0037e..26f51172 100644 --- a/ccip-cli/src/providers/ton.ts +++ b/ccip-cli/src/providers/ton.ts @@ -1,20 +1,26 @@ import { existsSync, readFileSync } from 'node:fs' +import util from 'node:util' -import { TONChain } from '@chainlink/ccip-sdk/src/index.ts' import type { TONWallet } from '@chainlink/ccip-sdk/src/ton/types.ts' import { keyPairFromSecretKey, mnemonicToPrivateKey } from '@ton/crypto' import { WalletContractV4 } from '@ton/ton' -TONChain.getWallet = async function loadTONWallet({ - wallet, +/** + * 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 { - if (!wallet) wallet = process.env['USER_KEY'] || process.env['OWNER_KEY'] + if (!walletOpt) walletOpt = process.env['USER_KEY'] || process.env['OWNER_KEY'] - if (typeof wallet !== 'string') throw new Error(`Invalid wallet option: ${String(wallet)}`) + if (typeof walletOpt !== 'string') + throw new Error(`Invalid wallet option: ${util.inspect(walletOpt)}`) // Handle mnemonic phrase - if (wallet.includes(' ')) { - const mnemonic = wallet.trim().split(' ') + if (walletOpt.includes(' ')) { + const mnemonic = walletOpt.trim().split(' ') const keyPair = await mnemonicToPrivateKey(mnemonic) const contract = WalletContractV4.create({ workchain: 0, @@ -24,8 +30,8 @@ TONChain.getWallet = async function loadTONWallet({ } // Handle hex private key - if (wallet.startsWith('0x')) { - const secretKey = Buffer.from(wallet.slice(2), 'hex') + 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.', @@ -43,8 +49,8 @@ TONChain.getWallet = async function loadTONWallet({ } // Handle file path - if (existsSync(wallet)) { - const content = readFileSync(wallet, 'utf8').trim() + 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') @@ -54,7 +60,7 @@ TONChain.getWallet = async function loadTONWallet({ workchain: 0, publicKey: keyPair.publicKey, }) - return { contract, keyPair } as TONWallet + return { contract, keyPair } } throw new Error('Wallet not specified') diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index fe50fb44..72fa3f3c 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -14,6 +14,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, @@ -101,8 +102,8 @@ export type UnsignedTx = { [ChainFamily.EVM]: UnsignedEVMTx [ChainFamily.Solana]: UnsignedSolanaTx [ChainFamily.Aptos]: UnsignedAptosTx + [ChainFamily.TON]: UnsignedTONTx [ChainFamily.Sui]: never // TODO - [ChainFamily.TON]: never // TODO } /** diff --git a/ccip-sdk/src/ton/exec.test.ts b/ccip-sdk/src/ton/exec.test.ts index b0c60358..3be6bb3b 100644 --- a/ccip-sdk/src/ton/exec.test.ts +++ b/ccip-sdk/src/ton/exec.test.ts @@ -5,9 +5,9 @@ import { type Cell, Address, toNano } from '@ton/core' import type { KeyPair } from '@ton/crypto' import type { WalletContractV4 } from '@ton/ton' -import { executeReport } from './exec.ts' +import { executeReport, generateUnsignedExecuteReport } from './exec.ts' import type { ExecutionReport } from '../types.ts' -import type { CCIPMessage_V1_6_TON, TONWallet } from './types.ts' +import { type CCIPMessage_V1_6_TON, type TONWallet, MANUALLY_EXECUTE_OPCODE } from './types.ts' describe('TON executeReport', () => { const offrampAddress = '0:' + '5'.repeat(64) @@ -169,9 +169,9 @@ describe('TON executeReport', () => { const body = captured.messages[0].body const slice = body.beginParse() - // Verify opcode (0xa00785cf for manuallyExecute) + // Verify opcode for manuallyExecute const opcode = slice.loadUint(32) - assert.equal(opcode, 0xa00785cf) + assert.equal(opcode, MANUALLY_EXECUTE_OPCODE) // Verify queryID is 0 const queryId = slice.loadUint(64) @@ -281,3 +281,75 @@ describe('TON executeReport', () => { assert.doesNotThrow(() => Address.parse(fullAddress), 'Address should be parseable') }) }) + +describe('TON generateUnsignedExecuteReport', () => { + const offrampAddress = '0:' + '5'.repeat(64) + + const baseExecReport: ExecutionReport = { + message: { + header: { + messageId: '0x' + '1'.repeat(64), + sourceChainSelector: 743186221051783445n, + destChainSelector: 16015286601757825753n, + sequenceNumber: 1n, + nonce: 0n, + }, + sender: '0x' + '2'.repeat(40), + receiver: '0:' + '3'.repeat(64), + data: '0x', + extraArgs: '0x181dcf10000000000000000000000000000000000000000000000000000000000000000001', + feeToken: '0x' + '0'.repeat(40), + feeTokenAmount: 0n, + feeValueJuels: 0n, + tokenAmounts: [], + gasLimit: 200000n, + allowOutOfOrderExecution: true, + }, + proofs: [], + proofFlagBits: 0n, + merkleRoot: '0x' + '4'.repeat(64), + offchainTokenData: [], + } + + it('should return unsigned transaction data with correct structure', () => { + const unsigned = generateUnsignedExecuteReport(offrampAddress, baseExecReport) + + assert.equal(unsigned.to, offrampAddress) + assert.equal(unsigned.value, toNano('0.5')) + assert.ok(unsigned.body, 'Body should be defined') + + // Parse the body Cell to verify opcode + const slice = unsigned.body.beginParse() + const opcode = slice.loadUint(32) + assert.equal(opcode, MANUALLY_EXECUTE_OPCODE) + + const queryId = slice.loadUint(64) + assert.equal(queryId, 0) + }) + + it('should include gas override when provided', () => { + const unsigned = generateUnsignedExecuteReport(offrampAddress, baseExecReport, { + gasLimit: 1_000_000_000, + }) + + const slice = unsigned.body.beginParse() + slice.loadUint(32) // opcode + slice.loadUint(64) // queryID + slice.loadRef() // execution report reference + + const gasOverride = slice.loadCoins() + assert.equal(gasOverride, 1_000_000_000n) + }) + + it('should set gasOverride to 0 when not provided', () => { + const unsigned = generateUnsignedExecuteReport(offrampAddress, baseExecReport) + + const slice = unsigned.body.beginParse() + slice.loadUint(32) // opcode + slice.loadUint(64) // queryID + slice.loadRef() // execution report reference + + const gasOverride = slice.loadCoins() + assert.equal(gasOverride, 0n) + }) +}) diff --git a/ccip-sdk/src/ton/exec.ts b/ccip-sdk/src/ton/exec.ts index d5d14062..5629d8b9 100644 --- a/ccip-sdk/src/ton/exec.ts +++ b/ccip-sdk/src/ton/exec.ts @@ -2,28 +2,31 @@ import { Address, beginCell, toNano } from '@ton/core' import { type TonClient, internal } from '@ton/ton' import type { ExecutionReport } from '../types.ts' -import { type CCIPMessage_V1_6_TON, type TONWallet, serializeExecutionReport } from './types.ts' +import { + type CCIPMessage_V1_6_TON, + type TONWallet, + MANUALLY_EXECUTE_OPCODE, + serializeExecutionReport, +} from './types.ts' import { waitForTransaction } from './utils.ts' /** - * Executes a CCIP message on the TON OffRamp contract. - * Serializes the execution report, constructs the OffRamp_ManuallyExecute message, - * sends the transaction via the wallet, and waits for confirmation. + * Generates an unsigned execute report payload for the TON OffRamp contract. * - * @param client - TonClient instance for RPC calls. - * @param wallet - TON wallet with contract and keypair for signing. * @param offRamp - OffRamp contract address. * @param execReport - Execution report containing the CCIP message and proofs. * @param opts - Optional execution options. Gas limit override for execution (0 = no override). - * @returns Transaction hash in format "workchain:address:lt:hash". + * @returns Object with target address, value, and payload cell. */ -export async function executeReport( - client: TonClient, - wallet: TONWallet, +export function generateUnsignedExecuteReport( offRamp: string, execReport: ExecutionReport, opts?: { gasLimit?: number }, -): Promise<{ hash: string }> { +): { + to: string + value: bigint + body: ReturnType['endCell'] extends () => infer R ? R : never +} { // Serialize the execution report const serializedReport = serializeExecutionReport(execReport) @@ -32,12 +35,40 @@ export async function executeReport( // Construct the OffRamp_ManuallyExecute message const payload = beginCell() - .storeUint(0xa00785cf, 32) // Opcode for OffRamp_ManuallyExecute + .storeUint(MANUALLY_EXECUTE_OPCODE, 32) // Opcode for OffRamp_ManuallyExecute .storeUint(0, 64) // queryID (default 0) .storeRef(serializedReport) // ExecutionReport as reference .storeCoins(gasOverride) // gasOverride (optional, 0 = no override) .endCell() + return { + to: offRamp, + value: toNano('0.5'), + body: payload, + } +} + +/** + * Executes a CCIP message on the TON OffRamp contract. + * Serializes the execution report, constructs the OffRamp_ManuallyExecute message, + * sends the transaction via the wallet, and waits for confirmation. + * + * @param client - TonClient instance for RPC calls. + * @param wallet - TON wallet with contract and keypair for signing. + * @param offRamp - OffRamp contract address. + * @param execReport - Execution report containing the CCIP message and proofs. + * @param opts - Optional execution options. Gas limit override for execution (0 = no override). + * @returns Transaction hash in format "workchain:address:lt:hash". + */ +export async function executeReport( + client: TonClient, + wallet: TONWallet, + offRamp: string, + execReport: ExecutionReport, + opts?: { gasLimit?: number }, +): Promise<{ hash: string }> { + const unsigned = generateUnsignedExecuteReport(offRamp, execReport, opts) + // Open wallet and send transaction const openedWallet = client.open(wallet.contract) const seqno = await openedWallet.getSeqno() @@ -48,9 +79,9 @@ export async function executeReport( secretKey: wallet.keyPair.secretKey, messages: [ internal({ - to: offRamp, - value: toNano('0.5'), - body: payload, + to: unsigned.to, + value: unsigned.value, + body: unsigned.body, }), ], }) diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 1e8609b1..8e036a51 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -1,6 +1,5 @@ import { Address, Cell, beginCell } from '@ton/core' -import { keyPairFromSecretKey, mnemonicToPrivateKey } from '@ton/crypto' -import { TonClient, WalletContractV4 } from '@ton/ton' +import { TonClient, internal } from '@ton/ton' import { type BytesLike, isBytesLike } from 'ethers' import { memoize } from 'micro-memoize' import type { PickDeep } from 'type-fest' @@ -23,11 +22,12 @@ import { type WithLogger, ChainFamily, } from '../types.ts' -import { getDataBytes, networkInfo } from '../utils.ts' +import { getDataBytes, networkInfo, util } from '../utils.ts' // import { parseTONLogs } from './utils.ts' -import { executeReport } from './exec.ts' +import { generateUnsignedExecuteReport as generateUnsignedExecuteReportImpl } from './exec.ts' import { getTONLeafHasher } from './hasher.ts' -import type { CCIPMessage_V1_6_TON, TONWallet } from './types.ts' +import { type CCIPMessage_V1_6_TON, type UnsignedTONTx, isTONWallet } from './types.ts' +import { waitForTransaction } from './utils.ts' const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2Tag, 16) @@ -229,76 +229,6 @@ export class TONChain extends Chain { return Promise.reject(new Error('Not implemented')) } - /** - * Static wallet loading not available for TON. - * @param _opts - Wallet options (unused). - * @returns Never resolves, always throws. - */ - static getWallet(_opts: { wallet?: unknown } = {}): Promise { - throw new Error('static TON wallet loading not available') - } - - /** - * Loads a TON wallet from various input formats. - * @param opts - Wallet options (mnemonic, secret key, or TONWallet instance). - * @returns TONWallet instance. - */ - async getWallet(opts: { wallet?: unknown } = {}): Promise { - // Handle private key string (hex or base64) - if (typeof opts.wallet === 'string') { - // Try mnemonic phrase first (space-separated words) - const words = opts.wallet.trim().split(/\s+/) - if (words.length >= 12 && words.length <= 24) { - const keyPair = await mnemonicToPrivateKey(words) - const contract = WalletContractV4.create({ - workchain: 0, - publicKey: keyPair.publicKey, - }) - return { contract, keyPair } - } - - // Try hex or base64 secret key (64 bytes) - let secretKey: Buffer - - if (opts.wallet.startsWith('0x')) { - secretKey = Buffer.from(opts.wallet.slice(2), 'hex') - } else { - try { - secretKey = Buffer.from(opts.wallet, 'base64') - if (secretKey.length !== 64) { - secretKey = Buffer.from(opts.wallet, 'hex') - } - } catch { - secretKey = Buffer.from(opts.wallet, 'hex') - } - } - - if (secretKey.length === 64) { - const keyPair = keyPairFromSecretKey(secretKey) - const contract = WalletContractV4.create({ - workchain: 0, - publicKey: keyPair.publicKey, - }) - return { contract, keyPair } - } - - throw new Error('Invalid key format. Expected 64-byte secret key or mnemonic phrase.') - } - - // Handle TONWallet instance directly - if ( - opts.wallet && - typeof opts.wallet === 'object' && - 'contract' in opts.wallet && - 'keyPair' in opts.wallet - ) { - return opts.wallet as TONWallet - } - - // Delegate to static method (for CLI overrides) - return (this.constructor as typeof TONChain).getWallet(opts) - } - // Static methods for decoding /** * Decodes a CCIP message from a TON log event. @@ -439,30 +369,76 @@ export class TONChain extends Chain { /** {@inheritDoc Chain.generateUnsignedExecuteReport} */ generateUnsignedExecuteReport( _payer: string, - _offRamp: string, - _execReport: ExecutionReport, - _opts?: { wallet?: unknown; gasLimit?: number }, - ): Promise { - return Promise.reject(new Error('Not implemented')) + offRamp: string, + execReport: ExecutionReport, + opts?: { gasLimit?: number }, + ): Promise { + if (!('allowOutOfOrderExecution' in execReport.message && 'gasLimit' in execReport.message)) { + throw new Error('TON expects GenericExtraArgsV2 reports') + } + + const unsigned = generateUnsignedExecuteReportImpl( + offRamp, + execReport as ExecutionReport, + opts, + ) + + return Promise.resolve({ + family: ChainFamily.TON, + to: unsigned.to, + value: unsigned.value, + body: unsigned.body, + }) } /** {@inheritDoc Chain.executeReport} */ async executeReport( offRamp: string, execReport: ExecutionReport, - opts?: { wallet?: unknown; gasLimit?: number }, + opts: { wallet: unknown; gasLimit?: number }, ): Promise { - const wallet = await this.getWallet(opts) + const wallet = opts.wallet + if (!isTONWallet(wallet)) { + throw new Error( + `${this.constructor.name}.executeReport requires a TON wallet, got=${util.inspect(wallet)}`, + ) + } - const result = await executeReport( - this.provider, - wallet, + const unsigned = await this.generateUnsignedExecuteReport( + wallet.contract.address.toString(), offRamp, execReport as ExecutionReport, opts, ) - return this.getTransaction(result.hash) + // Open wallet and send transaction using the unsigned data + const openedWallet = this.provider.open(wallet.contract) + const seqno = await openedWallet.getSeqno() + + await openedWallet.sendTransfer({ + seqno, + secretKey: wallet.keyPair.secretKey, + messages: [ + internal({ + to: unsigned.to, + value: unsigned.value, + body: unsigned.body, + }), + ], + }) + + // Wait for transaction to be confirmed + const offRampAddress = Address.parse(offRamp) + const txInfo = await waitForTransaction( + this.provider, + wallet.contract.address, + seqno, + offRampAddress, + ) + + // Return composite hash in format "workchain:address:lt:hash" + const hash = `${wallet.contract.address.toRawString()}:${txInfo.lt}:${txInfo.hash}` + return this.getTransaction(hash) } /** diff --git a/ccip-sdk/src/ton/types.ts b/ccip-sdk/src/ton/types.ts index 34e1b19c..555804b1 100644 --- a/ccip-sdk/src/ton/types.ts +++ b/ccip-sdk/src/ton/types.ts @@ -4,12 +4,15 @@ import type { WalletContractV4 } from '@ton/ton' import { toBigInt } from 'ethers' import type { GenericExtraArgsV2 } from '../extra-args.ts' -import type { CCIPMessage_V1_6, ExecutionReport } from '../types.ts' +import type { CCIPMessage_V1_6, ChainFamily, ExecutionReport } from '../types.ts' import { bytesToBuffer } from '../utils.ts' /** TON-specific CCIP v1.6 message type with GenericExtraArgsV2 (gasLimit + allowOutOfOrderExecution). */ export type CCIPMessage_V1_6_TON = CCIPMessage_V1_6 & GenericExtraArgsV2 +/** Opcode for OffRamp_ManuallyExecute message on TON */ +export const MANUALLY_EXECUTE_OPCODE = 0xa00785cf + /** * TON wallet with keypair for signing transactions */ @@ -18,6 +21,36 @@ export interface TONWallet { keyPair: KeyPair } +/** + * Unsigned TON transaction data. + * Contains all information needed to construct and sign a transaction. + */ +export type UnsignedTONTx = { + family: typeof ChainFamily.TON + /** Target contract address */ + to: string + /** Amount of TON to send (in nanotons) */ + value: bigint + /** Message payload as BOC-serialized Cell */ + body: Cell +} + +/** Typeguard for TON Wallet */ +export function isTONWallet(wallet: unknown): wallet is TONWallet { + return ( + typeof wallet === 'object' && + wallet !== null && + 'contract' in wallet && + 'keyPair' in wallet && + typeof wallet.contract === 'object' && + wallet.contract !== null && + 'address' in wallet.contract && + typeof wallet.keyPair === 'object' && + wallet.keyPair !== null && + 'secretKey' in wallet.keyPair + ) +} + // asSnakeData helper for encoding variable-length arrays function asSnakeData(array: T[], builderFn: (item: T) => Builder): Cell { const cells: Builder[] = [] From 19f3ab18c5ea8b8d426dfc1712ddbc5627a216da Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 18:35:31 -0300 Subject: [PATCH 26/33] use EvmExtraArgs instead of generic --- ccip-sdk/src/chain.ts | 2 -- ccip-sdk/src/extra-args.ts | 3 +-- ccip-sdk/src/ton/hasher.ts | 2 +- ccip-sdk/src/ton/index.ts | 16 ++++++++++++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index 72fa3f3c..112bc428 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -8,7 +8,6 @@ import type { EVMExtraArgsV1, EVMExtraArgsV2, ExtraArgs, - GenericExtraArgsV2, SVMExtraArgsV1, SuiExtraArgsV1, } from './extra-args.ts' @@ -496,7 +495,6 @@ export type ChainStatic = Function & { | (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | (SVMExtraArgsV1 & { _tag: 'SVMExtraArgsV1' }) | (SuiExtraArgsV1 & { _tag: 'SuiExtraArgsV1' }) - | (GenericExtraArgsV2 & { _tag: 'GenericExtraArgsV2' }) | undefined encodeExtraArgs(extraArgs: ExtraArgs): string /** diff --git a/ccip-sdk/src/extra-args.ts b/ccip-sdk/src/extra-args.ts index acff3c3d..1336fcf6 100644 --- a/ccip-sdk/src/extra-args.ts +++ b/ccip-sdk/src/extra-args.ts @@ -7,11 +7,11 @@ import { ChainFamily } from './types.ts' export const EVMExtraArgsV1Tag = id('CCIP EVMExtraArgsV1').substring(0, 10) as '0x97a657c9' /** Tag identifier for EVMExtraArgsV2 encoding. */ export const EVMExtraArgsV2Tag = id('CCIP EVMExtraArgsV2').substring(0, 10) as '0x181dcf10' +export const GenericExtraArgsV2Tag = EVMExtraArgsV2Tag /** Tag identifier for SVMExtraArgsV1 encoding. */ export const SVMExtraArgsV1Tag = id('CCIP SVMExtraArgsV1').substring(0, 10) as '0x1f3b3aba' /** Tag identifier for SuiExtraArgsV1 encoding. */ export const SuiExtraArgsV1Tag = id('CCIP SuiExtraArgsV1').substring(0, 10) as '0x21ea4ca9' -export const GenericExtraArgsV2Tag = EVMExtraArgsV2Tag /** * EVM extra arguments version 1 with gas limit only. @@ -92,7 +92,6 @@ export function decodeExtraArgs( | (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | (SVMExtraArgsV1 & { _tag: 'SVMExtraArgsV1' }) | (SuiExtraArgsV1 & { _tag: 'SuiExtraArgsV1' }) - | (GenericExtraArgsV2 & { _tag: 'GenericExtraArgsV2' }) | undefined { if (!data || data === '') return let chains diff --git a/ccip-sdk/src/ton/hasher.ts b/ccip-sdk/src/ton/hasher.ts index 190aaeb0..7709cddf 100644 --- a/ccip-sdk/src/ton/hasher.ts +++ b/ccip-sdk/src/ton/hasher.ts @@ -94,7 +94,7 @@ function hashV16TONMessage(message: CCIPMessage_V1_6, metadataHash: string): str message.extraArgs, networkInfo(message.header.sourceChainSelector).family, ) - if (!parsedArgs || parsedArgs._tag !== 'GenericExtraArgsV2') { + if (!parsedArgs || parsedArgs._tag !== 'EVMExtraArgsV2') { throw new Error('Invalid extraArgs for TON message, must be GenericExtraArgsV2') } gasLimit = parsedArgs.gasLimit || 0n diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 8e036a51..0fd09c66 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -5,7 +5,7 @@ import { memoize } from 'micro-memoize' import type { PickDeep } from 'type-fest' import { type LogFilter, Chain } from '../chain.ts' -import { type ExtraArgs, type GenericExtraArgsV2, GenericExtraArgsV2Tag } from '../extra-args.ts' +import { type EVMExtraArgsV2, type ExtraArgs, GenericExtraArgsV2Tag } from '../extra-args.ts' import type { LeafHasher } from '../hasher/common.ts' import { supportedChains } from '../supported-chains.ts' import { @@ -242,6 +242,10 @@ export class TONChain extends Chain { /** * Encodes extra args from TON messages into BOC serialization format. * + * 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) + * * @param args - Extra arguments containing gas limit and execution flags * @returns Hex string of BOC-encoded extra args (0x-prefixed) */ @@ -261,16 +265,20 @@ export class TONChain extends Chain { } /** - * Decodes BOC-encoded extra arguments from TON messages + * Decodes BOC-encoded extra arguments from TON messages. * Parses the BOC format and extracts extra args, validating the magic tag * to ensure correct type. Returns undefined if parsing fails or tag doesn't match. * + * 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) + * * @param extraArgs - BOC-encoded extra args as hex string or bytes * @returns Decoded GenericExtraArgsV2 object or undefined if invalid */ static decodeExtraArgs( extraArgs: BytesLike, - ): (GenericExtraArgsV2 & { _tag: 'GenericExtraArgsV2' }) | undefined { + ): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | undefined { const data = Buffer.from(getDataBytes(extraArgs)) try { @@ -283,7 +291,7 @@ export class TONChain extends Chain { if (magicTag !== GENERIC_V2_EXTRA_ARGS_TAG) return undefined return { - _tag: 'GenericExtraArgsV2', + _tag: 'EVMExtraArgsV2', gasLimit: slice.loadUintBig(256), allowOutOfOrderExecution: slice.loadBit(), } From cfd789f471fac008a27dfa2336fbd5a5acebfffe Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 18:44:17 -0300 Subject: [PATCH 27/33] use evmExtraArgsV2 --- ccip-sdk/src/extra-args.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index fbc2b562..597c303f 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -81,7 +81,7 @@ describe('encodeExtraArgs', () => { }) }) describe('TON extra args', () => { - it('should encode GenericExtraArgsV2', () => { + it('should encode EVMExtraArgsV2', () => { const encoded = encodeExtraArgs( { gasLimit: 400_000n, allowOutOfOrderExecution: true }, ChainFamily.TON, @@ -166,27 +166,27 @@ describe('parseExtraArgs', () => { }) }) describe('TON extra args (TLB encoding)', () => { - it('should parse GenericExtraArgsV2', () => { + 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: 'GenericExtraArgsV2', + _tag: 'EVMExtraArgsV2', gasLimit: 400000n, allowOutOfOrderExecution: true, }) }) - it('should parse GenericExtraArgsV2 with allowOutOfOrderExecution false', () => { + 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: 'GenericExtraArgsV2', + _tag: 'EVMExtraArgsV2', gasLimit: 500000n, allowOutOfOrderExecution: false, }) @@ -251,11 +251,11 @@ describe('parseExtraArgs', () => { assert.deepEqual(decoded, { ...original, _tag: 'EVMExtraArgsV2' }) }) - it('should round-trip TON GenericExtraArgsV2', () => { + 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: 'GenericExtraArgsV2' }) + assert.deepEqual(decoded, { ...original, _tag: 'EVMExtraArgsV2' }) }) }) From e89612bc4b7b07d6042db0404c623c0856c0e786 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 18:50:08 -0300 Subject: [PATCH 28/33] remove genericextraargs --- ccip-sdk/src/extra-args.test.ts | 17 ++++++----------- ccip-sdk/src/extra-args.ts | 7 ------- ccip-sdk/src/index.ts | 1 - ccip-sdk/src/ton/hasher.ts | 4 +++- ccip-sdk/src/ton/index.ts | 14 ++++++-------- ccip-sdk/src/ton/types.ts | 6 +++--- ccip-sdk/src/ton/utils.test.ts | 4 ++-- 7 files changed, 20 insertions(+), 33 deletions(-) diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index 597c303f..3e5ea190 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -5,12 +5,7 @@ import { dataSlice, getNumber } from 'ethers' // Import index.ts to ensure all Chain classes are loaded and registered import './index.ts' -import { - EVMExtraArgsV2Tag, - GenericExtraArgsV2Tag, - 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' @@ -81,23 +76,23 @@ describe('encodeExtraArgs', () => { }) }) describe('TON extra args', () => { - it('should encode EVMExtraArgsV2', () => { + it('should encode EVMExtraArgsV2 (GenericExtraArgsV2)', () => { const encoded = encodeExtraArgs( { gasLimit: 400_000n, allowOutOfOrderExecution: true }, ChainFamily.TON, ) - assert.equal(extractMagicTag(encoded), GenericExtraArgsV2Tag) + assert.equal(extractMagicTag(encoded), EVMExtraArgsV2Tag) assert.ok(encoded.length > 10) }) - it('should encode GenericExtraArgsV2 with allowOutOfOrderExecution false', () => { + it('should encode EVMExtraArgsV2 (GenericExtraArgsV2) with allowOutOfOrderExecution false', () => { const encoded = encodeExtraArgs( { gasLimit: 500_000n, allowOutOfOrderExecution: false }, ChainFamily.TON, ) - assert.equal(extractMagicTag(encoded), GenericExtraArgsV2Tag) + assert.equal(extractMagicTag(encoded), EVMExtraArgsV2Tag) assert.ok(encoded.length > 10) }) }) @@ -288,7 +283,7 @@ describe('parseExtraArgs', () => { const tonEncoded = encodeExtraArgs(args, ChainFamily.TON) assert.equal(evmEncoded.substring(0, 10), EVMExtraArgsV2Tag) - assert.equal(extractMagicTag(tonEncoded), GenericExtraArgsV2Tag) + assert.equal(extractMagicTag(tonEncoded), EVMExtraArgsV2Tag) assert.notEqual(evmEncoded, tonEncoded) }) }) diff --git a/ccip-sdk/src/extra-args.ts b/ccip-sdk/src/extra-args.ts index 1336fcf6..ad64e695 100644 --- a/ccip-sdk/src/extra-args.ts +++ b/ccip-sdk/src/extra-args.ts @@ -7,7 +7,6 @@ import { ChainFamily } from './types.ts' export const EVMExtraArgsV1Tag = id('CCIP EVMExtraArgsV1').substring(0, 10) as '0x97a657c9' /** Tag identifier for EVMExtraArgsV2 encoding. */ export const EVMExtraArgsV2Tag = id('CCIP EVMExtraArgsV2').substring(0, 10) as '0x181dcf10' -export const GenericExtraArgsV2Tag = EVMExtraArgsV2Tag /** Tag identifier for SVMExtraArgsV1 encoding. */ export const SVMExtraArgsV1Tag = id('CCIP SVMExtraArgsV1').substring(0, 10) as '0x1f3b3aba' /** Tag identifier for SuiExtraArgsV1 encoding. */ @@ -56,12 +55,6 @@ export type SuiExtraArgsV1 = EVMExtraArgsV2 & { receiverObjectIds: string[] } -/** - * Tag identifier for GenericExtraArgsV2 encoding. - * Uses the same tag as EVMExtraArgsV2 since they share the same structure. - */ -export type GenericExtraArgsV2 = EVMExtraArgsV2 - /** * Union type of all supported extra arguments formats. */ diff --git a/ccip-sdk/src/index.ts b/ccip-sdk/src/index.ts index 18665378..fe247dc4 100644 --- a/ccip-sdk/src/index.ts +++ b/ccip-sdk/src/index.ts @@ -12,7 +12,6 @@ export { type EVMExtraArgsV1, type EVMExtraArgsV2, type ExtraArgs, - type GenericExtraArgsV2, type SVMExtraArgsV1, type SuiExtraArgsV1, decodeExtraArgs, diff --git a/ccip-sdk/src/ton/hasher.ts b/ccip-sdk/src/ton/hasher.ts index 7709cddf..b3310a18 100644 --- a/ccip-sdk/src/ton/hasher.ts +++ b/ccip-sdk/src/ton/hasher.ts @@ -95,7 +95,9 @@ function hashV16TONMessage(message: CCIPMessage_V1_6, metadataHash: string): str networkInfo(message.header.sourceChainSelector).family, ) if (!parsedArgs || parsedArgs._tag !== 'EVMExtraArgsV2') { - throw new Error('Invalid extraArgs for TON message, must be GenericExtraArgsV2') + throw new Error( + 'Invalid extraArgs for TON message, must be EVMExtraArgsV2 (GenericExtraArgsV2)', + ) } gasLimit = parsedArgs.gasLimit || 0n } diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts index 0fd09c66..72be93fc 100644 --- a/ccip-sdk/src/ton/index.ts +++ b/ccip-sdk/src/ton/index.ts @@ -5,7 +5,7 @@ import { memoize } from 'micro-memoize' import type { PickDeep } from 'type-fest' import { type LogFilter, Chain } from '../chain.ts' -import { type EVMExtraArgsV2, type ExtraArgs, GenericExtraArgsV2Tag } from '../extra-args.ts' +import { type EVMExtraArgsV2, type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts' import type { LeafHasher } from '../hasher/common.ts' import { supportedChains } from '../supported-chains.ts' import { @@ -29,8 +29,6 @@ import { getTONLeafHasher } from './hasher.ts' import { type CCIPMessage_V1_6_TON, type UnsignedTONTx, isTONWallet } from './types.ts' import { waitForTransaction } from './utils.ts' -const GENERIC_V2_EXTRA_ARGS_TAG = Number.parseInt(GenericExtraArgsV2Tag, 16) - /** * TON chain implementation supporting TON networks. */ @@ -253,7 +251,7 @@ export class TONChain extends Chain { if (!args) return '0x' if ('gasLimit' in args && 'allowOutOfOrderExecution' in args) { const cell = beginCell() - .storeUint(GENERIC_V2_EXTRA_ARGS_TAG, 32) // magic tag + .storeUint(Number(EVMExtraArgsV2Tag), 32) // magic tag .storeUint(args.gasLimit, 256) // gasLimit .storeBit(args.allowOutOfOrderExecution) // bool .endCell() @@ -269,12 +267,12 @@ export class TONChain extends Chain { * Parses the BOC format and extracts extra args, validating the magic tag * to ensure correct type. Returns undefined if parsing fails or tag doesn't match. * - * Currently only supports GenericExtraArgsV2 (EVMExtraArgsV2) encoding since TON + * 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) * * @param extraArgs - BOC-encoded extra args as hex string or bytes - * @returns Decoded GenericExtraArgsV2 object or undefined if invalid + * @returns Decoded EVMExtraArgsV2 (GenericExtraArgsV2) object or undefined if invalid */ static decodeExtraArgs( extraArgs: BytesLike, @@ -288,7 +286,7 @@ export class TONChain extends Chain { // Load and verify magic tag to ensure correct extra args type const magicTag = slice.loadUint(32) - if (magicTag !== GENERIC_V2_EXTRA_ARGS_TAG) return undefined + if (magicTag !== Number(EVMExtraArgsV2Tag)) return undefined return { _tag: 'EVMExtraArgsV2', @@ -382,7 +380,7 @@ export class TONChain extends Chain { opts?: { gasLimit?: number }, ): Promise { if (!('allowOutOfOrderExecution' in execReport.message && 'gasLimit' in execReport.message)) { - throw new Error('TON expects GenericExtraArgsV2 reports') + throw new Error('TON expects EVMExtraArgsV2 (GenericExtraArgsV2) reports') } const unsigned = generateUnsignedExecuteReportImpl( diff --git a/ccip-sdk/src/ton/types.ts b/ccip-sdk/src/ton/types.ts index 555804b1..4f90c92b 100644 --- a/ccip-sdk/src/ton/types.ts +++ b/ccip-sdk/src/ton/types.ts @@ -3,12 +3,12 @@ import type { KeyPair } from '@ton/crypto' import type { WalletContractV4 } from '@ton/ton' import { toBigInt } from 'ethers' -import type { GenericExtraArgsV2 } from '../extra-args.ts' +import type { EVMExtraArgsV2 } from '../../dist/extra-args.js' import type { CCIPMessage_V1_6, ChainFamily, ExecutionReport } from '../types.ts' import { bytesToBuffer } from '../utils.ts' -/** TON-specific CCIP v1.6 message type with GenericExtraArgsV2 (gasLimit + allowOutOfOrderExecution). */ -export type CCIPMessage_V1_6_TON = CCIPMessage_V1_6 & GenericExtraArgsV2 +/** TON-specific CCIP v1.6 message type with EVMExtraArgsV2 (GenericExtraArgsV2) */ +export type CCIPMessage_V1_6_TON = CCIPMessage_V1_6 & EVMExtraArgsV2 /** Opcode for OffRamp_ManuallyExecute message on TON */ export const MANUALLY_EXECUTE_OPCODE = 0xa00785cf diff --git a/ccip-sdk/src/ton/utils.test.ts b/ccip-sdk/src/ton/utils.test.ts index 965e5c7b..2c81135b 100644 --- a/ccip-sdk/src/ton/utils.test.ts +++ b/ccip-sdk/src/ton/utils.test.ts @@ -7,7 +7,7 @@ import { sha256, toBigInt } from 'ethers' import { extractMagicTag, hexToBuffer, tryParseCell } from './utils.ts' import { EVMExtraArgsV1Tag, - GenericExtraArgsV2Tag, + EVMExtraArgsV2Tag, SVMExtraArgsV1Tag, SuiExtraArgsV1Tag, } from '../extra-args.ts' @@ -104,7 +104,7 @@ describe('TON utils', () => { describe('extractMagicTag', () => { it('should extract magic tag from BOC', () => { const cell = beginCell() - .storeUint(Number(GenericExtraArgsV2Tag), 32) + .storeUint(Number(EVMExtraArgsV2Tag), 32) .storeUint(123456, 256) .storeBit(true) .endCell() From ffcc0e9f4f4e9054e58249f7405131b74eb0d785 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Wed, 10 Dec 2025 18:53:17 -0300 Subject: [PATCH 29/33] fix dist --- ccip-sdk/src/ton/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccip-sdk/src/ton/types.ts b/ccip-sdk/src/ton/types.ts index 4f90c92b..d2070f55 100644 --- a/ccip-sdk/src/ton/types.ts +++ b/ccip-sdk/src/ton/types.ts @@ -3,7 +3,7 @@ import type { KeyPair } from '@ton/crypto' import type { WalletContractV4 } from '@ton/ton' import { toBigInt } from 'ethers' -import type { EVMExtraArgsV2 } from '../../dist/extra-args.js' +import type { EVMExtraArgsV2 } from '../extra-args.ts' import type { CCIPMessage_V1_6, ChainFamily, ExecutionReport } from '../types.ts' import { bytesToBuffer } from '../utils.ts' From e811609002e0c5a61c46c7a1ebf7fa4bd5fc5b5b Mon Sep 17 00:00:00 2001 From: Andre Matos Date: Thu, 11 Dec 2025 07:38:03 -0500 Subject: [PATCH 30/33] adjust selector --- ccip-sdk/src/selectors.ts | 4 ++-- ccip-sdk/src/types.ts | 8 ++++---- ccip-sdk/src/utils.ts | 13 ++++++++----- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/ccip-sdk/src/selectors.ts b/ccip-sdk/src/selectors.ts index 1698fd31..47a8fd45 100644 --- a/ccip-sdk/src/selectors.ts +++ b/ccip-sdk/src/selectors.ts @@ -1340,7 +1340,7 @@ const selectors: Selectors = { // 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]) => [`ton:${k}`, { ...v, family: 'ton' }]))) + // .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', @@ -1357,7 +1357,7 @@ const selectors: Selectors = { selector: 13879075125137744094n, family: 'ton', }, + // end:generate } -// end:generate export default selectors diff --git a/ccip-sdk/src/types.ts b/ccip-sdk/src/types.ts index efc9ee7e..2e5db1a0 100644 --- a/ccip-sdk/src/types.ts +++ b/ccip-sdk/src/types.ts @@ -81,15 +81,15 @@ export const CCIPVersion = { export type CCIPVersion = (typeof CCIPVersion)[keyof typeof CCIPVersion] /** Helper type that maps chain family to its chain ID format. */ -type ChainFamilyWithId = F extends typeof ChainFamily.EVM +type ChainFamilyWithId = F extends + | typeof ChainFamily.EVM + | typeof ChainFamily.TON ? { readonly family: F; readonly chainId: number } : F extends typeof ChainFamily.Solana ? { readonly family: F; readonly chainId: string } : F extends typeof ChainFamily.Aptos | typeof ChainFamily.Sui ? { readonly family: F; readonly chainId: `${F}:${number}` } - : F extends typeof ChainFamily.TON - ? { readonly family: F; readonly chainId: number } - : never + : never /** * Network information including chain selector and metadata. diff --git a/ccip-sdk/src/utils.ts b/ccip-sdk/src/utils.ts index d55fca88..dab000a3 100644 --- a/ccip-sdk/src/utils.ts +++ b/ccip-sdk/src/utils.ts @@ -118,14 +118,17 @@ const networkInfoFromChainId = memoize((chainId: NetworkInfo['chainId']): Networ export const networkInfo = memoize(function networkInfo_( selectorOrIdOrName: bigint | number | string, ): NetworkInfo { - let chainId + let chainId, match if (typeof selectorOrIdOrName === 'number') { chainId = selectorOrIdOrName - } else if (typeof selectorOrIdOrName === 'string' && selectorOrIdOrName.match(/^\d+$/)) { - selectorOrIdOrName = BigInt(selectorOrIdOrName) + } else if ( + typeof selectorOrIdOrName === 'string' && + (match = selectorOrIdOrName.match(/^(-?\d+)n?$/)) + ) { + selectorOrIdOrName = BigInt(match[1]) } if (typeof selectorOrIdOrName === 'bigint') { - // maybe we got a number deserialized as bigint + // maybe we got a chainId deserialized as bigint if (selectorOrIdOrName.toString() in SELECTORS) { chainId = Number(selectorOrIdOrName) } else { @@ -138,7 +141,7 @@ export const networkInfo = memoize(function networkInfo_( if (!chainId) throw new Error(`Selector not found: ${selectorOrIdOrName}`) } } else if (typeof selectorOrIdOrName === 'string') { - if (selectorOrIdOrName.includes('-')) { + if (selectorOrIdOrName.includes('-', 1)) { for (const id in SELECTORS) { if (SELECTORS[id].name === selectorOrIdOrName) { chainId = id From e393329638e6f7e3f5f813cf26289b2313a99e3a Mon Sep 17 00:00:00 2001 From: Farber98 Date: Thu, 11 Dec 2025 13:45:09 -0300 Subject: [PATCH 31/33] address andre feedback --- ccip-sdk/src/extra-args.test.ts | 13 +++++++++++++ ccip-sdk/src/utils.test.ts | 2 +- ccip-sdk/src/utils.ts | 5 ++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index 3e5ea190..f603e055 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -95,6 +95,19 @@ describe('encodeExtraArgs', () => { 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, + }) + }) }) }) diff --git a/ccip-sdk/src/utils.test.ts b/ccip-sdk/src/utils.test.ts index 5a6dd112..ce7c20a1 100644 --- a/ccip-sdk/src/utils.test.ts +++ b/ccip-sdk/src/utils.test.ts @@ -814,7 +814,7 @@ describe('toLeArray', () => { it('should handle zero', () => { const result = toLeArray(0n) - assert.deepEqual(result, new Uint8Array([0x00])) + assert.deepEqual(result, new Uint8Array([])) }) it('should handle custom width', () => { diff --git a/ccip-sdk/src/utils.ts b/ccip-sdk/src/utils.ts index dab000a3..9455a65d 100644 --- a/ccip-sdk/src/utils.ts +++ b/ccip-sdk/src/utils.ts @@ -8,7 +8,7 @@ import { decodeBase64, getBytes, isBytesLike, - toBeHex, + toBeArray, toBigInt, } from 'ethers' import { memoize } from 'micro-memoize' @@ -248,9 +248,8 @@ export function leToBigInt(data: BytesLike | readonly number[]): bigint { * @returns Little-endian Uint8Array. */ export function toLeArray(value: BigNumberish, width?: Numeric): Uint8Array { - return getBytes(toBeHex(value, width)).reverse() + return toBeArray(value, width).reverse() } - /** * Checks if the given data is a valid Base64 encoded string. * @param data - Data to check. From c0290f04c828912a77de9b9ed9d2eccfd39e4df5 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Thu, 11 Dec 2025 15:48:03 -0300 Subject: [PATCH 32/33] fix hasher leaf domain separator and improve tests --- ccip-sdk/src/ton/hasher.test.ts | 98 ++++++++++++++++++++++++--------- ccip-sdk/src/ton/hasher.ts | 9 +-- ccip-sdk/src/ton/utils.test.ts | 16 +----- 3 files changed, 79 insertions(+), 44 deletions(-) diff --git a/ccip-sdk/src/ton/hasher.test.ts b/ccip-sdk/src/ton/hasher.test.ts index 5093a4b0..1d12427e 100644 --- a/ccip-sdk/src/ton/hasher.test.ts +++ b/ccip-sdk/src/ton/hasher.test.ts @@ -8,22 +8,39 @@ const ZERO_ADDRESS = '0x' + '0'.repeat(40) const TON_RECEIVER = '0:' + '3'.repeat(64) describe('TON hasher', () => { - const sourceChainSelector = 743186221051783445n - const destChainSelector = 16015286601757825753n - const onRamp = '0x1234567890123456789012345678901234567890' + const CHAINSEL_EVM_TEST_90000001 = 909606746561742123n + const CHAINSEL_TON = 13879075125137744094n + const EVM_ONRAMP_ADDRESS_TEST = '0x111111c891c5d4e6ad68064ae45d43146d4f9f3a' + const EVM_SENDER_ADDRESS_TEST = '0x1a5fdbc891c5d4e6ad68064ae45d43146d4f9f3a' describe('hashTONMetadata', () => { it('should create consistent metadata hash', () => { - const hash1 = hashTONMetadata(sourceChainSelector, destChainSelector, onRamp) - const hash2 = hashTONMetadata(sourceChainSelector, destChainSelector, onRamp) + const hash1 = hashTONMetadata( + CHAINSEL_EVM_TEST_90000001, + CHAINSEL_TON, + EVM_ONRAMP_ADDRESS_TEST, + ) + const hash2 = hashTONMetadata( + CHAINSEL_EVM_TEST_90000001, + CHAINSEL_TON, + EVM_ONRAMP_ADDRESS_TEST, + ) assert.equal(hash1, hash2) assert.match(hash1, /^0x[a-f0-9]{64}$/) }) it('should create different hashes for different parameters', () => { - const hash1 = hashTONMetadata(sourceChainSelector, destChainSelector, onRamp) - const hash2 = hashTONMetadata(sourceChainSelector + 1n, destChainSelector, onRamp) + const hash1 = hashTONMetadata( + CHAINSEL_EVM_TEST_90000001, + CHAINSEL_TON, + EVM_ONRAMP_ADDRESS_TEST, + ) + const hash2 = hashTONMetadata( + CHAINSEL_EVM_TEST_90000001 + 1n, + CHAINSEL_TON, + EVM_ONRAMP_ADDRESS_TEST, + ) assert.notEqual(hash1, hash2) }) @@ -33,9 +50,9 @@ describe('TON hasher', () => { it('should throw error for unsupported version', () => { assert.throws(() => { getTONLeafHasher({ - sourceChainSelector, - destChainSelector, - onRamp, + sourceChainSelector: CHAINSEL_EVM_TEST_90000001, + destChainSelector: CHAINSEL_TON, + onRamp: EVM_ONRAMP_ADDRESS_TEST, version: CCIPVersion.V1_2, }) }, /TON only supports CCIP v1.6/) @@ -43,9 +60,9 @@ describe('TON hasher', () => { it('should create hasher for v1.6', () => { const hasher = getTONLeafHasher({ - sourceChainSelector, - destChainSelector, - onRamp, + sourceChainSelector: CHAINSEL_EVM_TEST_90000001, + destChainSelector: CHAINSEL_TON, + onRamp: EVM_ONRAMP_ADDRESS_TEST, version: CCIPVersion.V1_6, }) @@ -55,9 +72,9 @@ describe('TON hasher', () => { describe('message hashing', () => { const hasher = getTONLeafHasher({ - sourceChainSelector, - destChainSelector, - onRamp, + sourceChainSelector: CHAINSEL_EVM_TEST_90000001, + destChainSelector: CHAINSEL_TON, + onRamp: EVM_ONRAMP_ADDRESS_TEST, version: CCIPVersion.V1_6, }) @@ -70,10 +87,10 @@ describe('TON hasher', () => { messageId: '0x' + '1'.repeat(64), sequenceNumber: 123n, nonce: 456n, - sourceChainSelector, - destChainSelector, + sourceChainSelector: CHAINSEL_EVM_TEST_90000001, + destChainSelector: CHAINSEL_TON, }, - sender: '0x' + '2'.repeat(40), + sender: EVM_SENDER_ADDRESS_TEST, receiver: TON_RECEIVER, data: '0x1234', extraArgs: '0x181dcf10000000000000000000000000000000000000000000000000000000000000000001', @@ -109,10 +126,10 @@ describe('TON hasher', () => { messageId: '0x' + '1'.repeat(64), sequenceNumber: 123n, nonce: 456n, - sourceChainSelector, - destChainSelector, + sourceChainSelector: CHAINSEL_EVM_TEST_90000001, + destChainSelector: CHAINSEL_TON, }, - sender: '0x' + '2'.repeat(40), + sender: EVM_SENDER_ADDRESS_TEST, receiver: TON_RECEIVER, data: '0x1234', extraArgs: '0x181dcf10000000000000000000000000000000000000000000000000000000000000000001', @@ -137,10 +154,10 @@ describe('TON hasher', () => { messageId: '0x' + '1'.repeat(64), sequenceNumber: 123n, nonce: 456n, - sourceChainSelector, - destChainSelector, + sourceChainSelector: CHAINSEL_EVM_TEST_90000001, + destChainSelector: CHAINSEL_TON, }, - sender: '0x' + '2'.repeat(40), + sender: EVM_SENDER_ADDRESS_TEST, receiver: TON_RECEIVER, data: '0x1234', extraArgs: '0x', @@ -155,5 +172,36 @@ describe('TON hasher', () => { const hash = hasher(message) assert.match(hash, /^0x[a-f0-9]{64}$/) }) + + it('should compute leaf hash matching chainlink-ton for Merkle verification', () => { + // https://github.com/smartcontractkit/chainlink-ton/blob/f56790ae36317956ec09a53f9524bef77fddcadc/contracts/tests/ccip/OffRamp.spec.ts#L989-L990 + const expectedHash = '0xce60f1962af3c7c7f9d3e434dea13530564dbff46704d628ff4b2206bbc93289' + const message: CCIPMessage_V1_6 & { + gasLimit: bigint + allowOutOfOrderExecution: boolean + } = { + header: { + messageId: '0x' + '0'.repeat(63) + '1', + sequenceNumber: 1n, + nonce: 0n, + sourceChainSelector: CHAINSEL_EVM_TEST_90000001, + destChainSelector: CHAINSEL_TON, + }, + sender: EVM_SENDER_ADDRESS_TEST, + receiver: 'EQDtFpEwcFAEcRe5mLVh2N6C0x-_hJEM7W61_JLnSF74p4q2', + data: '0x', + extraArgs: '0x', + gasLimit: 100_000_000n, + allowOutOfOrderExecution: false, + tokenAmounts: [] as CCIPMessage_V1_6['tokenAmounts'], + feeToken: ZERO_ADDRESS, + feeTokenAmount: 0n, + feeValueJuels: 0n, + } + + const computedHash = hasher(message) + + assert.equal(computedHash, expectedHash) + }) }) }) diff --git a/ccip-sdk/src/ton/hasher.ts b/ccip-sdk/src/ton/hasher.ts index b3310a18..699dd0a6 100644 --- a/ccip-sdk/src/ton/hasher.ts +++ b/ccip-sdk/src/ton/hasher.ts @@ -2,13 +2,13 @@ import { type Cell, Address, beginCell } from '@ton/core' import { sha256, toBigInt } from 'ethers' import { decodeExtraArgs } from '../extra-args.ts' -import { type LeafHasher, LEAF_DOMAIN_SEPARATOR } from '../hasher/common.ts' +import type { LeafHasher } from '../hasher/common.ts' import { type CCIPMessage, type CCIPMessage_V1_6, CCIPVersion } from '../types.ts' import { networkInfo } from '../utils.ts' import { hexToBuffer, tryParseCell } from './utils.ts' -// Convert LEAF_DOMAIN_SEPARATOR from hex string to Buffer for cell storage -const LEAF_DOMAIN_BUFFER = Buffer.from(LEAF_DOMAIN_SEPARATOR.slice(2).padStart(64, '0'), 'hex') +// TON uses 256 bits (32 bytes) of zeros as leaf domain separator +const TON_LEAF_DOMAIN_SEPARATOR = 0n /** * Creates a leaf hasher for TON messages. @@ -123,8 +123,9 @@ function hashV16TONMessage(message: CCIPMessage_V1_6, metadataHash: string): str message.tokenAmounts.length > 0 ? buildTokenAmountsCell(message.tokenAmounts) : null // Assemble the complete message cell + // LEAF_DOMAIN_SEPARATOR (256 bits) + metadataHash (256 bits) + refs const messageCell = beginCell() - .storeBuffer(LEAF_DOMAIN_BUFFER) + .storeUint(TON_LEAF_DOMAIN_SEPARATOR, 256) .storeUint(toBigInt(metadataHash), 256) .storeRef(headerCell) .storeRef(senderCell) diff --git a/ccip-sdk/src/ton/utils.test.ts b/ccip-sdk/src/ton/utils.test.ts index 2c81135b..e2cda382 100644 --- a/ccip-sdk/src/ton/utils.test.ts +++ b/ccip-sdk/src/ton/utils.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' import { beginCell } from '@ton/core' -import { sha256, toBigInt } from 'ethers' +import { toBigInt } from 'ethers' import { extractMagicTag, hexToBuffer, tryParseCell } from './utils.ts' import { @@ -13,20 +13,6 @@ import { } from '../extra-args.ts' describe('TON utils', () => { - describe('sha256', () => { - it('should compute SHA256 hash of data', () => { - const data = new Uint8Array([1, 2, 3, 4]) - const hash = sha256(data) - assert.match(hash, /^0x[a-f0-9]{64}$/) - assert.equal(hash, '0x9f64a747e1b97f131fabb6b447296c9b6f0201e79fb3c5356e6c77e89b6a806a') - }) - - it('should handle empty data', () => { - const hash = sha256(new Uint8Array()) - assert.equal(hash, '0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') - }) - }) - describe('hexToBuffer', () => { it('should convert hex string with 0x prefix', () => { const buffer = hexToBuffer('0x48656c6c6f') From de87e6f618b5128ace18075b6a7455d1bd596698 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Thu, 11 Dec 2025 15:53:44 -0300 Subject: [PATCH 33/33] remove random hashes tests --- ccip-sdk/src/ton/hasher.test.ts | 97 --------------------------------- 1 file changed, 97 deletions(-) diff --git a/ccip-sdk/src/ton/hasher.test.ts b/ccip-sdk/src/ton/hasher.test.ts index 1d12427e..b24a03c6 100644 --- a/ccip-sdk/src/ton/hasher.test.ts +++ b/ccip-sdk/src/ton/hasher.test.ts @@ -5,7 +5,6 @@ import { type CCIPMessage_V1_6, CCIPVersion } from '../types.ts' import { getTONLeafHasher, hashTONMetadata } from './hasher.ts' const ZERO_ADDRESS = '0x' + '0'.repeat(40) -const TON_RECEIVER = '0:' + '3'.repeat(64) describe('TON hasher', () => { const CHAINSEL_EVM_TEST_90000001 = 909606746561742123n @@ -27,7 +26,6 @@ describe('TON hasher', () => { ) assert.equal(hash1, hash2) - assert.match(hash1, /^0x[a-f0-9]{64}$/) }) it('should create different hashes for different parameters', () => { @@ -78,101 +76,6 @@ describe('TON hasher', () => { version: CCIPVersion.V1_6, }) - it('should hash basic message', () => { - const message: CCIPMessage_V1_6 & { - gasLimit: bigint - allowOutOfOrderExecution: boolean - } = { - header: { - messageId: '0x' + '1'.repeat(64), - sequenceNumber: 123n, - nonce: 456n, - sourceChainSelector: CHAINSEL_EVM_TEST_90000001, - destChainSelector: CHAINSEL_TON, - }, - sender: EVM_SENDER_ADDRESS_TEST, - receiver: TON_RECEIVER, - data: '0x1234', - extraArgs: '0x181dcf10000000000000000000000000000000000000000000000000000000000000000001', - gasLimit: 0n, - allowOutOfOrderExecution: true, - tokenAmounts: [] as CCIPMessage_V1_6['tokenAmounts'], - feeToken: ZERO_ADDRESS, - feeTokenAmount: 0n, - feeValueJuels: 0n, - } - - const hash = hasher(message) - assert.match(hash, /^0x[a-f0-9]{64}$/) - }) - - it('should hash message with tokens', () => { - const tokenAmounts: CCIPMessage_V1_6['tokenAmounts'] = [ - { - sourcePoolAddress: '0x123456789abcdef123456789abcdef123456789a', - destTokenAddress: '0:' + '5'.repeat(64), - extraData: '0x', - destGasAmount: 1000n, - amount: 1000n, - destExecData: '0x', - }, - ] - - const message: CCIPMessage_V1_6 & { - gasLimit: bigint - allowOutOfOrderExecution: boolean - } = { - header: { - messageId: '0x' + '1'.repeat(64), - sequenceNumber: 123n, - nonce: 456n, - sourceChainSelector: CHAINSEL_EVM_TEST_90000001, - destChainSelector: CHAINSEL_TON, - }, - sender: EVM_SENDER_ADDRESS_TEST, - receiver: TON_RECEIVER, - data: '0x1234', - extraArgs: '0x181dcf10000000000000000000000000000000000000000000000000000000000000000001', - gasLimit: 0n, - allowOutOfOrderExecution: true, - tokenAmounts, - feeToken: ZERO_ADDRESS, - feeTokenAmount: 0n, - feeValueJuels: 0n, - } - - const hash = hasher(message) - assert.match(hash, /^0x[a-f0-9]{64}$/) - }) - - it('should handle embedded gasLimit', () => { - const message: CCIPMessage_V1_6 & { - gasLimit: bigint - allowOutOfOrderExecution: boolean - } = { - header: { - messageId: '0x' + '1'.repeat(64), - sequenceNumber: 123n, - nonce: 456n, - sourceChainSelector: CHAINSEL_EVM_TEST_90000001, - destChainSelector: CHAINSEL_TON, - }, - sender: EVM_SENDER_ADDRESS_TEST, - receiver: TON_RECEIVER, - data: '0x1234', - extraArgs: '0x', - gasLimit: 500000n, - allowOutOfOrderExecution: false, - tokenAmounts: [] as CCIPMessage_V1_6['tokenAmounts'], - feeToken: ZERO_ADDRESS, - feeTokenAmount: 0n, - feeValueJuels: 0n, - } - - const hash = hasher(message) - assert.match(hash, /^0x[a-f0-9]{64}$/) - }) - it('should compute leaf hash matching chainlink-ton for Merkle verification', () => { // https://github.com/smartcontractkit/chainlink-ton/blob/f56790ae36317956ec09a53f9524bef77fddcadc/contracts/tests/ccip/OffRamp.spec.ts#L989-L990 const expectedHash = '0xce60f1962af3c7c7f9d3e434dea13530564dbff46704d628ff4b2206bbc93289'