@twin.org/dlt-iota 0.0.2-next.1 → 0.0.2-next.11
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.cjs +490 -59
- package/dist/esm/index.mjs +490 -61
- package/dist/types/index.d.ts +7 -0
- package/dist/types/iota.d.ts +11 -7
- package/dist/types/iotaSmartContractUtils.d.ts +105 -0
- package/dist/types/models/IAdminCapFields.d.ts +14 -0
- package/dist/types/models/IContractData.d.ts +29 -0
- package/dist/types/models/IIotaConfig.d.ts +5 -0
- package/dist/types/models/IMigrationStateFields.d.ts +18 -0
- package/dist/types/models/ISmartContractDeployments.d.ts +8 -0
- package/dist/types/models/ISmartContractObject.d.ts +18 -0
- package/dist/types/models/networkTypes.d.ts +21 -0
- package/docs/changelog.md +71 -0
- package/docs/reference/classes/Iota.md +18 -10
- package/docs/reference/classes/IotaSmartContractUtils.md +473 -0
- package/docs/reference/index.md +14 -0
- package/docs/reference/interfaces/IAdminCapFields.md +17 -0
- package/docs/reference/interfaces/IContractData.md +51 -0
- package/docs/reference/interfaces/IIotaConfig.md +14 -0
- package/docs/reference/interfaces/IMigrationStateFields.md +25 -0
- package/docs/reference/interfaces/ISmartContractObject.md +25 -0
- package/docs/reference/type-aliases/ISmartContractDeployments.md +5 -0
- package/docs/reference/type-aliases/NetworkTypes.md +5 -0
- package/docs/reference/variables/NetworkTypes.md +25 -0
- package/locales/en.json +23 -2
- package/package.json +17 -4
package/dist/esm/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toB64 } from '@iota/bcs';
|
|
1
|
+
import { toB64, bcs } from '@iota/bcs';
|
|
2
2
|
import { IotaClient } from '@iota/iota-sdk/client';
|
|
3
3
|
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
|
|
4
4
|
import { Transaction } from '@iota/iota-sdk/transactions';
|
|
@@ -34,18 +34,17 @@ class Iota {
|
|
|
34
34
|
static DEFAULT_INCLUSION_TIMEOUT = 60;
|
|
35
35
|
/**
|
|
36
36
|
* Runtime name for the class.
|
|
37
|
-
* @internal
|
|
38
37
|
*/
|
|
39
|
-
static
|
|
38
|
+
static CLASS_NAME = "Iota";
|
|
40
39
|
/**
|
|
41
40
|
* Create a new IOTA client.
|
|
42
41
|
* @param config The configuration.
|
|
43
42
|
* @returns The client instance.
|
|
44
43
|
*/
|
|
45
44
|
static createClient(config) {
|
|
46
|
-
Guards.object(Iota.
|
|
47
|
-
Guards.object(Iota.
|
|
48
|
-
Guards.string(Iota.
|
|
45
|
+
Guards.object(Iota.CLASS_NAME, "config", config);
|
|
46
|
+
Guards.object(Iota.CLASS_NAME, "config.clientOptions", config.clientOptions);
|
|
47
|
+
Guards.string(Iota.CLASS_NAME, "config.clientOptions.url", config.clientOptions.url);
|
|
49
48
|
return new IotaClient(config.clientOptions);
|
|
50
49
|
}
|
|
51
50
|
/**
|
|
@@ -53,7 +52,7 @@ class Iota {
|
|
|
53
52
|
* @param config The configuration to populate.
|
|
54
53
|
*/
|
|
55
54
|
static populateConfig(config) {
|
|
56
|
-
Guards.object(Iota.
|
|
55
|
+
Guards.object(Iota.CLASS_NAME, "config.clientOptions", config.clientOptions);
|
|
57
56
|
config.vaultMnemonicId ??= Iota.DEFAULT_MNEMONIC_SECRET_NAME;
|
|
58
57
|
config.vaultSeedId ??= Iota.DEFAULT_SEED_SECRET_NAME;
|
|
59
58
|
config.coinType ??= Iota.DEFAULT_COIN_TYPE;
|
|
@@ -70,10 +69,10 @@ class Iota {
|
|
|
70
69
|
* @returns The list of addresses.
|
|
71
70
|
*/
|
|
72
71
|
static getAddresses(seed, coinType, accountIndex, startAddressIndex, count, isInternal) {
|
|
73
|
-
Guards.integer(Iota.
|
|
74
|
-
Guards.integer(Iota.
|
|
75
|
-
Guards.integer(Iota.
|
|
76
|
-
Guards.integer(Iota.
|
|
72
|
+
Guards.integer(Iota.CLASS_NAME, "coinType", coinType);
|
|
73
|
+
Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
|
|
74
|
+
Guards.integer(Iota.CLASS_NAME, "startAddressIndex", startAddressIndex);
|
|
75
|
+
Guards.integer(Iota.CLASS_NAME, "count", count);
|
|
77
76
|
const addresses = [];
|
|
78
77
|
for (let i = startAddressIndex; i < startAddressIndex + count; i++) {
|
|
79
78
|
// Derive the keypair using the seed
|
|
@@ -93,9 +92,9 @@ class Iota {
|
|
|
93
92
|
* @returns The key pair containing private key and public key.
|
|
94
93
|
*/
|
|
95
94
|
static getKeyPair(seed, coinType, accountIndex, addressIndex, isInternal) {
|
|
96
|
-
Guards.integer(Iota.
|
|
97
|
-
Guards.integer(Iota.
|
|
98
|
-
Guards.integer(Iota.
|
|
95
|
+
Guards.integer(Iota.CLASS_NAME, "coinType", coinType);
|
|
96
|
+
Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
|
|
97
|
+
Guards.integer(Iota.CLASS_NAME, "addressIndex", addressIndex);
|
|
99
98
|
const keyPair = Bip44.keyPair(seed, KeyType.Ed25519, coinType ?? Iota.DEFAULT_COIN_TYPE, accountIndex, isInternal ?? false, addressIndex);
|
|
100
99
|
return keyPair;
|
|
101
100
|
}
|
|
@@ -103,7 +102,7 @@ class Iota {
|
|
|
103
102
|
* Prepare and post a transaction.
|
|
104
103
|
* @param config The configuration.
|
|
105
104
|
* @param vaultConnector The vault connector.
|
|
106
|
-
* @param
|
|
105
|
+
* @param logging The logging component.
|
|
107
106
|
* @param identity The identity of the user to access the vault keys.
|
|
108
107
|
* @param client The client instance.
|
|
109
108
|
* @param source The source address.
|
|
@@ -112,27 +111,27 @@ class Iota {
|
|
|
112
111
|
* @param options The transaction options.
|
|
113
112
|
* @returns The transaction result.
|
|
114
113
|
*/
|
|
115
|
-
static async prepareAndPostValueTransaction(config, vaultConnector,
|
|
114
|
+
static async prepareAndPostValueTransaction(config, vaultConnector, logging, identity, client, source, amount, recipient, options) {
|
|
116
115
|
try {
|
|
117
116
|
const txb = new Transaction();
|
|
118
117
|
const [coin] = txb.splitCoins(txb.gas, [txb.pure.u64(amount)]);
|
|
119
118
|
txb.transferObjects([coin], txb.pure.address(recipient));
|
|
120
119
|
// Check if gas station configuration is present
|
|
121
120
|
if (Is.object(config.gasStation)) {
|
|
122
|
-
return await
|
|
121
|
+
return await Iota.prepareAndPostGasStationTransaction(config, vaultConnector, identity, client, source, txb);
|
|
123
122
|
}
|
|
124
|
-
const result = await
|
|
123
|
+
const result = await Iota.prepareAndPostTransaction(config, vaultConnector, logging, identity, client, source, txb, options);
|
|
125
124
|
return result;
|
|
126
125
|
}
|
|
127
126
|
catch (error) {
|
|
128
|
-
throw new GeneralError(Iota.
|
|
127
|
+
throw new GeneralError(Iota.CLASS_NAME, "valueTransactionFailed", undefined, Iota.extractPayloadError(error));
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
130
|
/**
|
|
132
131
|
* Prepare and post a transaction.
|
|
133
132
|
* @param config The configuration.
|
|
134
133
|
* @param vaultConnector The vault connector.
|
|
135
|
-
* @param
|
|
134
|
+
* @param logging The logging component.
|
|
136
135
|
* @param identity The identity of the user to access the vault keys.
|
|
137
136
|
* @param client The client instance.
|
|
138
137
|
* @param owner The owner of the address.
|
|
@@ -140,17 +139,17 @@ class Iota {
|
|
|
140
139
|
* @param options The transaction options.
|
|
141
140
|
* @returns The transaction response.
|
|
142
141
|
*/
|
|
143
|
-
static async prepareAndPostTransaction(config, vaultConnector,
|
|
142
|
+
static async prepareAndPostTransaction(config, vaultConnector, logging, identity, client, owner, transaction, options) {
|
|
144
143
|
// Check if gas station configuration is present
|
|
145
144
|
if (Is.object(config.gasStation)) {
|
|
146
|
-
return
|
|
145
|
+
return Iota.prepareAndPostGasStationTransaction(config, vaultConnector, identity, client, owner, transaction, options);
|
|
147
146
|
}
|
|
148
147
|
// Traditional transaction flow
|
|
149
148
|
// Dry run the transaction if cost logging is enabled to get the gas and storage costs
|
|
150
149
|
if (Is.stringValue(options?.dryRunLabel)) {
|
|
151
|
-
await Iota.dryRunTransaction(client,
|
|
150
|
+
await Iota.dryRunTransaction(client, logging, transaction, owner, options.dryRunLabel);
|
|
152
151
|
}
|
|
153
|
-
const seed = await
|
|
152
|
+
const seed = await Iota.getSeed(config, vaultConnector, identity);
|
|
154
153
|
const addressKeyPair = Iota.findAddress(config.maxAddressScanRange ?? Iota.DEFAULT_SCAN_RANGE, config.coinType ?? Iota.DEFAULT_COIN_TYPE, seed, owner);
|
|
155
154
|
const keypair = Ed25519Keypair.fromSecretKey(addressKeyPair.privateKey);
|
|
156
155
|
try {
|
|
@@ -176,7 +175,7 @@ class Iota {
|
|
|
176
175
|
return response;
|
|
177
176
|
}
|
|
178
177
|
catch (error) {
|
|
179
|
-
throw new GeneralError(Iota.
|
|
178
|
+
throw new GeneralError(Iota.CLASS_NAME, "transactionFailed", undefined, Iota.extractPayloadError(error));
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
181
|
/**
|
|
@@ -211,7 +210,7 @@ class Iota {
|
|
|
211
210
|
return addressKeyPair;
|
|
212
211
|
}
|
|
213
212
|
}
|
|
214
|
-
throw new GeneralError(Iota.
|
|
213
|
+
throw new GeneralError(Iota.CLASS_NAME, "addressNotFound", { address });
|
|
215
214
|
}
|
|
216
215
|
/**
|
|
217
216
|
* Extract error from SDK payload.
|
|
@@ -224,8 +223,11 @@ class Iota {
|
|
|
224
223
|
if (!Is.empty(error.inner)) {
|
|
225
224
|
error.inner = Iota.extractPayloadError(error.inner);
|
|
226
225
|
}
|
|
226
|
+
if (!Is.empty(error.cause)) {
|
|
227
|
+
error.cause = Iota.extractPayloadError(error.cause);
|
|
228
|
+
}
|
|
227
229
|
if (error.code === "InsufficientGas") {
|
|
228
|
-
return new GeneralError(Iota.
|
|
230
|
+
return new GeneralError(Iota.CLASS_NAME, "insufficientFunds");
|
|
229
231
|
}
|
|
230
232
|
else if (error.message?.startsWith("ErrorObject")) {
|
|
231
233
|
const msg = /message: "(.*)"/.exec(error.message);
|
|
@@ -237,7 +239,7 @@ class Iota {
|
|
|
237
239
|
const baseError = BaseError.fromError(error);
|
|
238
240
|
if (baseError.name === "Base" && !Is.stringValue(baseError.source)) {
|
|
239
241
|
baseError.name = "IOTA";
|
|
240
|
-
baseError.source = Iota.
|
|
242
|
+
baseError.source = Iota.CLASS_NAME;
|
|
241
243
|
}
|
|
242
244
|
return baseError;
|
|
243
245
|
}
|
|
@@ -277,7 +279,7 @@ class Iota {
|
|
|
277
279
|
if (packageObject?.error?.code === "notExists") {
|
|
278
280
|
return false;
|
|
279
281
|
}
|
|
280
|
-
throw new GeneralError(Iota.
|
|
282
|
+
throw new GeneralError(Iota.CLASS_NAME, "packageObjectError", {
|
|
281
283
|
packageId,
|
|
282
284
|
error: packageObject.error
|
|
283
285
|
});
|
|
@@ -285,16 +287,15 @@ class Iota {
|
|
|
285
287
|
return true;
|
|
286
288
|
}
|
|
287
289
|
catch (error) {
|
|
288
|
-
throw new GeneralError(Iota.
|
|
289
|
-
packageId
|
|
290
|
-
|
|
291
|
-
});
|
|
290
|
+
throw new GeneralError(Iota.CLASS_NAME, "packageNotFoundOnNetwork", {
|
|
291
|
+
packageId
|
|
292
|
+
}, Iota.extractPayloadError(error));
|
|
292
293
|
}
|
|
293
294
|
}
|
|
294
295
|
/**
|
|
295
296
|
* Dry run a transaction and log the results.
|
|
296
297
|
* @param client The IOTA client.
|
|
297
|
-
* @param logging The logging
|
|
298
|
+
* @param logging The logging component.
|
|
298
299
|
* @param txb The transaction to dry run.
|
|
299
300
|
* @param sender The sender address.
|
|
300
301
|
* @param operation The operation to log.
|
|
@@ -311,7 +312,7 @@ class Iota {
|
|
|
311
312
|
transactionBlock: builtTx
|
|
312
313
|
});
|
|
313
314
|
if (dryRunResult.effects.status?.status !== "success") {
|
|
314
|
-
throw new GeneralError(
|
|
315
|
+
throw new GeneralError(Iota.CLASS_NAME, "dryRunFailed", {
|
|
315
316
|
error: dryRunResult.effects?.status?.error
|
|
316
317
|
});
|
|
317
318
|
}
|
|
@@ -328,25 +329,23 @@ class Iota {
|
|
|
328
329
|
balanceChanges: dryRunResult.balanceChanges ?? [],
|
|
329
330
|
objectChanges: dryRunResult.objectChanges ?? []
|
|
330
331
|
};
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
}
|
|
332
|
+
await logging?.log({
|
|
333
|
+
level: "info",
|
|
334
|
+
source: Iota.CLASS_NAME,
|
|
335
|
+
ts: Date.now(),
|
|
336
|
+
message: "transactionCosts",
|
|
337
|
+
data: {
|
|
338
|
+
operation,
|
|
339
|
+
cost: JSON.stringify(result)
|
|
340
|
+
}
|
|
341
|
+
});
|
|
343
342
|
return result;
|
|
344
343
|
}
|
|
345
344
|
catch (error) {
|
|
346
|
-
if (error
|
|
345
|
+
if (BaseError.isErrorName(error, GeneralError.CLASS_NAME)) {
|
|
347
346
|
throw error;
|
|
348
347
|
}
|
|
349
|
-
throw new GeneralError(Iota.
|
|
348
|
+
throw new GeneralError(Iota.CLASS_NAME, "dryRunFailed", undefined, Iota.extractPayloadError(error));
|
|
350
349
|
}
|
|
351
350
|
}
|
|
352
351
|
/**
|
|
@@ -400,11 +399,11 @@ class Iota {
|
|
|
400
399
|
* @returns The transaction response.
|
|
401
400
|
*/
|
|
402
401
|
static async prepareAndPostGasStationTransaction(config, vaultConnector, identity, client, owner, transaction, options) {
|
|
403
|
-
Guards.object(
|
|
402
|
+
Guards.object(Iota.CLASS_NAME, "config.gasStation", config.gasStation);
|
|
404
403
|
try {
|
|
405
404
|
// Reserve gas from the gas station
|
|
406
405
|
const gasBudget = config.gasBudget ?? 50000000;
|
|
407
|
-
const gasReservation = await
|
|
406
|
+
const gasReservation = await Iota.reserveGas(config, gasBudget);
|
|
408
407
|
// Set transaction parameters for sponsoring
|
|
409
408
|
transaction.setSender(owner);
|
|
410
409
|
transaction.setGasOwner(gasReservation.sponsorAddress);
|
|
@@ -413,14 +412,14 @@ class Iota {
|
|
|
413
412
|
// Build and sign transaction
|
|
414
413
|
const unsignedTxBytes = await transaction.build({ client });
|
|
415
414
|
// Sign the transaction with the user's private key
|
|
416
|
-
const seed = await
|
|
415
|
+
const seed = await Iota.getSeed(config, vaultConnector, identity);
|
|
417
416
|
const addressKeyPair = Iota.findAddress(config.maxAddressScanRange ?? Iota.DEFAULT_SCAN_RANGE, config.coinType ?? Iota.DEFAULT_COIN_TYPE, seed, owner);
|
|
418
417
|
const keypair = Ed25519Keypair.fromSecretKey(addressKeyPair.privateKey);
|
|
419
418
|
const signature = await keypair.signTransaction(unsignedTxBytes);
|
|
420
|
-
return await
|
|
419
|
+
return await Iota.executeAndConfirmGasStationTransaction(config, client, gasReservation.reservationId, unsignedTxBytes, signature.signature, options);
|
|
421
420
|
}
|
|
422
421
|
catch (error) {
|
|
423
|
-
throw new GeneralError(Iota.
|
|
422
|
+
throw new GeneralError(Iota.CLASS_NAME, "gasStationTransactionFailed", undefined, Iota.extractPayloadError(error));
|
|
424
423
|
}
|
|
425
424
|
}
|
|
426
425
|
/**
|
|
@@ -430,7 +429,7 @@ class Iota {
|
|
|
430
429
|
* @returns The gas reservation result.
|
|
431
430
|
*/
|
|
432
431
|
static async reserveGas(config, gasBudget) {
|
|
433
|
-
Guards.object(
|
|
432
|
+
Guards.object(Iota.CLASS_NAME, "config.gasStation", config.gasStation);
|
|
434
433
|
const requestData = {
|
|
435
434
|
// eslint-disable-next-line camelcase
|
|
436
435
|
gas_budget: gasBudget,
|
|
@@ -438,7 +437,7 @@ class Iota {
|
|
|
438
437
|
reserve_duration_secs: 30
|
|
439
438
|
};
|
|
440
439
|
const baseUrl = StringHelper.trimTrailingSlashes(config.gasStation.gasStationUrl);
|
|
441
|
-
const result = await FetchHelper.fetchJson(
|
|
440
|
+
const result = await FetchHelper.fetchJson(Iota.CLASS_NAME, `${baseUrl}/v1/reserve_gas`, HttpMethod.POST, requestData, {
|
|
442
441
|
headers: {
|
|
443
442
|
Authorization: `Bearer ${config.gasStation.gasStationAuthToken}`
|
|
444
443
|
}
|
|
@@ -459,7 +458,7 @@ class Iota {
|
|
|
459
458
|
* @returns The transaction response.
|
|
460
459
|
*/
|
|
461
460
|
static async executeGasStationTransaction(config, reservationId, transactionBytes, userSignature) {
|
|
462
|
-
Guards.object(
|
|
461
|
+
Guards.object(Iota.CLASS_NAME, "config.gasStation", config.gasStation);
|
|
463
462
|
const requestData = {
|
|
464
463
|
// eslint-disable-next-line camelcase
|
|
465
464
|
reservation_id: reservationId,
|
|
@@ -469,7 +468,7 @@ class Iota {
|
|
|
469
468
|
user_sig: userSignature
|
|
470
469
|
};
|
|
471
470
|
const baseUrl = StringHelper.trimTrailingSlashes(config.gasStation.gasStationUrl);
|
|
472
|
-
const result = await FetchHelper.fetchJson(
|
|
471
|
+
const result = await FetchHelper.fetchJson(Iota.CLASS_NAME, `${baseUrl}/v1/execute_tx`, HttpMethod.POST, requestData, {
|
|
473
472
|
headers: {
|
|
474
473
|
Authorization: `Bearer ${config.gasStation.gasStationAuthToken}`
|
|
475
474
|
}
|
|
@@ -495,8 +494,8 @@ class Iota {
|
|
|
495
494
|
* @returns The transaction response (confirmed if waitForConfirmation is true).
|
|
496
495
|
*/
|
|
497
496
|
static async executeAndConfirmGasStationTransaction(config, client, reservationId, transactionBytes, userSignature, options) {
|
|
498
|
-
Guards.object(
|
|
499
|
-
const response = await
|
|
497
|
+
Guards.object(Iota.CLASS_NAME, "config.gasStation", config.gasStation);
|
|
498
|
+
const response = await Iota.executeGasStationTransaction(config, reservationId, transactionBytes, userSignature);
|
|
500
499
|
if (options?.waitForConfirmation ?? true) {
|
|
501
500
|
const confirmedTransaction = await Iota.waitForTransactionConfirmation(client, response.digest, config, {
|
|
502
501
|
showEffects: options?.showEffects ?? true,
|
|
@@ -509,4 +508,434 @@ class Iota {
|
|
|
509
508
|
}
|
|
510
509
|
}
|
|
511
510
|
|
|
512
|
-
|
|
511
|
+
// Copyright 2024 IOTA Stiftung.
|
|
512
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
513
|
+
/**
|
|
514
|
+
* Utility class providing common smart contract operations for IOTA-based contracts.
|
|
515
|
+
* This class uses composition pattern to provide shared functionality without inheritance complexity.
|
|
516
|
+
*/
|
|
517
|
+
class IotaSmartContractUtils {
|
|
518
|
+
/**
|
|
519
|
+
* Runtime name for the class.
|
|
520
|
+
*/
|
|
521
|
+
static CLASS_NAME = "IotaSmartContractUtils";
|
|
522
|
+
/**
|
|
523
|
+
* Migrate a smart contract object to the current version using admin privileges.
|
|
524
|
+
* This is a generic migration method that works with any IOTA smart contract.
|
|
525
|
+
* @param config The IOTA configuration.
|
|
526
|
+
* @param client The IOTA client instance.
|
|
527
|
+
* @param vaultConnector The vault connector for key management.
|
|
528
|
+
* @param walletConnector The wallet connector for address generation.
|
|
529
|
+
* @param logging Optional logging component.
|
|
530
|
+
* @param gasBudget The gas budget for the transaction.
|
|
531
|
+
* @param identity The identity of the controller with admin privileges.
|
|
532
|
+
* @param objectId The ID of the object to migrate.
|
|
533
|
+
* @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
|
|
534
|
+
* @param packageId The deployed package ID for the contract.
|
|
535
|
+
* @param deploymentConfig The deployment configuration containing object IDs.
|
|
536
|
+
* @param walletAddressIndex Optional wallet address index for the controller.
|
|
537
|
+
* @returns Promise that resolves when migration is complete.
|
|
538
|
+
*/
|
|
539
|
+
static async migrateSmartContract(config, client, vaultConnector, walletConnector, logging, gasBudget, identity, objectId, namespace, packageId, deploymentConfig, walletAddressIndex) {
|
|
540
|
+
try {
|
|
541
|
+
const txb = new Transaction();
|
|
542
|
+
txb.setGasBudget(gasBudget);
|
|
543
|
+
const moduleName = IotaSmartContractUtils.getModuleName(namespace);
|
|
544
|
+
// Get admin address for the transaction
|
|
545
|
+
const adminAddress = await IotaSmartContractUtils.getPackageControllerAddress(walletConnector, identity, walletAddressIndex);
|
|
546
|
+
// Get the required object IDs from deployment config
|
|
547
|
+
const { adminCapId, migrationStateId } = await IotaSmartContractUtils.getContractObjectIds(client, namespace, config.network, deploymentConfig, packageId, adminAddress);
|
|
548
|
+
txb.moveCall({
|
|
549
|
+
target: `${packageId}::${moduleName}::migrate_${moduleName}`,
|
|
550
|
+
arguments: [txb.object(adminCapId), txb.object(migrationStateId), txb.object(objectId)]
|
|
551
|
+
});
|
|
552
|
+
const result = await Iota.prepareAndPostTransaction(config, vaultConnector, logging, identity, client, adminAddress, txb, {
|
|
553
|
+
dryRunLabel: config.enableCostLogging ? "migrate_object" : undefined
|
|
554
|
+
});
|
|
555
|
+
if (result.effects?.status?.status !== "success") {
|
|
556
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "migrationFailed", {
|
|
557
|
+
error: result.effects?.status?.error,
|
|
558
|
+
objectId
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "migrateSmartContractFailed", { objectId }, error);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Enable migration operations using admin privileges.
|
|
568
|
+
* @param config The IOTA configuration.
|
|
569
|
+
* @param client The IOTA client instance.
|
|
570
|
+
* @param vaultConnector The vault connector for key management.
|
|
571
|
+
* @param walletConnector The wallet connector for address generation.
|
|
572
|
+
* @param logging Optional logging component.
|
|
573
|
+
* @param gasBudget The gas budget for the transaction.
|
|
574
|
+
* @param identity The identity of the controller with admin privileges.
|
|
575
|
+
* @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
|
|
576
|
+
* @param packageId The deployed package ID for the contract.
|
|
577
|
+
* @param deploymentConfig The deployment configuration containing object IDs.
|
|
578
|
+
* @param walletAddressIndex Optional wallet address index for the controller.
|
|
579
|
+
* @returns Promise that resolves when migration is enabled.
|
|
580
|
+
*/
|
|
581
|
+
static async enableMigration(config, client, vaultConnector, walletConnector, logging, gasBudget, identity, namespace, packageId, deploymentConfig, walletAddressIndex) {
|
|
582
|
+
try {
|
|
583
|
+
const txb = new Transaction();
|
|
584
|
+
txb.setGasBudget(gasBudget);
|
|
585
|
+
const moduleName = IotaSmartContractUtils.getModuleName(namespace);
|
|
586
|
+
// Get admin address for the transaction
|
|
587
|
+
const adminAddress = await IotaSmartContractUtils.getPackageControllerAddress(walletConnector, identity, walletAddressIndex);
|
|
588
|
+
// Get the required object IDs from deployment config
|
|
589
|
+
const { adminCapId, migrationStateId } = await IotaSmartContractUtils.getContractObjectIds(client, namespace, config.network, deploymentConfig, packageId, adminAddress);
|
|
590
|
+
txb.moveCall({
|
|
591
|
+
target: `${packageId}::${moduleName}::enable_migration`,
|
|
592
|
+
arguments: [txb.object(adminCapId), txb.object(migrationStateId)]
|
|
593
|
+
});
|
|
594
|
+
const result = await Iota.prepareAndPostTransaction(config, vaultConnector, logging, identity, client, adminAddress, txb, {
|
|
595
|
+
dryRunLabel: config.enableCostLogging ? "enable_migration" : undefined
|
|
596
|
+
});
|
|
597
|
+
if (result.effects?.status?.status !== "success") {
|
|
598
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "enableMigrationFailed", {
|
|
599
|
+
error: result.effects?.status?.error
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "enableMigrationFailed", undefined, error);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Disable migration operations using admin privileges.
|
|
609
|
+
* @param config The IOTA configuration.
|
|
610
|
+
* @param client The IOTA client instance.
|
|
611
|
+
* @param vaultConnector The vault connector for key management.
|
|
612
|
+
* @param walletConnector The wallet connector for address generation.
|
|
613
|
+
* @param logging Optional logging component.
|
|
614
|
+
* @param gasBudget The gas budget for the transaction.
|
|
615
|
+
* @param identity The identity of the controller with admin privileges.
|
|
616
|
+
* @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
|
|
617
|
+
* @param packageId The deployed package ID for the contract.
|
|
618
|
+
* @param deploymentConfig The deployment configuration containing object IDs.
|
|
619
|
+
* @param walletAddressIndex Optional wallet address index for the controller.
|
|
620
|
+
* @returns Promise that resolves when migration is disabled.
|
|
621
|
+
*/
|
|
622
|
+
static async disableMigration(config, client, vaultConnector, walletConnector, logging, gasBudget, identity, namespace, packageId, deploymentConfig, walletAddressIndex) {
|
|
623
|
+
try {
|
|
624
|
+
const txb = new Transaction();
|
|
625
|
+
txb.setGasBudget(gasBudget);
|
|
626
|
+
const moduleName = IotaSmartContractUtils.getModuleName(namespace);
|
|
627
|
+
// Get admin address for the transaction
|
|
628
|
+
const adminAddress = await IotaSmartContractUtils.getPackageControllerAddress(walletConnector, identity, walletAddressIndex);
|
|
629
|
+
// Get the required object IDs from deployment config
|
|
630
|
+
const { adminCapId, migrationStateId } = await IotaSmartContractUtils.getContractObjectIds(client, namespace, config.network, deploymentConfig, packageId, adminAddress);
|
|
631
|
+
txb.moveCall({
|
|
632
|
+
target: `${packageId}::${moduleName}::disable_migration`,
|
|
633
|
+
arguments: [txb.object(adminCapId), txb.object(migrationStateId)]
|
|
634
|
+
});
|
|
635
|
+
const result = await Iota.prepareAndPostTransaction(config, vaultConnector, logging, identity, client, adminAddress, txb, {
|
|
636
|
+
dryRunLabel: config.enableCostLogging ? "disable_migration" : undefined
|
|
637
|
+
});
|
|
638
|
+
if (result.effects?.status?.status !== "success") {
|
|
639
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "disableMigrationFailed", {
|
|
640
|
+
error: result.effects?.status?.error
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "disableMigrationFailed", undefined, error);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Check if migration is currently active for a smart contract.
|
|
650
|
+
* @param config The IOTA configuration.
|
|
651
|
+
* @param client The IOTA client instance.
|
|
652
|
+
* @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
|
|
653
|
+
* @param packageId The deployed package ID for the contract.
|
|
654
|
+
* @param deploymentConfig The deployment configuration containing object IDs.
|
|
655
|
+
* @param identity The identity for MigrationState discovery.
|
|
656
|
+
* @param walletConnector The wallet connector for address generation.
|
|
657
|
+
* @param walletAddressIndex Optional wallet address index.
|
|
658
|
+
* @returns True if migration is enabled, false otherwise.
|
|
659
|
+
*/
|
|
660
|
+
static async isMigrationActive(config, client, namespace, packageId, deploymentConfig, identity, walletConnector, walletAddressIndex) {
|
|
661
|
+
try {
|
|
662
|
+
// Get admin address for discovery
|
|
663
|
+
const adminAddress = await IotaSmartContractUtils.getPackageControllerAddress(walletConnector, identity, walletAddressIndex);
|
|
664
|
+
// Get the migration state ID
|
|
665
|
+
const { migrationStateId } = await IotaSmartContractUtils.getContractObjectIds(client, namespace, config.network, deploymentConfig, packageId, adminAddress);
|
|
666
|
+
const migrationStateResponse = await client.getObject({
|
|
667
|
+
id: migrationStateId,
|
|
668
|
+
options: {
|
|
669
|
+
showContent: true,
|
|
670
|
+
showType: true
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
if (!migrationStateResponse.data?.content) {
|
|
674
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "migrationStateNotReadable", {
|
|
675
|
+
migrationStateId
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
const content = migrationStateResponse.data.content;
|
|
679
|
+
if (content.dataType === "moveObject" && Is.objectValue(content.fields)) {
|
|
680
|
+
const fields = content.fields;
|
|
681
|
+
return Is.boolean(fields.enabled) ? fields.enabled : false;
|
|
682
|
+
}
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "isMigrationActiveFailed", undefined, error);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Get the current contract version from the deployed smart contract.
|
|
691
|
+
* @param config The IOTA configuration.
|
|
692
|
+
* @param client The IOTA client instance.
|
|
693
|
+
* @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
|
|
694
|
+
* @param packageId The deployed package ID for the contract.
|
|
695
|
+
* @param identity The identity for package controller address.
|
|
696
|
+
* @param walletConnector The wallet connector for address generation.
|
|
697
|
+
* @param walletAddressIndex Optional wallet address index.
|
|
698
|
+
* @returns The current version number of the contract.
|
|
699
|
+
*/
|
|
700
|
+
static async getCurrentContractVersion(config, client, namespace, packageId, identity, walletConnector, walletAddressIndex) {
|
|
701
|
+
try {
|
|
702
|
+
const tx = new Transaction();
|
|
703
|
+
const moduleName = IotaSmartContractUtils.getModuleName(namespace);
|
|
704
|
+
tx.moveCall({
|
|
705
|
+
target: `${packageId}::${moduleName}::get_current_version`,
|
|
706
|
+
arguments: []
|
|
707
|
+
});
|
|
708
|
+
const controllerAddress = await IotaSmartContractUtils.getPackageControllerAddress(walletConnector, identity, walletAddressIndex);
|
|
709
|
+
const result = await client.devInspectTransactionBlock({
|
|
710
|
+
sender: controllerAddress,
|
|
711
|
+
transactionBlock: tx
|
|
712
|
+
});
|
|
713
|
+
if (Is.arrayValue(result.results) &&
|
|
714
|
+
Is.object(result.results[0]) &&
|
|
715
|
+
Is.arrayValue(result.results[0].returnValues)) {
|
|
716
|
+
const versionBytes = result.results[0].returnValues[0];
|
|
717
|
+
// Convert to Uint8Array if it's a regular array
|
|
718
|
+
const byteData = versionBytes[0];
|
|
719
|
+
if (!Is.arrayValue(byteData) && !Is.uint8Array(byteData)) {
|
|
720
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "invalidVersionData");
|
|
721
|
+
}
|
|
722
|
+
// Convert to Uint8Array for BCS parsing
|
|
723
|
+
const uint8Data = Is.uint8Array(byteData) ? byteData : new Uint8Array(byteData);
|
|
724
|
+
// The version is returned as a u64, decode it from bytes using BCS
|
|
725
|
+
// IOTA Move contracts return data in BCS format
|
|
726
|
+
const version = Number(bcs.u64().parse(uint8Data));
|
|
727
|
+
return version;
|
|
728
|
+
}
|
|
729
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "getCurrentContractVersionNoData", {
|
|
730
|
+
resultExists: Is.arrayValue(result.results),
|
|
731
|
+
resultLength: Is.arrayValue(result.results) ? result.results.length : 0,
|
|
732
|
+
hasReturnValues: Is.arrayValue(result.results?.[0]?.returnValues)
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "getCurrentContractVersionFailed", undefined, error);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Validate that an object version is compatible with the current contract.
|
|
741
|
+
* @param config The IOTA configuration.
|
|
742
|
+
* @param client The IOTA client instance.
|
|
743
|
+
* @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
|
|
744
|
+
* @param packageId The deployed package ID for the contract.
|
|
745
|
+
* @param identity The identity for version checking.
|
|
746
|
+
* @param objectId The object ID to validate.
|
|
747
|
+
* @param walletConnector The wallet connector for address generation.
|
|
748
|
+
* @param versionExtractor Function to extract version from object content.
|
|
749
|
+
* @param walletAddressIndex Optional wallet address index.
|
|
750
|
+
* @returns True if the object version is compatible, false otherwise.
|
|
751
|
+
*/
|
|
752
|
+
static async validateObjectVersion(config, client, namespace, packageId, identity, objectId, walletConnector, versionExtractor, walletAddressIndex) {
|
|
753
|
+
try {
|
|
754
|
+
// Get current contract version
|
|
755
|
+
const currentVersion = await IotaSmartContractUtils.getCurrentContractVersion(config, client, namespace, packageId, identity, walletConnector, walletAddressIndex);
|
|
756
|
+
// Get object version
|
|
757
|
+
const objectResponse = await client.getObject({
|
|
758
|
+
id: objectId,
|
|
759
|
+
options: {
|
|
760
|
+
showContent: true,
|
|
761
|
+
showType: true
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
if (!objectResponse.data?.content) {
|
|
765
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "objectNotReadable", {
|
|
766
|
+
objectId
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
const content = objectResponse.data.content;
|
|
770
|
+
if (content.dataType === "moveObject" && Is.objectValue(content.fields)) {
|
|
771
|
+
const objectVersion = versionExtractor(content);
|
|
772
|
+
return objectVersion <= currentVersion;
|
|
773
|
+
}
|
|
774
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "objectInvalidFormat", {
|
|
775
|
+
objectId,
|
|
776
|
+
content
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "validateObjectVersionFailed", { objectId }, error);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Get the module name for a given namespace.
|
|
785
|
+
* @param namespace The contract namespace.
|
|
786
|
+
* @returns The module name in snake_case format.
|
|
787
|
+
* @internal
|
|
788
|
+
*/
|
|
789
|
+
static getModuleName(namespace) {
|
|
790
|
+
// Convert namespace to snake_case for module name
|
|
791
|
+
return StringHelper.snakeCase(namespace);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Get the package controller address for transactions.
|
|
795
|
+
* @param walletConnector The wallet connector for address generation.
|
|
796
|
+
* @param identity The identity to use.
|
|
797
|
+
* @param addressIndex Optional address index to use.
|
|
798
|
+
* @returns The controller address.
|
|
799
|
+
* @internal
|
|
800
|
+
*/
|
|
801
|
+
static async getPackageControllerAddress(walletConnector, identity, addressIndex = 0) {
|
|
802
|
+
const addresses = await walletConnector.getAddresses(identity, 0, addressIndex, 1);
|
|
803
|
+
return addresses[0];
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Get contract object IDs (AdminCap and MigrationState) from deployment config with fallback discovery.
|
|
807
|
+
* @param client The IOTA client instance.
|
|
808
|
+
* @param namespace The contract namespace.
|
|
809
|
+
* @param network The network name.
|
|
810
|
+
* @param deploymentConfig The deployment configuration.
|
|
811
|
+
* @param packageId The package ID.
|
|
812
|
+
* @param adminAddress The admin address for object discovery.
|
|
813
|
+
* @returns Object containing adminCapId and migrationStateId.
|
|
814
|
+
* @internal
|
|
815
|
+
*/
|
|
816
|
+
static async getContractObjectIds(client, namespace, network, deploymentConfig, packageId, adminAddress) {
|
|
817
|
+
try {
|
|
818
|
+
// First try to load from deployment JSON
|
|
819
|
+
const networkConfig = deploymentConfig[network];
|
|
820
|
+
if (Is.objectValue(networkConfig)) {
|
|
821
|
+
const migrationStateId = networkConfig.migrationStateId;
|
|
822
|
+
// AdminCap must be discovered from blockchain (not stored in JSON)
|
|
823
|
+
const adminCapId = await IotaSmartContractUtils.discoverAdminCap(client, packageId, namespace, adminAddress);
|
|
824
|
+
if (Is.stringValue(migrationStateId) && Is.stringValue(adminCapId)) {
|
|
825
|
+
return { adminCapId, migrationStateId };
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Fallback: discover both from blockchain
|
|
829
|
+
return await IotaSmartContractUtils.discoverContractObjectsFromBlockchain(client, packageId, namespace, adminAddress);
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "getContractObjectIdsFailed", { namespace, network, packageId }, error);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Discover AdminCap object from the blockchain.
|
|
837
|
+
* @param client The IOTA client instance.
|
|
838
|
+
* @param packageId The package ID.
|
|
839
|
+
* @param namespace The contract namespace.
|
|
840
|
+
* @param adminAddress The admin address.
|
|
841
|
+
* @returns The AdminCap object ID.
|
|
842
|
+
* @internal
|
|
843
|
+
*/
|
|
844
|
+
static async discoverAdminCap(client, packageId, namespace, adminAddress) {
|
|
845
|
+
const adminCapType = `${packageId}::${IotaSmartContractUtils.getModuleName(namespace)}::AdminCap`;
|
|
846
|
+
const adminCapObjects = await client.getOwnedObjects({
|
|
847
|
+
owner: adminAddress,
|
|
848
|
+
filter: {
|
|
849
|
+
StructType: adminCapType
|
|
850
|
+
},
|
|
851
|
+
options: {
|
|
852
|
+
showContent: true,
|
|
853
|
+
showType: true
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
if (Is.arrayValue(adminCapObjects.data) && adminCapObjects.data.length > 0) {
|
|
857
|
+
const adminCapObject = adminCapObjects.data[0];
|
|
858
|
+
if (Is.stringValue(adminCapObject.data?.objectId)) {
|
|
859
|
+
return adminCapObject.data.objectId;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "adminCapNotFound", {
|
|
863
|
+
adminCapType,
|
|
864
|
+
adminAddress
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Discover contract objects from blockchain as fallback.
|
|
869
|
+
* @param client The IOTA client instance.
|
|
870
|
+
* @param packageId The package ID.
|
|
871
|
+
* @param namespace The contract namespace.
|
|
872
|
+
* @param adminAddress The admin address.
|
|
873
|
+
* @returns Object containing adminCapId and migrationStateId.
|
|
874
|
+
* @internal
|
|
875
|
+
*/
|
|
876
|
+
static async discoverContractObjectsFromBlockchain(client, packageId, namespace, adminAddress) {
|
|
877
|
+
// Discover AdminCap
|
|
878
|
+
const adminCapId = await IotaSmartContractUtils.discoverAdminCap(client, packageId, namespace, adminAddress);
|
|
879
|
+
// Discover MigrationState through transaction history
|
|
880
|
+
const migrationStateType = `${packageId}::${IotaSmartContractUtils.getModuleName(namespace)}::MigrationState`;
|
|
881
|
+
const transactions = await client.queryTransactionBlocks({
|
|
882
|
+
filter: {
|
|
883
|
+
FromAddress: adminAddress
|
|
884
|
+
},
|
|
885
|
+
options: {
|
|
886
|
+
showObjectChanges: true,
|
|
887
|
+
showEffects: true
|
|
888
|
+
},
|
|
889
|
+
limit: 20,
|
|
890
|
+
order: "descending"
|
|
891
|
+
});
|
|
892
|
+
// Look for MigrationState object creation in transaction history
|
|
893
|
+
let migrationStateId;
|
|
894
|
+
for (const tx of transactions.data) {
|
|
895
|
+
const objectChanges = tx.objectChanges;
|
|
896
|
+
if (Is.arrayValue(objectChanges)) {
|
|
897
|
+
for (const change of objectChanges) {
|
|
898
|
+
if ((change.type === "created" || change.type === "mutated") &&
|
|
899
|
+
"objectType" in change &&
|
|
900
|
+
change.objectType === migrationStateType) {
|
|
901
|
+
migrationStateId = change.objectId;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (migrationStateId) {
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (!migrationStateId) {
|
|
911
|
+
throw new GeneralError(IotaSmartContractUtils.CLASS_NAME, "migrationStateNotFound", {
|
|
912
|
+
migrationStateType,
|
|
913
|
+
adminAddress
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
return { adminCapId, migrationStateId };
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Copyright 2024 IOTA Stiftung.
|
|
921
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
922
|
+
/**
|
|
923
|
+
* Network types supported for deployment
|
|
924
|
+
*/
|
|
925
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
926
|
+
const NetworkTypes = {
|
|
927
|
+
/**
|
|
928
|
+
* Testnet.
|
|
929
|
+
*/
|
|
930
|
+
Testnet: "testnet",
|
|
931
|
+
/**
|
|
932
|
+
* Devnet.
|
|
933
|
+
*/
|
|
934
|
+
Devnet: "devnet",
|
|
935
|
+
/**
|
|
936
|
+
* Mainnet.
|
|
937
|
+
*/
|
|
938
|
+
Mainnet: "mainnet"
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
export { Iota, IotaSmartContractUtils, NetworkTypes };
|