diff --git a/ccip-cli/src/providers/index.ts b/ccip-cli/src/providers/index.ts index 9f5be52..383303f 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 new file mode 100644 index 0000000..26f5117 --- /dev/null +++ b/ccip-cli/src/providers/ton.ts @@ -0,0 +1,67 @@ +import { existsSync, readFileSync } from 'node:fs' +import util from 'node:util' + +import type { TONWallet } from '@chainlink/ccip-sdk/src/ton/types.ts' +import { keyPairFromSecretKey, mnemonicToPrivateKey } from '@ton/crypto' +import { WalletContractV4 } from '@ton/ton' + +/** + * Loads a TON wallet from the provided options. + * @param wallet - wallet options (as passed from yargs argv) + * @returns Promise to TONWallet instance + */ +export async function loadTonWallet({ + wallet: walletOpt, +}: { wallet?: unknown } = {}): Promise { + if (!walletOpt) walletOpt = process.env['USER_KEY'] || process.env['OWNER_KEY'] + + if (typeof walletOpt !== 'string') + throw new Error(`Invalid wallet option: ${util.inspect(walletOpt)}`) + + // Handle mnemonic phrase + if (walletOpt.includes(' ')) { + const mnemonic = walletOpt.trim().split(' ') + const keyPair = await mnemonicToPrivateKey(mnemonic) + const contract = WalletContractV4.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }) + return { contract, keyPair } + } + + // Handle hex private key + if (walletOpt.startsWith('0x')) { + const secretKey = Buffer.from(walletOpt.slice(2), 'hex') + if (secretKey.length === 32) { + throw new Error( + 'Invalid private key: 32-byte seeds not supported. Use 64-byte secret key or mnemonic.', + ) + } + if (secretKey.length !== 64) { + throw new Error('Invalid private key: must be 64 bytes (or use mnemonic)') + } + const keyPair = keyPairFromSecretKey(secretKey) + const contract = WalletContractV4.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }) + return { contract, keyPair } + } + + // Handle file path + if (existsSync(walletOpt)) { + const content = readFileSync(walletOpt, 'utf8').trim() + const secretKey = Buffer.from(content.startsWith('0x') ? content.slice(2) : content, 'hex') + if (secretKey.length !== 64) { + throw new Error('Invalid private key in file: must be 64 bytes') + } + const keyPair = keyPairFromSecretKey(secretKey) + const contract = WalletContractV4.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }) + return { contract, keyPair } + } + + throw new Error('Wallet not specified') +} diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index b6ee030..1e88495 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -55,6 +55,8 @@ "@mysten/sui": "^1.45.2", "@solana/spl-token": "0.4.14", "@solana/web3.js": "^1.98.4", + "@ton/core": "0.62.0", + "@ton/ton": "^16.1.0", "abitype": "1.2.1", "bn.js": "^5.2.2", "borsh": "^2.0.0", @@ -68,4 +70,4 @@ "bigint-buffer": "npm:@trufflesuite/bigint-buffer@1.1.10", "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 792500e..112bc42 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -13,6 +13,7 @@ import type { } from './extra-args.ts' import type { LeafHasher } from './hasher/common.ts' import type { UnsignedSolanaTx } from './solana/types.ts' +import type { UnsignedTONTx } from './ton/types.ts' import { type AnyMessage, type CCIPCommit, @@ -100,6 +101,7 @@ export type UnsignedTx = { [ChainFamily.EVM]: UnsignedEVMTx [ChainFamily.Solana]: UnsignedSolanaTx [ChainFamily.Aptos]: UnsignedAptosTx + [ChainFamily.TON]: UnsignedTONTx [ChainFamily.Sui]: never // TODO } diff --git a/ccip-sdk/src/extra-args.test.ts b/ccip-sdk/src/extra-args.test.ts index b308dad..f603e05 100644 --- a/ccip-sdk/src/extra-args.test.ts +++ b/ccip-sdk/src/extra-args.test.ts @@ -5,7 +5,8 @@ import { dataSlice, getNumber } from 'ethers' // Import index.ts to ensure all Chain classes are loaded and registered import './index.ts' -import { decodeExtraArgs, encodeExtraArgs } from './extra-args.ts' +import { EVMExtraArgsV2Tag, decodeExtraArgs, encodeExtraArgs } from './extra-args.ts' +import { extractMagicTag } from './ton/utils.ts' import { ChainFamily } from './types.ts' describe('encodeExtraArgs', () => { @@ -74,6 +75,40 @@ describe('encodeExtraArgs', () => { assert.equal(encoded.length, 2 + 2 * (4 + 32 + 1)) // Much shorter than EVM encoding }) }) + describe('TON extra args', () => { + it('should encode EVMExtraArgsV2 (GenericExtraArgsV2)', () => { + const encoded = encodeExtraArgs( + { gasLimit: 400_000n, allowOutOfOrderExecution: true }, + ChainFamily.TON, + ) + + assert.equal(extractMagicTag(encoded), EVMExtraArgsV2Tag) + assert.ok(encoded.length > 10) + }) + + it('should encode EVMExtraArgsV2 (GenericExtraArgsV2) with allowOutOfOrderExecution false', () => { + const encoded = encodeExtraArgs( + { gasLimit: 500_000n, allowOutOfOrderExecution: false }, + ChainFamily.TON, + ) + + assert.equal(extractMagicTag(encoded), EVMExtraArgsV2Tag) + assert.ok(encoded.length > 10) + }) + + it('should parse real Sepolia->TON message extraArgs', () => { + // https://sepolia.etherscan.io/tx/0x6bdfcce8def68f19f40d340bc38d01866c10a4c92685df1c3d08180280a4ccac + const res = decodeExtraArgs( + '0x181dcf100000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000001', + ChainFamily.EVM, + ) + assert.deepEqual(res, { + _tag: 'EVMExtraArgsV2', + gasLimit: 100_000_000n, + allowOutOfOrderExecution: true, + }) + }) + }) }) describe('parseExtraArgs', () => { @@ -138,6 +173,33 @@ describe('parseExtraArgs', () => { }) }) }) + describe('TON extra args (TLB encoding)', () => { + it('should parse EVMExtraArgsV2 (GenericExtraArgsV2)', () => { + const encoded = encodeExtraArgs( + { gasLimit: 400_000n, allowOutOfOrderExecution: true }, + ChainFamily.TON, + ) + const res = decodeExtraArgs(encoded, ChainFamily.TON) + assert.deepEqual(res, { + _tag: 'EVMExtraArgsV2', + gasLimit: 400000n, + allowOutOfOrderExecution: true, + }) + }) + + it('should parse EVMExtraArgsV2 (GenericExtraArgsV2) with allowOutOfOrderExecution false', () => { + const encoded = encodeExtraArgs( + { gasLimit: 500_000n, allowOutOfOrderExecution: false }, + ChainFamily.TON, + ) + const res = decodeExtraArgs(encoded, ChainFamily.TON) + assert.deepEqual(res, { + _tag: 'EVMExtraArgsV2', + gasLimit: 500000n, + allowOutOfOrderExecution: false, + }) + }) + }) describe('auto-detect chain family', () => { it('should auto-detect EVM v1 args', () => { @@ -196,6 +258,13 @@ describe('parseExtraArgs', () => { const decoded = decodeExtraArgs(encoded, ChainFamily.Aptos) assert.deepEqual(decoded, { ...original, _tag: 'EVMExtraArgsV2' }) }) + + it('should round-trip TON EVMExtraArgsV2', () => { + const original = { gasLimit: 400_000n, allowOutOfOrderExecution: true } + const encoded = encodeExtraArgs(original, ChainFamily.TON) + const decoded = decodeExtraArgs(encoded, ChainFamily.TON) + assert.deepEqual(decoded, { ...original, _tag: 'EVMExtraArgsV2' }) + }) }) describe('encoding format differences', () => { @@ -220,5 +289,15 @@ describe('parseExtraArgs', () => { // But different lengths assert.ok(evmEncoded.length > aptosEncoded.length) }) + + it('should produce different encodings for EVM vs TON', () => { + const args = { gasLimit: 300_000n, allowOutOfOrderExecution: false } + const evmEncoded = encodeExtraArgs(args, ChainFamily.EVM) + const tonEncoded = encodeExtraArgs(args, ChainFamily.TON) + + assert.equal(evmEncoded.substring(0, 10), EVMExtraArgsV2Tag) + assert.equal(extractMagicTag(tonEncoded), EVMExtraArgsV2Tag) + assert.notEqual(evmEncoded, tonEncoded) + }) }) }) diff --git a/ccip-sdk/src/index.ts b/ccip-sdk/src/index.ts index 34020ce..fe247dc 100644 --- a/ccip-sdk/src/index.ts +++ b/ccip-sdk/src/index.ts @@ -44,8 +44,9 @@ import { AptosChain } from './aptos/index.ts' import { EVMChain } from './evm/index.ts' import { SolanaChain } from './solana/index.ts' import { SuiChain } from './sui/index.ts' +import { TONChain } from './ton/index.ts' import { ChainFamily } from './types.ts' -export { AptosChain, ChainFamily, EVMChain, SolanaChain, SuiChain } +export { AptosChain, ChainFamily, EVMChain, SolanaChain, SuiChain, TONChain } // use `supportedChains` to override/register derived classes, if needed export { supportedChains } from './supported-chains.ts' // import `allSupportedChains` to get them all registered, in tree-shaken environments @@ -54,4 +55,5 @@ export const allSupportedChains = { [ChainFamily.Solana]: SolanaChain, [ChainFamily.Aptos]: AptosChain, [ChainFamily.Sui]: SuiChain, + [ChainFamily.TON]: TONChain, } diff --git a/ccip-sdk/src/selectors.ts b/ccip-sdk/src/selectors.ts index 76e4f80..47a8fd4 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]) => [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/solana/exec.ts b/ccip-sdk/src/solana/exec.ts index 058b6d0..7beccd3 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 { 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 dcb755a..29cd3b3 100644 --- a/ccip-sdk/src/solana/index.ts +++ b/ccip-sdk/src/solana/index.ts @@ -61,6 +61,7 @@ import { ExecutionState, } from '../types.ts' import { + bytesToBuffer, createRateLimitedFetch, decodeAddress, decodeOnRampAddress, @@ -83,7 +84,6 @@ import { fetchSolanaOffchainTokenData } from './offchain.ts' import { generateUnsignedCcipSend, getFee } from './send.ts' import { type CCIPMessage_V1_6_Solana, type UnsignedSolanaTx, isWallet } from './types.ts' import { - bytesToBuffer, getErrorFromLogs, hexDiscriminator, parseSolanaLogs, diff --git a/ccip-sdk/src/solana/offchain.ts b/ccip-sdk/src/solana/offchain.ts index ddaa67d..ed64ed6 100644 --- a/ccip-sdk/src/solana/offchain.ts +++ b/ccip-sdk/src/solana/offchain.ts @@ -4,11 +4,11 @@ import { hexlify } from 'ethers' import { getUsdcAttestation } from '../offchain.ts' import type { CCIPMessage, CCIPRequest, OffchainTokenData, WithLogger } from '../types.ts' -import { networkInfo, util } from '../utils.ts' +import { bytesToBuffer, networkInfo, util } from '../utils.ts' import { IDL as BASE_TOKEN_POOL } from './idl/1.6.0/BASE_TOKEN_POOL.ts' import { IDL as CCTP_TOKEN_POOL } from './idl/1.6.0/CCIP_CCTP_TOKEN_POOL.ts' import type { SolanaLog, SolanaTransaction } from './index.ts' -import { bytesToBuffer, hexDiscriminator } from './utils.ts' +import { hexDiscriminator } from './utils.ts' interface CcipCctpMessageSentEvent { originalSender: PublicKey diff --git a/ccip-sdk/src/solana/send.ts b/ccip-sdk/src/solana/send.ts index 32f7bae..23dda2b 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 { bytesToBuffer, toLeArray, util } from '../utils.ts' import { IDL as CCIP_ROUTER_IDL } from './idl/1.6.0/CCIP_ROUTER.ts' import type { UnsignedSolanaTx } from './types.ts' -import { bytesToBuffer, simulationProvider } from './utils.ts' +import { simulationProvider } from './utils.ts' function anyToSvmMessage(message: AnyMessage): IdlTypes['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 41275b0..e4a2ec7 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, @@ -14,7 +12,7 @@ import { TransactionMessage, VersionedTransaction, } from '@solana/web3.js' -import { type BytesLike, dataLength, dataSlice, hexlify } from 'ethers' +import { dataLength, dataSlice, hexlify } from 'ethers' import type { Log_, WithLogger } from '../types.ts' import { getDataBytes, sleep } from '../utils.ts' @@ -29,15 +27,6 @@ export function hexDiscriminator(eventName: string): string { return hexlify(eventDiscriminator(eventName)) } -/** - * Converts bytes to a Node.js Buffer. - * @param bytes - Bytes to convert. - * @returns Node.js Buffer. - */ -export function bytesToBuffer(bytes: BytesLike): Buffer { - return Buffer.from(getDataBytes(bytes).buffer) -} - /** * Waits for a Solana transaction to reach finalized status. * @param connection - Solana connection instance. diff --git a/ccip-sdk/src/ton/exec.test.ts b/ccip-sdk/src/ton/exec.test.ts new file mode 100644 index 0000000..3be6bb3 --- /dev/null +++ b/ccip-sdk/src/ton/exec.test.ts @@ -0,0 +1,355 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { type Cell, Address, toNano } from '@ton/core' +import type { KeyPair } from '@ton/crypto' +import type { WalletContractV4 } from '@ton/ton' + +import { executeReport, generateUnsignedExecuteReport } from './exec.ts' +import type { ExecutionReport } 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) + + // Mock KeyPair (64-byte secret key = 32 seed + 32 public) + const mockKeyPair: KeyPair = { + publicKey: Buffer.alloc(32, 0x01), + secretKey: Buffer.alloc(64, 0x02), + } + + // 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: { + 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 { client, wallet, getCapturedTransfer, mockTxLt, mockTxHash } = createMockClientAndWallet( + { seqno: 42 }, + ) + + const execReport: ExecutionReport = { + ...baseExecReport, + message: { + ...baseExecReport.message, + data: '0x1234', + gasLimit: 500000n, + }, + proofs: ['0x' + '0'.repeat(63) + '1'], + } + + const result = await executeReport(client, wallet, offrampAddress, execReport) + + const captured = getCapturedTransfer() + assert.ok(captured, 'Transfer should be captured') + + // Verify seqno was used + assert.equal(captured.seqno, 42) + + // 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 the body Cell to verify opcode + const body = captured.messages[0].body + const slice = body.beginParse() + + // Verify opcode for manuallyExecute + const opcode = slice.loadUint(32) + assert.equal(opcode, MANUALLY_EXECUTE_OPCODE) + + // Verify queryID is 0 + const queryId = slice.loadUint(64) + assert.equal(queryId, 0) + + // 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 () => { + const { client, wallet, getCapturedTransfer } = createMockClientAndWallet() + + await executeReport(client, wallet, offrampAddress, baseExecReport, { + gasLimit: 1_000_000_000, + }) + + const captured = getCapturedTransfer() + assert.ok(captured, 'Transfer should be captured') + + // 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 + slice.loadRef() // execution report reference + + // 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 + + // 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 + header: { + messageId: '0x' + '1'.repeat(64), + }, + }, + proofs: [], + proofFlagBits: 0n, + merkleRoot: '0x' + '4'.repeat(64), + offchainTokenData: [], + } + + await assert.rejects( + executeReport(client, wallet, offrampAddress, invalidReport as any), + /Cannot convert undefined to a BigInt/, + ) + }) + + it('should handle wallet sendTransfer failure', async () => { + const { client, wallet } = createMockClientAndWallet({ shouldFail: true }) + + await assert.rejects( + 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', + }) + + const result = await executeReport(client, wallet, offrampAddress, baseExecReport) + + // 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 the full address can be parsed + const fullAddress = `${parts[0]}:${parts[1]}` + 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 new file mode 100644 index 0000000..5629d8b --- /dev/null +++ b/ccip-sdk/src/ton/exec.ts @@ -0,0 +1,98 @@ +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, + MANUALLY_EXECUTE_OPCODE, + serializeExecutionReport, +} from './types.ts' +import { waitForTransaction } from './utils.ts' + +/** + * Generates an unsigned execute report payload for the TON OffRamp contract. + * + * @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 Object with target address, value, and payload cell. + */ +export function generateUnsignedExecuteReport( + offRamp: string, + execReport: ExecutionReport, + opts?: { gasLimit?: number }, +): { + to: string + value: bigint + body: ReturnType['endCell'] extends () => infer R ? R : never +} { + // 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(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() + const walletAddress = wallet.contract.address + + 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(client, walletAddress, seqno, offRampAddress) + + // 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/hasher.test.ts b/ccip-sdk/src/ton/hasher.test.ts new file mode 100644 index 0000000..b24a03c --- /dev/null +++ b/ccip-sdk/src/ton/hasher.test.ts @@ -0,0 +1,110 @@ +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) + +describe('TON hasher', () => { + 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( + 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) + }) + + it('should create different hashes for different parameters', () => { + 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) + }) + }) + + describe('getTONLeafHasher', () => { + it('should throw error for unsupported version', () => { + assert.throws(() => { + getTONLeafHasher({ + sourceChainSelector: CHAINSEL_EVM_TEST_90000001, + destChainSelector: CHAINSEL_TON, + onRamp: EVM_ONRAMP_ADDRESS_TEST, + version: CCIPVersion.V1_2, + }) + }, /TON only supports CCIP v1.6/) + }) + + it('should create hasher for v1.6', () => { + const hasher = getTONLeafHasher({ + sourceChainSelector: CHAINSEL_EVM_TEST_90000001, + destChainSelector: CHAINSEL_TON, + onRamp: EVM_ONRAMP_ADDRESS_TEST, + version: CCIPVersion.V1_6, + }) + + assert.equal(typeof hasher, 'function') + }) + }) + + describe('message hashing', () => { + const hasher = getTONLeafHasher({ + sourceChainSelector: CHAINSEL_EVM_TEST_90000001, + destChainSelector: CHAINSEL_TON, + onRamp: EVM_ONRAMP_ADDRESS_TEST, + version: CCIPVersion.V1_6, + }) + + 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 new file mode 100644 index 0000000..699dd0a --- /dev/null +++ b/ccip-sdk/src/ton/hasher.ts @@ -0,0 +1,182 @@ +import { type Cell, Address, beginCell } from '@ton/core' +import { sha256, toBigInt } from 'ethers' + +import { decodeExtraArgs } from '../extra-args.ts' +import type { LeafHasher } from '../hasher/common.ts' +import { type CCIPMessage, type CCIPMessage_V1_6, CCIPVersion } from '../types.ts' +import { networkInfo } from '../utils.ts' +import { hexToBuffer, tryParseCell } from './utils.ts' + +// 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. + * + * @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, + 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 - 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, + 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 !== 'EVMExtraArgsV2') { + throw new Error( + 'Invalid extraArgs for TON message, must be EVMExtraArgsV2 (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 + // LEAF_DOMAIN_SEPARATOR (256 bits) + metadataHash (256 bits) + refs + const messageCell = beginCell() + .storeUint(TON_LEAF_DOMAIN_SEPARATOR, 256) + .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() +} diff --git a/ccip-sdk/src/ton/index.ts b/ccip-sdk/src/ton/index.ts new file mode 100644 index 0000000..72be93f --- /dev/null +++ b/ccip-sdk/src/ton/index.ts @@ -0,0 +1,486 @@ +import { Address, Cell, beginCell } from '@ton/core' +import { TonClient, internal } 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' +import { type EVMExtraArgsV2, type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.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 WithLogger, + ChainFamily, +} from '../types.ts' +import { getDataBytes, networkInfo, util } from '../utils.ts' +// import { parseTONLogs } from './utils.ts' +import { generateUnsignedExecuteReport as generateUnsignedExecuteReportImpl } from './exec.ts' +import { getTONLeafHasher } from './hasher.ts' +import { type CCIPMessage_V1_6_TON, type UnsignedTONTx, isTONWallet } from './types.ts' +import { waitForTransaction } from './utils.ts' + +/** + * TON chain implementation supporting TON networks. + */ +export class TONChain extends Chain { + static { + supportedChains[ChainFamily.TON] = TONChain + } + static readonly family = ChainFamily.TON + static readonly decimals = 9 // TON uses 9 decimals (nanotons) + + 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, ctx?: WithLogger) { + super(network, ctx) + this.provider = client + + this.getTransaction = memoize(this.getTransaction.bind(this), { + maxSize: 100, + }) + } + + /** + * 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, ctx?: WithLogger): 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) { + const message = error instanceof Error ? error.message : String(error) + throw new Error(`Failed to connect to TON endpoint ${url}: ${message}`) + } + + // Detect network from URL + let networkId: string + if (url.includes('testnet')) { + networkId = 'ton-testnet' + } else if (url.includes('sandbox') || url.includes('localhost') || url.includes('127.0.0.1')) { + networkId = 'ton-localnet' + } else { + // Default to mainnet for production endpoints + networkId = 'ton-mainnet' + } + + return new TONChain(client, networkInfo(networkId), ctx) + } + + /** {@inheritDoc Chain.getBlockTimestamp} */ + async getBlockTimestamp(_version: number | 'finalized'): Promise { + return Promise.reject(new Error('Not implemented')) + } + + /** + * 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 { + 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: [], // TODO + blockNumber: Number(tx.lt), + timestamp: tx.now, + from: address.toString(), + } + } + + /** {@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, + 'lane' | `log.${'topics' | 'address' | 'blockNumber'}` | 'message.header.sequenceNumber' + >, + >( + _request: R, + _commit: Pick, + _opts?: { page?: number }, + ): Promise { + return Promise.reject(new Error('Not implemented')) + } + + /** {@inheritDoc Chain.typeAndVersion} */ + 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')) + } + + /** {@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')) + } + + // 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') + } + + /** + * 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) + */ + static encodeExtraArgs(args: ExtraArgs): string { + if (!args) return '0x' + if ('gasLimit' in args && 'allowOutOfOrderExecution' in args) { + const cell = beginCell() + .storeUint(Number(EVMExtraArgsV2Tag), 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. + * + * 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 EVMExtraArgsV2 (GenericExtraArgsV2) object or undefined if invalid + */ + static decodeExtraArgs( + extraArgs: BytesLike, + ): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | 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 !== Number(EVMExtraArgsV2Tag)) return undefined + + return { + _tag: 'EVMExtraArgsV2', + gasLimit: slice.loadUintBig(256), + allowOutOfOrderExecution: slice.loadBit(), + } + } catch { + // Return undefined for any parsing errors (invalid BOC, malformed data, etc.) + return undefined + } + } + + /** + * 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, _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, + _message: AnyMessage & { fee: bigint }, + _opts?: { wallet?: unknown; approveMax?: boolean }, + ): Promise { + 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') + } + // default offchain token data + return Promise.resolve(request.message.tokenAmounts.map(() => undefined)) + } + + /** {@inheritDoc Chain.generateUnsignedExecuteReport} */ + generateUnsignedExecuteReport( + _payer: string, + offRamp: string, + execReport: ExecutionReport, + opts?: { gasLimit?: number }, + ): Promise { + if (!('allowOutOfOrderExecution' in execReport.message && 'gasLimit' in execReport.message)) { + throw new Error('TON expects EVMExtraArgsV2 (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 }, + ): Promise { + const wallet = opts.wallet + if (!isTONWallet(wallet)) { + throw new Error( + `${this.constructor.name}.executeReport requires a TON wallet, got=${util.inspect(wallet)}`, + ) + } + + const unsigned = await this.generateUnsignedExecuteReport( + wallet.contract.address.toString(), + offRamp, + execReport as ExecutionReport, + opts, + ) + + // 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) + } + + /** + * 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)) { + const parsedExtraArgs = this.decodeExtraArgs(data) + if (parsedExtraArgs) return parsedExtraArgs + } + } + + /** {@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 new file mode 100644 index 0000000..d2070f5 --- /dev/null +++ b/ccip-sdk/src/ton/types.ts @@ -0,0 +1,149 @@ +import { type Builder, Address, Cell, beginCell } from '@ton/core' +import type { KeyPair } from '@ton/crypto' +import type { WalletContractV4 } from '@ton/ton' +import { toBigInt } from 'ethers' + +import type { EVMExtraArgsV2 } from '../extra-args.ts' +import type { CCIPMessage_V1_6, ChainFamily, ExecutionReport } from '../types.ts' +import { bytesToBuffer } from '../utils.ts' + +/** 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 + +/** + * TON wallet with keypair for signing transactions + */ +export interface TONWallet { + contract: WalletContractV4 + 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[] = [] + 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 +} + +/** + * 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() + .storeUint(execReport.message.header.sourceChainSelector, 64) + .storeRef(asSnakeData([execReport.message], serializeMessage)) + .storeRef(Cell.EMPTY) + .storeRef( + asSnakeData(execReport.proofs.map(toBigInt), (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 = bytesToBuffer(sender) + return beginCell().storeUint(senderBytes.length, 8).storeBuffer(senderBytes) +} + +function serializeData(data: string): Builder { + return beginCell().storeBuffer(bytesToBuffer(data)) +} + +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(bytesToBuffer(ta.extraData)).endCell()) + .endCell(), + ) + } + return builder +} + +function serializeSourcePool(address: string): Builder { + 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 new file mode 100644 index 0000000..e2cda38 --- /dev/null +++ b/ccip-sdk/src/ton/utils.test.ts @@ -0,0 +1,130 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { beginCell } from '@ton/core' +import { toBigInt } from 'ethers' + +import { extractMagicTag, hexToBuffer, tryParseCell } from './utils.ts' +import { + EVMExtraArgsV1Tag, + EVMExtraArgsV2Tag, + SVMExtraArgsV1Tag, + SuiExtraArgsV1Tag, +} from '../extra-args.ts' + +describe('TON utils', () => { + 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(EVMExtraArgsV2Tag), 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) + } + }) + }) +}) diff --git a/ccip-sdk/src/ton/utils.ts b/ccip-sdk/src/ton/utils.ts new file mode 100644 index 0000000..2fb9f78 --- /dev/null +++ b/ccip-sdk/src/ton/utils.ts @@ -0,0 +1,130 @@ +import { type Address, Cell, beginCell } from '@ton/core' +import type { TonClient } from '@ton/ton' + +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 => { + 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) +} + +/** + * 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')}` +} + +/** + * 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 840176f..2e5db1a 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' @@ -63,6 +64,7 @@ export const ChainFamily = { Solana: 'solana', Aptos: 'aptos', Sui: 'sui', + TON: 'ton', } as const /** Type representing one of the supported chain families. */ export type ChainFamily = (typeof ChainFamily)[keyof typeof ChainFamily] @@ -79,7 +81,9 @@ 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 } @@ -120,7 +124,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 /** * Generic log structure compatible across chain families. diff --git a/ccip-sdk/src/utils.ts b/ccip-sdk/src/utils.ts index d1ecac3..9455a65 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 @@ -247,7 +250,6 @@ export function leToBigInt(data: BytesLike | readonly number[]): bigint { export function toLeArray(value: BigNumberish, width?: Numeric): Uint8Array { return toBeArray(value, width).reverse() } - /** * Checks if the given data is a valid Base64 encoded string. * @param data - Data to check. @@ -278,6 +280,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. diff --git a/package-lock.json b/package-lock.json index 6c332bb..370d98e 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", @@ -105,6 +105,8 @@ "@mysten/sui": "^1.45.2", "@solana/spl-token": "0.4.14", "@solana/web3.js": "^1.98.4", + "@ton/core": "0.62.0", + "@ton/ton": "^16.1.0", "abitype": "1.2.1", "bn.js": "^5.2.2", "borsh": "^2.0.0", @@ -153,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", @@ -185,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" @@ -1939,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", @@ -2071,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", @@ -2817,6 +2819,57 @@ "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", + "peer": true, + "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", + "dependencies": { + "jssha": "3.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": { + "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": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2988,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", @@ -2998,6 +3202,22 @@ "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", @@ -3024,7 +3244,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==", @@ -3046,7 +3266,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==", @@ -3064,7 +3284,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==", @@ -3081,32 +3301,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==", @@ -3120,7 +3315,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==", @@ -3148,7 +3343,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==", @@ -3158,7 +3371,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==", @@ -3174,17 +3387,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" @@ -3194,18 +3406,379 @@ "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==", - "dev": true, - "license": "MIT", + "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/@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", + "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": { - "@typescript-eslint/types": "8.48.1", + "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": { @@ -3216,6 +3789,20 @@ "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", @@ -4337,6 +4924,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", @@ -5027,9 +5620,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": { @@ -5091,179 +5684,11 @@ "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==", - "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": { @@ -5606,24 +6031,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", @@ -7004,6 +7411,15 @@ "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", + "engines": { + "node": "*" + } + }, "node_modules/jwt-decode": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", @@ -7179,19 +7595,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", @@ -7689,14 +8092,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" @@ -8725,6 +9127,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", @@ -8781,6 +9189,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", @@ -8889,6 +9303,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", @@ -8995,6 +9441,12 @@ "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" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9009,9 +9461,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" @@ -9139,6 +9591,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", @@ -9701,6 +10320,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" + } } } }