@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/utils.ts
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { type Address, Cell, Dictionary, beginCell } from '@ton/core'
|
|
2
|
+
import type { TonClient4 } from '@ton/ton'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CCIPTransactionNotFinalizedError,
|
|
6
|
+
CCIPTransactionNotFoundError,
|
|
7
|
+
} from '../errors/specialized.ts'
|
|
8
|
+
import type { WithLogger } from '../types.ts'
|
|
9
|
+
import { bytesToBuffer, sleep } from '../utils.ts'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Converts hex string to Buffer, handling 0x prefix normalization
|
|
13
|
+
* Returns empty buffer for empty input
|
|
14
|
+
*/
|
|
15
|
+
export const hexToBuffer = (value: string): Buffer => {
|
|
16
|
+
if (!value || value === '0x' || value === '0X') return Buffer.alloc(0)
|
|
17
|
+
// Normalize to lowercase 0x prefix for bytesToBuffer/getDataBytes
|
|
18
|
+
let normalized: string
|
|
19
|
+
if (value.startsWith('0x')) {
|
|
20
|
+
normalized = value
|
|
21
|
+
} else if (value.startsWith('0X')) {
|
|
22
|
+
normalized = `0x${value.slice(2)}`
|
|
23
|
+
} else {
|
|
24
|
+
normalized = `0x${value}`
|
|
25
|
+
}
|
|
26
|
+
return bytesToBuffer(normalized)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Attempts to parse hex string as TON BOC (Bag of Cells) format
|
|
31
|
+
* Falls back to storing raw bytes as cell data if BOC parsing fails
|
|
32
|
+
* Used for parsing message data, extra data, and other hex-encoded fields
|
|
33
|
+
*/
|
|
34
|
+
export const tryParseCell = (hex: string): Cell => {
|
|
35
|
+
const bytes = hexToBuffer(hex)
|
|
36
|
+
if (bytes.length === 0) return beginCell().endCell()
|
|
37
|
+
try {
|
|
38
|
+
return Cell.fromBoc(bytes)[0]
|
|
39
|
+
} catch {
|
|
40
|
+
return beginCell().storeBuffer(bytes).endCell()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extracts the 32-bit magic tag from a BOC-encoded cell
|
|
46
|
+
* Magic tags identify the type of TON structures (e.g., extra args types)
|
|
47
|
+
* Used for type detection and validation when decoding CCIP extra args
|
|
48
|
+
* Returns tag as 0x-prefixed hex string for easy comparison
|
|
49
|
+
*/
|
|
50
|
+
export function extractMagicTag(bocHex: string): string {
|
|
51
|
+
const cell = Cell.fromBoc(hexToBuffer(bocHex))[0]
|
|
52
|
+
const tag = cell.beginParse().loadUint(32)
|
|
53
|
+
return `0x${tag.toString(16).padStart(8, '0')}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Waits for a transaction to be confirmed by polling until the wallet's seqno advances.
|
|
58
|
+
* Once seqno advances past expectedSeqno, fetches the latest transaction details.
|
|
59
|
+
*
|
|
60
|
+
* @param client - TON V4 client
|
|
61
|
+
* @param walletAddress - Address of the wallet that sent the transaction
|
|
62
|
+
* @param expectedSeqno - The seqno used when sending the transaction
|
|
63
|
+
* @param expectedDestination - Optional destination address to verify (e.g., offRamp)
|
|
64
|
+
* @param maxAttempts - Maximum polling attempts (default: 25)
|
|
65
|
+
* @param intervalMs - Polling interval in ms (default: 1000)
|
|
66
|
+
* @returns Transaction info with lt and hash
|
|
67
|
+
*/
|
|
68
|
+
export async function waitForTransaction(
|
|
69
|
+
client: TonClient4,
|
|
70
|
+
walletAddress: Address,
|
|
71
|
+
expectedSeqno: number,
|
|
72
|
+
expectedDestination?: Address,
|
|
73
|
+
maxAttempts = 25,
|
|
74
|
+
intervalMs = 1000,
|
|
75
|
+
): Promise<{ lt: string; hash: string; timestamp: number }> {
|
|
76
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
77
|
+
try {
|
|
78
|
+
// Get latest block for state lookup (V4 API requires block seqno)
|
|
79
|
+
const lastBlock = await client.getLastBlock()
|
|
80
|
+
|
|
81
|
+
// Check current seqno by running the getter
|
|
82
|
+
const seqnoResult = await client.runMethod(lastBlock.last.seqno, walletAddress, 'seqno')
|
|
83
|
+
const currentSeqno = seqnoResult.reader.readNumber()
|
|
84
|
+
|
|
85
|
+
const seqnoAdvanced = currentSeqno > expectedSeqno
|
|
86
|
+
|
|
87
|
+
if (seqnoAdvanced) {
|
|
88
|
+
// Get account state to find latest transaction
|
|
89
|
+
const account = await client.getAccountLite(lastBlock.last.seqno, walletAddress)
|
|
90
|
+
if (!account.account.last) {
|
|
91
|
+
await sleep(intervalMs)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get recent transactions using V4 API
|
|
96
|
+
const txs = await client.getAccountTransactions(
|
|
97
|
+
walletAddress,
|
|
98
|
+
BigInt(account.account.last.lt),
|
|
99
|
+
Buffer.from(account.account.last.hash, 'base64'),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
for (const { tx } of txs) {
|
|
103
|
+
// If destination verification requested, check outgoing messages
|
|
104
|
+
if (expectedDestination) {
|
|
105
|
+
const outMessages = tx.outMessages.values()
|
|
106
|
+
let destinationMatch = false
|
|
107
|
+
|
|
108
|
+
for (const msg of outMessages) {
|
|
109
|
+
if (msg.info.type === 'internal' && msg.info.dest.equals(expectedDestination)) {
|
|
110
|
+
destinationMatch = true
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!destinationMatch) continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
lt: tx.lt.toString(),
|
|
120
|
+
hash: tx.hash().toString('hex'),
|
|
121
|
+
timestamp: tx.now,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle case where contract was just deployed (seqno 0 -> 1)
|
|
127
|
+
if (expectedSeqno === 0 && attempt > 0) {
|
|
128
|
+
const account = await client.getAccountLite(lastBlock.last.seqno, walletAddress)
|
|
129
|
+
if (account.account.last) {
|
|
130
|
+
const txs = await client.getAccountTransactions(
|
|
131
|
+
walletAddress,
|
|
132
|
+
BigInt(account.account.last.lt),
|
|
133
|
+
Buffer.from(account.account.last.hash, 'base64'),
|
|
134
|
+
)
|
|
135
|
+
if (txs.length > 0) {
|
|
136
|
+
const { tx } = txs[0]
|
|
137
|
+
return {
|
|
138
|
+
lt: tx.lt.toString(),
|
|
139
|
+
hash: tx.hash().toString('hex'),
|
|
140
|
+
timestamp: tx.now,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Contract might not be initialized yet, or network error - retry
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await sleep(intervalMs)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
throw new CCIPTransactionNotFinalizedError(String(expectedSeqno))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Parses snake format data from a cell.
|
|
157
|
+
* Snake format: first byte indicates format (0x00), followed by string data that may span multiple cells.
|
|
158
|
+
*/
|
|
159
|
+
function parseSnakeData(cell: Cell): string {
|
|
160
|
+
const slice = cell.beginParse()
|
|
161
|
+
|
|
162
|
+
// Check first byte. Should be 0x00 for snake format
|
|
163
|
+
if (slice.remainingBits >= 8) {
|
|
164
|
+
const firstByte = slice.preloadUint(8)
|
|
165
|
+
if (firstByte === 0x00) {
|
|
166
|
+
// Standard snake format. skip the indicator byte
|
|
167
|
+
slice.loadUint(8)
|
|
168
|
+
}
|
|
169
|
+
// If not 0x00, the data might be stored directly without indicator
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Load the string, following references if needed
|
|
173
|
+
let result = ''
|
|
174
|
+
|
|
175
|
+
// Load available bits as string
|
|
176
|
+
const bits = slice.remainingBits
|
|
177
|
+
if (bits > 0) {
|
|
178
|
+
// Round down to nearest byte
|
|
179
|
+
const bytes = Math.floor(bits / 8)
|
|
180
|
+
if (bytes > 0) {
|
|
181
|
+
const buffer = slice.loadBuffer(bytes)
|
|
182
|
+
result = buffer.toString('utf-8')
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Follow references for continuation (snake format can span multiple cells)
|
|
187
|
+
while (slice.remainingRefs > 0) {
|
|
188
|
+
const refCell = slice.loadRef()
|
|
189
|
+
const refSlice = refCell.beginParse()
|
|
190
|
+
const refBits = refSlice.remainingBits
|
|
191
|
+
if (refBits > 0) {
|
|
192
|
+
const refBytes = Math.floor(refBits / 8)
|
|
193
|
+
if (refBytes > 0) {
|
|
194
|
+
const buffer = refSlice.loadBuffer(refBytes)
|
|
195
|
+
result += buffer.toString('utf-8')
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Fetches Jetton metadata from an external URI.
|
|
206
|
+
* Handles IPFS and HTTP(S) URIs.
|
|
207
|
+
*/
|
|
208
|
+
async function fetchOffchainJettonMetadata(
|
|
209
|
+
uri: string,
|
|
210
|
+
rateLimitedFetch: typeof fetch,
|
|
211
|
+
logger?: { debug?: (...args: unknown[]) => void },
|
|
212
|
+
): Promise<{ symbol: string; decimals: number }> {
|
|
213
|
+
// Default values
|
|
214
|
+
let symbol = 'JETTON'
|
|
215
|
+
let decimals = 9
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// Normalize URI
|
|
219
|
+
let normalizedUri = uri
|
|
220
|
+
if (uri.startsWith('ipfs://')) {
|
|
221
|
+
normalizedUri = 'https://ipfs.io/ipfs/' + uri.slice(7)
|
|
222
|
+
} else if (uri.startsWith('Qm') && uri.length >= 46) {
|
|
223
|
+
normalizedUri = 'https://ipfs.io/ipfs/' + uri
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!normalizedUri.startsWith('http://') && !normalizedUri.startsWith('https://')) {
|
|
227
|
+
return { symbol, decimals }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const response = await rateLimitedFetch(normalizedUri, {
|
|
231
|
+
headers: { Accept: 'application/json' },
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
return { symbol, decimals }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const metadata = (await response.json()) as {
|
|
239
|
+
symbol?: string
|
|
240
|
+
decimals?: number | string
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (metadata.symbol && typeof metadata.symbol === 'string') {
|
|
244
|
+
symbol = metadata.symbol
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (metadata.decimals !== undefined) {
|
|
248
|
+
const dec =
|
|
249
|
+
typeof metadata.decimals === 'string' ? parseInt(metadata.decimals, 10) : metadata.decimals
|
|
250
|
+
if (!isNaN(dec) && dec >= 0 && dec <= 255) {
|
|
251
|
+
decimals = dec
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
logger?.debug?.(`Failed to fetch offchain jetton metadata from ${uri}:`, error)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { symbol, decimals }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** SHA256 hashes of known TEP-64 attribute names */
|
|
262
|
+
const TEP64_HASHES = {
|
|
263
|
+
symbol: BigInt('0xb76a7ca153c24671658335bbd08946350ffc621fa1c516e7123095d4ffd5c581'),
|
|
264
|
+
decimals: BigInt('0xee80fd2f1e03480e2282363596ee752d7bb27f50776b95086a0279189675923e'),
|
|
265
|
+
uri: BigInt('0x70e5d7b6a29b392f85076fe15ca2f2053c56c2338728c4e33c9e8ddb1ee827cc'),
|
|
266
|
+
} as const
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Parses onchain metadata dictionary to extract symbol and decimals.
|
|
270
|
+
* If symbol is not found, checks for URI key to fetch offchain metadata.
|
|
271
|
+
*/
|
|
272
|
+
async function parseOnchainDict(
|
|
273
|
+
dict: Dictionary<bigint, Cell>,
|
|
274
|
+
rateLimitedFetch: typeof fetch,
|
|
275
|
+
logger?: { debug?: (...args: unknown[]) => void },
|
|
276
|
+
): Promise<{ symbol: string; decimals: number }> {
|
|
277
|
+
let symbol = 'JETTON'
|
|
278
|
+
let decimals = 9
|
|
279
|
+
|
|
280
|
+
// Try to get symbol from dict
|
|
281
|
+
const symbolCell = dict.get(TEP64_HASHES.symbol)
|
|
282
|
+
if (symbolCell) {
|
|
283
|
+
const parsed = parseSnakeData(symbolCell)
|
|
284
|
+
if (parsed) {
|
|
285
|
+
symbol = parsed
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Try to get decimals from dict
|
|
290
|
+
const decimalsCell = dict.get(TEP64_HASHES.decimals)
|
|
291
|
+
if (decimalsCell) {
|
|
292
|
+
const decStr = parseSnakeData(decimalsCell)
|
|
293
|
+
const parsed = parseInt(decStr, 10)
|
|
294
|
+
if (!isNaN(parsed) && parsed >= 0 && parsed <= 255) {
|
|
295
|
+
decimals = parsed
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// If symbol not found in dict, check for URI key and fetch offchain
|
|
300
|
+
if (symbol === 'JETTON') {
|
|
301
|
+
const uriCell = dict.get(TEP64_HASHES.uri)
|
|
302
|
+
if (uriCell) {
|
|
303
|
+
const uri = parseSnakeData(uriCell)
|
|
304
|
+
if (uri && (uri.startsWith('http') || uri.startsWith('ipfs://') || uri.startsWith('Qm'))) {
|
|
305
|
+
const offchain = await fetchOffchainJettonMetadata(uri, rateLimitedFetch, logger)
|
|
306
|
+
symbol = offchain.symbol
|
|
307
|
+
// Only use offchain decimals if we didn't get it from onchain
|
|
308
|
+
if (decimals === 9) {
|
|
309
|
+
decimals = offchain.decimals
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { symbol, decimals }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Parses Jetton content cell to extract metadata.
|
|
320
|
+
* Supports onchain (0x00), offchain (0x01), and semichain (0x02) formats per TEP-64.
|
|
321
|
+
*/
|
|
322
|
+
export async function parseJettonContent(
|
|
323
|
+
contentCell: Cell,
|
|
324
|
+
rateLimitedFetch: typeof fetch,
|
|
325
|
+
logger?: { debug?: (...args: unknown[]) => void },
|
|
326
|
+
): Promise<{ symbol: string; decimals: number }> {
|
|
327
|
+
const slice = contentCell.beginParse()
|
|
328
|
+
|
|
329
|
+
// Default values
|
|
330
|
+
const symbol = 'JETTON'
|
|
331
|
+
const decimals = 9
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
// Check content type (first byte)
|
|
335
|
+
const contentType = slice.loadUint(8)
|
|
336
|
+
|
|
337
|
+
if (contentType === 0x00) {
|
|
338
|
+
// Onchain metadata - dictionary may be inline or in a reference
|
|
339
|
+
let dict: Dictionary<bigint, Cell> | undefined
|
|
340
|
+
|
|
341
|
+
// Check if there's remaining data for inline dict
|
|
342
|
+
if (slice.remainingBits > 1) {
|
|
343
|
+
try {
|
|
344
|
+
dict = slice.loadDict(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell())
|
|
345
|
+
} catch {
|
|
346
|
+
// Failed, will try from ref below
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// If no inline dict, check for Maybe ^Cell pattern (1 bit + ref)
|
|
351
|
+
if (!dict && slice.remainingBits >= 1 && slice.remainingRefs > 0) {
|
|
352
|
+
const hasDict = slice.loadBit()
|
|
353
|
+
if (hasDict) {
|
|
354
|
+
const dictCell = slice.loadRef()
|
|
355
|
+
try {
|
|
356
|
+
dict = dictCell
|
|
357
|
+
.beginParse()
|
|
358
|
+
.loadDictDirect(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell())
|
|
359
|
+
} catch {
|
|
360
|
+
try {
|
|
361
|
+
dict = Dictionary.loadDirect(
|
|
362
|
+
Dictionary.Keys.BigUint(256),
|
|
363
|
+
Dictionary.Values.Cell(),
|
|
364
|
+
dictCell.beginParse(),
|
|
365
|
+
)
|
|
366
|
+
} catch {
|
|
367
|
+
logger?.debug?.('Onchain: failed to load dict from ref')
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// If still no dict, try loading directly from first ref
|
|
374
|
+
if (!dict && contentCell.refs.length > 0) {
|
|
375
|
+
try {
|
|
376
|
+
const refSlice = contentCell.refs[0].beginParse()
|
|
377
|
+
dict = refSlice.loadDictDirect(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell())
|
|
378
|
+
} catch {
|
|
379
|
+
logger?.debug?.('Onchain: failed to load dict directly from ref')
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (dict) {
|
|
384
|
+
return await parseOnchainDict(dict, rateLimitedFetch, logger)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { symbol, decimals }
|
|
388
|
+
} else if (contentType === 0x01) {
|
|
389
|
+
// Offchain metadata: URI stored in remaining bits
|
|
390
|
+
const uri = slice.loadStringTail()
|
|
391
|
+
return fetchOffchainJettonMetadata(uri, rateLimitedFetch, logger)
|
|
392
|
+
} else if (contentType === 0x02) {
|
|
393
|
+
// Semichain metadata per TEP-64
|
|
394
|
+
let onchainResult = { symbol: 'JETTON', decimals: 9 }
|
|
395
|
+
let uri = ''
|
|
396
|
+
|
|
397
|
+
// Load dictionary directly from remaining slice data
|
|
398
|
+
try {
|
|
399
|
+
const dict = slice.loadDictDirect(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell())
|
|
400
|
+
onchainResult = await parseOnchainDict(dict, rateLimitedFetch, logger)
|
|
401
|
+
} catch (e) {
|
|
402
|
+
logger?.debug?.('Semichain: failed to load dict directly:', e)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// After dictionary, there may be a URI in remaining bits or refs
|
|
406
|
+
if (slice.remainingBits > 0) {
|
|
407
|
+
try {
|
|
408
|
+
uri = slice.loadStringTail()
|
|
409
|
+
} catch {
|
|
410
|
+
logger?.debug?.('Semichain: failed to load URI from remaining bits')
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// If no URI in bits, try from cell reference
|
|
415
|
+
if (!uri && slice.remainingRefs > 0) {
|
|
416
|
+
try {
|
|
417
|
+
const uriCell = slice.loadRef()
|
|
418
|
+
const uriSlice = uriCell.beginParse()
|
|
419
|
+
|
|
420
|
+
if (uriSlice.remainingBits >= 8) {
|
|
421
|
+
const firstByte = uriSlice.preloadUint(8)
|
|
422
|
+
if (firstByte === 0x01) {
|
|
423
|
+
uriSlice.loadUint(8)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
uri = uriSlice.loadStringTail()
|
|
427
|
+
} catch {
|
|
428
|
+
logger?.debug?.('Semichain: failed to load URI from ref')
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// If we got valid symbol from onchain dict, use it
|
|
433
|
+
if (onchainResult.symbol !== 'JETTON') {
|
|
434
|
+
return onchainResult
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Otherwise try fetching from URI
|
|
438
|
+
if (uri && (uri.startsWith('http') || uri.startsWith('ipfs://') || uri.startsWith('Qm'))) {
|
|
439
|
+
const offchainResult = await fetchOffchainJettonMetadata(uri, rateLimitedFetch, logger)
|
|
440
|
+
return {
|
|
441
|
+
symbol: offchainResult.symbol,
|
|
442
|
+
decimals: onchainResult.decimals !== 9 ? onchainResult.decimals : offchainResult.decimals,
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return onchainResult
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
logger?.debug?.('Failed to parse jetton content:', error)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return { symbol, decimals }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Looks up a transaction by raw hash using the TonCenter V3 API.
|
|
457
|
+
*
|
|
458
|
+
* This is necessary because TON's V4 API requires (address, lt, hash) for lookups,
|
|
459
|
+
* but users typically only have the raw transaction hash from explorers.
|
|
460
|
+
* TonCenter V3 provides an index that allows hash-only lookups.
|
|
461
|
+
*
|
|
462
|
+
* @param hash - Raw 64-char hex transaction hash
|
|
463
|
+
* @param isTestnet - Whether to use testnet API
|
|
464
|
+
* @param rateLimitedFetch - Rate-limited fetch function
|
|
465
|
+
* @param logger - Logger instance
|
|
466
|
+
* @returns Transaction identifier components needed for V4 API lookup
|
|
467
|
+
*/
|
|
468
|
+
export async function lookupTxByRawHash(
|
|
469
|
+
hash: string,
|
|
470
|
+
isTestnet: boolean,
|
|
471
|
+
rateLimitedFetch: typeof fetch,
|
|
472
|
+
logger: WithLogger['logger'],
|
|
473
|
+
): Promise<{
|
|
474
|
+
account: string
|
|
475
|
+
lt: string
|
|
476
|
+
hash: string
|
|
477
|
+
}> {
|
|
478
|
+
const baseUrl = isTestnet
|
|
479
|
+
? 'https://testnet.toncenter.com/api/v3/transactions'
|
|
480
|
+
: 'https://toncenter.com/api/v3/transactions'
|
|
481
|
+
|
|
482
|
+
// TonCenter V3 accepts hex directly
|
|
483
|
+
const cleanHash = hash.startsWith('0x') ? hash.slice(2) : hash
|
|
484
|
+
|
|
485
|
+
const url = `${baseUrl}?hash=${cleanHash}`
|
|
486
|
+
logger?.debug?.(`TonCenter V3 lookup: ${url}`)
|
|
487
|
+
|
|
488
|
+
let response: Response
|
|
489
|
+
try {
|
|
490
|
+
response = await rateLimitedFetch(url, {
|
|
491
|
+
headers: { Accept: 'application/json' },
|
|
492
|
+
})
|
|
493
|
+
} catch (error) {
|
|
494
|
+
logger?.error?.(`TonCenter V3 fetch failed:`, error)
|
|
495
|
+
throw new CCIPTransactionNotFoundError(hash, { cause: error as Error })
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let data: { transactions?: Array<{ account: string; lt: string; hash: string }> }
|
|
499
|
+
try {
|
|
500
|
+
data = (await response.json()) as typeof data
|
|
501
|
+
} catch (error) {
|
|
502
|
+
logger?.error?.(`TonCenter V3 JSON parse failed:`, error)
|
|
503
|
+
throw new CCIPTransactionNotFoundError(hash, { cause: error as Error })
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
logger?.debug?.(`TonCenter V3 response:`, data)
|
|
507
|
+
|
|
508
|
+
if (!data.transactions || data.transactions.length === 0) {
|
|
509
|
+
logger?.debug?.(`TonCenter V3: no transactions found for hash ${cleanHash}`)
|
|
510
|
+
throw new CCIPTransactionNotFoundError(hash)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return data.transactions[0]
|
|
514
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { CCIPMessage_EVM, CCIPMessage_V1_6_EVM } from './evm/messages.ts'
|
|
|
6
6
|
import type { ExtraArgs } from './extra-args.ts'
|
|
7
7
|
import type { CCIPMessage_V1_6_Solana } from './solana/types.ts'
|
|
8
8
|
import type { CCIPMessage_V1_6_Sui } from './sui/types.ts'
|
|
9
|
+
import type { CCIPMessage_V1_6_TON } from './ton/types.ts'
|
|
9
10
|
// v1.6 Base type from EVM contains the intersection of all other CCIPMessage v1.6 types
|
|
10
11
|
export type { CCIPMessage_V1_6 } from './evm/messages.ts'
|
|
11
12
|
|
|
@@ -63,6 +64,7 @@ export const ChainFamily = {
|
|
|
63
64
|
Solana: 'solana',
|
|
64
65
|
Aptos: 'aptos',
|
|
65
66
|
Sui: 'sui',
|
|
67
|
+
TON: 'ton',
|
|
66
68
|
} as const
|
|
67
69
|
/** Type representing one of the supported chain families. */
|
|
68
70
|
export type ChainFamily = (typeof ChainFamily)[keyof typeof ChainFamily]
|
|
@@ -79,7 +81,9 @@ export const CCIPVersion = {
|
|
|
79
81
|
export type CCIPVersion = (typeof CCIPVersion)[keyof typeof CCIPVersion]
|
|
80
82
|
|
|
81
83
|
/** Helper type that maps chain family to its chain ID format. */
|
|
82
|
-
type ChainFamilyWithId<F extends ChainFamily> = F extends
|
|
84
|
+
type ChainFamilyWithId<F extends ChainFamily> = F extends
|
|
85
|
+
| typeof ChainFamily.EVM
|
|
86
|
+
| typeof ChainFamily.TON
|
|
83
87
|
? { readonly family: F; readonly chainId: number }
|
|
84
88
|
: F extends typeof ChainFamily.Solana
|
|
85
89
|
? { readonly family: F; readonly chainId: string }
|
|
@@ -120,7 +124,7 @@ export type CCIPMessage<V extends CCIPVersion = CCIPVersion> = V extends
|
|
|
120
124
|
| typeof CCIPVersion.V1_2
|
|
121
125
|
| typeof CCIPVersion.V1_5
|
|
122
126
|
? CCIPMessage_EVM<V>
|
|
123
|
-
: CCIPMessage_V1_6_EVM | CCIPMessage_V1_6_Solana | CCIPMessage_V1_6_Sui
|
|
127
|
+
: CCIPMessage_V1_6_EVM | CCIPMessage_V1_6_Solana | CCIPMessage_V1_6_Sui | CCIPMessage_V1_6_TON
|
|
124
128
|
|
|
125
129
|
/**
|
|
126
130
|
* Generic log structure compatible across chain families.
|