@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.
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +26 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/ntt.d.ts +68 -0
- package/dist/cjs/ntt.d.ts.map +1 -0
- package/dist/cjs/ntt.js +1260 -0
- package/dist/cjs/ntt.js.map +1 -0
- package/dist/cjs/nttWithExecutor.d.ts +35 -0
- package/dist/cjs/nttWithExecutor.d.ts.map +1 -0
- package/dist/cjs/nttWithExecutor.js +399 -0
- package/dist/cjs/nttWithExecutor.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/ntt.d.ts +68 -0
- package/dist/esm/ntt.d.ts.map +1 -0
- package/dist/esm/ntt.js +1256 -0
- package/dist/esm/ntt.js.map +1 -0
- package/dist/esm/nttWithExecutor.d.ts +35 -0
- package/dist/esm/nttWithExecutor.d.ts.map +1 -0
- package/dist/esm/nttWithExecutor.js +395 -0
- package/dist/esm/nttWithExecutor.js.map +1 -0
- package/package.json +65 -0
package/dist/esm/ntt.js
ADDED
|
@@ -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
|