@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.
@@ -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 _CLASS_NAME = "Iota";
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._CLASS_NAME, "config", config);
47
- Guards.object(Iota._CLASS_NAME, "config.clientOptions", config.clientOptions);
48
- Guards.string(Iota._CLASS_NAME, "config.clientOptions.url", config.clientOptions.url);
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._CLASS_NAME, "config.clientOptions", config.clientOptions);
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._CLASS_NAME, "coinType", coinType);
74
- Guards.integer(Iota._CLASS_NAME, "accountIndex", accountIndex);
75
- Guards.integer(Iota._CLASS_NAME, "startAddressIndex", startAddressIndex);
76
- Guards.integer(Iota._CLASS_NAME, "count", count);
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._CLASS_NAME, "coinType", coinType);
97
- Guards.integer(Iota._CLASS_NAME, "accountIndex", accountIndex);
98
- Guards.integer(Iota._CLASS_NAME, "addressIndex", addressIndex);
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 loggingConnector The logging connector.
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, loggingConnector, identity, client, source, amount, recipient, options) {
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 this.prepareAndPostGasStationTransaction(config, vaultConnector, identity, client, source, txb);
121
+ return await Iota.prepareAndPostGasStationTransaction(config, vaultConnector, identity, client, source, txb);
123
122
  }
124
- const result = await this.prepareAndPostTransaction(config, vaultConnector, loggingConnector, identity, client, source, txb, options);
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._CLASS_NAME, "valueTransactionFailed", undefined, Iota.extractPayloadError(error));
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 loggingConnector The logging connector.
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, loggingConnector, identity, client, owner, transaction, options) {
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 this.prepareAndPostGasStationTransaction(config, vaultConnector, identity, client, owner, transaction, options);
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, loggingConnector, transaction, owner, options.dryRunLabel);
150
+ await Iota.dryRunTransaction(client, logging, transaction, owner, options.dryRunLabel);
152
151
  }
153
- const seed = await this.getSeed(config, vaultConnector, identity);
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._CLASS_NAME, "transactionFailed", undefined, Iota.extractPayloadError(error));
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._CLASS_NAME, "addressNotFound", { address });
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._CLASS_NAME, "insufficientFunds");
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._CLASS_NAME;
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._CLASS_NAME, "packageObjectError", {
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._CLASS_NAME, "packageNotFoundOnNetwork", {
289
- packageId,
290
- error: Iota.extractPayloadError(error)
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 connector.
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(this._CLASS_NAME, "dryRunFailed", {
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
- if (logging) {
332
- await logging.log({
333
- level: "info",
334
- source: Iota._CLASS_NAME,
335
- ts: Date.now(),
336
- message: "transactionCosts",
337
- data: {
338
- operation,
339
- ...result
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 instanceof GeneralError) {
345
+ if (BaseError.isErrorName(error, GeneralError.CLASS_NAME)) {
347
346
  throw error;
348
347
  }
349
- throw new GeneralError(Iota._CLASS_NAME, "dryRunFailed", undefined, Iota.extractPayloadError(error));
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(this._CLASS_NAME, "config.gasStation", config.gasStation);
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 this.reserveGas(config, gasBudget);
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 this.getSeed(config, vaultConnector, identity);
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 this.executeAndConfirmGasStationTransaction(config, client, gasReservation.reservationId, unsignedTxBytes, signature.signature, options);
419
+ return await Iota.executeAndConfirmGasStationTransaction(config, client, gasReservation.reservationId, unsignedTxBytes, signature.signature, options);
421
420
  }
422
421
  catch (error) {
423
- throw new GeneralError(Iota._CLASS_NAME, "gasStationTransactionFailed", undefined, Iota.extractPayloadError(error));
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(this._CLASS_NAME, "config.gasStation", config.gasStation);
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(this._CLASS_NAME, `${baseUrl}/v1/reserve_gas`, HttpMethod.POST, requestData, {
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(this._CLASS_NAME, "config.gasStation", config.gasStation);
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(this._CLASS_NAME, `${baseUrl}/v1/execute_tx`, HttpMethod.POST, requestData, {
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(this._CLASS_NAME, "config.gasStation", config.gasStation);
499
- const response = await this.executeGasStationTransaction(config, reservationId, transactionBytes, userSignature);
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
- export { Iota };
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 };