@wormhole-foundation/sdk-sui-ntt 2.0.0-beta-1

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.
@@ -0,0 +1,1260 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SuiNtt = void 0;
4
+ const sdk_definitions_1 = require("@wormhole-foundation/sdk-definitions");
5
+ const sdk_base_1 = require("@wormhole-foundation/sdk-base");
6
+ const sdk_sui_1 = require("@wormhole-foundation/sdk-sui");
7
+ const transactions_1 = require("@mysten/sui/transactions");
8
+ const utils_1 = require("@mysten/sui/utils");
9
+ const SUI_ADDRESSES = {
10
+ Mainnet: {
11
+ coreBridgeStateId: "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
12
+ },
13
+ Testnet: {
14
+ coreBridgeStateId: "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790",
15
+ },
16
+ };
17
+ class SuiNtt {
18
+ contracts;
19
+ coreBridgeStateId;
20
+ // Helper function to extract token type from Sui state object
21
+ static async extractTokenTypeFromSuiState(provider, stateObjectId) {
22
+ const response = await provider.getObject({
23
+ id: stateObjectId,
24
+ options: { showType: true },
25
+ });
26
+ if (!response.data?.type) {
27
+ throw new Error("Failed to fetch state object type");
28
+ }
29
+ // Parse the generic type parameter from the state object type
30
+ // Format: "packageId::ntt::State<TokenType>"
31
+ const objectType = response.data.type;
32
+ const genericStart = objectType.indexOf("<");
33
+ const genericEnd = objectType.lastIndexOf(">");
34
+ if (genericStart === -1 || genericEnd === -1) {
35
+ throw new Error(`No generic type parameter found in state object type: ${objectType}`);
36
+ }
37
+ const tokenType = objectType.substring(genericStart + 1, genericEnd);
38
+ return tokenType;
39
+ }
40
+ // Helper method to fetch and validate NTT state object with proper typing
41
+ async getNttState() {
42
+ const response = await this.provider.getObject({
43
+ id: this.contracts.ntt["manager"],
44
+ options: { showContent: true },
45
+ });
46
+ if (!response.data?.content ||
47
+ response.data.content.dataType !== "moveObject") {
48
+ throw new Error("Failed to fetch NTT state object");
49
+ }
50
+ const content = response.data.content;
51
+ return content.fields;
52
+ }
53
+ // Helper method to fetch and validate any Sui object with proper typing
54
+ async getSuiObject(objectId, errorMessage) {
55
+ const response = await this.provider.getObject({
56
+ id: objectId,
57
+ options: { showContent: true },
58
+ });
59
+ if (!response.data?.content ||
60
+ response.data.content.dataType !== "moveObject") {
61
+ throw new Error(errorMessage || `Failed to fetch object ${objectId}`);
62
+ }
63
+ return response.data.content;
64
+ }
65
+ network;
66
+ chain;
67
+ provider;
68
+ adminCapId; // Cached NTT AdminCap object ID
69
+ packageId; // Cached NTT package ID for move calls
70
+ constructor(network, chain, provider, contracts) {
71
+ this.contracts = contracts;
72
+ if (!contracts.ntt) {
73
+ throw new Error("NTT contracts not found");
74
+ }
75
+ if (!contracts.coreBridge) {
76
+ throw new Error("Core Bridge contract not found");
77
+ }
78
+ this.network = network;
79
+ this.chain = chain;
80
+ this.provider = provider;
81
+ this.coreBridgeStateId =
82
+ SUI_ADDRESSES[network].coreBridgeStateId;
83
+ }
84
+ static async fromRpc(provider, config) {
85
+ const [network, chain] = await sdk_sui_1.SuiPlatform.chainFromRpc(provider);
86
+ const conf = config[chain];
87
+ if (conf.network !== network)
88
+ throw new Error(`Network mismatch: ${conf.network} != ${network}`);
89
+ if (!("ntt" in conf.contracts))
90
+ throw new Error("Ntt contracts not found");
91
+ const ntt = conf.contracts["ntt"];
92
+ return new SuiNtt(network, chain, provider, {
93
+ ...conf.contracts,
94
+ ntt,
95
+ });
96
+ }
97
+ // State & Configuration Methods
98
+ async getMode() {
99
+ const state = await this.getNttState();
100
+ const modeField = state.mode;
101
+ // Mode is an enum with a variant field: { variant: "Locking" } or { variant: "Burning" }
102
+ if (modeField.variant === "Locking") {
103
+ return "locking";
104
+ }
105
+ else if (modeField.variant === "Burning") {
106
+ return "burning";
107
+ }
108
+ throw new Error("Invalid mode in NTT state");
109
+ }
110
+ async isPaused() {
111
+ const state = await this.getNttState();
112
+ return state.paused;
113
+ }
114
+ async getAdminCapId() {
115
+ if (this.adminCapId) {
116
+ return this.adminCapId;
117
+ }
118
+ const state = await this.getNttState();
119
+ if (!state.admin_cap_id) {
120
+ throw new Error("AdminCap ID not found in NTT state");
121
+ }
122
+ this.adminCapId = state.admin_cap_id;
123
+ return this.adminCapId;
124
+ }
125
+ async getPackageId() {
126
+ if (this.packageId) {
127
+ return this.packageId;
128
+ }
129
+ const packageIdFromType = await this.getPackageIdFromObject(this.contracts.ntt["manager"]);
130
+ this.packageId = packageIdFromType;
131
+ return this.packageId;
132
+ }
133
+ async getPackageIdFromObject(objectId) {
134
+ // TODO: replace with getOriginalPackageId from our sdk?
135
+ const object = await this.getSuiObject(objectId, "Failed to fetch state object");
136
+ // The package ID can be inferred from the object type
137
+ const objectType = object.type;
138
+ // Object type format: "packageId::module::Type<...>"
139
+ const packageId = objectType.split("::")[0];
140
+ if (!packageId || !packageId.startsWith("0x")) {
141
+ throw new Error("Could not extract package ID from state object type");
142
+ }
143
+ // If we find an upgrade cap id, fetch it and grab the latest package id from there
144
+ if (object.fields.upgrade_cap_id) {
145
+ const upgradeCap = await this.getSuiObject(object.fields.upgrade_cap_id, "Failed to fetch upgrade cap object");
146
+ return upgradeCap.fields.cap.fields.package;
147
+ }
148
+ return packageId;
149
+ }
150
+ async getOwner() {
151
+ const adminCapId = await this.getAdminCapId();
152
+ try {
153
+ const adminCap = await this.provider.getObject({
154
+ id: adminCapId,
155
+ options: {
156
+ showOwner: true,
157
+ },
158
+ });
159
+ if (!adminCap.data?.owner) {
160
+ throw new Error("Could not fetch AdminCap owner information");
161
+ }
162
+ // Extract owner address from the owner field
163
+ let ownerAddress;
164
+ if (typeof adminCap.data.owner === "object" &&
165
+ "AddressOwner" in adminCap.data.owner) {
166
+ ownerAddress = adminCap.data.owner.AddressOwner;
167
+ }
168
+ else if (typeof adminCap.data.owner === "string") {
169
+ ownerAddress = adminCap.data.owner;
170
+ }
171
+ else {
172
+ throw new Error(`AdminCap has unexpected owner type: ${JSON.stringify(adminCap.data.owner)}`);
173
+ }
174
+ return ownerAddress;
175
+ }
176
+ catch (error) {
177
+ throw new Error(`Failed to get AdminCap owner: ${error}`);
178
+ }
179
+ }
180
+ async getPauser() {
181
+ // TODO
182
+ return null;
183
+ }
184
+ async getThreshold() {
185
+ const state = await this.getNttState();
186
+ return parseInt(state.threshold, 10);
187
+ }
188
+ async *setThreshold(threshold, payer) {
189
+ const adminCapId = await this.getAdminCapId();
190
+ const packageId = await this.getPackageId();
191
+ // Build transaction to set threshold
192
+ const txb = new transactions_1.Transaction();
193
+ txb.moveCall({
194
+ target: `${packageId}::state::set_threshold`,
195
+ typeArguments: [this.contracts.ntt["token"]], // Use the token type from contracts
196
+ arguments: [
197
+ txb.object(adminCapId), // AdminCap
198
+ txb.object(this.contracts.ntt["manager"]), // NTT state
199
+ txb.pure.u8(threshold), // New threshold
200
+ ],
201
+ });
202
+ const unsignedTx = new sdk_sui_1.SuiUnsignedTransaction(txb, this.network, this.chain, "Set Threshold");
203
+ yield unsignedTx;
204
+ }
205
+ async getTokenDecimals() {
206
+ const coinMetadata = await this.provider.getCoinMetadata({
207
+ coinType: this.contracts.ntt["token"],
208
+ });
209
+ if (!coinMetadata?.decimals) {
210
+ throw new Error(`CoinMetadata not found for ${this.contracts.ntt["token"]}`);
211
+ }
212
+ return coinMetadata.decimals;
213
+ }
214
+ async getCustodyAddress() {
215
+ // In Sui, custody is managed by the State object itself
216
+ // Return the state object ID as the custody address
217
+ return this.contracts.ntt["manager"];
218
+ }
219
+ // Admin Methods
220
+ async *pause() {
221
+ const adminCapId = await this.getAdminCapId();
222
+ const packageId = await this.getPackageId();
223
+ // Build transaction to pause the contract
224
+ const txb = new transactions_1.Transaction();
225
+ txb.moveCall({
226
+ target: `${packageId}::state::pause`,
227
+ typeArguments: [this.contracts.ntt["token"]], // Use the token type from contracts
228
+ arguments: [
229
+ txb.object(adminCapId), // AdminCap
230
+ txb.object(this.contracts.ntt["manager"]), // NTT state
231
+ ],
232
+ });
233
+ const unsignedTx = new sdk_sui_1.SuiUnsignedTransaction(txb, this.network, this.chain, "Pause Contract");
234
+ yield unsignedTx;
235
+ }
236
+ async *unpause() {
237
+ const adminCapId = await this.getAdminCapId();
238
+ const packageId = await this.getPackageId();
239
+ // Build transaction to unpause the contract
240
+ const txb = new transactions_1.Transaction();
241
+ txb.moveCall({
242
+ target: `${packageId}::state::unpause`,
243
+ typeArguments: [this.contracts.ntt["token"]], // Use the token type from contracts
244
+ arguments: [
245
+ txb.object(adminCapId), // AdminCap
246
+ txb.object(this.contracts.ntt["manager"]), // NTT state
247
+ ],
248
+ });
249
+ const unsignedTx = new sdk_sui_1.SuiUnsignedTransaction(txb, this.network, this.chain, "Unpause Contract");
250
+ yield unsignedTx;
251
+ }
252
+ async *setOwner(newOwner, payer) {
253
+ throw new Error("Not implemented");
254
+ }
255
+ async *setPauser(newPauser, payer) {
256
+ throw new Error("Not implemented");
257
+ }
258
+ // Peer Management
259
+ async *setPeer(peer, tokenDecimals, inboundLimit, payer) {
260
+ const adminCapId = await this.getAdminCapId();
261
+ const packageId = await this.getPackageId();
262
+ // Build transaction to set peer
263
+ const txb = new transactions_1.Transaction();
264
+ // Convert chain to wormhole chain ID
265
+ const wormholeChainId = (0, sdk_base_1.chainToChainId)(peer.chain);
266
+ // Convert peer address to ExternalAddress format
267
+ const peerAddressBytes = peer.address.toUint8Array();
268
+ try {
269
+ // Query the wormhole package ID from the state object
270
+ const wormholePackageId = await this.getPackageIdFromObject(this.contracts.coreBridge);
271
+ const bytes32 = txb.moveCall({
272
+ target: `${wormholePackageId}::bytes32::from_bytes`,
273
+ arguments: [txb.pure.vector("u8", peerAddressBytes)],
274
+ });
275
+ const externalAddress = txb.moveCall({
276
+ target: `${wormholePackageId}::external_address::new`,
277
+ arguments: [bytes32],
278
+ });
279
+ txb.moveCall({
280
+ target: `${packageId}::state::set_peer`,
281
+ typeArguments: [this.contracts.ntt["token"]], // Use the token type from contracts
282
+ arguments: [
283
+ txb.object(adminCapId), // AdminCap
284
+ txb.object(this.contracts.ntt["manager"]), // NTT state
285
+ txb.pure.u16(wormholeChainId), // Chain ID
286
+ externalAddress, // ExternalAddress object (properly created)
287
+ txb.pure.u8(tokenDecimals), // Token decimals
288
+ txb.pure.u64(inboundLimit.toString()), // Inbound limit
289
+ txb.object(utils_1.SUI_CLOCK_OBJECT_ID),
290
+ ],
291
+ });
292
+ }
293
+ catch (error) {
294
+ throw new Error(`Failed to create setPeer transaction: ${error instanceof Error ? error.message : String(error)}`);
295
+ }
296
+ const unsignedTx = new sdk_sui_1.SuiUnsignedTransaction(txb, this.network, this.chain, "Set Peer");
297
+ yield unsignedTx;
298
+ }
299
+ async getPeer(chain) {
300
+ const state = await this.provider.getObject({
301
+ id: this.contracts.ntt["manager"],
302
+ options: {
303
+ showContent: true,
304
+ },
305
+ });
306
+ if (!state.data?.content || state.data.content.dataType !== "moveObject") {
307
+ throw new Error("Failed to fetch NTT state object");
308
+ }
309
+ const fields = state.data.content.fields;
310
+ const peersTable = fields.peers;
311
+ // Convert chain name to chain ID and look up in peers table
312
+ const chainId = (0, sdk_base_1.chainToChainId)(chain);
313
+ try {
314
+ // Query the dynamic field for this chain ID in the peers table
315
+ const peerField = await this.provider.getDynamicFieldObject({
316
+ parentId: peersTable.fields.id.id,
317
+ name: {
318
+ type: "u16",
319
+ value: chainId,
320
+ },
321
+ });
322
+ if (!peerField.data?.content ||
323
+ peerField.data.content.dataType !== "moveObject") {
324
+ // Peer not found for this chain
325
+ return null;
326
+ }
327
+ const peerData = peerField.data.content.fields.value
328
+ .fields;
329
+ // Extract address bytes from ExternalAddress
330
+ const externalAddress = peerData.address;
331
+ const addressBytes = externalAddress.fields.value.fields.data;
332
+ // Convert address bytes to ChainAddress
333
+ // The address bytes are stored as a vector of u8, we need to convert to the appropriate format
334
+ const addressUint8Array = new Uint8Array(addressBytes);
335
+ const chainAddress = {
336
+ chain: chain,
337
+ address: (0, sdk_definitions_1.toUniversal)(chain, addressUint8Array),
338
+ };
339
+ // Extract token decimals
340
+ const tokenDecimals = parseInt(peerData.token_decimals, 10);
341
+ // Extract inbound limit from rate limit state
342
+ const inboundRateLimit = peerData.inbound_rate_limit.fields;
343
+ const inboundLimit = BigInt(inboundRateLimit.limit);
344
+ return {
345
+ address: chainAddress,
346
+ tokenDecimals: tokenDecimals,
347
+ inboundLimit: inboundLimit,
348
+ };
349
+ }
350
+ catch (error) {
351
+ // If we get an error (like object not found), the peer doesn't exist
352
+ console.error(error);
353
+ return null;
354
+ }
355
+ }
356
+ async *setTransceiverPeer(ix, peer, payer) {
357
+ // For now, only support index 0 which is the wormhole transceiver
358
+ if (ix !== 0) {
359
+ throw new Error("Only transceiver index 0 (wormhole) is currently supported");
360
+ }
361
+ const wormholeTransceiverStateId = this.contracts.ntt["transceiver"]?.["wormhole"];
362
+ if (!wormholeTransceiverStateId) {
363
+ throw new Error("Wormhole transceiver not found in contracts");
364
+ }
365
+ // Get the transceiver package ID and admin cap ID
366
+ const transceiverPackageId = await this.getPackageIdFromObject(wormholeTransceiverStateId);
367
+ // Query the transceiver admin cap ID from the state object
368
+ const transceiverState = await this.provider.getObject({
369
+ id: wormholeTransceiverStateId,
370
+ options: { showContent: true },
371
+ });
372
+ if (!transceiverState.data?.content ||
373
+ transceiverState.data.content.dataType !== "moveObject") {
374
+ throw new Error("Failed to fetch transceiver state object");
375
+ }
376
+ const transceiverFields = transceiverState.data.content
377
+ .fields;
378
+ const transceiverAdminCapId = transceiverFields.admin_cap_id;
379
+ // Build transaction to set transceiver peer
380
+ const txb = new transactions_1.Transaction();
381
+ // Convert chain to wormhole chain ID
382
+ const chainId = (0, sdk_base_1.chainToChainId)(peer.chain);
383
+ // Convert peer address to ExternalAddress format
384
+ const peerAddressBytes = peer.address.toUint8Array();
385
+ // Convert Uint8Array to regular array
386
+ const peerAddressBytesArray = Array.from(peerAddressBytes);
387
+ try {
388
+ // Query the wormhole package ID from the core bridge state object
389
+ const wormholePackageId = await this.getPackageIdFromObject(this.contracts.coreBridge);
390
+ // Create ExternalAddress from the peer address bytes
391
+ const bytes32 = txb.moveCall({
392
+ target: `${wormholePackageId}::bytes32::from_bytes`,
393
+ arguments: [txb.pure.vector("u8", peerAddressBytesArray)],
394
+ });
395
+ const externalAddress = txb.moveCall({
396
+ target: `${wormholePackageId}::external_address::new`,
397
+ arguments: [bytes32],
398
+ });
399
+ // Get the NTT package ID for the manager auth type
400
+ const nttPackageId = await this.getPackageId();
401
+ // Call the transceiver's set_peer function which returns a MessageTicket
402
+ const messageTicket = txb.moveCall({
403
+ target: `${transceiverPackageId}::wormhole_transceiver::set_peer`,
404
+ typeArguments: [`${nttPackageId}::auth::ManagerAuth`], // Fully qualified manager auth type
405
+ arguments: [
406
+ txb.object(transceiverAdminCapId), // Transceiver AdminCap
407
+ txb.object(wormholeTransceiverStateId), // Transceiver state
408
+ txb.pure.u16(chainId), // Chain ID
409
+ externalAddress, // ExternalAddress object
410
+ ],
411
+ });
412
+ // Get the wormhole state ID from the core bridge
413
+ const wormholeStateId = this.contracts.coreBridge;
414
+ // Create a zero coin for the message fee (0 SUI)
415
+ const [messageFee] = txb.splitCoins(txb.gas, [0]);
416
+ // Publish the message to emit the WormholeMessage event
417
+ txb.moveCall({
418
+ target: `${wormholePackageId}::publish_message::publish_message`,
419
+ arguments: [
420
+ txb.object(wormholeStateId), // Wormhole state
421
+ messageFee, // Message fee (0 SUI)
422
+ messageTicket, // MessageTicket from set_peer
423
+ txb.object(utils_1.SUI_CLOCK_OBJECT_ID),
424
+ ],
425
+ });
426
+ }
427
+ catch (error) {
428
+ throw new Error(`Failed to create setTransceiverPeer transaction: ${error instanceof Error ? error.message : String(error)}`);
429
+ }
430
+ const unsignedTx = new sdk_sui_1.SuiUnsignedTransaction(txb, this.network, this.chain, "Set Transceiver Peer");
431
+ yield unsignedTx;
432
+ }
433
+ // Transfer Methods
434
+ async *transfer(sender, amount, destination, options) {
435
+ const packageId = await this.getPackageId();
436
+ // Build the transaction for Sui transfer
437
+ const txb = new transactions_1.Transaction();
438
+ // Convert destination chain to wormhole chain ID
439
+ const destinationChainId = (0, sdk_base_1.chainToChainId)(destination.chain);
440
+ // Convert destination address to bytes
441
+ // TODO: do this address handling stuff properly
442
+ let destinationAddressBytes;
443
+ try {
444
+ if (typeof destination.address.toUint8Array === "function") {
445
+ destinationAddressBytes = destination.address.toUint8Array();
446
+ }
447
+ else if (typeof destination.address.toUniversalAddress === "function") {
448
+ const universalAddr = destination.address.toUniversalAddress();
449
+ if (!universalAddr) {
450
+ throw new Error("toUniversalAddress() returned null or undefined");
451
+ }
452
+ destinationAddressBytes = universalAddr.toUint8Array();
453
+ }
454
+ else {
455
+ throw new Error(`destination.address does not have expected methods. Type: ${typeof destination.address}`);
456
+ }
457
+ }
458
+ catch (error) {
459
+ throw new Error(`Failed to convert destination address to bytes: ${error instanceof Error ? error.message : String(error)}`);
460
+ }
461
+ // Query the CoinMetadata object ID dynamically
462
+ let coinMetadataId;
463
+ try {
464
+ const coinMetadata = await this.provider.getCoinMetadata({
465
+ coinType: this.contracts.ntt["token"],
466
+ });
467
+ if (!coinMetadata?.id) {
468
+ throw new Error(`CoinMetadata not found for ${this.contracts.ntt["token"]}`);
469
+ }
470
+ coinMetadataId = coinMetadata.id;
471
+ }
472
+ catch (error) {
473
+ throw new Error(`Failed to get CoinMetadata for ${this.contracts.ntt["token"]}: ${error instanceof Error ? error.message : String(error)}`);
474
+ }
475
+ // 1. Split coins from gas to get the required amount
476
+ const coin = txb.splitCoins(txb.gas, [amount.toString()]);
477
+ // 2. Create VersionGated object
478
+ const versionGated = txb.moveCall({
479
+ target: `${packageId}::upgrades::new_version_gated`,
480
+ arguments: [],
481
+ });
482
+ // Since prepare_transfer returns a tuple (TransferTicket, Balance), we need to properly
483
+ // extract the individual elements. In Sui's transaction builder, we can access tuple elements
484
+ // using array-like indexing on the result.
485
+ const prepareResult = txb.moveCall({
486
+ target: `${packageId}::ntt::prepare_transfer`,
487
+ typeArguments: [this.contracts.ntt["token"]],
488
+ arguments: [
489
+ txb.object(this.contracts.ntt["manager"]), // state
490
+ coin, // coins
491
+ txb.object(coinMetadataId), // coin_meta
492
+ txb.pure.u16(destinationChainId), // recipient_chain
493
+ txb.pure.vector("u8", Array.from(destinationAddressBytes)), // recipient (as vector<u8>)
494
+ txb.pure.option("vector<u8>", null), // payload (no payload for now)
495
+ txb.pure.bool(options.queue || false), // should_queue
496
+ ],
497
+ });
498
+ // Extract the TransferTicket (first element) from the tuple result
499
+ // Use type assertions to bypass TypeScript's strict checking for tuple access
500
+ const ticket = prepareResult[0];
501
+ // const dust = (prepareResult)[1]; // Not using dust for now
502
+ // Now call transfer_tx_sender with just the ticket
503
+ txb.moveCall({
504
+ target: `${packageId}::ntt::transfer_tx_sender`,
505
+ typeArguments: [this.contracts.ntt["token"]],
506
+ arguments: [
507
+ txb.object(this.contracts.ntt["manager"]), // state (mutable)
508
+ versionGated, // version_gated
509
+ txb.object(coinMetadataId), // coin_meta
510
+ ticket, // Just the TransferTicket from the tuple
511
+ txb.object(utils_1.SUI_CLOCK_OBJECT_ID),
512
+ ],
513
+ });
514
+ // Note: For simplicity, we're not handling the dust balance for now
515
+ // In a production implementation, you would want to handle the dust by:
516
+ // - Converting the Balance to a Coin using coin::from_balance
517
+ // - Transferring it back to the sender or handling it appropriately
518
+ const unsignedTx = new sdk_sui_1.SuiUnsignedTransaction(txb, this.network, this.chain, "NTT Transfer");
519
+ yield unsignedTx;
520
+ }
521
+ async *redeem(attestations, payer) {
522
+ // Check if paused
523
+ const isPaused = await this.isPaused();
524
+ if (isPaused) {
525
+ throw new Error("Contract is paused");
526
+ }
527
+ if (attestations.length === 0) {
528
+ throw new Error("No attestations provided");
529
+ }
530
+ const packageId = await this.getPackageId();
531
+ // Get coin metadata
532
+ const coinMetadata = await this.provider.getCoinMetadata({
533
+ coinType: this.contracts.ntt["token"],
534
+ });
535
+ if (!coinMetadata?.id) {
536
+ throw new Error(`CoinMetadata not found for ${this.contracts.ntt["token"]}`);
537
+ }
538
+ // Process each attestation separately (like Circle Bridge and Solana NTT)
539
+ for (const attestation of attestations) {
540
+ // Build transaction for this attestation
541
+ const txb = new transactions_1.Transaction();
542
+ // Create VersionGated object
543
+ const versionGated = txb.moveCall({
544
+ target: `${packageId}::upgrades::new_version_gated`,
545
+ arguments: [],
546
+ });
547
+ // Add redeem calls for this attestation
548
+ await this.addRedeemCall(txb, attestation, packageId, versionGated, coinMetadata.id);
549
+ const unsignedTx = new sdk_sui_1.SuiUnsignedTransaction(txb, this.network, this.chain, "Redeem NTT Transfer");
550
+ yield unsignedTx;
551
+ }
552
+ }
553
+ async quoteDeliveryPrice(destination, options) {
554
+ throw new Error("Not implemented");
555
+ }
556
+ async isRelayingAvailable(destination) {
557
+ // We don't have a quoter in Sui NTT, so relaying is currently not available
558
+ return false;
559
+ }
560
+ // Rate Limiting
561
+ async getCurrentOutboundCapacity() {
562
+ const state = await this.provider.getObject({
563
+ id: this.contracts.ntt["manager"],
564
+ options: {
565
+ showContent: true,
566
+ },
567
+ });
568
+ if (!state.data?.content || state.data.content.dataType !== "moveObject") {
569
+ throw new Error("Failed to fetch NTT state object");
570
+ }
571
+ const fields = state.data.content.fields;
572
+ const outboxRateLimit = fields.outbox.fields.rate_limit.fields;
573
+ // Get current timestamp (this would ideally come from Clock object)
574
+ const currentTime = Date.now();
575
+ // Calculate capacity using the rate limit formula
576
+ // This is a simplified version - in practice we'd need the exact formula from Move
577
+ const limit = BigInt(outboxRateLimit.limit);
578
+ const capacityAtLastTx = BigInt(outboxRateLimit.capacity_at_last_tx);
579
+ const lastTxTimestamp = BigInt(outboxRateLimit.last_tx_timestamp);
580
+ // Simplified capacity calculation
581
+ const timePassed = BigInt(currentTime) - lastTxTimestamp;
582
+ const rateLimitDuration = BigInt(24 * 60 * 60 * 1000); // 24 hours in ms
583
+ const additionalCapacity = (timePassed * limit) / rateLimitDuration;
584
+ const currentCapacity = capacityAtLastTx + additionalCapacity;
585
+ return currentCapacity > limit ? limit : currentCapacity;
586
+ }
587
+ async getOutboundLimit() {
588
+ const state = await this.provider.getObject({
589
+ id: this.contracts.ntt["manager"],
590
+ options: {
591
+ showContent: true,
592
+ },
593
+ });
594
+ if (!state.data?.content || state.data.content.dataType !== "moveObject") {
595
+ throw new Error("Failed to fetch NTT state object");
596
+ }
597
+ const fields = state.data.content.fields;
598
+ const outboxRateLimit = fields.outbox.fields.rate_limit.fields;
599
+ return BigInt(outboxRateLimit.limit);
600
+ }
601
+ async *setOutboundLimit(limit, payer) {
602
+ const adminCapId = await this.getAdminCapId();
603
+ if (!adminCapId) {
604
+ throw new Error("AdminCap ID not found");
605
+ }
606
+ const packageId = await this.getPackageId();
607
+ if (!packageId) {
608
+ throw new Error("Package ID not found");
609
+ }
610
+ // Build the transaction to set the outbound rate limit
611
+ const txb = new transactions_1.Transaction();
612
+ txb.moveCall({
613
+ target: `${packageId}::state::set_outbound_rate_limit`,
614
+ typeArguments: [this.contracts.ntt["token"]], // Use the token type from contracts
615
+ arguments: [
616
+ txb.object(adminCapId), // AdminCap
617
+ txb.object(this.contracts.ntt["manager"]), // NTT state
618
+ txb.pure.u64(limit.toString()), // New outbound limit
619
+ txb.object(utils_1.SUI_CLOCK_OBJECT_ID), // Clock object
620
+ ],
621
+ });
622
+ const unsignedTx = new sdk_sui_1.SuiUnsignedTransaction(txb, this.network, this.chain, "Set Outbound Limit");
623
+ yield unsignedTx;
624
+ }
625
+ async getCurrentInboundCapacity(fromChain) {
626
+ const state = await this.provider.getObject({
627
+ id: this.contracts.ntt["manager"],
628
+ options: {
629
+ showContent: true,
630
+ },
631
+ });
632
+ if (!state.data?.content || state.data.content.dataType !== "moveObject") {
633
+ throw new Error("Failed to fetch NTT state object");
634
+ }
635
+ const fields = state.data.content.fields;
636
+ const peersTable = fields.peers;
637
+ // Convert chain to wormhole chain ID
638
+ const chainId = (0, sdk_base_1.chainToChainId)(fromChain);
639
+ try {
640
+ // Query the dynamic field for this chain ID in the peers table
641
+ const peerField = await this.provider.getDynamicFieldObject({
642
+ parentId: peersTable.fields.id.id,
643
+ name: {
644
+ type: "u16",
645
+ value: chainId,
646
+ },
647
+ });
648
+ if (!peerField.data?.content ||
649
+ peerField.data.content.dataType !== "moveObject") {
650
+ throw new Error(`No peer found for chain ${fromChain}`);
651
+ }
652
+ const peerData = peerField.data.content.fields.value
653
+ .fields;
654
+ // Extract inbound rate limit state
655
+ const inboundRateLimit = peerData.inbound_rate_limit.fields;
656
+ // Get current timestamp (this would ideally come from Clock object)
657
+ const currentTime = Date.now();
658
+ // Calculate capacity using the rate limit formula
659
+ const limit = BigInt(inboundRateLimit.limit);
660
+ const capacityAtLastTx = BigInt(inboundRateLimit.capacity_at_last_tx);
661
+ const lastTxTimestamp = BigInt(inboundRateLimit.last_tx_timestamp);
662
+ // Simplified capacity calculation (same formula as outbound)
663
+ const timePassed = BigInt(currentTime) - lastTxTimestamp;
664
+ const rateLimitDuration = BigInt(24 * 60 * 60 * 1000); // 24 hours in ms
665
+ const additionalCapacity = (timePassed * limit) / rateLimitDuration;
666
+ const currentCapacity = capacityAtLastTx + additionalCapacity;
667
+ return currentCapacity > limit ? limit : currentCapacity;
668
+ }
669
+ catch (error) {
670
+ throw new Error(`Failed to get inbound capacity for chain ${fromChain}: ${error instanceof Error ? error.message : String(error)}`);
671
+ }
672
+ }
673
+ async getInboundLimit(fromChain) {
674
+ const peer = await this.getPeer(fromChain);
675
+ if (!peer) {
676
+ throw new Error(`No peer found for chain ${fromChain}. Set up the peer first using setPeer.`);
677
+ }
678
+ return peer.inboundLimit;
679
+ }
680
+ async *setInboundLimit(fromChain, limit, payer) {
681
+ const adminCapId = await this.getAdminCapId();
682
+ const packageId = await this.getPackageId();
683
+ // Get the existing peer to preserve its address and token decimals
684
+ const existingPeer = await this.getPeer(fromChain);
685
+ if (!existingPeer) {
686
+ throw new Error(`No peer found for chain ${fromChain}. Set up the peer first using setPeer.`);
687
+ }
688
+ // Build transaction to set inbound limit by updating the existing peer
689
+ const txb = new transactions_1.Transaction();
690
+ // Convert chain to wormhole chain ID
691
+ const wormholeChainId = (0, sdk_base_1.chainToChainId)(fromChain);
692
+ // Convert peer address to ExternalAddress format (reuse the existing address)
693
+ const peerAddressBytes = existingPeer.address.address.toUint8Array();
694
+ // Convert Uint8Array to regular array
695
+ const peerAddressBytesArray = Array.from(peerAddressBytes);
696
+ try {
697
+ // Query the wormhole package ID from the state object
698
+ const wormholePackageId = await this.getPackageIdFromObject(this.contracts.coreBridge);
699
+ // Create ExternalAddress from the peer address bytes
700
+ const bytes32 = txb.moveCall({
701
+ target: `${wormholePackageId}::bytes32::from_bytes`,
702
+ arguments: [txb.pure.vector("u8", peerAddressBytesArray)],
703
+ });
704
+ const externalAddress = txb.moveCall({
705
+ target: `${wormholePackageId}::external_address::new`,
706
+ arguments: [bytes32],
707
+ });
708
+ // Call set_peer with the existing address and token decimals but new inbound limit
709
+ txb.moveCall({
710
+ target: `${packageId}::state::set_peer`,
711
+ typeArguments: [this.contracts.ntt["token"]], // Use the token type from contracts
712
+ arguments: [
713
+ txb.object(adminCapId), // AdminCap
714
+ txb.object(this.contracts.ntt["manager"]), // NTT state
715
+ txb.pure.u16(wormholeChainId), // Chain ID
716
+ externalAddress, // ExternalAddress object (reuse existing address)
717
+ txb.pure.u8(existingPeer.tokenDecimals), // Keep existing token decimals
718
+ txb.pure.u64(limit.toString()), // New inbound limit
719
+ txb.object(utils_1.SUI_CLOCK_OBJECT_ID),
720
+ ],
721
+ });
722
+ }
723
+ catch (error) {
724
+ throw new Error(`Failed to create setInboundLimit transaction: ${error instanceof Error ? error.message : String(error)}`);
725
+ }
726
+ const unsignedTx = new sdk_sui_1.SuiUnsignedTransaction(txb, this.network, this.chain, "Set Inbound Limit");
727
+ yield unsignedTx;
728
+ }
729
+ async getRateLimitDuration() {
730
+ // Rate limit duration is a constant in the Move contract
731
+ // 24 hours in milliseconds
732
+ return BigInt(24 * 60 * 60 * 1000);
733
+ }
734
+ // Transfer Status
735
+ async getIsApproved(attestation) {
736
+ const inboxItem = await this.getInboxItem(attestation);
737
+ if (!inboxItem) {
738
+ return false;
739
+ }
740
+ const { inboxItemFields, threshold } = inboxItem;
741
+ // votes is a Bitmap object, not a simple integer
742
+ // We need to count the number of set bits in the bitmap
743
+ const votesBitmap = inboxItemFields.votes;
744
+ let voteCount = 0;
745
+ if (votesBitmap?.fields?.bitmap) {
746
+ // The bitmap is stored as a string representation of a number
747
+ // Count the number of set bits (votes)
748
+ voteCount = this.countSetBits(parseInt(votesBitmap.fields.bitmap));
749
+ }
750
+ // Check if votes >= threshold
751
+ return voteCount >= threshold;
752
+ }
753
+ async getIsExecuted(attestation) {
754
+ const releaseStatus = await this.getTransferReleaseStatus(attestation);
755
+ // Check if release_status is Released
756
+ // In Move, this would be an enum variant, so we check for the Released variant
757
+ return releaseStatus?.variant === "Released";
758
+ }
759
+ async getIsTransferInboundQueued(attestation) {
760
+ const releaseStatus = await this.getTransferReleaseStatus(attestation);
761
+ // Check if release_status is ReleaseAfter(timestamp)
762
+ return releaseStatus?.variant === "ReleaseAfter";
763
+ }
764
+ async getInboundQueuedTransfer(fromChain, transceiverMessage) {
765
+ // Create an attestation object from the transceiver message
766
+ const attestation = {
767
+ emitterChain: fromChain,
768
+ hash: transceiverMessage.id,
769
+ };
770
+ // Get the release status
771
+ const releaseStatus = await this.getTransferReleaseStatus(attestation);
772
+ // Check if it's queued (ReleaseAfter)
773
+ if (releaseStatus?.variant !== "ReleaseAfter") {
774
+ return null;
775
+ }
776
+ // The timestamp should be in the fields of the enum variant
777
+ // TODO Not sure if this is the correct way to get the timestamp
778
+ // I wasn't able to get the exact field name while debugging live
779
+ const releaseTimestamp = parseInt(releaseStatus.fields?.[0]);
780
+ // Get the full inbox item to access the transfer data
781
+ const inboxItem = await this.getInboxItem(attestation);
782
+ if (!inboxItem) {
783
+ return null;
784
+ }
785
+ const { inboxItemFields } = inboxItem;
786
+ // Parse recipient and amount from inbox item data
787
+ // The data field should contain the transfer details
788
+ const transferData = inboxItemFields.data || {};
789
+ // Try to get recipient address - prefer message payload, fallback to inbox data
790
+ let recipientAddress;
791
+ if (transceiverMessage.payload?.recipientAddress) {
792
+ recipientAddress = transceiverMessage.payload.recipientAddress;
793
+ }
794
+ else if (transferData.recipient) {
795
+ recipientAddress = (0, sdk_definitions_1.toUniversal)(this.chain, transferData.recipient);
796
+ }
797
+ else if (transferData.recipient_address) {
798
+ recipientAddress = (0, sdk_definitions_1.toUniversal)(this.chain, transferData.recipient_address);
799
+ }
800
+ else {
801
+ // If we can't find recipient, return null
802
+ return null;
803
+ }
804
+ // Try to get amount - prefer message payload, fallback to inbox data
805
+ let amount;
806
+ if (transceiverMessage.payload?.trimmedAmount) {
807
+ amount = BigInt(transceiverMessage.payload.trimmedAmount.toString());
808
+ }
809
+ else if (transferData.amount) {
810
+ amount = BigInt(transferData.amount.toString());
811
+ }
812
+ else {
813
+ // If we can't find amount, return null
814
+ return null;
815
+ }
816
+ // Return the queued transfer info matching Solana's structure
817
+ const xfer = {
818
+ recipient: recipientAddress,
819
+ amount: amount,
820
+ rateLimitExpiryTimestamp: releaseTimestamp,
821
+ };
822
+ return xfer;
823
+ }
824
+ async *completeInboundQueuedTransfer(fromChain, transceiverMessage, payer) {
825
+ // Check if paused
826
+ const isPaused = await this.isPaused();
827
+ if (isPaused) {
828
+ throw new Error("Contract is paused");
829
+ }
830
+ // Create an attestation from the transceiverMessage
831
+ const attestation = {
832
+ emitterChain: fromChain,
833
+ hash: transceiverMessage.id,
834
+ };
835
+ // Call redeem with the attestation
836
+ yield* this.redeem([attestation], payer);
837
+ }
838
+ // Transceiver Management
839
+ async getTransceiver(ix) {
840
+ // For now, only support index 0 which is the wormhole transceiver
841
+ if (ix !== 0) {
842
+ return null;
843
+ }
844
+ // Return a wormhole transceiver if we have the state ID from contracts
845
+ const wormholeTransceiverStateId = this.contracts.ntt["transceiver"]?.["wormhole"];
846
+ if (wormholeTransceiverStateId) {
847
+ const chain = this.chain;
848
+ // Create a transceiver implementation that supports getPeer and setPeer
849
+ const suiNtt = this;
850
+ return {
851
+ async getTransceiverType() {
852
+ return "wormhole";
853
+ },
854
+ async getAddress() {
855
+ const state = await suiNtt.getSuiObject(wormholeTransceiverStateId);
856
+ return {
857
+ chain: chain,
858
+ address: (0, sdk_definitions_1.toUniversal)(chain, state.fields.emitter_cap.fields.id.id),
859
+ };
860
+ },
861
+ async *setPeer(peer, payer) {
862
+ yield* suiNtt.setTransceiverPeer(0, peer, payer);
863
+ },
864
+ async getPeer(targetChain) {
865
+ return await suiNtt.getTransceiverPeer(0, targetChain);
866
+ },
867
+ async *setPauser() {
868
+ throw new Error("setPauser not implemented for Sui transceiver");
869
+ },
870
+ async getPauser() {
871
+ return null;
872
+ },
873
+ async *receive() {
874
+ throw new Error("receive not implemented for Sui transceiver");
875
+ },
876
+ };
877
+ }
878
+ return null;
879
+ }
880
+ async getTransceiverPeer(ix, targetChain) {
881
+ // For now, only support index 0 which is the wormhole transceiver
882
+ if (ix !== 0) {
883
+ return null;
884
+ }
885
+ const wormholeTransceiverStateId = this.contracts.ntt["transceiver"]?.["wormhole"];
886
+ if (!wormholeTransceiverStateId) {
887
+ return null;
888
+ }
889
+ // chainToChainId is already imported at the top of the file
890
+ try {
891
+ // Get the transceiver state object
892
+ const transceiverState = await this.provider.getObject({
893
+ id: wormholeTransceiverStateId,
894
+ options: { showContent: true },
895
+ });
896
+ if (!transceiverState.data?.content ||
897
+ transceiverState.data.content.dataType !== "moveObject") {
898
+ return null;
899
+ }
900
+ const fields = transceiverState.data.content.fields;
901
+ const peersTable = fields.peers;
902
+ // Convert target chain to chain ID
903
+ const chainId = (0, sdk_base_1.chainToChainId)(targetChain);
904
+ // Query the dynamic field for this chain ID in the transceiver peers table
905
+ const peerField = await this.provider.getDynamicFieldObject({
906
+ parentId: peersTable.fields.id.id,
907
+ name: {
908
+ type: "u16",
909
+ value: chainId,
910
+ },
911
+ });
912
+ if (!peerField.data?.content ||
913
+ peerField.data.content.dataType !== "moveObject") {
914
+ // Peer not found for this chain
915
+ return null;
916
+ }
917
+ // Extract the ExternalAddress from the peer field
918
+ const externalAddress = peerField.data.content.fields
919
+ .value;
920
+ const addressBytes = externalAddress.fields.value.fields.data;
921
+ // Convert address bytes to ChainAddress
922
+ const addressUint8Array = new Uint8Array(addressBytes);
923
+ const chainAddress = {
924
+ chain: targetChain,
925
+ address: (0, sdk_definitions_1.toUniversal)(targetChain, addressUint8Array),
926
+ };
927
+ return chainAddress;
928
+ }
929
+ catch (error) {
930
+ console.error(error);
931
+ // If we get an error (like object not found), the peer doesn't exist
932
+ return null;
933
+ }
934
+ }
935
+ async getTransceiverType(transceiverIndex = 0) {
936
+ // For now, only support index 0 which is the wormhole transceiver
937
+ if (transceiverIndex !== 0) {
938
+ throw new Error(`Transceiver index ${transceiverIndex} not supported`);
939
+ }
940
+ const wormholeTransceiverStateId = this.contracts.ntt["transceiver"]?.["wormhole"];
941
+ if (!wormholeTransceiverStateId) {
942
+ throw new Error("Wormhole transceiver not found in contracts");
943
+ }
944
+ // Get the transceiver state object
945
+ const transceiverState = await this.provider.getObject({
946
+ id: wormholeTransceiverStateId,
947
+ options: { showType: true },
948
+ });
949
+ if (!transceiverState.data?.type) {
950
+ throw new Error("Unable to determine transceiver object type");
951
+ }
952
+ // Extract package ID from the object type
953
+ // Type format: "packageId::module::Type<...>"
954
+ const packageId = transceiverState.data.type.split("::")[0];
955
+ // Build transaction to call get_transceiver_type from the standard transceiver module
956
+ const tx = new transactions_1.Transaction();
957
+ tx.moveCall({
958
+ target: `${packageId}::transceiver::get_transceiver_type`,
959
+ arguments: [],
960
+ });
961
+ // Use devInspectTransactionBlock to call the view function
962
+ const response = await this.provider.devInspectTransactionBlock({
963
+ transactionBlock: tx,
964
+ sender: "0x0000000000000000000000000000000000000000000000000000000000000000",
965
+ });
966
+ // Parse the response
967
+ if (response.results && response.results.length > 0) {
968
+ const result = response.results[0];
969
+ if (result && result.returnValues && result.returnValues.length > 0) {
970
+ const returnValue = result.returnValues[0];
971
+ if (returnValue &&
972
+ Array.isArray(returnValue) &&
973
+ returnValue.length > 0) {
974
+ // The return value should be [bytes, type] where bytes is an array of numbers
975
+ const bytesData = returnValue[0];
976
+ if (Array.isArray(bytesData)) {
977
+ const transceiverType = new TextDecoder().decode(new Uint8Array(bytesData));
978
+ return transceiverType;
979
+ }
980
+ }
981
+ }
982
+ }
983
+ throw new Error("Failed to get transceiver info from response");
984
+ }
985
+ async verifyAddresses() {
986
+ // Verify that the addresses in the contracts configuration are valid
987
+ try {
988
+ // Check if manager address exists and is a valid NTT state object
989
+ const state = await this.provider.getObject({
990
+ id: this.contracts.ntt["manager"],
991
+ options: { showContent: true },
992
+ });
993
+ if (!state.data?.content ||
994
+ state.data.content.dataType !== "moveObject") {
995
+ return null;
996
+ }
997
+ const fields = state.data.content.fields;
998
+ // Look up registered transceivers in the transceiver registry
999
+ const transceiverRegistry = fields.transceivers;
1000
+ const registryId = transceiverRegistry.fields.id.id;
1001
+ // Query the registry's dynamic fields to find registered transceivers
1002
+ const dynamicFields = await this.provider.getDynamicFields({
1003
+ parentId: registryId,
1004
+ });
1005
+ const result = {
1006
+ manager: this.contracts.ntt["manager"],
1007
+ token: await SuiNtt.extractTokenTypeFromSuiState(this.provider, this.contracts.ntt["manager"]),
1008
+ transceiver: {},
1009
+ };
1010
+ // For now, we only look for the wormhole transceiver at index 0
1011
+ // The dynamic field key structure is based on the Move code in transceiver_registry.move
1012
+ for (const field of dynamicFields.data) {
1013
+ if (field.name?.type?.includes("transceiver_registry::Key")) {
1014
+ // This is a transceiver registration
1015
+ try {
1016
+ const transceiverInfo = await this.provider.getObject({
1017
+ id: field.objectId,
1018
+ options: { showContent: true },
1019
+ });
1020
+ if (transceiverInfo.data?.content &&
1021
+ transceiverInfo.data.content.dataType === "moveObject") {
1022
+ const infoFields = transceiverInfo.data.content
1023
+ .fields.value.fields;
1024
+ const transceiverStateId = infoFields.state_object_id;
1025
+ const transceiverIndex = infoFields.id;
1026
+ // For index 0, assume it's the wormhole transceiver
1027
+ if (transceiverIndex === 0) {
1028
+ result.transceiver["wormhole"] = transceiverStateId;
1029
+ }
1030
+ }
1031
+ }
1032
+ catch (e) {
1033
+ // Skip this transceiver if we can't read it
1034
+ console.warn(`Failed to read transceiver info: ${e}`);
1035
+ }
1036
+ }
1037
+ }
1038
+ // Compare with what we have locally
1039
+ const local = {
1040
+ manager: this.contracts.ntt["manager"],
1041
+ token: this.contracts.ntt["token"],
1042
+ transceiver: this.contracts.ntt["transceiver"] || {},
1043
+ };
1044
+ const deleteMatching = (a, b) => {
1045
+ for (const k in a) {
1046
+ if (typeof a[k] === "object" &&
1047
+ a[k] !== null &&
1048
+ typeof b[k] === "object" &&
1049
+ b[k] !== null) {
1050
+ deleteMatching(a[k], b[k]);
1051
+ if (Object.keys(a[k]).length === 0)
1052
+ delete a[k];
1053
+ }
1054
+ else if (a[k] === b[k]) {
1055
+ delete a[k];
1056
+ }
1057
+ }
1058
+ };
1059
+ deleteMatching(result, local);
1060
+ return Object.keys(result).length > 0 ? result : null;
1061
+ }
1062
+ catch (e) {
1063
+ console.warn(`Failed to verify addresses: ${e}`);
1064
+ return null;
1065
+ }
1066
+ }
1067
+ async getUpgradeCapId() {
1068
+ const state = await this.getNttState();
1069
+ if (!state.upgrade_cap_id) {
1070
+ throw new Error("UpgradeCap ID not found in NTT state");
1071
+ }
1072
+ return state.upgrade_cap_id;
1073
+ }
1074
+ // Helper function to add redeem call for a single attestation
1075
+ async addRedeemCall(txb, attestation, packageId, versionGated, coinMetadataId) {
1076
+ // Get the transceiver
1077
+ const wormholeTransceiverStateId = this.contracts.ntt["transceiver"]?.["wormhole"];
1078
+ if (!wormholeTransceiverStateId) {
1079
+ throw new Error("Wormhole transceiver not found in contracts");
1080
+ }
1081
+ const transceiverPackageId = await this.getPackageIdFromObject(wormholeTransceiverStateId);
1082
+ // Get wormhole core package ID
1083
+ const coreBridgePackageId = await this.getWormholePackageId(this.provider, this.coreBridgeStateId);
1084
+ // Serialize the attestation to get VAA bytes
1085
+ const vaa = (0, sdk_definitions_1.serialize)(attestation);
1086
+ // First parse the VAA bytes into a VAA struct using Wormhole core
1087
+ const [parsedVAA] = txb.moveCall({
1088
+ target: `${coreBridgePackageId}::vaa::parse_and_verify`,
1089
+ arguments: [
1090
+ txb.object(this.coreBridgeStateId), // wormhole core state
1091
+ txb.pure.vector("u8", Array.from(vaa)), // VAA bytes
1092
+ txb.object(utils_1.SUI_CLOCK_OBJECT_ID), // clock
1093
+ ],
1094
+ });
1095
+ if (!parsedVAA) {
1096
+ throw new Error("Failed to parse VAA");
1097
+ }
1098
+ // Get the NTT package ID for the manager auth type
1099
+ const nttPackageId = await this.getPackageId();
1100
+ // Then pass the parsed VAA struct to validate_message
1101
+ const [validatedMessage] = txb.moveCall({
1102
+ target: `${transceiverPackageId}::wormhole_transceiver::validate_message`,
1103
+ typeArguments: [`${nttPackageId}::auth::ManagerAuth`], // Fully qualified manager auth type
1104
+ arguments: [
1105
+ txb.object(wormholeTransceiverStateId), // transceiver_state
1106
+ parsedVAA, // VAA struct from parse_and_verify
1107
+ ],
1108
+ });
1109
+ if (!validatedMessage) {
1110
+ throw new Error("Failed to validate VAA through transceiver");
1111
+ }
1112
+ // Now call redeem function with the validated message
1113
+ txb.moveCall({
1114
+ target: `${packageId}::ntt::redeem`,
1115
+ typeArguments: [
1116
+ this.contracts.ntt["token"], // CoinType
1117
+ `${transceiverPackageId}::wormhole_transceiver::TransceiverAuth`, // Transceiver type
1118
+ ],
1119
+ arguments: [
1120
+ txb.object(this.contracts.ntt["manager"]), // state
1121
+ versionGated, // version_gated
1122
+ txb.object(coinMetadataId), // coin_meta
1123
+ validatedMessage, // validated_message
1124
+ txb.object(utils_1.SUI_CLOCK_OBJECT_ID),
1125
+ ],
1126
+ });
1127
+ }
1128
+ // Helper function to get the release status from an attestation
1129
+ async getTransferReleaseStatus(attestation) {
1130
+ const inboxItem = await this.getInboxItem(attestation);
1131
+ if (!inboxItem) {
1132
+ return null;
1133
+ }
1134
+ const { inboxItemFields } = inboxItem;
1135
+ return inboxItemFields.release_status;
1136
+ }
1137
+ // Helper function to get inbox item from an NTT attestation
1138
+ async getInboxItem(attestation) {
1139
+ try {
1140
+ // Get the NTT state to access inbox and threshold
1141
+ const state = await this.provider.getObject({
1142
+ id: this.contracts.ntt["manager"],
1143
+ options: {
1144
+ showContent: true,
1145
+ },
1146
+ });
1147
+ if (!state.data?.content ||
1148
+ state.data.content.dataType !== "moveObject") {
1149
+ throw new Error("Failed to fetch NTT state object");
1150
+ }
1151
+ const fields = state.data.content.fields;
1152
+ const inboxTable = fields.inbox.fields.entries;
1153
+ const threshold = parseInt(fields.threshold);
1154
+ // Get chain ID
1155
+ const sourceChain = attestation.emitterChain;
1156
+ if (!sourceChain) {
1157
+ return null;
1158
+ }
1159
+ const sourceChainId = (0, sdk_base_1.chainToChainId)(sourceChain);
1160
+ // Since we can't easily query by the complex key structure,
1161
+ // let's get all dynamic fields and find the matching one
1162
+ const dynamicFields = await this.provider.getDynamicFields({
1163
+ parentId: inboxTable.fields.id.id,
1164
+ });
1165
+ // Look for an inbox entry that matches our chain and message
1166
+ let inboxEntry = null;
1167
+ for (const field of dynamicFields.data) {
1168
+ try {
1169
+ // Check if this field matches our criteria
1170
+ if (field.name?.value) {
1171
+ const keyValue = field.name.value;
1172
+ // Check if chain_id matches
1173
+ if (keyValue?.chain_id === sourceChainId) {
1174
+ // Get the first matching chain_id
1175
+ const inboxEntryObject = await this.provider.getObject({
1176
+ id: field.objectId,
1177
+ options: { showContent: true },
1178
+ });
1179
+ // Verify this is the right message by checking the message ID if available
1180
+ if (inboxEntryObject.data?.content?.dataType === "moveObject") {
1181
+ // Check if the message ID matches (if we have it in the attestation)
1182
+ if (attestation.payload.nttManagerPayload?.id &&
1183
+ keyValue?.message?.id?.data) {
1184
+ // Compare the message ID from the key with our expected hash
1185
+ // Convert both Uint8Arrays to hex strings for proper comparison
1186
+ const msgIdStr = Buffer.from(keyValue.message.id.data).toString("hex");
1187
+ const attestationMsgIdStr = Buffer.from(attestation.payload.nttManagerPayload?.id).toString("hex");
1188
+ if (msgIdStr === attestationMsgIdStr) {
1189
+ // Found the exact match
1190
+ inboxEntry = inboxEntryObject;
1191
+ break;
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+ }
1197
+ }
1198
+ catch (e) {
1199
+ // Skip this field if we can't read it
1200
+ continue;
1201
+ }
1202
+ }
1203
+ // Check if we found a matching inbox entry
1204
+ if (!inboxEntry) {
1205
+ return null;
1206
+ }
1207
+ const inboxItemFields = inboxEntry.data.content.fields
1208
+ .value.fields;
1209
+ return { inboxItemFields, threshold };
1210
+ }
1211
+ catch (error) {
1212
+ // Entry not found or there was an error
1213
+ return null;
1214
+ }
1215
+ }
1216
+ // Helper function to count set bits in a number
1217
+ countSetBits(n) {
1218
+ let count = 0;
1219
+ while (n) {
1220
+ count += n & 1;
1221
+ n >>= 1;
1222
+ }
1223
+ return count;
1224
+ }
1225
+ async getWormholePackageId(provider, coreBridgeStateId) {
1226
+ let currentPackage;
1227
+ let nextCursor;
1228
+ do {
1229
+ const dynamicFields = await provider.getDynamicFields({
1230
+ parentId: coreBridgeStateId,
1231
+ cursor: nextCursor,
1232
+ });
1233
+ currentPackage = dynamicFields.data.find((field) => field.name.type.endsWith("CurrentPackage"));
1234
+ nextCursor = dynamicFields.hasNextPage ? dynamicFields.nextCursor : null;
1235
+ } while (nextCursor && !currentPackage);
1236
+ if (!currentPackage) {
1237
+ throw new Error("Unable to get current package");
1238
+ }
1239
+ const res = await provider.getObject({
1240
+ id: currentPackage.objectId,
1241
+ options: {
1242
+ showContent: true,
1243
+ },
1244
+ });
1245
+ const content = res.data?.content;
1246
+ const fields = content && content.dataType === "moveObject"
1247
+ ? content.fields
1248
+ : null;
1249
+ if (!fields) {
1250
+ throw new Error("Unable to get fields from current package");
1251
+ }
1252
+ const packageId = fields?.["value"]?.fields?.package;
1253
+ if (!packageId) {
1254
+ throw new Error("Unable to get package ID from current package");
1255
+ }
1256
+ return packageId;
1257
+ }
1258
+ }
1259
+ exports.SuiNtt = SuiNtt;
1260
+ //# sourceMappingURL=ntt.js.map