@chainlink/ccip-sdk 0.91.0 → 0.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -80
- package/dist/aptos/hasher.d.ts.map +1 -1
- package/dist/aptos/hasher.js +7 -6
- package/dist/aptos/hasher.js.map +1 -1
- package/dist/aptos/index.d.ts +7 -2
- package/dist/aptos/index.d.ts.map +1 -1
- package/dist/aptos/index.js +29 -20
- package/dist/aptos/index.js.map +1 -1
- package/dist/aptos/logs.d.ts +5 -3
- package/dist/aptos/logs.d.ts.map +1 -1
- package/dist/aptos/logs.js +64 -27
- package/dist/aptos/logs.js.map +1 -1
- package/dist/aptos/token.d.ts.map +1 -1
- package/dist/aptos/token.js +2 -1
- package/dist/aptos/token.js.map +1 -1
- package/dist/aptos/types.js +6 -6
- package/dist/aptos/types.js.map +1 -1
- package/dist/chain.d.ts +36 -11
- package/dist/chain.d.ts.map +1 -1
- package/dist/chain.js +34 -2
- package/dist/chain.js.map +1 -1
- package/dist/commits.d.ts +2 -3
- package/dist/commits.d.ts.map +1 -1
- package/dist/commits.js +19 -8
- package/dist/commits.js.map +1 -1
- package/dist/errors/CCIPError.d.ts +48 -0
- package/dist/errors/CCIPError.d.ts.map +1 -0
- package/dist/errors/CCIPError.js +65 -0
- package/dist/errors/CCIPError.js.map +1 -0
- package/dist/errors/codes.d.ts +120 -0
- package/dist/errors/codes.d.ts.map +1 -0
- package/dist/errors/codes.js +156 -0
- package/dist/errors/codes.js.map +1 -0
- package/dist/errors/index.d.ts +26 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +51 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/recovery.d.ts +6 -0
- package/dist/errors/recovery.d.ts.map +1 -0
- package/dist/errors/recovery.js +118 -0
- package/dist/errors/recovery.js.map +1 -0
- package/dist/errors/specialized.d.ts +637 -0
- package/dist/errors/specialized.d.ts.map +1 -0
- package/dist/errors/specialized.js +1298 -0
- package/dist/errors/specialized.js.map +1 -0
- package/dist/errors/utils.d.ts +11 -0
- package/dist/errors/utils.d.ts.map +1 -0
- package/dist/errors/utils.js +61 -0
- package/dist/errors/utils.js.map +1 -0
- package/dist/evm/abi/CommitStore_1_5.js +1 -1
- package/dist/evm/abi/LockReleaseTokenPool_1_5.js +1 -1
- package/dist/evm/abi/OffRamp_1_5.js +1 -1
- package/dist/evm/abi/OnRamp_1_5.js +1 -1
- package/dist/evm/abi/PriceRegistry_1_2.d.ts +443 -0
- package/dist/evm/abi/PriceRegistry_1_2.d.ts.map +1 -0
- package/dist/evm/abi/PriceRegistry_1_2.js +439 -0
- package/dist/evm/abi/PriceRegistry_1_2.js.map +1 -0
- package/dist/evm/const.d.ts +1 -0
- package/dist/evm/const.d.ts.map +1 -1
- package/dist/evm/const.js +2 -0
- package/dist/evm/const.js.map +1 -1
- package/dist/evm/hasher.d.ts.map +1 -1
- package/dist/evm/hasher.js +7 -6
- package/dist/evm/hasher.js.map +1 -1
- package/dist/evm/index.d.ts +9 -13
- package/dist/evm/index.d.ts.map +1 -1
- package/dist/evm/index.js +85 -68
- package/dist/evm/index.js.map +1 -1
- package/dist/evm/logs.d.ts.map +1 -1
- package/dist/evm/logs.js +47 -16
- package/dist/evm/logs.js.map +1 -1
- package/dist/evm/messages.d.ts +7 -6
- package/dist/evm/messages.d.ts.map +1 -1
- package/dist/evm/offchain.js +1 -1
- package/dist/evm/offchain.js.map +1 -1
- package/dist/evm/types.d.ts +10 -0
- package/dist/evm/types.d.ts.map +1 -0
- package/dist/evm/types.js +2 -0
- package/dist/evm/types.js.map +1 -0
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +9 -5
- package/dist/execution.js.map +1 -1
- package/dist/extra-args.d.ts.map +1 -1
- package/dist/extra-args.js +4 -3
- package/dist/extra-args.js.map +1 -1
- package/dist/gas.d.ts.map +1 -1
- package/dist/gas.js +3 -2
- package/dist/gas.js.map +1 -1
- package/dist/hasher/hasher.d.ts.map +1 -1
- package/dist/hasher/hasher.js +2 -1
- package/dist/hasher/hasher.js.map +1 -1
- package/dist/hasher/merklemulti.d.ts.map +1 -1
- package/dist/hasher/merklemulti.js +9 -8
- package/dist/hasher/merklemulti.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/offchain.d.ts.map +1 -1
- package/dist/offchain.js +5 -8
- package/dist/offchain.js.map +1 -1
- package/dist/requests.d.ts +1 -1
- package/dist/requests.d.ts.map +1 -1
- package/dist/requests.js +37 -43
- package/dist/requests.js.map +1 -1
- package/dist/selectors.d.ts.map +1 -1
- package/dist/selectors.js +22 -0
- package/dist/selectors.js.map +1 -1
- package/dist/solana/cleanup.d.ts +2 -2
- package/dist/solana/cleanup.d.ts.map +1 -1
- package/dist/solana/cleanup.js +2 -3
- package/dist/solana/cleanup.js.map +1 -1
- package/dist/solana/exec.d.ts.map +1 -1
- package/dist/solana/exec.js +12 -12
- package/dist/solana/exec.js.map +1 -1
- package/dist/solana/hasher.d.ts.map +1 -1
- package/dist/solana/hasher.js +6 -5
- package/dist/solana/hasher.js.map +1 -1
- package/dist/solana/index.d.ts +30 -13
- package/dist/solana/index.d.ts.map +1 -1
- package/dist/solana/index.js +96 -143
- package/dist/solana/index.js.map +1 -1
- package/dist/solana/logs.d.ts +15 -0
- package/dist/solana/logs.d.ts.map +1 -0
- package/dist/solana/logs.js +106 -0
- package/dist/solana/logs.js.map +1 -0
- package/dist/solana/offchain.d.ts.map +1 -1
- package/dist/solana/offchain.js +6 -5
- package/dist/solana/offchain.js.map +1 -1
- package/dist/solana/patchBorsh.d.ts.map +1 -1
- package/dist/solana/patchBorsh.js +3 -2
- package/dist/solana/patchBorsh.js.map +1 -1
- package/dist/solana/send.d.ts.map +1 -1
- package/dist/solana/send.js +8 -7
- package/dist/solana/send.js.map +1 -1
- package/dist/solana/utils.d.ts +7 -8
- package/dist/solana/utils.d.ts.map +1 -1
- package/dist/solana/utils.js +23 -11
- package/dist/solana/utils.js.map +1 -1
- package/dist/sui/discovery.d.ts +18 -0
- package/dist/sui/discovery.d.ts.map +1 -0
- package/dist/sui/discovery.js +116 -0
- package/dist/sui/discovery.js.map +1 -0
- package/dist/sui/events.d.ts +36 -0
- package/dist/sui/events.d.ts.map +1 -0
- package/dist/sui/events.js +179 -0
- package/dist/sui/events.js.map +1 -0
- package/dist/sui/hasher.d.ts.map +1 -1
- package/dist/sui/hasher.js +6 -5
- package/dist/sui/hasher.js.map +1 -1
- package/dist/sui/index.d.ts +69 -41
- package/dist/sui/index.d.ts.map +1 -1
- package/dist/sui/index.js +402 -65
- package/dist/sui/index.js.map +1 -1
- package/dist/sui/manuallyExec/encoder.d.ts +8 -0
- package/dist/sui/manuallyExec/encoder.d.ts.map +1 -0
- package/dist/sui/manuallyExec/encoder.js +76 -0
- package/dist/sui/manuallyExec/encoder.js.map +1 -0
- package/dist/sui/manuallyExec/index.d.ts +37 -0
- package/dist/sui/manuallyExec/index.d.ts.map +1 -0
- package/dist/sui/manuallyExec/index.js +81 -0
- package/dist/sui/manuallyExec/index.js.map +1 -0
- package/dist/sui/objects.d.ts +46 -0
- package/dist/sui/objects.d.ts.map +1 -0
- package/dist/sui/objects.js +259 -0
- package/dist/sui/objects.js.map +1 -0
- package/dist/ton/bindings/offramp.d.ts +48 -0
- package/dist/ton/bindings/offramp.d.ts.map +1 -0
- package/dist/ton/bindings/offramp.js +63 -0
- package/dist/ton/bindings/offramp.js.map +1 -0
- package/dist/ton/bindings/onramp.d.ts +40 -0
- package/dist/ton/bindings/onramp.d.ts.map +1 -0
- package/dist/ton/bindings/onramp.js +51 -0
- package/dist/ton/bindings/onramp.js.map +1 -0
- package/dist/ton/bindings/router.d.ts +47 -0
- package/dist/ton/bindings/router.d.ts.map +1 -0
- package/dist/ton/bindings/router.js +51 -0
- package/dist/ton/bindings/router.js.map +1 -0
- package/dist/ton/exec.d.ts +18 -0
- package/dist/ton/exec.d.ts.map +1 -0
- package/dist/ton/exec.js +28 -0
- package/dist/ton/exec.js.map +1 -0
- package/dist/ton/hasher.d.ts +27 -0
- package/dist/ton/hasher.d.ts.map +1 -0
- package/dist/ton/hasher.js +134 -0
- package/dist/ton/hasher.js.map +1 -0
- package/dist/ton/index.d.ts +247 -0
- package/dist/ton/index.d.ts.map +1 -0
- package/dist/ton/index.js +781 -0
- package/dist/ton/index.js.map +1 -0
- package/dist/ton/logs.d.ts +26 -0
- package/dist/ton/logs.d.ts.map +1 -0
- package/dist/ton/logs.js +126 -0
- package/dist/ton/logs.js.map +1 -0
- package/dist/ton/types.d.ts +37 -0
- package/dist/ton/types.d.ts.map +1 -0
- package/dist/ton/types.js +92 -0
- package/dist/ton/types.js.map +1 -0
- package/dist/ton/utils.d.ts +67 -0
- package/dist/ton/utils.d.ts.map +1 -0
- package/dist/ton/utils.js +425 -0
- package/dist/ton/utils.js.map +1 -0
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +52 -17
- package/dist/utils.js.map +1 -1
- package/package.json +12 -10
- package/src/aptos/hasher.ts +10 -6
- package/src/aptos/index.ts +50 -31
- package/src/aptos/logs.ts +85 -29
- package/src/aptos/token.ts +5 -1
- package/src/aptos/types.ts +6 -6
- package/src/chain.ts +83 -12
- package/src/commits.ts +23 -11
- package/src/errors/CCIPError.ts +86 -0
- package/src/errors/codes.ts +179 -0
- package/src/errors/index.ts +175 -0
- package/src/errors/recovery.ts +170 -0
- package/src/errors/specialized.ts +1655 -0
- package/src/errors/utils.ts +73 -0
- package/src/evm/abi/CommitStore_1_5.ts +1 -1
- package/src/evm/abi/LockReleaseTokenPool_1_5.ts +1 -1
- package/src/evm/abi/OffRamp_1_5.ts +1 -1
- package/src/evm/abi/OnRamp_1_5.ts +1 -1
- package/src/evm/abi/PriceRegistry_1_2.ts +438 -0
- package/src/evm/const.ts +2 -0
- package/src/evm/hasher.ts +7 -6
- package/src/evm/index.ts +104 -86
- package/src/evm/logs.ts +64 -16
- package/src/evm/messages.ts +14 -14
- package/src/evm/offchain.ts +1 -1
- package/src/evm/types.ts +11 -0
- package/src/execution.ts +13 -9
- package/src/extra-args.ts +4 -3
- package/src/gas.ts +10 -3
- package/src/hasher/hasher.ts +2 -1
- package/src/hasher/merklemulti.ts +18 -8
- package/src/index.ts +14 -2
- package/src/offchain.ts +10 -14
- package/src/requests.ts +51 -53
- package/src/selectors.ts +23 -0
- package/src/solana/cleanup.ts +2 -4
- package/src/solana/exec.ts +13 -13
- package/src/solana/hasher.ts +9 -5
- package/src/solana/index.ts +126 -200
- package/src/solana/logs.ts +155 -0
- package/src/solana/offchain.ts +10 -7
- package/src/solana/patchBorsh.ts +3 -2
- package/src/solana/send.ts +14 -7
- package/src/solana/utils.ts +31 -17
- package/src/sui/discovery.ts +163 -0
- package/src/sui/events.ts +328 -0
- package/src/sui/hasher.ts +6 -5
- package/src/sui/index.ts +528 -80
- package/src/sui/manuallyExec/encoder.ts +88 -0
- package/src/sui/manuallyExec/index.ts +137 -0
- package/src/sui/objects.ts +358 -0
- package/src/ton/bindings/offramp.ts +96 -0
- package/src/ton/bindings/onramp.ts +72 -0
- package/src/ton/bindings/router.ts +65 -0
- package/src/ton/exec.ts +44 -0
- package/src/ton/hasher.ts +184 -0
- package/src/ton/index.ts +989 -0
- package/src/ton/logs.ts +157 -0
- package/src/ton/types.ts +143 -0
- package/src/ton/utils.ts +514 -0
- package/src/types.ts +6 -2
- package/src/utils.ts +58 -23
- package/tsconfig.json +2 -1
package/src/ton/index.ts
ADDED
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
import { Address, Cell, beginCell, toNano } from '@ton/core'
|
|
2
|
+
import { TonClient4, internal } from '@ton/ton'
|
|
3
|
+
import { type BytesLike, getAddress as checksumAddress, isBytesLike } from 'ethers'
|
|
4
|
+
import { memoize } from 'micro-memoize'
|
|
5
|
+
import type { PickDeep } from 'type-fest'
|
|
6
|
+
|
|
7
|
+
import { type LogDecoders, fetchLogs } from './logs.ts'
|
|
8
|
+
import { type LogFilter, Chain } from '../chain.ts'
|
|
9
|
+
import {
|
|
10
|
+
CCIPArgumentInvalidError,
|
|
11
|
+
CCIPExtraArgsInvalidError,
|
|
12
|
+
CCIPHttpError,
|
|
13
|
+
CCIPNotImplementedError,
|
|
14
|
+
CCIPSourceChainUnsupportedError,
|
|
15
|
+
CCIPTransactionNotFoundError,
|
|
16
|
+
CCIPWalletInvalidError,
|
|
17
|
+
} from '../errors/specialized.ts'
|
|
18
|
+
import { type EVMExtraArgsV2, type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts'
|
|
19
|
+
import { fetchCCIPRequestsInTx } from '../requests.ts'
|
|
20
|
+
import { supportedChains } from '../supported-chains.ts'
|
|
21
|
+
import {
|
|
22
|
+
type AnyMessage,
|
|
23
|
+
type CCIPRequest,
|
|
24
|
+
type ChainTransaction,
|
|
25
|
+
type CommitReport,
|
|
26
|
+
type ExecutionReceipt,
|
|
27
|
+
type ExecutionReport,
|
|
28
|
+
type Lane,
|
|
29
|
+
type Log_,
|
|
30
|
+
type NetworkInfo,
|
|
31
|
+
type OffchainTokenData,
|
|
32
|
+
type WithLogger,
|
|
33
|
+
ChainFamily,
|
|
34
|
+
} from '../types.ts'
|
|
35
|
+
import {
|
|
36
|
+
bytesToBuffer,
|
|
37
|
+
createRateLimitedFetch,
|
|
38
|
+
decodeAddress,
|
|
39
|
+
getDataBytes,
|
|
40
|
+
networkInfo,
|
|
41
|
+
parseTypeAndVersion,
|
|
42
|
+
} from '../utils.ts'
|
|
43
|
+
import { OffRamp } from './bindings/offramp.ts'
|
|
44
|
+
import { OnRamp } from './bindings/onramp.ts'
|
|
45
|
+
import { Router } from './bindings/router.ts'
|
|
46
|
+
import { generateUnsignedExecuteReport as generateUnsignedExecuteReportImpl } from './exec.ts'
|
|
47
|
+
import { getTONLeafHasher } from './hasher.ts'
|
|
48
|
+
import { type CCIPMessage_V1_6_TON, type UnsignedTONTx, isTONWallet } from './types.ts'
|
|
49
|
+
import { lookupTxByRawHash, parseJettonContent, waitForTransaction } from './utils.ts'
|
|
50
|
+
import type { LeafHasher } from '../hasher/common.ts'
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Type guard to check if an error is a TVM error with an exit code.
|
|
54
|
+
* TON VM errors include an exitCode property indicating the error type.
|
|
55
|
+
*/
|
|
56
|
+
function isTvmError(error: unknown): error is Error & { exitCode: number } {
|
|
57
|
+
return error instanceof Error && 'exitCode' in error && typeof error.exitCode === 'number'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* TON chain implementation supporting TON networks.
|
|
62
|
+
*
|
|
63
|
+
* TON uses two different ordering concepts:
|
|
64
|
+
* - `seqno` (sequence number): The actual block number in the blockchain
|
|
65
|
+
* - `lt` (logical time): A per-account transaction ordering timestamp
|
|
66
|
+
*
|
|
67
|
+
* This implementation uses `lt` for the `blockNumber` field in logs and transactions
|
|
68
|
+
* because TON's transaction APIs are indexed by `lt`, not `seqno`. The `lt` is
|
|
69
|
+
* monotonically increasing per account and suitable for pagination and ordering.
|
|
70
|
+
*/
|
|
71
|
+
export class TONChain extends Chain<typeof ChainFamily.TON> {
|
|
72
|
+
static {
|
|
73
|
+
supportedChains[ChainFamily.TON] = TONChain
|
|
74
|
+
}
|
|
75
|
+
static readonly family = ChainFamily.TON
|
|
76
|
+
static readonly decimals = 9 // TON uses 9 decimals (nanotons)
|
|
77
|
+
private readonly rateLimitedFetch: typeof fetch
|
|
78
|
+
readonly provider: TonClient4
|
|
79
|
+
/**
|
|
80
|
+
* Cache mapping logical time (lt) to Unix timestamp.
|
|
81
|
+
* Populated during getLogs iteration for later getBlockTimestamp lookups.
|
|
82
|
+
*/
|
|
83
|
+
private readonly ltTimestampCache: Map<number, number> = new Map()
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Creates a new TONChain instance.
|
|
87
|
+
* @param client - TonClient instance.
|
|
88
|
+
* @param network - Network information for this chain.
|
|
89
|
+
* @param ctx - Context containing logger.
|
|
90
|
+
*/
|
|
91
|
+
constructor(client: TonClient4, network: NetworkInfo, ctx?: WithLogger) {
|
|
92
|
+
super(network, ctx)
|
|
93
|
+
this.provider = client
|
|
94
|
+
|
|
95
|
+
// Rate-limited fetch for TonCenter API (public tier: ~1 req/sec)
|
|
96
|
+
const rateLimitedFetch = createRateLimitedFetch(
|
|
97
|
+
{ maxRequests: 1, windowMs: 1500, maxRetries: 5 },
|
|
98
|
+
ctx,
|
|
99
|
+
)
|
|
100
|
+
this.rateLimitedFetch = (input, init) => {
|
|
101
|
+
this.logger.warn?.(
|
|
102
|
+
'Public TONCenter API calls are rate-limited to ~1 req/sec, some commands may be slow',
|
|
103
|
+
)
|
|
104
|
+
return rateLimitedFetch(input, init)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.getTransaction = memoize(this.getTransaction.bind(this), {
|
|
108
|
+
maxSize: 100,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates a TONChain instance from an RPC URL.
|
|
114
|
+
* Verifies the connection and detects the network.
|
|
115
|
+
*
|
|
116
|
+
* @param url - RPC endpoint URL for TonClient4.
|
|
117
|
+
* @param ctx - Context containing logger.
|
|
118
|
+
* @returns A new TONChain instance.
|
|
119
|
+
*/
|
|
120
|
+
static async fromUrl(url: string, ctx?: WithLogger): Promise<TONChain> {
|
|
121
|
+
const { logger = console } = ctx ?? {}
|
|
122
|
+
|
|
123
|
+
// Parse URL for validation
|
|
124
|
+
let parsedUrl: URL
|
|
125
|
+
try {
|
|
126
|
+
parsedUrl = new URL(url)
|
|
127
|
+
} catch {
|
|
128
|
+
throw new CCIPArgumentInvalidError('url', `Invalid URL format: ${url}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hostname = parsedUrl.hostname.toLowerCase()
|
|
132
|
+
const client = new TonClient4({ endpoint: url })
|
|
133
|
+
|
|
134
|
+
// Verify connection by getting the latest block
|
|
135
|
+
try {
|
|
136
|
+
await client.getLastBlock()
|
|
137
|
+
logger.debug?.(`Connected to TON V4 endpoint: ${url}`)
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
140
|
+
throw new CCIPHttpError(0, `Failed to connect to TON V4 endpoint ${url}: ${message}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Detect network from hostname
|
|
144
|
+
let networkId: string
|
|
145
|
+
if (hostname.includes('testnet')) {
|
|
146
|
+
networkId = 'ton-testnet'
|
|
147
|
+
} else if (
|
|
148
|
+
hostname === 'localhost' ||
|
|
149
|
+
hostname === '127.0.0.1' ||
|
|
150
|
+
hostname.includes('sandbox')
|
|
151
|
+
) {
|
|
152
|
+
networkId = 'ton-localnet'
|
|
153
|
+
} else {
|
|
154
|
+
// Default to mainnet for production endpoints
|
|
155
|
+
networkId = 'ton-mainnet'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return new TONChain(client, networkInfo(networkId), ctx)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Fetch the timestamp for a given logical time (lt) or finalized block.
|
|
163
|
+
*
|
|
164
|
+
* Note: For TON, the `block` parameter represents logical time (lt), not block seqno.
|
|
165
|
+
* This is because TON transaction APIs are indexed by lt. The lt must have been
|
|
166
|
+
* previously cached via getLogs or getTransaction calls.
|
|
167
|
+
*
|
|
168
|
+
* @param block - Logical time (lt) as number, or 'finalized' for latest block timestamp
|
|
169
|
+
* @returns Unix timestamp in seconds
|
|
170
|
+
*/
|
|
171
|
+
async getBlockTimestamp(block: number | 'finalized'): Promise<number> {
|
|
172
|
+
if (block === 'finalized') {
|
|
173
|
+
// Get the latest block timestamp from V4 API
|
|
174
|
+
const lastBlock = await this.provider.getLastBlock()
|
|
175
|
+
return lastBlock.now
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check lt → timestamp cache
|
|
179
|
+
const cached = this.ltTimestampCache.get(block)
|
|
180
|
+
if (cached !== undefined) {
|
|
181
|
+
return cached
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// For TON, we cannot look up timestamp by lt alone without the account address.
|
|
185
|
+
// The lt must have been cached during a previous getLogs or getTransaction call.
|
|
186
|
+
throw new CCIPNotImplementedError(
|
|
187
|
+
`getBlockTimestamp: lt ${block} not in cache. ` +
|
|
188
|
+
`TON requires lt to be cached from getLogs or getTransaction calls first.`,
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Fetches a transaction by its hash.
|
|
194
|
+
*
|
|
195
|
+
* Supports two formats:
|
|
196
|
+
* 1. Composite format: "workchain:address:lt:hash" (e.g., "0:abc123...def:12345:abc123...def")
|
|
197
|
+
* 2. Raw hash format: 64-character hex string resolved via TonCenter V3 API
|
|
198
|
+
*
|
|
199
|
+
* Note: TON's V4 API requires (address, lt, hash) for lookups. Raw hash lookups
|
|
200
|
+
* use TonCenter's V3 index API to resolve the hash to a full identifier first.
|
|
201
|
+
*
|
|
202
|
+
* @param hash - Transaction identifier in either format
|
|
203
|
+
* @returns ChainTransaction with transaction details
|
|
204
|
+
* Note: `blockNumber` contains logical time (lt), not block seqno
|
|
205
|
+
*/
|
|
206
|
+
async getTransaction(hash: string): Promise<ChainTransaction> {
|
|
207
|
+
const parts = hash.split(':')
|
|
208
|
+
|
|
209
|
+
// If not composite format (4 parts), check if it's a raw 64-char hex hash
|
|
210
|
+
if (parts.length !== 4) {
|
|
211
|
+
const cleanHash = hash.startsWith('0x') || hash.startsWith('0X') ? hash.slice(2) : hash
|
|
212
|
+
|
|
213
|
+
if (/^[a-fA-F0-9]{64}$/.test(cleanHash)) {
|
|
214
|
+
const isTestnet = this.network.name?.includes('testnet') ?? false
|
|
215
|
+
const txInfo = await lookupTxByRawHash(
|
|
216
|
+
cleanHash,
|
|
217
|
+
isTestnet,
|
|
218
|
+
this.rateLimitedFetch,
|
|
219
|
+
this.logger,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
const compositeHash = `${txInfo.account}:${txInfo.lt}:${cleanHash}`
|
|
223
|
+
this.logger.debug?.(`Resolved raw hash to composite: ${compositeHash}`)
|
|
224
|
+
|
|
225
|
+
return this.getTransaction(compositeHash)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
throw new CCIPArgumentInvalidError(
|
|
229
|
+
'hash',
|
|
230
|
+
`Invalid TON transaction hash format: "${hash}". Expected "workchain:address:lt:hash" or 64-char hex hash`,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Parse composite format: workchain:address:lt:hash
|
|
235
|
+
const address = Address.parseRaw(`${parts[0]}:${parts[1]}`)
|
|
236
|
+
const lt = parts[2]
|
|
237
|
+
const txHash = parts[3]
|
|
238
|
+
|
|
239
|
+
// Get the latest block to use as reference
|
|
240
|
+
const lastBlock = await this.provider.getLastBlock()
|
|
241
|
+
|
|
242
|
+
// Get account transactions using V4 API
|
|
243
|
+
const account = await this.provider.getAccountLite(lastBlock.last.seqno, address)
|
|
244
|
+
if (!account.account.last) {
|
|
245
|
+
throw new CCIPTransactionNotFoundError(hash)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fetch transactions and find the one we're looking for
|
|
249
|
+
const txs = await this.provider.getAccountTransactions(
|
|
250
|
+
address,
|
|
251
|
+
BigInt(lt),
|
|
252
|
+
Buffer.from(txHash, 'hex'),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if (!txs || txs.length === 0) {
|
|
256
|
+
throw new CCIPTransactionNotFoundError(hash)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const tx = txs[0].tx
|
|
260
|
+
const txLt = Number(tx.lt)
|
|
261
|
+
|
|
262
|
+
// Cache lt → timestamp for later getBlockTimestamp lookups
|
|
263
|
+
this.ltTimestampCache.set(txLt, tx.now)
|
|
264
|
+
|
|
265
|
+
// Extract logs from outgoing external messages
|
|
266
|
+
const logs: Log_[] = []
|
|
267
|
+
const outMessages = tx.outMessages.values()
|
|
268
|
+
let index = 0
|
|
269
|
+
for (const msg of outMessages) {
|
|
270
|
+
if (msg.info.type === 'external-out') {
|
|
271
|
+
logs.push({
|
|
272
|
+
address: address.toRawString(),
|
|
273
|
+
topics: [],
|
|
274
|
+
data: msg.body.toBoc().toString('base64'),
|
|
275
|
+
blockNumber: txLt, // Note: This is lt (logical time), not block seqno
|
|
276
|
+
transactionHash: hash,
|
|
277
|
+
index: index,
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
index++
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
hash,
|
|
285
|
+
logs,
|
|
286
|
+
blockNumber: txLt, // Note: This is lt (logical time), not block seqno
|
|
287
|
+
timestamp: tx.now,
|
|
288
|
+
from: address.toRawString(),
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Async generator that yields logs from TON transactions.
|
|
294
|
+
*
|
|
295
|
+
* Note: For TON, `startBlock` and `endBlock` in opts represent logical time (lt),
|
|
296
|
+
* not block sequence numbers. This is because TON transaction APIs are indexed by lt.
|
|
297
|
+
*
|
|
298
|
+
* @param opts - Log filter options (startBlock/endBlock are interpreted as lt values)
|
|
299
|
+
*/
|
|
300
|
+
async *getLogs(opts: LogFilter & { versionAsHash?: boolean }): AsyncIterableIterator<Log_> {
|
|
301
|
+
const decoders: LogDecoders = {
|
|
302
|
+
tryDecodeAsMessage: (log) => TONChain.decodeMessage(log),
|
|
303
|
+
tryDecodeAsCommit: (log) => TONChain.decodeCommits(log as Log_),
|
|
304
|
+
}
|
|
305
|
+
yield* fetchLogs(this.provider, opts, this.ltTimestampCache, decoders)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** {@inheritDoc Chain.fetchRequestsInTx} */
|
|
309
|
+
override async fetchRequestsInTx(tx: string | ChainTransaction): Promise<CCIPRequest[]> {
|
|
310
|
+
return fetchCCIPRequestsInTx(this, typeof tx === 'string' ? await this.getTransaction(tx) : tx)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** {@inheritDoc Chain.fetchAllMessagesInBatch} */
|
|
314
|
+
override async fetchAllMessagesInBatch<
|
|
315
|
+
R extends PickDeep<
|
|
316
|
+
CCIPRequest,
|
|
317
|
+
'lane' | `log.${'topics' | 'address' | 'blockNumber'}` | 'message.sequenceNumber'
|
|
318
|
+
>,
|
|
319
|
+
>(
|
|
320
|
+
_request: R,
|
|
321
|
+
_commit: Pick<CommitReport, 'minSeqNr' | 'maxSeqNr'>,
|
|
322
|
+
_opts?: { page?: number },
|
|
323
|
+
): Promise<R['message'][]> {
|
|
324
|
+
return Promise.reject(new CCIPNotImplementedError('fetchAllMessagesInBatch'))
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** {@inheritDoc Chain.typeAndVersion} */
|
|
328
|
+
async typeAndVersion(
|
|
329
|
+
address: string,
|
|
330
|
+
): Promise<
|
|
331
|
+
| [type_: string, version: string, typeAndVersion: string]
|
|
332
|
+
| [type_: string, version: string, typeAndVersion: string, suffix: string]
|
|
333
|
+
> {
|
|
334
|
+
const tonAddress = Address.parse(address)
|
|
335
|
+
|
|
336
|
+
// Get current block for state lookup
|
|
337
|
+
const lastBlock = await this.provider.getLastBlock()
|
|
338
|
+
|
|
339
|
+
// Call the typeAndVersion getter method on the contract
|
|
340
|
+
const result = await this.provider.runMethod(lastBlock.last.seqno, tonAddress, 'typeAndVersion')
|
|
341
|
+
|
|
342
|
+
// Parse the two string slices returned by the contract
|
|
343
|
+
// TON contracts return strings as cells with snake format encoding
|
|
344
|
+
const typeCell = result.reader.readCell()
|
|
345
|
+
const versionCell = result.reader.readCell()
|
|
346
|
+
|
|
347
|
+
// Load strings from cells using snake format
|
|
348
|
+
const contractType = typeCell.beginParse().loadStringTail()
|
|
349
|
+
const version = versionCell.beginParse().loadStringTail()
|
|
350
|
+
|
|
351
|
+
// Extract just the last part of the type (e.g., "OffRamp" from "com.chainlink.ton.ccip.OffRamp")
|
|
352
|
+
const typeParts = contractType.split('.')
|
|
353
|
+
const shortType = typeParts[typeParts.length - 1]
|
|
354
|
+
|
|
355
|
+
// Format as "Type Version" and use the common parser
|
|
356
|
+
const typeAndVersionStr = `${shortType} ${version}`
|
|
357
|
+
|
|
358
|
+
return parseTypeAndVersion(typeAndVersionStr) as
|
|
359
|
+
| [type_: string, version: string, typeAndVersion: string]
|
|
360
|
+
| [type_: string, version: string, typeAndVersion: string, suffix: string]
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** {@inheritDoc Chain.getRouterForOnRamp} */
|
|
364
|
+
async getRouterForOnRamp(onRamp: string, destChainSelector: bigint): Promise<string> {
|
|
365
|
+
const rawAddress = TONChain.getAddress(onRamp)
|
|
366
|
+
const onRampAddress = Address.parseRaw(rawAddress)
|
|
367
|
+
|
|
368
|
+
const onRampContract = OnRamp.createFromAddress(onRampAddress)
|
|
369
|
+
const openedContract = this.provider.open(onRampContract)
|
|
370
|
+
const destConfig = await openedContract.getDestChainConfig(destChainSelector)
|
|
371
|
+
|
|
372
|
+
return destConfig.router.toString()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** {@inheritDoc Chain.getRouterForOffRamp} */
|
|
376
|
+
async getRouterForOffRamp(offRamp: string, sourceChainSelector: bigint): Promise<string> {
|
|
377
|
+
const offRampAddress = Address.parse(offRamp)
|
|
378
|
+
const offRampContract = OffRamp.createFromAddress(offRampAddress)
|
|
379
|
+
const openedContract = this.provider.open(offRampContract)
|
|
380
|
+
|
|
381
|
+
const sourceConfig = await openedContract.getSourceChainConfig(sourceChainSelector)
|
|
382
|
+
return sourceConfig.router.toString()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** {@inheritDoc Chain.getNativeTokenForRouter} */
|
|
386
|
+
getNativeTokenForRouter(_router: string): Promise<string> {
|
|
387
|
+
return Promise.reject(new CCIPNotImplementedError('getNativeTokenForRouter'))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** {@inheritDoc Chain.getOffRampsForRouter} */
|
|
391
|
+
async getOffRampsForRouter(router: string, sourceChainSelector: bigint): Promise<string[]> {
|
|
392
|
+
const routerAddress = Address.parse(router)
|
|
393
|
+
const routerContract = Router.createFromAddress(routerAddress)
|
|
394
|
+
const openedContract = this.provider.open(routerContract)
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
// Get the specific OffRamp for the source chain selector
|
|
398
|
+
const offRamp = await openedContract.getOffRamp(sourceChainSelector)
|
|
399
|
+
return [offRamp.toString()]
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (isTvmError(error) && error.exitCode === 261) {
|
|
402
|
+
return [] // Return empty array if no OffRamp configured for this source chain
|
|
403
|
+
}
|
|
404
|
+
throw error
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** {@inheritDoc Chain.getOnRampForRouter} */
|
|
409
|
+
async getOnRampForRouter(router: string, destChainSelector: bigint): Promise<string> {
|
|
410
|
+
const routerAddress = Address.parse(router)
|
|
411
|
+
const routerContract = Router.createFromAddress(routerAddress)
|
|
412
|
+
const openedContract = this.provider.open(routerContract)
|
|
413
|
+
|
|
414
|
+
const onRamp = await openedContract.getOnRamp(destChainSelector)
|
|
415
|
+
return onRamp.toString()
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** {@inheritDoc Chain.getOnRampForOffRamp} */
|
|
419
|
+
async getOnRampForOffRamp(offRamp: string, sourceChainSelector: bigint): Promise<string> {
|
|
420
|
+
const offRampAddress = Address.parse(offRamp)
|
|
421
|
+
const offRampContract = OffRamp.createFromAddress(offRampAddress)
|
|
422
|
+
const openedContract = this.provider.open(offRampContract)
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const sourceConfig = await openedContract.getSourceChainConfig(sourceChainSelector)
|
|
426
|
+
// Convert CrossChainAddress (buffer) to checksummed EVM address
|
|
427
|
+
return checksumAddress('0x' + sourceConfig.onRamp.toString('hex'))
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (isTvmError(error) && error.exitCode === 266) {
|
|
430
|
+
throw new CCIPSourceChainUnsupportedError(sourceChainSelector, {
|
|
431
|
+
context: { offRamp },
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
throw error
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** {@inheritDoc Chain.getCommitStoreForOffRamp} */
|
|
439
|
+
async getCommitStoreForOffRamp(offRamp: string): Promise<string> {
|
|
440
|
+
// TODO: FIXME: check assumption
|
|
441
|
+
return Promise.resolve(offRamp)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** {@inheritDoc Chain.getTokenForTokenPool} */
|
|
445
|
+
async getTokenForTokenPool(_tokenPool: string): Promise<string> {
|
|
446
|
+
return Promise.reject(new CCIPNotImplementedError('getTokenForTokenPool'))
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** {@inheritDoc Chain.getTokenInfo} */
|
|
450
|
+
async getTokenInfo(token: string): Promise<{ symbol: string; decimals: number }> {
|
|
451
|
+
const tokenAddress = Address.parse(token)
|
|
452
|
+
const lastBlock = await this.provider.getLastBlock()
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const result = await this.provider.runMethod(
|
|
456
|
+
lastBlock.last.seqno,
|
|
457
|
+
tokenAddress,
|
|
458
|
+
'get_jetton_data',
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
// skips
|
|
462
|
+
result.reader.readBigNumber() // total_supply
|
|
463
|
+
result.reader.readBigNumber() // mintable
|
|
464
|
+
result.reader.readAddress() // admin_address
|
|
465
|
+
|
|
466
|
+
const contentCell = result.reader.readCell()
|
|
467
|
+
return parseJettonContent(contentCell, this.rateLimitedFetch, this.logger)
|
|
468
|
+
} catch (error) {
|
|
469
|
+
this.logger.debug?.(`Failed to get jetton data for ${token}:`, error)
|
|
470
|
+
return { symbol: '', decimals: 9 }
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** {@inheritDoc Chain.getTokenAdminRegistryFor} */
|
|
475
|
+
getTokenAdminRegistryFor(_address: string): Promise<string> {
|
|
476
|
+
return Promise.reject(new CCIPNotImplementedError('getTokenAdminRegistryFor'))
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Decodes a CCIP message from a TON log event.
|
|
481
|
+
* @param log - Log with data field.
|
|
482
|
+
* @returns Decoded CCIPMessage or undefined if not valid.
|
|
483
|
+
*/
|
|
484
|
+
static decodeMessage(log: Pick<Log_, 'data'>): CCIPMessage_V1_6_TON | undefined {
|
|
485
|
+
if (!log.data || typeof log.data !== 'string') return undefined
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
// Parse BOC from base64
|
|
489
|
+
const boc = Buffer.from(log.data, 'base64')
|
|
490
|
+
const cell = Cell.fromBoc(boc)[0]
|
|
491
|
+
const slice = cell.beginParse()
|
|
492
|
+
|
|
493
|
+
// Load header fields directly (no topic prefix)
|
|
494
|
+
// Structure from TVM2AnyRampMessage:
|
|
495
|
+
// header: RampMessageHeader + sender: address + body: Cell + feeValueJuels: uint96
|
|
496
|
+
const header = {
|
|
497
|
+
messageId: '0x' + slice.loadUintBig(256).toString(16).padStart(64, '0'),
|
|
498
|
+
sourceChainSelector: slice.loadUintBig(64),
|
|
499
|
+
destChainSelector: slice.loadUintBig(64),
|
|
500
|
+
sequenceNumber: slice.loadUintBig(64),
|
|
501
|
+
nonce: slice.loadUintBig(64),
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Load sender address
|
|
505
|
+
const sender = slice.loadAddress()?.toString() ?? ''
|
|
506
|
+
|
|
507
|
+
// Load body cell ref
|
|
508
|
+
const bodyCell = slice.loadRef()
|
|
509
|
+
|
|
510
|
+
// Load feeValueJuels (96 bits) at message level, after body ref
|
|
511
|
+
const feeValueJuels = slice.loadUintBig(96)
|
|
512
|
+
|
|
513
|
+
// Parse body cell: TVM2AnyRampMessageBody
|
|
514
|
+
// Order: receiver (ref) + data (ref) + extraArgs (ref) + tokenAmounts (ref) + feeToken (inline) + feeTokenAmount (256 bits)
|
|
515
|
+
const bodySlice = bodyCell.beginParse()
|
|
516
|
+
|
|
517
|
+
// Load receiver from ref 0 (CrossChainAddress: length(8 bits) + bytes)
|
|
518
|
+
const receiverSlice = bodySlice.loadRef().beginParse()
|
|
519
|
+
const receiverLength = receiverSlice.loadUint(8)
|
|
520
|
+
const receiverBytes = receiverSlice.loadBuffer(receiverLength)
|
|
521
|
+
|
|
522
|
+
// Decode receiver address using destination chain's format
|
|
523
|
+
let receiver: string
|
|
524
|
+
try {
|
|
525
|
+
const destFamily = networkInfo(header.destChainSelector).family
|
|
526
|
+
receiver = decodeAddress(receiverBytes, destFamily)
|
|
527
|
+
} catch {
|
|
528
|
+
// Fallback to raw hex if chain not registered or decoding fails
|
|
529
|
+
receiver = '0x' + receiverBytes.toString('hex')
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Load data from ref 1
|
|
533
|
+
const dataSlice = bodySlice.loadRef().beginParse()
|
|
534
|
+
const dataBytes = dataSlice.loadBuffer(dataSlice.remainingBits / 8)
|
|
535
|
+
const data = '0x' + dataBytes.toString('hex')
|
|
536
|
+
|
|
537
|
+
// Load extraArgs from ref 2
|
|
538
|
+
const extraArgsCell = bodySlice.loadRef()
|
|
539
|
+
const extraArgsSlice = extraArgsCell.beginParse()
|
|
540
|
+
|
|
541
|
+
// Read tag (32 bits)
|
|
542
|
+
const extraArgsTag = extraArgsSlice.loadUint(32)
|
|
543
|
+
if (extraArgsTag !== Number(EVMExtraArgsV2Tag)) return undefined
|
|
544
|
+
|
|
545
|
+
// Read gasLimit (maybe uint256): 1 bit flag + 256 bits if present
|
|
546
|
+
const hasGasLimit = extraArgsSlice.loadBit()
|
|
547
|
+
const gasLimit = hasGasLimit ? extraArgsSlice.loadUintBig(256) : 0n
|
|
548
|
+
|
|
549
|
+
// Read allowOutOfOrderExecution (1 bit)
|
|
550
|
+
const allowOutOfOrderExecution = extraArgsSlice.loadBit()
|
|
551
|
+
|
|
552
|
+
// Build extraArgs as raw hex matching reference format
|
|
553
|
+
const tagHex = extraArgsTag.toString(16).padStart(8, '0')
|
|
554
|
+
const gasLimitHex = (hasGasLimit ? '8' : '0') + gasLimit.toString(16).padStart(63, '0')
|
|
555
|
+
const oooByte = allowOutOfOrderExecution ? '40' : '00'
|
|
556
|
+
const extraArgs = '0x' + tagHex + gasLimitHex + oooByte
|
|
557
|
+
|
|
558
|
+
// Load tokenAmounts from ref 3
|
|
559
|
+
const _tokenAmountsCell = bodySlice.loadRef()
|
|
560
|
+
const tokenAmounts: CCIPMessage_V1_6_TON['tokenAmounts'] = [] // TODO: FIXME: parse when implemented
|
|
561
|
+
|
|
562
|
+
// Load feeToken (inline address in body)
|
|
563
|
+
const feeToken = bodySlice.loadMaybeAddress()?.toString() ?? ''
|
|
564
|
+
|
|
565
|
+
// Load feeTokenAmount (256 bits)
|
|
566
|
+
const feeTokenAmount = bodySlice.loadUintBig(256)
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
...header,
|
|
570
|
+
sender,
|
|
571
|
+
receiver,
|
|
572
|
+
data,
|
|
573
|
+
tokenAmounts,
|
|
574
|
+
feeToken,
|
|
575
|
+
feeTokenAmount,
|
|
576
|
+
feeValueJuels,
|
|
577
|
+
extraArgs,
|
|
578
|
+
gasLimit,
|
|
579
|
+
allowOutOfOrderExecution,
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
582
|
+
return undefined
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Encodes extra args from TON messages into BOC serialization format.
|
|
588
|
+
*
|
|
589
|
+
* Currently only supports GenericExtraArgsV2 (EVMExtraArgsV2) encoding since TON
|
|
590
|
+
* lanes are only connected to EVM chains. When new lanes are planned to be added,
|
|
591
|
+
* this should be extended to support them (eg. Solana and SVMExtraArgsV1)
|
|
592
|
+
*
|
|
593
|
+
* @param args - Extra arguments containing gas limit and execution flags
|
|
594
|
+
* @returns Hex string of BOC-encoded extra args (0x-prefixed)
|
|
595
|
+
*/
|
|
596
|
+
static encodeExtraArgs(args: ExtraArgs): string {
|
|
597
|
+
if (!args) return '0x'
|
|
598
|
+
if ('gasLimit' in args && 'allowOutOfOrderExecution' in args) {
|
|
599
|
+
const cell = beginCell()
|
|
600
|
+
.storeUint(Number(EVMExtraArgsV2Tag), 32) // magic tag
|
|
601
|
+
.storeUint(args.gasLimit, 256) // gasLimit
|
|
602
|
+
.storeBit(args.allowOutOfOrderExecution) // bool
|
|
603
|
+
.endCell()
|
|
604
|
+
|
|
605
|
+
// Return full BOC including headers
|
|
606
|
+
return '0x' + cell.toBoc().toString('hex')
|
|
607
|
+
}
|
|
608
|
+
return '0x'
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Decodes BOC-encoded extra arguments from TON messages.
|
|
613
|
+
* Parses the BOC format and extracts extra args, validating the magic tag
|
|
614
|
+
* to ensure correct type. Returns undefined if parsing fails or tag doesn't match.
|
|
615
|
+
*
|
|
616
|
+
* Currently only supports EVMExtraArgsV2 (GenericExtraArgsV2) encoding since TON
|
|
617
|
+
* lanes are only connected to EVM chains. When new lanes are planned to be added,
|
|
618
|
+
* this should be extended to support them (eg. Solana and SVMExtraArgsV1)
|
|
619
|
+
*
|
|
620
|
+
* @param extraArgs - BOC-encoded extra args as hex string or bytes
|
|
621
|
+
* @returns Decoded EVMExtraArgsV2 (GenericExtraArgsV2) object or undefined if invalid
|
|
622
|
+
*/
|
|
623
|
+
static decodeExtraArgs(
|
|
624
|
+
extraArgs: BytesLike,
|
|
625
|
+
): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | undefined {
|
|
626
|
+
const data = Buffer.from(getDataBytes(extraArgs))
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
// Parse BOC format to extract cell data
|
|
630
|
+
const cell = Cell.fromBoc(data)[0]
|
|
631
|
+
const slice = cell.beginParse()
|
|
632
|
+
|
|
633
|
+
// Load and verify magic tag to ensure correct extra args type
|
|
634
|
+
const magicTag = slice.loadUint(32)
|
|
635
|
+
if (magicTag !== Number(EVMExtraArgsV2Tag)) return undefined
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
_tag: 'EVMExtraArgsV2',
|
|
639
|
+
gasLimit: slice.loadUintBig(256),
|
|
640
|
+
allowOutOfOrderExecution: slice.loadBit(),
|
|
641
|
+
}
|
|
642
|
+
} catch {
|
|
643
|
+
// Return undefined for any parsing errors (invalid BOC, malformed data, etc.)
|
|
644
|
+
return undefined
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Decodes commit reports from a TON log event (CommitReportAccepted).
|
|
650
|
+
*
|
|
651
|
+
* @param log - Log with data field (base64-encoded BOC).
|
|
652
|
+
* @param lane - Optional lane info for filtering.
|
|
653
|
+
* @returns Array of CommitReport or undefined if not a valid commit event.
|
|
654
|
+
*/
|
|
655
|
+
static decodeCommits(log: Log_, lane?: Lane): CommitReport[] | undefined {
|
|
656
|
+
if (!log.data || typeof log.data !== 'string') return undefined
|
|
657
|
+
|
|
658
|
+
try {
|
|
659
|
+
const boc = Buffer.from(log.data, 'base64')
|
|
660
|
+
const cell = Cell.fromBoc(boc)[0]
|
|
661
|
+
const slice = cell.beginParse()
|
|
662
|
+
|
|
663
|
+
// Cell body starts directly with hasMerkleRoot (topic is in message header)
|
|
664
|
+
const hasMerkleRoot = slice.loadBit()
|
|
665
|
+
|
|
666
|
+
if (!hasMerkleRoot) {
|
|
667
|
+
// No merkle root: could be price-only update, skip for now
|
|
668
|
+
return undefined
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Read MerkleRoot fields inline
|
|
672
|
+
const sourceChainSelector = slice.loadUintBig(64)
|
|
673
|
+
const onRampLen = slice.loadUint(8)
|
|
674
|
+
|
|
675
|
+
if (onRampLen === 0 || onRampLen > 32) {
|
|
676
|
+
// Invalid onRamp length
|
|
677
|
+
return undefined
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const onRampBytes = slice.loadBuffer(onRampLen)
|
|
681
|
+
const minSeqNr = slice.loadUintBig(64)
|
|
682
|
+
const maxSeqNr = slice.loadUintBig(64)
|
|
683
|
+
const merkleRoot = '0x' + slice.loadUintBig(256).toString(16).padStart(64, '0')
|
|
684
|
+
|
|
685
|
+
// Read hasPriceUpdates (1 bit): we don't need the data but should consume it
|
|
686
|
+
if (slice.remainingBits >= 1) {
|
|
687
|
+
const hasPriceUpdates = slice.loadBit()
|
|
688
|
+
if (hasPriceUpdates && slice.remainingRefs > 0) {
|
|
689
|
+
slice.loadRef() // Skip price updates ref
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const report: CommitReport = {
|
|
694
|
+
sourceChainSelector,
|
|
695
|
+
onRampAddress: '0x' + onRampBytes.toString('hex'),
|
|
696
|
+
minSeqNr,
|
|
697
|
+
maxSeqNr,
|
|
698
|
+
merkleRoot,
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Filter by lane if provided
|
|
702
|
+
if (lane) {
|
|
703
|
+
if (report.sourceChainSelector !== lane.sourceChainSelector) return undefined
|
|
704
|
+
if (report.onRampAddress?.toLowerCase() !== lane.onRamp?.toLowerCase()) return undefined
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return [report]
|
|
708
|
+
} catch {
|
|
709
|
+
return undefined
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Decodes an execution receipt from a TON log event.
|
|
715
|
+
* @param _log - Log with data field.
|
|
716
|
+
* @returns ExecutionReceipt or undefined if not valid.
|
|
717
|
+
*/
|
|
718
|
+
static decodeReceipt(_log: Log_): ExecutionReceipt | undefined {
|
|
719
|
+
throw new CCIPNotImplementedError('decodeReceipt')
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Converts bytes to a TON address.
|
|
724
|
+
* Handles:
|
|
725
|
+
* - 36-byte CCIP format: workchain(4 bytes, big-endian) + hash(32 bytes)
|
|
726
|
+
* - 33-byte format: workchain(1 byte) + hash(32 bytes)
|
|
727
|
+
* - 32-byte format: hash only (assumes workchain 0)
|
|
728
|
+
* Also handles user-friendly format strings (e.g., "EQ...", "UQ...", "kQ...", "0Q...")
|
|
729
|
+
* and raw format strings ("workchain:hash").
|
|
730
|
+
* @param bytes - Bytes or string to convert.
|
|
731
|
+
* @returns TON raw address string in format "workchain:hash".
|
|
732
|
+
*/
|
|
733
|
+
static getAddress(bytes: BytesLike): string {
|
|
734
|
+
// If it's already a string address, try to parse and return raw format
|
|
735
|
+
if (typeof bytes === 'string') {
|
|
736
|
+
// Handle raw format "workchain:hash"
|
|
737
|
+
if (bytes.includes(':') && !bytes.startsWith('0x')) {
|
|
738
|
+
return bytes
|
|
739
|
+
}
|
|
740
|
+
// Handle user-friendly format (EQ..., UQ..., etc.)
|
|
741
|
+
if (
|
|
742
|
+
bytes.startsWith('EQ') ||
|
|
743
|
+
bytes.startsWith('UQ') ||
|
|
744
|
+
bytes.startsWith('kQ') ||
|
|
745
|
+
bytes.startsWith('0Q')
|
|
746
|
+
) {
|
|
747
|
+
return Address.parse(bytes).toRawString()
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const data = bytesToBuffer(bytes)
|
|
752
|
+
|
|
753
|
+
if (data.length === 36) {
|
|
754
|
+
// CCIP cross-chain format: workchain(4 bytes, big-endian) + hash(32 bytes)
|
|
755
|
+
const workchain = data.readInt32BE(0)
|
|
756
|
+
const hash = data.subarray(4).toString('hex')
|
|
757
|
+
return `${workchain}:${hash}`
|
|
758
|
+
} else if (data.length === 33) {
|
|
759
|
+
// workchain (1 byte) + hash (32 bytes)
|
|
760
|
+
const workchain = data[0] === 0xff ? -1 : data[0]
|
|
761
|
+
const hash = data.subarray(1).toString('hex')
|
|
762
|
+
return `${workchain}:${hash}`
|
|
763
|
+
} else if (data.length === 32) {
|
|
764
|
+
// hash only, assume workchain 0
|
|
765
|
+
return `0:${data.toString('hex')}`
|
|
766
|
+
} else {
|
|
767
|
+
throw new CCIPArgumentInvalidError(
|
|
768
|
+
'bytes',
|
|
769
|
+
`Invalid TON address bytes length: ${data.length}. Expected 32, 33, or 36 bytes.`,
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Formats a TON address for human-friendly display.
|
|
776
|
+
* Converts raw format (workchain:hash) to user-friendly format (EQ..., UQ..., etc.)
|
|
777
|
+
* @param address - Address in any recognized format
|
|
778
|
+
* @returns User-friendly TON address string
|
|
779
|
+
*/
|
|
780
|
+
static formatAddress(address: string): string {
|
|
781
|
+
try {
|
|
782
|
+
// Parse the address (handles both raw and friendly formats)
|
|
783
|
+
const parsed = Address.parse(address)
|
|
784
|
+
// Return user-friendly format (bounceable by default)
|
|
785
|
+
return parsed.toString()
|
|
786
|
+
} catch {
|
|
787
|
+
// If parsing fails, return original
|
|
788
|
+
return address
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Formats a TON transaction hash for human-friendly display.
|
|
794
|
+
* Extracts the raw 64-char hash from composite format for cleaner display.
|
|
795
|
+
* @param hash - Transaction hash in composite or raw format
|
|
796
|
+
* @returns The raw 64-char hex hash for display
|
|
797
|
+
*/
|
|
798
|
+
static formatTxHash(hash: string): string {
|
|
799
|
+
const parts = hash.split(':')
|
|
800
|
+
if (parts.length === 4) {
|
|
801
|
+
// Composite format: workchain:address:lt:hash - return just the hash part
|
|
802
|
+
return parts[3]
|
|
803
|
+
}
|
|
804
|
+
// Already raw format or unknown - return as-is
|
|
805
|
+
return hash
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Validates a transaction hash format for TON.
|
|
810
|
+
* Supports:
|
|
811
|
+
* - Raw 64-char hex hash (with or without 0x prefix)
|
|
812
|
+
* - Composite format: "workchain:address:lt:hash"
|
|
813
|
+
*/
|
|
814
|
+
static isTxHash(v: unknown): v is string {
|
|
815
|
+
if (typeof v !== 'string') return false
|
|
816
|
+
|
|
817
|
+
// Check for raw 64-char hex hash (with or without 0x prefix)
|
|
818
|
+
const cleanHash = v.startsWith('0x') || v.startsWith('0X') ? v.slice(2) : v
|
|
819
|
+
if (/^[a-fA-F0-9]{64}$/.test(cleanHash)) {
|
|
820
|
+
return true
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Check for composite format: workchain:address:lt:hash
|
|
824
|
+
const parts = v.split(':')
|
|
825
|
+
if (parts.length === 4) {
|
|
826
|
+
const [workchain, address, lt, hash] = parts
|
|
827
|
+
// workchain should be a number (typically 0 or -1)
|
|
828
|
+
if (!/^-?\d+$/.test(workchain)) return false
|
|
829
|
+
// address should be 64-char hex
|
|
830
|
+
if (!/^[a-fA-F0-9]{64}$/.test(address)) return false
|
|
831
|
+
// lt should be a number
|
|
832
|
+
if (!/^\d+$/.test(lt)) return false
|
|
833
|
+
// hash should be 64-char hex
|
|
834
|
+
if (!/^[a-fA-F0-9]{64}$/.test(hash)) return false
|
|
835
|
+
return true
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return false
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Gets the leaf hasher for TON destination chains.
|
|
842
|
+
* @param lane - Lane configuration.
|
|
843
|
+
* @param _ctx - Context containing logger.
|
|
844
|
+
* @returns Leaf hasher function.
|
|
845
|
+
*/
|
|
846
|
+
static getDestLeafHasher(lane: Lane, _ctx?: WithLogger): LeafHasher {
|
|
847
|
+
return getTONLeafHasher(lane)
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** {@inheritDoc Chain.getFee} */
|
|
851
|
+
async getFee(_router: string, _destChainSelector: bigint, _message: AnyMessage): Promise<bigint> {
|
|
852
|
+
return Promise.reject(new CCIPNotImplementedError('getFee'))
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** {@inheritDoc Chain.generateUnsignedSendMessage} */
|
|
856
|
+
generateUnsignedSendMessage(
|
|
857
|
+
_sender: string,
|
|
858
|
+
_router: string,
|
|
859
|
+
_destChainSelector: bigint,
|
|
860
|
+
_message: AnyMessage & { fee?: bigint },
|
|
861
|
+
_opts?: { approveMax?: boolean },
|
|
862
|
+
): Promise<never> {
|
|
863
|
+
return Promise.reject(new CCIPNotImplementedError('generateUnsignedSendMessage'))
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** {@inheritDoc Chain.sendMessage} */
|
|
867
|
+
async sendMessage(
|
|
868
|
+
_router: string,
|
|
869
|
+
_destChainSelector: bigint,
|
|
870
|
+
_message: AnyMessage & { fee: bigint },
|
|
871
|
+
_opts?: { wallet?: unknown; approveMax?: boolean },
|
|
872
|
+
): Promise<CCIPRequest> {
|
|
873
|
+
return Promise.reject(new CCIPNotImplementedError('sendMessage'))
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** {@inheritDoc Chain.fetchOffchainTokenData} */
|
|
877
|
+
fetchOffchainTokenData(request: CCIPRequest): Promise<OffchainTokenData[]> {
|
|
878
|
+
return Promise.resolve(request.message.tokenAmounts.map(() => undefined))
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/** {@inheritDoc Chain.generateUnsignedExecuteReport} */
|
|
882
|
+
generateUnsignedExecuteReport(
|
|
883
|
+
_payer: string,
|
|
884
|
+
offRamp: string,
|
|
885
|
+
execReport: ExecutionReport,
|
|
886
|
+
opts?: { gasLimit?: number },
|
|
887
|
+
): Promise<UnsignedTONTx> {
|
|
888
|
+
if (!('allowOutOfOrderExecution' in execReport.message && 'gasLimit' in execReport.message)) {
|
|
889
|
+
throw new CCIPExtraArgsInvalidError('TON')
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const unsigned = generateUnsignedExecuteReportImpl(
|
|
893
|
+
offRamp,
|
|
894
|
+
execReport as ExecutionReport<CCIPMessage_V1_6_TON>,
|
|
895
|
+
opts,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
return Promise.resolve({
|
|
899
|
+
family: ChainFamily.TON,
|
|
900
|
+
to: unsigned.to,
|
|
901
|
+
body: unsigned.body,
|
|
902
|
+
})
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** {@inheritDoc Chain.executeReport} */
|
|
906
|
+
async executeReport(
|
|
907
|
+
offRamp: string,
|
|
908
|
+
execReport: ExecutionReport,
|
|
909
|
+
opts: { wallet: unknown; gasLimit?: number },
|
|
910
|
+
): Promise<ChainTransaction> {
|
|
911
|
+
const wallet = opts.wallet
|
|
912
|
+
if (!isTONWallet(wallet)) {
|
|
913
|
+
throw new CCIPWalletInvalidError(wallet)
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const unsigned = await this.generateUnsignedExecuteReport(
|
|
917
|
+
wallet.contract.address.toString(),
|
|
918
|
+
offRamp,
|
|
919
|
+
execReport as ExecutionReport<CCIPMessage_V1_6_TON>,
|
|
920
|
+
opts,
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
// Open wallet and send transaction using the unsigned data
|
|
924
|
+
const openedWallet = this.provider.open(wallet.contract)
|
|
925
|
+
const seqno = await openedWallet.getSeqno()
|
|
926
|
+
|
|
927
|
+
await openedWallet.sendTransfer({
|
|
928
|
+
seqno,
|
|
929
|
+
secretKey: wallet.keyPair.secretKey,
|
|
930
|
+
messages: [
|
|
931
|
+
internal({
|
|
932
|
+
to: unsigned.to,
|
|
933
|
+
value: toNano('0.2'), // TODO: FIXME: estimate proper value for execution costs instead of hardcoding.
|
|
934
|
+
body: unsigned.body,
|
|
935
|
+
}),
|
|
936
|
+
],
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
// Wait for transaction to be confirmed
|
|
940
|
+
const offRampAddress = Address.parse(offRamp)
|
|
941
|
+
const txInfo = await waitForTransaction(
|
|
942
|
+
this.provider,
|
|
943
|
+
wallet.contract.address,
|
|
944
|
+
seqno,
|
|
945
|
+
offRampAddress,
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
// Return composite hash in format "workchain:address:lt:hash"
|
|
949
|
+
const hash = `${wallet.contract.address.toRawString()}:${txInfo.lt}:${txInfo.hash}`
|
|
950
|
+
return this.getTransaction(hash)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Parses raw TON data into typed structures.
|
|
955
|
+
* @param data - Raw data to parse.
|
|
956
|
+
* @returns Parsed data or undefined.
|
|
957
|
+
*/
|
|
958
|
+
static parse(data: unknown) {
|
|
959
|
+
if (isBytesLike(data)) {
|
|
960
|
+
const parsedExtraArgs = this.decodeExtraArgs(data)
|
|
961
|
+
if (parsedExtraArgs) return parsedExtraArgs
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** {@inheritDoc Chain.getSupportedTokens} */
|
|
966
|
+
async getSupportedTokens(_address: string): Promise<string[]> {
|
|
967
|
+
return Promise.reject(new CCIPNotImplementedError('getSupportedTokens'))
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/** {@inheritDoc Chain.getRegistryTokenConfig} */
|
|
971
|
+
async getRegistryTokenConfig(_address: string, _tokenName: string): Promise<never> {
|
|
972
|
+
return Promise.reject(new CCIPNotImplementedError('getRegistryTokenConfig'))
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** {@inheritDoc Chain.getTokenPoolConfigs} */
|
|
976
|
+
async getTokenPoolConfigs(_tokenPool: string): Promise<never> {
|
|
977
|
+
return Promise.reject(new CCIPNotImplementedError('getTokenPoolConfigs'))
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** {@inheritDoc Chain.getTokenPoolRemotes} */
|
|
981
|
+
async getTokenPoolRemotes(_tokenPool: string): Promise<never> {
|
|
982
|
+
return Promise.reject(new CCIPNotImplementedError('getTokenPoolRemotes'))
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** {@inheritDoc Chain.getFeeTokens} */
|
|
986
|
+
async getFeeTokens(_router: string): Promise<never> {
|
|
987
|
+
return Promise.reject(new CCIPNotImplementedError('getFeeTokens'))
|
|
988
|
+
}
|
|
989
|
+
}
|