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