@chainlink/ccip-cli 0.92.0 → 0.93.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 (50) hide show
  1. package/dist/commands/lane-latency.d.ts +26 -0
  2. package/dist/commands/lane-latency.d.ts.map +1 -0
  3. package/dist/commands/lane-latency.js +73 -0
  4. package/dist/commands/lane-latency.js.map +1 -0
  5. package/dist/commands/manual-exec.d.ts.map +1 -1
  6. package/dist/commands/manual-exec.js +20 -269
  7. package/dist/commands/manual-exec.js.map +1 -1
  8. package/dist/commands/send.d.ts +1 -1
  9. package/dist/commands/send.js +11 -2
  10. package/dist/commands/send.js.map +1 -1
  11. package/dist/commands/show.d.ts.map +1 -1
  12. package/dist/commands/show.js +36 -16
  13. package/dist/commands/show.js.map +1 -1
  14. package/dist/commands/types.d.ts +1 -1
  15. package/dist/commands/utils.d.ts.map +1 -1
  16. package/dist/commands/utils.js +2 -1
  17. package/dist/commands/utils.js.map +1 -1
  18. package/dist/index.d.ts +6 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +7 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/providers/aptos.js +1 -1
  23. package/dist/providers/aptos.js.map +1 -1
  24. package/dist/providers/evm.js +1 -1
  25. package/dist/providers/evm.js.map +1 -1
  26. package/dist/providers/index.d.ts +6 -1
  27. package/dist/providers/index.d.ts.map +1 -1
  28. package/dist/providers/index.js +11 -8
  29. package/dist/providers/index.js.map +1 -1
  30. package/dist/providers/sui.d.ts.map +1 -1
  31. package/dist/providers/sui.js +1 -2
  32. package/dist/providers/sui.js.map +1 -1
  33. package/dist/providers/ton.d.ts +9 -3
  34. package/dist/providers/ton.d.ts.map +1 -1
  35. package/dist/providers/ton.js +100 -26
  36. package/dist/providers/ton.js.map +1 -1
  37. package/package.json +10 -6
  38. package/src/commands/lane-latency.ts +93 -0
  39. package/src/commands/manual-exec.ts +18 -267
  40. package/src/commands/send.ts +11 -8
  41. package/src/commands/show.ts +40 -22
  42. package/src/commands/types.ts +1 -1
  43. package/src/commands/utils.ts +6 -4
  44. package/src/index.ts +8 -4
  45. package/src/providers/aptos.ts +1 -1
  46. package/src/providers/evm.ts +1 -1
  47. package/src/providers/index.ts +18 -14
  48. package/src/providers/sui.ts +1 -2
  49. package/src/providers/ton.ts +106 -30
  50. package/tsconfig.json +3 -2
@@ -296,7 +296,11 @@ async function sendMessage(
296
296
  }
297
297
 
298
298
  // calculate fee
299
- const fee = await source.getFee(argv.router, destNetwork.chainSelector, message)
299
+ const fee = await source.getFee({
300
+ ...argv,
301
+ destChainSelector: destNetwork.chainSelector,
302
+ message,
303
+ })
300
304
 
301
305
  logger.info(
302
306
  'Fee:',
@@ -310,12 +314,12 @@ async function sendMessage(
310
314
  if (argv.onlyGetFee) return
311
315
 
312
316
  if (!walletAddress) [walletAddress, wallet] = await loadChainWallet(source, argv)
313
- const request = await source.sendMessage(
314
- argv.router,
315
- destNetwork.chainSelector,
316
- { ...message, fee },
317
- { ...argv, wallet },
318
- )
317
+ const request = await source.sendMessage({
318
+ ...argv,
319
+ destChainSelector: destNetwork.chainSelector,
320
+ message: { ...message, fee },
321
+ wallet,
322
+ })
319
323
  logger.info(
320
324
  '🚀 Sending message to',
321
325
  tokenReceiver && tokenReceiver !== '11111111111111111111111111111111'
@@ -328,7 +332,6 @@ async function sendMessage(
328
332
  ', messageId =>',
329
333
  request.message.messageId,
330
334
  )
331
-
332
335
  await showRequests(ctx, {
333
336
  ...argv,
334
337
  txHash: request.tx.hash,
@@ -3,6 +3,8 @@ import {
3
3
  type ChainTransaction,
4
4
  CCIPExecTxRevertedError,
5
5
  CCIPNotImplementedError,
6
+ ExecutionState,
7
+ MessageStatus,
6
8
  bigIntReplacer,
7
9
  discoverOffRamp,
8
10
  isSupportedTxHash,
@@ -82,18 +84,18 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
82
84
  getChain = fetchChainsFromRpcs(ctx, argv)
83
85
  let idFromSource, onRamp
84
86
  if (argv.idFromSource.includes('@')) {
85
- ;[onRamp, idFromSource] = argv.idFromSource.split('@')
87
+ ;[onRamp, idFromSource] = argv.idFromSource.split('@') as [string, string]
86
88
  } else idFromSource = argv.idFromSource
87
89
  const sourceNetwork = networkInfo(idFromSource)
88
90
  source = await getChain(sourceNetwork.chainId)
89
- if (!source.fetchRequestById)
90
- throw new CCIPNotImplementedError(`fetchRequestById for ${source.constructor.name}`)
91
- request = await source.fetchRequestById(argv.txHash, onRamp, argv)
91
+ if (!source.getMessageById)
92
+ throw new CCIPNotImplementedError(`getMessageById for ${source.constructor.name}`)
93
+ request = await source.getMessageById(argv.txHash, onRamp, argv)
92
94
  } else {
93
95
  const [getChain_, tx$] = fetchChainsFromRpcs(ctx, argv, argv.txHash)
94
96
  getChain = getChain_
95
97
  ;[source, tx] = await tx$
96
- request = await selectRequest(await source.fetchRequestsInTx(tx), 'to know more', argv)
98
+ request = await selectRequest(await source.getMessagesInTx(tx), 'to know more', argv)
97
99
  }
98
100
 
99
101
  switch (argv.format) {
@@ -118,17 +120,16 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
118
120
  let cancelWaitFinalized: (() => void) | undefined
119
121
  const finalized$ = (async () => {
120
122
  if (argv.wait) {
121
- logger.info('Waiting for finalization...')
122
- await source.waitFinalized(
123
+ logger.info(`[${MessageStatus.Sent}] Waiting for source chain finalization...`)
124
+ await source.waitFinalized({
123
125
  request,
124
- undefined,
125
- new Promise<void>((resolve) => (cancelWaitFinalized = resolve)),
126
- )
127
- logger.info(`Transaction "${request.log.transactionHash}" finalized ✅`)
126
+ cancel$: new Promise<void>((resolve) => (cancelWaitFinalized = resolve)),
127
+ })
128
+ logger.info(`[${MessageStatus.SourceFinalized}] Source chain finalized`)
128
129
  }
129
130
 
130
- const offchainTokenData = await source.fetchOffchainTokenData(request)
131
- if (offchainTokenData?.length && offchainTokenData.some((d) => !!d)) {
131
+ const offchainTokenData = await source.getOffchainTokenData(request)
132
+ if (offchainTokenData.length && offchainTokenData.some((d) => !!d)) {
132
133
  switch (argv.format) {
133
134
  case Format.log: {
134
135
  logger.log('attestations =', offchainTokenData)
@@ -147,7 +148,8 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
147
148
  }
148
149
  }
149
150
 
150
- if (argv.wait) logger.info('Waiting for Commit (dest)...')
151
+ if (argv.wait)
152
+ logger.info(`[${MessageStatus.SourceFinalized}] Waiting for commit on destination chain...`)
151
153
  else logger.info('Commit (dest):')
152
154
  })()
153
155
 
@@ -157,13 +159,16 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
157
159
 
158
160
  let cancelWaitCommit: (() => void) | undefined
159
161
  const commit$ = (async () => {
160
- const commit = await dest.fetchCommitReport(commitStore, request, {
162
+ const commit = await dest.getCommitReport({
163
+ commitStore,
164
+ request,
161
165
  ...argv,
162
166
  watch: argv.wait && new Promise<void>((resolve) => (cancelWaitCommit = resolve)),
163
167
  })
164
168
  cancelWaitFinalized?.()
165
- if (!commit) return
166
169
  await finalized$
170
+ if (argv.wait)
171
+ logger.info(`[${MessageStatus.Committed}] Commit report accepted on destination chain`)
167
172
  switch (argv.format) {
168
173
  case Format.log:
169
174
  logger.log('commit =', commit)
@@ -175,20 +180,33 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
175
180
  logger.info(JSON.stringify(commit, bigIntReplacer, 2))
176
181
  break
177
182
  }
178
- if (argv.wait) logger.info('Waiting for Receipt (dest):')
183
+ if (argv.wait)
184
+ logger.info(`[${MessageStatus.Blessed}] Waiting for execution on destination chain...`)
179
185
  else logger.info('Receipts (dest):')
180
186
  return commit
181
187
  })()
182
188
 
183
189
  let found = false
184
- for await (const receipt of dest.fetchExecutionReceipts(
190
+ for await (const receipt of dest.getExecutionReceipts({
191
+ ...argv,
185
192
  offRamp,
186
- request,
187
- !argv.wait ? await commit$ : undefined,
188
- { ...argv, watch: argv.wait && ctx.destroy$ },
189
- )) {
193
+ messageId: request.message.messageId,
194
+ sourceChainSelector: request.message.sourceChainSelector,
195
+ startTime: request.tx.timestamp,
196
+ commit: !argv.wait ? await commit$ : undefined,
197
+ watch: argv.wait && ctx.destroy$,
198
+ })) {
190
199
  cancelWaitCommit?.()
191
200
  await commit$
201
+ const status =
202
+ receipt.receipt.state === ExecutionState.Success
203
+ ? MessageStatus.Success
204
+ : MessageStatus.Failed
205
+ const statusMessage =
206
+ receipt.receipt.state === ExecutionState.Success
207
+ ? 'Message executed on destination chain'
208
+ : 'Message execution failed on destination chain'
209
+ logger.info(`[${status}] ${statusMessage}`)
192
210
  switch (argv.format) {
193
211
  case Format.log:
194
212
  logger.log('receipt =', withDateTimestamp(receipt))
@@ -1,4 +1,4 @@
1
- import type { Logger } from '@chainlink/ccip-sdk/src/types.js'
1
+ import type { Logger } from '@chainlink/ccip-sdk/src/index.ts'
2
2
 
3
3
  /** Output format options for CLI commands. */
4
4
  export const Format = {
@@ -11,6 +11,7 @@ import {
11
11
  CCIPError,
12
12
  CCIPErrorCode,
13
13
  ExecutionState,
14
+ getCCIPExplorerUrl,
14
15
  networkInfo,
15
16
  supportedChains,
16
17
  } from '@chainlink/ccip-sdk/src/index.ts'
@@ -42,7 +43,7 @@ export async function selectRequest(
42
43
  hints?: { logIndex?: number },
43
44
  ): Promise<CCIPRequest> {
44
45
  if (hints?.logIndex != null) requests = requests.filter((req) => req.log.index === hints.logIndex)
45
- if (requests.length === 1) return requests[0]
46
+ if (requests.length === 1) return requests[0]!
46
47
  const answer = await select({
47
48
  message: `${requests.length} messageIds found; select one${promptSuffix ? ' ' + promptSuffix : ''}`,
48
49
  choices: [
@@ -66,7 +67,7 @@ tokenTransfers =\t[${req.message.tokenAmounts.map((ta) => ('token' in ta ? ta.to
66
67
  ],
67
68
  })
68
69
  if (answer < 0) throw new CCIPError(CCIPErrorCode.UNKNOWN, 'User requested exit')
69
- return requests[answer]
70
+ return requests[answer]!
70
71
  }
71
72
 
72
73
  /**
@@ -142,7 +143,7 @@ async function formatToken(
142
143
  * @returns Record with indexed keys.
143
144
  */
144
145
  export function formatArray<T>(name: string, values: readonly T[]): Record<string, T> {
145
- if (values.length <= 1) return { [name]: values[0] }
146
+ if (values.length <= 1) return { [name]: values[0]! }
146
147
  return Object.fromEntries(values.map((v, i) => [`${name}[${i}]`, v] as const))
147
148
  }
148
149
 
@@ -296,6 +297,7 @@ export async function prettyRequest(this: Ctx, source: Chain, request: CCIPReque
296
297
  ...('accounts' in request.message ? formatArray('accounts', request.message.accounts) : {}),
297
298
  ...rest,
298
299
  })
300
+ this.logger.info('CCIP Explorer:', getCCIPExplorerUrl('msg', request.message.messageId))
299
301
  }
300
302
 
301
303
  /**
@@ -521,7 +523,7 @@ export function logParsedError(this: Ctx, err: unknown): boolean {
521
523
  export async function parseTokenAmounts(source: Chain, transferTokens: readonly string[]) {
522
524
  return Promise.all(
523
525
  transferTokens.map(async (tokenAmount) => {
524
- const [token, amount_] = tokenAmount.split('=')
526
+ const [token, amount_] = tokenAmount.split('=') as [string, string]
525
527
  const { decimals } = await source.getTokenInfo(token)
526
528
  const amount = parseUnits(amount_, decimals)
527
529
  return { token, amount }
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ import { Format } from './commands/index.ts'
11
11
  util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests
12
12
  // generate:nofail
13
13
  // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'`
14
- const VERSION = '0.92.0-bbb73be'
14
+ const VERSION = '0.93.0-e6b317b'
15
15
  // generate:end
16
16
 
17
17
  const globalOpts = {
@@ -41,7 +41,11 @@ const globalOpts = {
41
41
  page: {
42
42
  type: 'number',
43
43
  describe: 'getLogs page/range size',
44
- default: 10_000,
44
+ },
45
+ 'no-api': {
46
+ type: 'boolean',
47
+ describe: 'Disable CCIP API integration (full decentralization mode)',
48
+ default: false,
45
49
  },
46
50
  } as const
47
51
 
@@ -66,12 +70,12 @@ async function main() {
66
70
  }
67
71
 
68
72
  function wasCalledAsScript() {
69
- const realPath = realpathSync(process.argv[1])
73
+ const realPath = realpathSync(process.argv[1]!)
70
74
  const realPathAsUrl = pathToFileURL(realPath).href
71
75
  return import.meta.url === realPathAsUrl
72
76
  }
73
77
 
74
- if (import.meta?.main || wasCalledAsScript()) {
78
+ if (import.meta.main || wasCalledAsScript()) {
75
79
  const later = setTimeout(() => {}, 2 ** 31 - 1) // keep event-loop alive
76
80
  await main()
77
81
  .catch((err) => {
@@ -99,7 +99,7 @@ export class AptosLedgerSigner /*implements AptosAsyncAccount*/ {
99
99
  */
100
100
  export async function loadAptosWallet({ wallet: walletOpt }: { wallet?: unknown }) {
101
101
  if (typeof walletOpt !== 'string') throw new CCIPArgumentInvalidError('wallet', String(walletOpt))
102
- if ((walletOpt ?? '').startsWith('ledger')) {
102
+ if (walletOpt.startsWith('ledger')) {
103
103
  let derivationPath = walletOpt.split(':')[1]
104
104
  if (!derivationPath) derivationPath = "m/44'/637'/0'/0'/0'"
105
105
  else if (!isNaN(Number(derivationPath))) derivationPath = `m/44'/637'/${derivationPath}'/0'/0'`
@@ -43,7 +43,7 @@ export async function loadEvmWallet(
43
43
  )
44
44
  }
45
45
  if (typeof walletOpt !== 'string') throw new CCIPArgumentInvalidError('wallet', String(walletOpt))
46
- if ((walletOpt ?? '').startsWith('ledger')) {
46
+ if (walletOpt.startsWith('ledger')) {
47
47
  let derivationPath = walletOpt.split(':')[1]
48
48
  if (derivationPath && !isNaN(Number(derivationPath)))
49
49
  derivationPath = `m/44'/60'/${derivationPath}'/0/0`
@@ -6,6 +6,7 @@ import {
6
6
  type ChainGetter,
7
7
  type ChainTransaction,
8
8
  type EVMChain,
9
+ type TONChain,
9
10
  CCIPChainFamilyUnsupportedError,
10
11
  CCIPNetworkFamilyUnsupportedError,
11
12
  CCIPRpcNotFoundError,
@@ -53,11 +54,11 @@ async function collectEndpoints(
53
54
 
54
55
  export function fetchChainsFromRpcs(
55
56
  ctx: Ctx,
56
- argv: { rpcs?: string[]; rpcsFile?: string },
57
+ argv: { rpcs?: string[]; rpcsFile?: string; noApi?: boolean },
57
58
  ): ChainGetter
58
59
  export function fetchChainsFromRpcs(
59
60
  ctx: Ctx,
60
- argv: { rpcs?: string[]; rpcsFile?: string },
61
+ argv: { rpcs?: string[]; rpcsFile?: string; noApi?: boolean },
61
62
  txHash: string,
62
63
  ): [ChainGetter, Promise<[Chain, ChainTransaction]>]
63
64
 
@@ -66,13 +67,13 @@ export function fetchChainsFromRpcs(
66
67
  * If txHash is provided, fetches matching families first and returns [chainGetter, txPromise];
67
68
  * Otherwise, spawns racing URLs for each family asked by `getChain` getter
68
69
  * @param ctx - Context object containing destroy$ promise and logger properties
69
- * @param argv - Options containing rpcs (list) and/or rpcs file
70
+ * @param argv - Options containing rpcs (list), rpcs file and noApi flag
70
71
  * @param txHash - Optional txHash to fetch concurrently; causes the function to return a [ChainGetter, Promise<ChainTransaction>]
71
72
  * @returns a ChainGetter (if txHash was provided), or a tuple of [ChainGetter, Promise<ChainTransaction>]
72
73
  */
73
74
  export function fetchChainsFromRpcs(
74
75
  ctx: Ctx,
75
- argv: { rpcs?: string[]; rpcsFile?: string },
76
+ argv: { rpcs?: string[]; rpcsFile?: string; noApi?: boolean },
76
77
  txHash?: string,
77
78
  ) {
78
79
  const chains: Record<string, Promise<Chain>> = {}
@@ -82,7 +83,7 @@ export function fetchChainsFromRpcs(
82
83
  > = {}
83
84
  const finished: Partial<Record<ChainFamily, boolean>> = {}
84
85
  const initFamily$: Partial<Record<ChainFamily, Promise<unknown>>> = {}
85
- let endpoints$: Promise<Set<string>>
86
+ let endpoints$: Promise<Set<string>> | undefined
86
87
 
87
88
  let txResolve: (value: [Chain, ChainTransaction]) => void, txReject: (reason?: unknown) => void
88
89
  const txResult = new Promise<[Chain, ChainTransaction]>((resolve, reject) => {
@@ -100,7 +101,10 @@ export function fetchChainsFromRpcs(
100
101
  const txs$: Promise<unknown>[] = []
101
102
  let txFound = false
102
103
  for (const url of endpoints) {
103
- const chain$ = C.fromUrl(url, ctx)
104
+ const chain$ = C.fromUrl(url, {
105
+ ...ctx,
106
+ apiClient: argv.noApi ? null : undefined,
107
+ })
104
108
  chains$.push(chain$)
105
109
 
106
110
  void chain$.then(
@@ -116,7 +120,7 @@ export function fetchChainsFromRpcs(
116
120
  chains[chain.network.name] = Promise.resolve(chain)
117
121
  } else if (chain.network.name in chainsCbs) {
118
122
  // chain detected, and there's a "pending request" by getChain: resolve
119
- const [resolve] = chainsCbs[chain.network.name]
123
+ const [resolve] = chainsCbs[chain.network.name]!
120
124
  resolve(chain)
121
125
  }
122
126
  return chain
@@ -152,16 +156,16 @@ export function fetchChainsFromRpcs(
152
156
 
153
157
  const chainGetter = async (idOrSelectorOrName: number | string | bigint): Promise<Chain> => {
154
158
  const network = networkInfo(idOrSelectorOrName)
155
- if (network.name in chains) return chains[network.name]
159
+ if (network.name in chains) return chains[network.name]!
156
160
  if (finished[network.family]) throw new CCIPRpcNotFoundError(network.name)
157
- chains[network.name] = new Promise((resolve, reject) => {
161
+ const c = (chains[network.name] = new Promise((resolve, reject) => {
158
162
  chainsCbs[network.name] = [resolve, reject]
159
- })
160
- void chains[network.name].finally(() => {
163
+ }))
164
+ void c.finally(() => {
161
165
  delete chainsCbs[network.name] // when chain is settled, delete the callbacks
162
166
  })
163
167
  void loadChainFamily(network.family)
164
- return chains[network.name]
168
+ return c
165
169
  }
166
170
 
167
171
  if (!txHash) return chainGetter
@@ -210,8 +214,8 @@ export async function loadChainWallet(chain: Chain, argv: { wallet?: unknown; rp
210
214
  wallet = loadSuiWallet(argv)
211
215
  return [wallet.toSuiAddress(), wallet] as const
212
216
  case ChainFamily.TON:
213
- wallet = await loadTonWallet(argv)
214
- return [wallet.contract.address.toString(), wallet] as const
217
+ wallet = await loadTonWallet((chain as TONChain).provider, argv, chain.network.isTestnet)
218
+ return [wallet.getAddress(), wallet] as const
215
219
  default:
216
220
  // TypeScript exhaustiveness check - this should never be reached
217
221
  throw new CCIPChainFamilyUnsupportedError((chain.network as { family: string }).family)
@@ -1,5 +1,4 @@
1
- import { CCIPArgumentInvalidError } from '@chainlink/ccip-sdk/src/index.ts'
2
- import { bytesToBuffer } from '@chainlink/ccip-sdk/src/utils.ts'
1
+ import { CCIPArgumentInvalidError, bytesToBuffer } from '@chainlink/ccip-sdk/src/index.ts'
3
2
  import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
4
3
 
5
4
  /**
@@ -1,36 +1,106 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
 
3
3
  import {
4
+ type UnsignedTONTx,
4
5
  CCIPArgumentInvalidError,
5
6
  CCIPWalletInvalidError,
6
- } from '@chainlink/ccip-sdk/src/errors/specialized.ts'
7
- import type { TONWallet } from '@chainlink/ccip-sdk/src/ton/types.ts'
7
+ bytesToBuffer,
8
+ } from '@chainlink/ccip-sdk/src/index.ts'
9
+ import HIDTransport from '@ledgerhq/hw-transport-node-hid'
8
10
  import { keyPairFromSecretKey, mnemonicToPrivateKey } from '@ton/crypto'
9
- import { WalletContractV4 } from '@ton/ton'
11
+ import { type TonClient, Address, SendMode, WalletContractV4, internal, toNano } from '@ton/ton'
12
+ import { TonTransport } from '@ton-community/ton-ledger'
10
13
 
11
14
  /**
12
15
  * Loads a TON wallet from the provided options.
16
+ * @param client - TON client instance
13
17
  * @param wallet - wallet options (as passed from yargs argv)
18
+ * @param isTestnet - whether the wallet is on the testnet
14
19
  * @returns Promise to TONWallet instance
15
20
  */
16
- export async function loadTonWallet({
17
- wallet: walletOpt,
18
- }: { wallet?: unknown } = {}): Promise<TONWallet> {
21
+ export async function loadTonWallet(
22
+ client: TonClient,
23
+ { wallet: walletOpt }: { wallet?: unknown } = {},
24
+ isTestnet?: boolean,
25
+ ) {
19
26
  if (typeof walletOpt !== 'string') throw new CCIPWalletInvalidError(walletOpt)
20
-
21
- // Handle mnemonic phrase
22
- if (walletOpt.includes(' ')) {
23
- const mnemonic = walletOpt.trim().split(' ')
24
- const keyPair = await mnemonicToPrivateKey(mnemonic)
27
+ if (walletOpt === 'ledger' || walletOpt.startsWith('ledger:')) {
28
+ const transport = await HIDTransport.default.create()
29
+ const ton = new TonTransport(transport)
30
+ let derivationPath = walletOpt.split(':')[1]
31
+ if (!derivationPath) derivationPath = `44'/607'/${isTestnet ? '1' : '0'}'/0/0/0`
32
+ else if (!isNaN(Number(derivationPath)))
33
+ derivationPath = `44'/607'/${isTestnet ? '1' : '0'}'/0/${derivationPath}/0`
34
+ const match = derivationPath.match(
35
+ /^(?:m\/)?(\d+)'?\/(\d+)'?\/(\d+)'?\/(\d+)'?\/(\d+)'?\/(\d+)'?$/,
36
+ )
37
+ if (!match) throw new CCIPWalletInvalidError(walletOpt)
38
+ const path = match.slice(1).map((x) => parseInt(x))
39
+ const { address, publicKey } = await ton.getAddress(path, {
40
+ chain: 0,
41
+ bounceable: false,
42
+ testOnly: isTestnet,
43
+ })
44
+ console.info('Ledger TON:', address, ', derivationPath:', derivationPath)
25
45
  const contract = WalletContractV4.create({
26
46
  workchain: 0,
27
- publicKey: keyPair.publicKey,
47
+ publicKey,
28
48
  })
29
- return { contract, keyPair }
49
+ const openedWallet = client.open(contract)
50
+ return {
51
+ getAddress: () => address,
52
+ sendTransaction: async ({ value, body, ...args }: UnsignedTONTx) => {
53
+ const seqno = await openedWallet.getSeqno()
54
+ const to = Address.parse(args.to)
55
+ if (!value) {
56
+ const { source_fees } = await client.estimateExternalMessageFee(to, {
57
+ ignoreSignature: true,
58
+ body,
59
+ initCode: null,
60
+ initData: null,
61
+ })
62
+ value =
63
+ BigInt(
64
+ source_fees.storage_fee +
65
+ source_fees.gas_fee +
66
+ source_fees.fwd_fee +
67
+ source_fees.in_fwd_fee,
68
+ ) + toNano('0.0001') // buffer
69
+ }
70
+ const signed = await ton.signTransaction(path, {
71
+ seqno,
72
+ amount: value,
73
+ sendMode: SendMode.IGNORE_ERRORS | SendMode.PAY_GAS_SEPARATELY,
74
+ timeout: Math.floor(Date.now() / 1000 + 60),
75
+ bounce: false,
76
+ ...args,
77
+ to,
78
+ payload: {
79
+ type: 'unsafe',
80
+ message: body,
81
+ },
82
+ })
83
+ await openedWallet.send(signed)
84
+ return seqno
85
+ },
86
+ }
30
87
  }
31
88
 
32
- // Handle hex private key
33
- if (walletOpt.startsWith('0x')) {
89
+ let keyPair
90
+ if (existsSync(walletOpt)) {
91
+ // Handle file path
92
+ const content = readFileSync(walletOpt, 'utf8').trim()
93
+ const secretKey = bytesToBuffer(content)
94
+ if (secretKey.length !== 64) {
95
+ throw new CCIPArgumentInvalidError('wallet', 'Invalid private key in file: must be 64 bytes')
96
+ }
97
+ keyPair = keyPairFromSecretKey(secretKey)
98
+ } else if (walletOpt.includes(' ')) {
99
+ // Handle mnemonic phrase
100
+ const mnemonic = walletOpt.trim().split(' ')
101
+ keyPair = await mnemonicToPrivateKey(mnemonic)
102
+ } else if (walletOpt.startsWith('0x')) {
103
+ // Handle hex private key
34
104
  const secretKey = Buffer.from(walletOpt.slice(2), 'hex')
35
105
  if (secretKey.length === 32) {
36
106
  throw new CCIPArgumentInvalidError(
@@ -41,27 +111,33 @@ export async function loadTonWallet({
41
111
  if (secretKey.length !== 64) {
42
112
  throw new CCIPArgumentInvalidError('wallet', 'must be 64 bytes (or use mnemonic)')
43
113
  }
44
- const keyPair = keyPairFromSecretKey(secretKey)
45
- const contract = WalletContractV4.create({
46
- workchain: 0,
47
- publicKey: keyPair.publicKey,
48
- })
49
- return { contract, keyPair }
114
+ keyPair = keyPairFromSecretKey(secretKey)
50
115
  }
51
116
 
52
- // Handle file path
53
- if (existsSync(walletOpt)) {
54
- const content = readFileSync(walletOpt, 'utf8').trim()
55
- const secretKey = Buffer.from(content.startsWith('0x') ? content.slice(2) : content, 'hex')
56
- if (secretKey.length !== 64) {
57
- throw new CCIPArgumentInvalidError('wallet', 'Invalid private key in file: must be 64 bytes')
58
- }
59
- const keyPair = keyPairFromSecretKey(secretKey)
117
+ if (keyPair) {
60
118
  const contract = WalletContractV4.create({
61
119
  workchain: 0,
62
120
  publicKey: keyPair.publicKey,
63
121
  })
64
- return { contract, keyPair }
122
+ const openedWallet = client.open(contract)
123
+ return {
124
+ getAddress: () => contract.address.toString(),
125
+ sendTransaction: async (args: UnsignedTONTx) => {
126
+ const seqno = await openedWallet.getSeqno()
127
+ const signed = await openedWallet.createTransfer({
128
+ ...keyPair,
129
+ seqno,
130
+ messages: [
131
+ internal({
132
+ value: toNano('0.3'), // TODO: FIXME: estimate proper value for execution costs instead of hardcoding.
133
+ ...args,
134
+ }),
135
+ ],
136
+ })
137
+ await openedWallet.send(signed)
138
+ return seqno
139
+ },
140
+ }
65
141
  }
66
142
 
67
143
  throw new CCIPArgumentInvalidError('wallet', 'Wallet not specified')
package/tsconfig.json CHANGED
@@ -14,6 +14,7 @@
14
14
  "verbatimModuleSyntax": true,
15
15
  "erasableSyntaxOnly": true,
16
16
  "resolveJsonModule": true,
17
- "noImplicitOverride": true
18
- }
17
+ "noImplicitOverride": true,
18
+ "noUncheckedIndexedAccess": true,
19
+ },
19
20
  }