@chainlink/ccip-sdk 0.95.0 → 0.96.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.
Files changed (115) hide show
  1. package/README.md +2 -2
  2. package/dist/all-chains.d.ts +23 -0
  3. package/dist/all-chains.d.ts.map +1 -0
  4. package/dist/all-chains.js +24 -0
  5. package/dist/all-chains.js.map +1 -0
  6. package/dist/api/index.d.ts +15 -12
  7. package/dist/api/index.d.ts.map +1 -1
  8. package/dist/api/index.js +20 -16
  9. package/dist/api/index.js.map +1 -1
  10. package/dist/api/types.d.ts +25 -29
  11. package/dist/api/types.d.ts.map +1 -1
  12. package/dist/aptos/index.d.ts +33 -8
  13. package/dist/aptos/index.d.ts.map +1 -1
  14. package/dist/aptos/index.js +74 -41
  15. package/dist/aptos/index.js.map +1 -1
  16. package/dist/chain.d.ts +220 -41
  17. package/dist/chain.d.ts.map +1 -1
  18. package/dist/chain.js +105 -15
  19. package/dist/chain.js.map +1 -1
  20. package/dist/errors/codes.d.ts +2 -0
  21. package/dist/errors/codes.d.ts.map +1 -1
  22. package/dist/errors/codes.js +2 -0
  23. package/dist/errors/codes.js.map +1 -1
  24. package/dist/errors/index.d.ts +1 -1
  25. package/dist/errors/index.d.ts.map +1 -1
  26. package/dist/errors/index.js +1 -1
  27. package/dist/errors/index.js.map +1 -1
  28. package/dist/errors/recovery.d.ts.map +1 -1
  29. package/dist/errors/recovery.js +2 -0
  30. package/dist/errors/recovery.js.map +1 -1
  31. package/dist/errors/specialized.d.ts +12 -6
  32. package/dist/errors/specialized.d.ts.map +1 -1
  33. package/dist/errors/specialized.js +19 -7
  34. package/dist/errors/specialized.js.map +1 -1
  35. package/dist/evm/extra-args.d.ts +25 -0
  36. package/dist/evm/extra-args.d.ts.map +1 -0
  37. package/dist/evm/extra-args.js +328 -0
  38. package/dist/evm/extra-args.js.map +1 -0
  39. package/dist/evm/gas.d.ts.map +1 -1
  40. package/dist/evm/gas.js +7 -12
  41. package/dist/evm/gas.js.map +1 -1
  42. package/dist/evm/index.d.ts +70 -24
  43. package/dist/evm/index.d.ts.map +1 -1
  44. package/dist/evm/index.js +72 -91
  45. package/dist/evm/index.js.map +1 -1
  46. package/dist/execution.d.ts.map +1 -1
  47. package/dist/execution.js +16 -2
  48. package/dist/execution.js.map +1 -1
  49. package/dist/extra-args.d.ts +103 -4
  50. package/dist/extra-args.d.ts.map +1 -1
  51. package/dist/extra-args.js +28 -3
  52. package/dist/extra-args.js.map +1 -1
  53. package/dist/gas.d.ts +6 -3
  54. package/dist/gas.d.ts.map +1 -1
  55. package/dist/gas.js +14 -6
  56. package/dist/gas.js.map +1 -1
  57. package/dist/index.d.ts +10 -9
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +8 -8
  60. package/dist/index.js.map +1 -1
  61. package/dist/requests.d.ts +17 -9
  62. package/dist/requests.d.ts.map +1 -1
  63. package/dist/requests.js +17 -9
  64. package/dist/requests.js.map +1 -1
  65. package/dist/selectors.d.ts.map +1 -1
  66. package/dist/selectors.js +12 -0
  67. package/dist/selectors.js.map +1 -1
  68. package/dist/solana/index.d.ts +70 -15
  69. package/dist/solana/index.d.ts.map +1 -1
  70. package/dist/solana/index.js +72 -16
  71. package/dist/solana/index.js.map +1 -1
  72. package/dist/sui/index.d.ts +37 -9
  73. package/dist/sui/index.d.ts.map +1 -1
  74. package/dist/sui/index.js +40 -11
  75. package/dist/sui/index.js.map +1 -1
  76. package/dist/ton/index.d.ts +65 -19
  77. package/dist/ton/index.d.ts.map +1 -1
  78. package/dist/ton/index.js +155 -25
  79. package/dist/ton/index.js.map +1 -1
  80. package/dist/ton/send.d.ts +52 -0
  81. package/dist/ton/send.d.ts.map +1 -0
  82. package/dist/ton/send.js +166 -0
  83. package/dist/ton/send.js.map +1 -0
  84. package/dist/types.d.ts +102 -1
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js.map +1 -1
  87. package/dist/utils.d.ts +15 -3
  88. package/dist/utils.d.ts.map +1 -1
  89. package/dist/utils.js +19 -6
  90. package/dist/utils.js.map +1 -1
  91. package/package.json +12 -7
  92. package/src/all-chains.ts +26 -0
  93. package/src/api/index.ts +26 -25
  94. package/src/api/types.ts +25 -30
  95. package/src/aptos/index.ts +79 -43
  96. package/src/chain.ts +274 -46
  97. package/src/errors/codes.ts +2 -0
  98. package/src/errors/index.ts +1 -1
  99. package/src/errors/recovery.ts +2 -0
  100. package/src/errors/specialized.ts +24 -7
  101. package/src/evm/extra-args.ts +377 -0
  102. package/src/evm/gas.ts +14 -13
  103. package/src/evm/index.ts +76 -125
  104. package/src/execution.ts +18 -2
  105. package/src/extra-args.ts +108 -4
  106. package/src/gas.ts +16 -9
  107. package/src/index.ts +12 -9
  108. package/src/requests.ts +17 -9
  109. package/src/selectors.ts +12 -0
  110. package/src/solana/index.ts +72 -16
  111. package/src/sui/index.ts +40 -11
  112. package/src/ton/index.ts +192 -27
  113. package/src/ton/send.ts +222 -0
  114. package/src/types.ts +103 -1
  115. package/src/utils.ts +19 -6
package/src/ton/index.ts CHANGED
@@ -8,6 +8,7 @@ import { type Memoized, memoize } from 'micro-memoize'
8
8
  import type { PickDeep } from 'type-fest'
9
9
 
10
10
  import { streamTransactionsForAddress } from './logs.ts'
11
+ import { generateUnsignedCcipSend, getFee as getFeeImpl } from './send.ts'
11
12
  import { type ChainContext, type GetBalanceOpts, type LogFilter, Chain } from '../chain.ts'
12
13
  import {
13
14
  CCIPArgumentInvalidError,
@@ -21,6 +22,8 @@ import {
21
22
  CCIPWalletInvalidError,
22
23
  } from '../errors/specialized.ts'
23
24
  import { type EVMExtraArgsV2, type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts'
25
+ import type { LeafHasher } from '../hasher/common.ts'
26
+ import { buildMessageForDest } from '../requests.ts'
24
27
  import { supportedChains } from '../supported-chains.ts'
25
28
  import {
26
29
  type CCIPExecution,
@@ -34,6 +37,7 @@ import {
34
37
  type NetworkInfo,
35
38
  type OffchainTokenData,
36
39
  type WithLogger,
40
+ CCIPVersion,
37
41
  ChainFamily,
38
42
  ExecutionState,
39
43
  } from '../types.ts'
@@ -49,7 +53,6 @@ import { generateUnsignedExecuteReport as generateUnsignedExecuteReportImpl } fr
49
53
  import { getTONLeafHasher } from './hasher.ts'
50
54
  import { type CCIPMessage_V1_6_TON, type UnsignedTONTx, isTONWallet } from './types.ts'
51
55
  import { crc32, lookupTxByRawHash, parseJettonContent } from './utils.ts'
52
- import type { LeafHasher } from '../hasher/common.ts'
53
56
  export type { TONWallet, UnsignedTONTx } from './types.ts'
54
57
 
55
58
  /**
@@ -179,6 +182,7 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
179
182
  * @param url - RPC endpoint URL for TonClient (v2).
180
183
  * @param ctx - Context containing logger.
181
184
  * @returns A new TONChain instance.
185
+ * @throws {@link CCIPHttpError} if connection to the RPC endpoint fails
182
186
  */
183
187
  static async fromUrl(url: string, ctx?: ChainContext): Promise<TONChain> {
184
188
  const { logger = console } = ctx ?? {}
@@ -219,6 +223,7 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
219
223
  *
220
224
  * @param block - Logical time (lt) as number, or 'finalized' for latest block timestamp
221
225
  * @returns Unix timestamp in seconds
226
+ * @throws {@link CCIPNotImplementedError} if lt is not in cache
222
227
  */
223
228
  async getBlockTimestamp(block: number | 'finalized'): Promise<number> {
224
229
  if (typeof block != 'number') {
@@ -246,6 +251,8 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
246
251
  * @param tx - Transaction identifier in either format
247
252
  * @returns ChainTransaction with transaction details
248
253
  * Note: `blockNumber` contains logical time (lt), not block seqno
254
+ * @throws {@link CCIPArgumentInvalidError} if hash format is invalid
255
+ * @throws {@link CCIPTransactionNotFoundError} if transaction not found
249
256
  */
250
257
  async getTransaction(tx: string | Transaction): Promise<ChainTransaction> {
251
258
  let address
@@ -340,6 +347,7 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
340
347
  * not block sequence numbers. This is because TON transaction APIs are indexed by lt.
341
348
  *
342
349
  * @param opts - Log filter options (startBlock/endBlock are interpreted as lt values)
350
+ * @throws {@link CCIPTopicsInvalidError} if topics format is invalid
343
351
  */
344
352
  async *getLogs(opts: LogFilter): AsyncIterableIterator<Log_> {
345
353
  let topics
@@ -362,7 +370,10 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
362
370
  }
363
371
  }
364
372
 
365
- /** {@inheritDoc Chain.getMessagesInBatch} */
373
+ /**
374
+ * {@inheritDoc Chain.getMessagesInBatch}
375
+ * @throws {@link CCIPNotImplementedError} always (not implemented for TON)
376
+ */
366
377
  override async getMessagesInBatch<
367
378
  R extends PickDeep<
368
379
  CCIPRequest,
@@ -420,9 +431,14 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
420
431
  return stack.readAddress().toRawString()
421
432
  }
422
433
 
423
- /** {@inheritDoc Chain.getNativeTokenForRouter} */
434
+ /**
435
+ * {@inheritDoc Chain.getNativeTokenForRouter}
436
+ * @throws {@link CCIPNotImplementedError} always (not implemented for TON)
437
+ */
424
438
  getNativeTokenForRouter(_router: string): Promise<string> {
425
- return Promise.reject(new CCIPNotImplementedError('getNativeTokenForRouter'))
439
+ // TON native token is represented as address 0:0...01 (workchain 0, hash = 1)
440
+ // This is a convention for representing native TON in CCIP
441
+ return Promise.resolve('0:0000000000000000000000000000000000000000000000000000000000000001')
426
442
  }
427
443
 
428
444
  /** {@inheritDoc Chain.getOffRampsForRouter} */
@@ -445,7 +461,10 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
445
461
  return stack.readAddress().toRawString()
446
462
  }
447
463
 
448
- /** {@inheritDoc Chain.getOnRampForOffRamp} */
464
+ /**
465
+ * {@inheritDoc Chain.getOnRampForOffRamp}
466
+ * @throws {@link CCIPSourceChainUnsupportedError} if source chain is not configured
467
+ */
449
468
  async getOnRampForOffRamp(offRamp: string, sourceChainSelector: bigint): Promise<string> {
450
469
  try {
451
470
  const offRampContract = this.provider.provider(Address.parse(offRamp))
@@ -491,7 +510,10 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
491
510
  return Promise.resolve(offRamp)
492
511
  }
493
512
 
494
- /** {@inheritDoc Chain.getTokenForTokenPool} */
513
+ /**
514
+ * {@inheritDoc Chain.getTokenForTokenPool}
515
+ * @throws {@link CCIPNotImplementedError} always (not implemented for TON)
516
+ */
495
517
  async getTokenForTokenPool(_tokenPool: string): Promise<string> {
496
518
  return Promise.reject(new CCIPNotImplementedError('getTokenForTokenPool'))
497
519
  }
@@ -519,12 +541,45 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
519
541
  }
520
542
  }
521
543
 
522
- /** {@inheritDoc Chain.getBalance} */
523
- async getBalance(_opts: GetBalanceOpts): Promise<bigint> {
524
- return Promise.reject(new CCIPNotImplementedError('TONChain.getBalance'))
544
+ /**
545
+ * {@inheritDoc Chain.getBalance}
546
+ * @throws {@link CCIPNotImplementedError} always (not implemented for TON)
547
+ */
548
+ async getBalance(opts: GetBalanceOpts): Promise<bigint> {
549
+ const { holder, token } = opts
550
+ const holderAddress = Address.parse(holder)
551
+
552
+ if (!token) {
553
+ // Get native TON balance
554
+ const state = await this.provider.getContractState(holderAddress)
555
+ return state.balance
556
+ }
557
+
558
+ // For jetton balance, we need to:
559
+ // 1. Derive the jetton wallet address for this holder
560
+ // 2. Query the balance from that wallet contract
561
+ const jettonMaster = Address.parse(token)
562
+ const { stack } = await this.provider.runMethod(jettonMaster, 'get_wallet_address', [
563
+ { type: 'slice', cell: beginCell().storeAddress(holderAddress).endCell() },
564
+ ])
565
+ const jettonWalletAddress = stack.readAddress()
566
+
567
+ try {
568
+ const { stack: balanceStack } = await this.provider.runMethod(
569
+ jettonWalletAddress,
570
+ 'get_wallet_data',
571
+ )
572
+ return balanceStack.readBigNumber() // First value is balance
573
+ } catch {
574
+ // Wallet doesn't exist yet = 0 balance
575
+ return 0n
576
+ }
525
577
  }
526
578
 
527
- /** {@inheritDoc Chain.getTokenAdminRegistryFor} */
579
+ /**
580
+ * {@inheritDoc Chain.getTokenAdminRegistryFor}
581
+ * @throws {@link CCIPNotImplementedError} always (not implemented for TON)
582
+ */
528
583
  getTokenAdminRegistryFor(_address: string): Promise<string> {
529
584
  return Promise.reject(new CCIPNotImplementedError('getTokenAdminRegistryFor'))
530
585
  }
@@ -832,6 +887,7 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
832
887
  * and raw format strings ("workchain:hash").
833
888
  * @param bytes - Bytes or string to convert.
834
889
  * @returns TON raw address string in format "workchain:hash".
890
+ * @throws {@link CCIPArgumentInvalidError} if bytes length is invalid
835
891
  */
836
892
  static getAddress(bytes: BytesLike): string {
837
893
  // If it's already a string address, try to parse and return raw format
@@ -951,20 +1007,107 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
951
1007
  }
952
1008
 
953
1009
  /** {@inheritDoc Chain.getFee} */
954
- async getFee(_opts: Parameters<Chain['getFee']>[0]): Promise<bigint> {
955
- return Promise.reject(new CCIPNotImplementedError('getFee'))
1010
+ async getFee({
1011
+ router,
1012
+ destChainSelector,
1013
+ message,
1014
+ }: Parameters<Chain['getFee']>[0]): Promise<bigint> {
1015
+ return getFeeImpl(
1016
+ this,
1017
+ router,
1018
+ destChainSelector,
1019
+ buildMessageForDest(message, networkInfo(destChainSelector).family),
1020
+ )
956
1021
  }
957
1022
 
958
1023
  /** {@inheritDoc Chain.generateUnsignedSendMessage} */
959
- generateUnsignedSendMessage(
960
- _opts: Parameters<Chain['generateUnsignedSendMessage']>[0],
961
- ): Promise<never> {
962
- return Promise.reject(new CCIPNotImplementedError('generateUnsignedSendMessage'))
1024
+ async generateUnsignedSendMessage({
1025
+ router,
1026
+ destChainSelector,
1027
+ message,
1028
+ sender,
1029
+ }: Parameters<Chain['generateUnsignedSendMessage']>[0]): Promise<UnsignedTONTx> {
1030
+ // Convert MessageInput to AnyMessage with defaults
1031
+ const populatedMessage = buildMessageForDest(message, networkInfo(destChainSelector).family)
1032
+
1033
+ // Calculate fee if not provided
1034
+ const fee =
1035
+ message.fee ??
1036
+ (await this.getFee({
1037
+ router,
1038
+ destChainSelector,
1039
+ message: populatedMessage,
1040
+ }))
1041
+
1042
+ const unsigned = generateUnsignedCcipSend(this, sender, router, destChainSelector, {
1043
+ ...populatedMessage,
1044
+ fee,
1045
+ })
1046
+
1047
+ return {
1048
+ family: ChainFamily.TON,
1049
+ ...unsigned,
1050
+ }
963
1051
  }
964
1052
 
965
1053
  /** {@inheritDoc Chain.sendMessage} */
966
- async sendMessage(_opts: Parameters<Chain['sendMessage']>[0]): Promise<CCIPRequest> {
967
- return Promise.reject(new CCIPNotImplementedError('sendMessage'))
1054
+ async sendMessage({
1055
+ router,
1056
+ destChainSelector,
1057
+ message,
1058
+ wallet,
1059
+ }: Parameters<Chain['sendMessage']>[0]): Promise<CCIPRequest> {
1060
+ if (!isTONWallet(wallet)) {
1061
+ throw new CCIPWalletInvalidError(wallet)
1062
+ }
1063
+
1064
+ const sender = await wallet.getAddress()
1065
+
1066
+ // Generate unsigned transaction with fee calculation if needed
1067
+ const { family: _, ...unsigned } = await this.generateUnsignedSendMessage({
1068
+ router,
1069
+ destChainSelector,
1070
+ message,
1071
+ sender,
1072
+ })
1073
+
1074
+ // Send transaction
1075
+ const startTime = Math.floor(Date.now() / 1000)
1076
+ const seqno = await wallet.sendTransaction(unsigned)
1077
+
1078
+ this.logger.info('CCIP send transaction submitted, seqno:', seqno)
1079
+
1080
+ // Wait for CCIPMessageSent event and extract the request
1081
+ // Query the OnRamp for the CCIPMessageSent event
1082
+ const onRamp = await this.getOnRampForRouter(router, destChainSelector)
1083
+
1084
+ // Poll for the message in recent logs
1085
+ for await (const log of this.getLogs({
1086
+ address: onRamp,
1087
+ topics: [crc32('CCIPMessageSent')],
1088
+ startTime,
1089
+ watch: sleep(5 * 60e3 /* 5m timeout */),
1090
+ })) {
1091
+ const msg = TONChain.decodeMessage(log)
1092
+ if (!msg) continue
1093
+
1094
+ // Found our message: construct and return the CCIPRequest
1095
+ const tx = log.tx ?? (await this.getTransaction(log.transactionHash))
1096
+
1097
+ return {
1098
+ lane: {
1099
+ sourceChainSelector: this.network.chainSelector,
1100
+ destChainSelector,
1101
+ onRamp,
1102
+ version: CCIPVersion.V1_6,
1103
+ },
1104
+ message: msg,
1105
+ log,
1106
+ tx,
1107
+ }
1108
+ }
1109
+
1110
+ throw new CCIPTransactionNotFoundError(seqno.toString())
968
1111
  }
969
1112
 
970
1113
  /** {@inheritDoc Chain.getOffchainTokenData} */
@@ -972,7 +1115,10 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
972
1115
  return Promise.resolve(request.message.tokenAmounts.map(() => undefined))
973
1116
  }
974
1117
 
975
- /** {@inheritDoc Chain.generateUnsignedExecuteReport} */
1118
+ /**
1119
+ * {@inheritDoc Chain.generateUnsignedExecuteReport}
1120
+ * @throws {@link CCIPExtraArgsInvalidError} if extra args are not EVMExtraArgsV2 format
1121
+ */
976
1122
  generateUnsignedExecuteReport({
977
1123
  offRamp,
978
1124
  execReport,
@@ -994,7 +1140,11 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
994
1140
  })
995
1141
  }
996
1142
 
997
- /** {@inheritDoc Chain.executeReport} */
1143
+ /**
1144
+ * {@inheritDoc Chain.executeReport}
1145
+ * @throws {@link CCIPWalletInvalidError} if wallet is not a valid TON wallet
1146
+ * @throws {@link CCIPReceiptNotFoundError} if execution receipt not found within timeout
1147
+ */
998
1148
  async executeReport(opts: Parameters<Chain['executeReport']>[0]): Promise<CCIPExecution> {
999
1149
  const { offRamp, wallet } = opts
1000
1150
  if (!isTONWallet(wallet)) {
@@ -1039,27 +1189,42 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
1039
1189
  }
1040
1190
  }
1041
1191
 
1042
- /** {@inheritDoc Chain.getSupportedTokens} */
1192
+ /**
1193
+ * {@inheritDoc Chain.getSupportedTokens}
1194
+ * @throws {@link CCIPNotImplementedError} always (not implemented for TON)
1195
+ */
1043
1196
  async getSupportedTokens(_address: string): Promise<string[]> {
1044
1197
  return Promise.reject(new CCIPNotImplementedError('getSupportedTokens'))
1045
1198
  }
1046
1199
 
1047
- /** {@inheritDoc Chain.getRegistryTokenConfig} */
1200
+ /**
1201
+ * {@inheritDoc Chain.getRegistryTokenConfig}
1202
+ * @throws {@link CCIPNotImplementedError} always (not implemented for TON)
1203
+ */
1048
1204
  async getRegistryTokenConfig(_address: string, _tokenName: string): Promise<never> {
1049
1205
  return Promise.reject(new CCIPNotImplementedError('getRegistryTokenConfig'))
1050
1206
  }
1051
1207
 
1052
- /** {@inheritDoc Chain.getTokenPoolConfigs} */
1053
- async getTokenPoolConfigs(_tokenPool: string): Promise<never> {
1054
- return Promise.reject(new CCIPNotImplementedError('getTokenPoolConfigs'))
1208
+ /**
1209
+ * {@inheritDoc Chain.getTokenPoolConfig}
1210
+ * @throws {@link CCIPNotImplementedError} always (not implemented for TON)
1211
+ */
1212
+ async getTokenPoolConfig(_tokenPool: string): Promise<never> {
1213
+ return Promise.reject(new CCIPNotImplementedError('getTokenPoolConfig'))
1055
1214
  }
1056
1215
 
1057
- /** {@inheritDoc Chain.getTokenPoolRemotes} */
1216
+ /**
1217
+ * {@inheritDoc Chain.getTokenPoolRemotes}
1218
+ * @throws {@link CCIPNotImplementedError} always (not implemented for TON)
1219
+ */
1058
1220
  async getTokenPoolRemotes(_tokenPool: string): Promise<never> {
1059
1221
  return Promise.reject(new CCIPNotImplementedError('getTokenPoolRemotes'))
1060
1222
  }
1061
1223
 
1062
- /** {@inheritDoc Chain.getFeeTokens} */
1224
+ /**
1225
+ * {@inheritDoc Chain.getFeeTokens}
1226
+ * @throws {@link CCIPNotImplementedError} always (not implemented for TON)
1227
+ */
1063
1228
  async getFeeTokens(_router: string): Promise<never> {
1064
1229
  return Promise.reject(new CCIPNotImplementedError('getFeeTokens'))
1065
1230
  }
@@ -0,0 +1,222 @@
1
+ import { type Cell, beginCell, toNano } from '@ton/core'
2
+ import { type TonClient, Address } from '@ton/ton'
3
+ import { zeroPadValue } from 'ethers'
4
+
5
+ import type { UnsignedTONTx } from './types.ts'
6
+ import { CCIPError, CCIPErrorCode } from '../errors/index.ts'
7
+ import { EVMExtraArgsV2Tag } from '../extra-args.ts'
8
+ import type { AnyMessage, WithLogger } from '../types.ts'
9
+ import { bytesToBuffer, getDataBytes } from '../utils.ts'
10
+
11
+ /** Opcode for Router ccipSend operation */
12
+ export const CCIP_SEND_OPCODE = 0x31768d95
13
+
14
+ /** Default gas buffer to add to fee for transaction execution */
15
+ export const DEFAULT_GAS_BUFFER = toNano('0.5')
16
+
17
+ /** Default gas limit for destination chain execution */
18
+ export const DEFAULT_GAS_LIMIT = 200_000n
19
+
20
+ /**
21
+ * WRAPPED_NATIVE address for TON - sentinel address representing native TON.
22
+ * Used as feeToken for native TON payments in FeeQuoter calls.
23
+ */
24
+ export const WRAPPED_NATIVE = Address.parse(
25
+ '0:0000000000000000000000000000000000000000000000000000000000000001',
26
+ )
27
+
28
+ /**
29
+ * Encodes token amounts as a snaked cell.
30
+ * Empty cell for no tokens.
31
+ */
32
+ function encodeTokenAmounts(
33
+ tokenAmounts: readonly { token: string; amount: bigint }[] | undefined,
34
+ ): Cell {
35
+ if (!tokenAmounts || tokenAmounts.length === 0) {
36
+ return beginCell().endCell()
37
+ }
38
+
39
+ const builder = beginCell()
40
+ for (const ta of tokenAmounts) {
41
+ builder.storeRef(
42
+ beginCell().storeAddress(Address.parse(ta.token)).storeUint(ta.amount, 256).endCell(),
43
+ )
44
+ }
45
+ return builder.endCell()
46
+ }
47
+
48
+ /**
49
+ * Encodes extraArgs as a Cell using the GenericExtraArgsV2 (EVMExtraArgsV2) format.
50
+ *
51
+ * Format per chainlink-ton TL-B:
52
+ * - tag: 32-bit opcode (0x181dcf10)
53
+ * - gasLimit: Maybe<uint256> (1 bit flag + 256 bits if present)
54
+ * - allowOutOfOrderExecution: 1 bit (must be true)
55
+ */
56
+ function encodeExtraArgsCell(extraArgs: AnyMessage['extraArgs']): Cell {
57
+ const allowOutOfOrderExecution = true
58
+
59
+ let gasLimit = 0n
60
+ let hasGasLimit = false
61
+
62
+ if ('gasLimit' in extraArgs && extraArgs.gasLimit > 0n) {
63
+ hasGasLimit = true
64
+ gasLimit = extraArgs.gasLimit
65
+ }
66
+
67
+ const builder = beginCell()
68
+ .storeUint(Number(EVMExtraArgsV2Tag), 32) // 0x181dcf10
69
+ .storeBit(hasGasLimit)
70
+
71
+ if (hasGasLimit) {
72
+ builder.storeUint(gasLimit, 256)
73
+ }
74
+
75
+ return builder.storeBit(allowOutOfOrderExecution).endCell()
76
+ }
77
+
78
+ /**
79
+ * Builds the Router ccipSend message cell.
80
+ *
81
+ * Relies on TL-B structure (Router_CCIPSend) from chainlink-ton repo.
82
+ */
83
+ export function buildCcipSendCell(
84
+ destChainSelector: bigint,
85
+ message: AnyMessage,
86
+ feeTokenAddress: Address | null = null,
87
+ queryId = 0n,
88
+ ): Cell {
89
+ // Get receiver bytes and pad to 32 bytes for cross-chain encoding
90
+ const paddedReceiver = bytesToBuffer(zeroPadValue(getDataBytes(message.receiver), 32))
91
+
92
+ // Data cell (ref 0)
93
+ const dataCell = beginCell()
94
+ .storeBuffer(bytesToBuffer(message.data || '0x'))
95
+ .endCell()
96
+
97
+ // Token amounts snaked cell (ref 1)
98
+ const tokenAmountsCell = encodeTokenAmounts(message.tokenAmounts)
99
+
100
+ // ExtraArgs cell (ref 2)
101
+ const extraArgsCell = encodeExtraArgsCell(message.extraArgs)
102
+
103
+ return beginCell()
104
+ .storeUint(CCIP_SEND_OPCODE, 32) // opcode
105
+ .storeUint(Number(queryId), 64) // queryID
106
+ .storeUint(destChainSelector, 64) // destChainSelector
107
+ .storeUint(paddedReceiver.length, 8) // receiver length in bytes
108
+ .storeBuffer(paddedReceiver) // receiver bytes (32 bytes, left-padded)
109
+ .storeRef(dataCell) // ref 0: data
110
+ .storeRef(tokenAmountsCell) // ref 1: tokenAmounts
111
+ .storeAddress(feeTokenAddress) // null = addr_none for native TON
112
+ .storeRef(extraArgsCell) // ref 2: extraArgs
113
+ .endCell()
114
+ }
115
+
116
+ /**
117
+ * Gets the fee for sending a CCIP message by calling FeeQuoter.validatedFee.
118
+ *
119
+ * @param ctx - Context with TonClient provider and logger
120
+ * @param router - Router contract address
121
+ * @param destChainSelector - Destination chain selector
122
+ * @param message - CCIP message to quote
123
+ * @returns Fee amount in nanotons
124
+ */
125
+ export async function getFee(
126
+ ctx: { provider: TonClient } & WithLogger,
127
+ router: string,
128
+ destChainSelector: bigint,
129
+ message: AnyMessage,
130
+ ): Promise<bigint> {
131
+ const { provider, logger = console } = ctx
132
+ const routerAddress = Address.parse(router)
133
+
134
+ // FeeQuoter requires WRAPPED_NATIVE for native TON
135
+ const feeTokenAddress = message.feeToken ? Address.parse(message.feeToken) : WRAPPED_NATIVE
136
+
137
+ // Get FeeQuoter address via OnRamp
138
+ let feeQuoterAddress: Address
139
+ try {
140
+ const { stack: onRampStack } = await provider.runMethod(routerAddress, 'onRamp', [
141
+ { type: 'int', value: destChainSelector },
142
+ ])
143
+ const onRampAddress = onRampStack.readAddress()
144
+ logger.debug('OnRamp:', onRampAddress.toString())
145
+
146
+ const { stack: feeQuoterStack } = await provider.runMethod(onRampAddress, 'feeQuoter', [
147
+ { type: 'int', value: destChainSelector },
148
+ ])
149
+ feeQuoterAddress = feeQuoterStack.readAddress()
150
+ logger.debug('FeeQuoter:', feeQuoterAddress.toString())
151
+ } catch (e) {
152
+ throw new CCIPError(
153
+ CCIPErrorCode.CONTRACT_TYPE_INVALID,
154
+ `Could not get FeeQuoter address: ${e instanceof Error ? e.message : String(e)}`,
155
+ )
156
+ }
157
+
158
+ // Build stack parameters for validatedFee call
159
+ const paddedReceiver = bytesToBuffer(zeroPadValue(getDataBytes(message.receiver), 32))
160
+ const receiverSlice = beginCell().storeBuffer(paddedReceiver).endCell()
161
+ const dataCell = beginCell()
162
+ .storeBuffer(bytesToBuffer(message.data || '0x'))
163
+ .endCell()
164
+ const tokenAmountsCell = encodeTokenAmounts(message.tokenAmounts)
165
+ const extraArgsCell = encodeExtraArgsCell(message.extraArgs)
166
+ const feeTokenSlice = beginCell().storeAddress(feeTokenAddress).endCell()
167
+
168
+ const { stack: feeStack } = await provider.runMethod(feeQuoterAddress, 'validatedFee', [
169
+ { type: 'int', value: 0n },
170
+ { type: 'int', value: destChainSelector },
171
+ { type: 'slice', cell: receiverSlice },
172
+ { type: 'cell', cell: dataCell },
173
+ { type: 'cell', cell: tokenAmountsCell },
174
+ { type: 'slice', cell: feeTokenSlice },
175
+ { type: 'cell', cell: extraArgsCell },
176
+ ])
177
+
178
+ const fee = feeStack.readBigNumber()
179
+ if (fee < 0n) {
180
+ throw new CCIPError(CCIPErrorCode.MESSAGE_INVALID, `Invalid fee: ${fee}`)
181
+ }
182
+ logger.debug('CCIP fee:', fee.toString(), 'nanotons')
183
+ return fee
184
+ }
185
+
186
+ /**
187
+ * Generates an unsigned CCIP send transaction for the Router.
188
+ *
189
+ * @param ctx - Context with TonClient provider and logger
190
+ * @param _sender - Sender address (unused, for interface compatibility)
191
+ * @param router - Router contract address
192
+ * @param destChainSelector - Destination chain selector
193
+ * @param message - CCIP message with fee included
194
+ * @param opts - Optional gas buffer override
195
+ * @returns Unsigned transaction ready for signing
196
+ */
197
+ export function generateUnsignedCcipSend(
198
+ ctx: { provider: TonClient } & WithLogger,
199
+ _sender: string,
200
+ router: string,
201
+ destChainSelector: bigint,
202
+ message: AnyMessage & { fee: bigint },
203
+ opts?: { gasBuffer?: bigint },
204
+ ): Omit<UnsignedTONTx, 'family'> {
205
+ const { logger = console } = ctx
206
+ const gasBuffer = opts?.gasBuffer ?? DEFAULT_GAS_BUFFER
207
+
208
+ // Router accepts addr_none for native TON (unlike FeeQuoter which needs WRAPPED_NATIVE)
209
+ const feeTokenAddress = message.feeToken ? Address.parse(message.feeToken) : null
210
+
211
+ const ccipSendCell = buildCcipSendCell(destChainSelector, message, feeTokenAddress)
212
+ const totalValue = message.fee + gasBuffer
213
+
214
+ logger.debug('Generating ccipSend tx to router:', router)
215
+ logger.debug('Total value:', totalValue.toString(), 'nanotons')
216
+
217
+ return {
218
+ to: router,
219
+ body: ccipSendCell,
220
+ value: totalValue,
221
+ }
222
+ }