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