@twin.org/dlt-iota 0.0.2-next.9 → 0.0.3-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +1 -81
  2. package/dist/es/index.js +19 -0
  3. package/dist/es/index.js.map +1 -0
  4. package/dist/es/iota.js +508 -0
  5. package/dist/es/iota.js.map +1 -0
  6. package/dist/es/iotaSmartContractUtils.js +413 -0
  7. package/dist/es/iotaSmartContractUtils.js.map +1 -0
  8. package/dist/es/models/IAdminCapFields.js +4 -0
  9. package/dist/es/models/IAdminCapFields.js.map +1 -0
  10. package/dist/es/models/IContractData.js +4 -0
  11. package/dist/es/models/IContractData.js.map +1 -0
  12. package/dist/es/models/IGasReservationResult.js +2 -0
  13. package/dist/es/models/IGasReservationResult.js.map +1 -0
  14. package/dist/es/models/IGasStationConfig.js +4 -0
  15. package/dist/es/models/IGasStationConfig.js.map +1 -0
  16. package/dist/es/models/IGasStationExecuteResponse.js +4 -0
  17. package/dist/es/models/IGasStationExecuteResponse.js.map +1 -0
  18. package/dist/es/models/IGasStationReserveGasResponse.js +2 -0
  19. package/dist/es/models/IGasStationReserveGasResponse.js.map +1 -0
  20. package/dist/es/models/IGasStationReserveGasResult.js +2 -0
  21. package/dist/es/models/IGasStationReserveGasResult.js.map +1 -0
  22. package/dist/es/models/IIotaConfig.js +2 -0
  23. package/dist/es/models/IIotaConfig.js.map +1 -0
  24. package/dist/es/models/IIotaDryRun.js +2 -0
  25. package/dist/es/models/IIotaDryRun.js.map +1 -0
  26. package/dist/es/models/IIotaResponseOptions.js +2 -0
  27. package/dist/es/models/IIotaResponseOptions.js.map +1 -0
  28. package/dist/es/models/IMigrationStateFields.js +4 -0
  29. package/dist/es/models/IMigrationStateFields.js.map +1 -0
  30. package/dist/es/models/ISmartContractDeployments.js +2 -0
  31. package/dist/es/models/ISmartContractDeployments.js.map +1 -0
  32. package/dist/es/models/ISmartContractObject.js +4 -0
  33. package/dist/es/models/ISmartContractObject.js.map +1 -0
  34. package/dist/es/models/networkTypes.js +21 -0
  35. package/dist/es/models/networkTypes.js.map +1 -0
  36. package/dist/types/index.d.ts +16 -16
  37. package/dist/types/iota.d.ts +8 -4
  38. package/dist/types/iotaSmartContractUtils.d.ts +2 -2
  39. package/dist/types/models/IGasStationReserveGasResponse.d.ts +1 -1
  40. package/dist/types/models/IIotaConfig.d.ts +1 -1
  41. package/dist/types/models/ISmartContractDeployments.d.ts +2 -2
  42. package/docs/changelog.md +44 -0
  43. package/docs/reference/classes/Iota.md +11 -3
  44. package/docs/reference/classes/IotaSmartContractUtils.md +3 -3
  45. package/docs/reference/interfaces/IGasStationExecuteResponse.md +1 -1
  46. package/docs/reference/interfaces/IGasStationReserveGasResponse.md +1 -1
  47. package/locales/en.json +6 -2
  48. package/package.json +21 -12
  49. package/dist/cjs/index.cjs +0 -946
  50. package/dist/esm/index.mjs +0 -942
@@ -1,946 +0,0 @@
1
- 'use strict';
2
-
3
- var bcs = require('@iota/bcs');
4
- var client = require('@iota/iota-sdk/client');
5
- var ed25519 = require('@iota/iota-sdk/keypairs/ed25519');
6
- var transactions = require('@iota/iota-sdk/transactions');
7
- var core = require('@twin.org/core');
8
- var crypto = require('@twin.org/crypto');
9
- var web = require('@twin.org/web');
10
-
11
- // Copyright 2024 IOTA Stiftung.
12
- // SPDX-License-Identifier: Apache-2.0.
13
- /**
14
- * Class for performing operations on IOTA.
15
- */
16
- class Iota {
17
- /**
18
- * Default name for the mnemonic secret.
19
- */
20
- static DEFAULT_MNEMONIC_SECRET_NAME = "mnemonic";
21
- /**
22
- * Default name for the seed secret.
23
- */
24
- static DEFAULT_SEED_SECRET_NAME = "seed";
25
- /**
26
- * Default coin type.
27
- */
28
- static DEFAULT_COIN_TYPE = 4218;
29
- /**
30
- * Default scan range.
31
- */
32
- static DEFAULT_SCAN_RANGE = 1000;
33
- /**
34
- * Default inclusion timeout.
35
- */
36
- static DEFAULT_INCLUSION_TIMEOUT = 60;
37
- /**
38
- * Runtime name for the class.
39
- * @internal
40
- */
41
- static _CLASS_NAME = "Iota";
42
- /**
43
- * Create a new IOTA client.
44
- * @param config The configuration.
45
- * @returns The client instance.
46
- */
47
- 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);
51
- return new client.IotaClient(config.clientOptions);
52
- }
53
- /**
54
- * Create configuration using defaults where necessary.
55
- * @param config The configuration to populate.
56
- */
57
- static populateConfig(config) {
58
- core.Guards.object(Iota._CLASS_NAME, "config.clientOptions", config.clientOptions);
59
- config.vaultMnemonicId ??= Iota.DEFAULT_MNEMONIC_SECRET_NAME;
60
- config.vaultSeedId ??= Iota.DEFAULT_SEED_SECRET_NAME;
61
- config.coinType ??= Iota.DEFAULT_COIN_TYPE;
62
- config.inclusionTimeoutSeconds ??= Iota.DEFAULT_INCLUSION_TIMEOUT;
63
- }
64
- /**
65
- * Get addresses for the identity.
66
- * @param seed The seed to use for generating addresses.
67
- * @param coinType The coin type to use.
68
- * @param accountIndex The account index to get the addresses for.
69
- * @param startAddressIndex The start index for the addresses.
70
- * @param count The number of addresses to generate.
71
- * @param isInternal Whether the addresses are internal.
72
- * @returns The list of addresses.
73
- */
74
- 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);
79
- const addresses = [];
80
- for (let i = startAddressIndex; i < startAddressIndex + count; i++) {
81
- // Derive the keypair using the seed
82
- const keyPair = crypto.Bip44.keyPair(seed, crypto.KeyType.Ed25519, coinType ?? Iota.DEFAULT_COIN_TYPE, accountIndex, isInternal ?? false, i);
83
- const keypair = ed25519.Ed25519Keypair.fromSecretKey(keyPair.privateKey);
84
- addresses.push(keypair.getPublicKey().toIotaAddress());
85
- }
86
- return addresses;
87
- }
88
- /**
89
- * Get a key pair for the specified index.
90
- * @param seed The seed to use for generating the key pair.
91
- * @param coinType The coin type to use.
92
- * @param accountIndex The account index to get the key pair for.
93
- * @param addressIndex The address index to get the key pair for.
94
- * @param isInternal Whether the address is internal.
95
- * @returns The key pair containing private key and public key.
96
- */
97
- 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);
101
- const keyPair = crypto.Bip44.keyPair(seed, crypto.KeyType.Ed25519, coinType ?? Iota.DEFAULT_COIN_TYPE, accountIndex, isInternal ?? false, addressIndex);
102
- return keyPair;
103
- }
104
- /**
105
- * Prepare and post a transaction.
106
- * @param config The configuration.
107
- * @param vaultConnector The vault connector.
108
- * @param logging The logging component.
109
- * @param identity The identity of the user to access the vault keys.
110
- * @param client The client instance.
111
- * @param source The source address.
112
- * @param amount The amount to transfer.
113
- * @param recipient The recipient address.
114
- * @param options The transaction options.
115
- * @returns The transaction result.
116
- */
117
- static async prepareAndPostValueTransaction(config, vaultConnector, logging, identity, client, source, amount, recipient, options) {
118
- try {
119
- const txb = new transactions.Transaction();
120
- const [coin] = txb.splitCoins(txb.gas, [txb.pure.u64(amount)]);
121
- txb.transferObjects([coin], txb.pure.address(recipient));
122
- // Check if gas station configuration is present
123
- if (core.Is.object(config.gasStation)) {
124
- return await this.prepareAndPostGasStationTransaction(config, vaultConnector, identity, client, source, txb);
125
- }
126
- const result = await this.prepareAndPostTransaction(config, vaultConnector, logging, identity, client, source, txb, options);
127
- return result;
128
- }
129
- catch (error) {
130
- throw new core.GeneralError(Iota._CLASS_NAME, "valueTransactionFailed", undefined, Iota.extractPayloadError(error));
131
- }
132
- }
133
- /**
134
- * Prepare and post a transaction.
135
- * @param config The configuration.
136
- * @param vaultConnector The vault connector.
137
- * @param logging The logging component.
138
- * @param identity The identity of the user to access the vault keys.
139
- * @param client The client instance.
140
- * @param owner The owner of the address.
141
- * @param transaction The transaction to execute.
142
- * @param options The transaction options.
143
- * @returns The transaction response.
144
- */
145
- static async prepareAndPostTransaction(config, vaultConnector, logging, identity, client, owner, transaction, options) {
146
- // Check if gas station configuration is present
147
- if (core.Is.object(config.gasStation)) {
148
- return this.prepareAndPostGasStationTransaction(config, vaultConnector, identity, client, owner, transaction, options);
149
- }
150
- // Traditional transaction flow
151
- // Dry run the transaction if cost logging is enabled to get the gas and storage costs
152
- if (core.Is.stringValue(options?.dryRunLabel)) {
153
- await Iota.dryRunTransaction(client, logging, transaction, owner, options.dryRunLabel);
154
- }
155
- const seed = await this.getSeed(config, vaultConnector, identity);
156
- const addressKeyPair = Iota.findAddress(config.maxAddressScanRange ?? Iota.DEFAULT_SCAN_RANGE, config.coinType ?? Iota.DEFAULT_COIN_TYPE, seed, owner);
157
- const keypair = ed25519.Ed25519Keypair.fromSecretKey(addressKeyPair.privateKey);
158
- try {
159
- const response = await client.signAndExecuteTransaction({
160
- transaction,
161
- signer: keypair,
162
- requestType: "WaitForLocalExecution",
163
- options: {
164
- showEffects: options?.showEffects ?? true,
165
- showEvents: options?.showEvents ?? true,
166
- showObjectChanges: options?.showObjectChanges ?? true
167
- }
168
- });
169
- if (options?.waitForConfirmation ?? true) {
170
- // Wait for transaction to be indexed and available over API
171
- const confirmedTransaction = await Iota.waitForTransactionConfirmation(client, response.digest, config, {
172
- showEffects: options?.showEffects ?? true,
173
- showEvents: options?.showEvents ?? true,
174
- showObjectChanges: options?.showObjectChanges ?? true
175
- });
176
- return confirmedTransaction;
177
- }
178
- return response;
179
- }
180
- catch (error) {
181
- throw new core.GeneralError(Iota._CLASS_NAME, "transactionFailed", undefined, Iota.extractPayloadError(error));
182
- }
183
- }
184
- /**
185
- * Get the seed from the vault.
186
- * @param config The configuration to use.
187
- * @param vaultConnector The vault connector to use.
188
- * @param identity The identity of the user to access the vault keys.
189
- * @returns The seed.
190
- */
191
- static async getSeed(config, vaultConnector, identity) {
192
- try {
193
- const seedBase64 = await vaultConnector.getSecret(Iota.buildSeedKey(identity, config.vaultSeedId));
194
- return core.Converter.base64ToBytes(seedBase64);
195
- }
196
- catch { }
197
- const mnemonic = await vaultConnector.getSecret(Iota.buildMnemonicKey(identity, config.vaultMnemonicId));
198
- return crypto.Bip39.mnemonicToSeed(mnemonic);
199
- }
200
- /**
201
- * Find the address in the seed.
202
- * @param maxScanRange The maximum range to scan.
203
- * @param coinType The coin type to use.
204
- * @param seed The seed to use.
205
- * @param address The address to find.
206
- * @returns The address key pair.
207
- * @throws Error if the address is not found.
208
- */
209
- static findAddress(maxScanRange, coinType, seed, address) {
210
- for (let i = 0; i < maxScanRange; i++) {
211
- const addressKeyPair = crypto.Bip44.address(seed, crypto.KeyType.Ed25519, coinType, 0, false, i);
212
- if (addressKeyPair.address === address) {
213
- return addressKeyPair;
214
- }
215
- }
216
- throw new core.GeneralError(Iota._CLASS_NAME, "addressNotFound", { address });
217
- }
218
- /**
219
- * Extract error from SDK payload.
220
- * Errors from the IOTA SDK are usually not JSON strings but objects.
221
- * @param error The error to extract.
222
- * @returns The extracted error.
223
- */
224
- static extractPayloadError(error) {
225
- if (core.Is.object(error)) {
226
- if (!core.Is.empty(error.inner)) {
227
- error.inner = Iota.extractPayloadError(error.inner);
228
- }
229
- if (error.code === "InsufficientGas") {
230
- return new core.GeneralError(Iota._CLASS_NAME, "insufficientFunds");
231
- }
232
- else if (error.message?.startsWith("ErrorObject")) {
233
- const msg = /message: "(.*)"/.exec(error.message);
234
- if (msg && msg.length > 1) {
235
- error = msg[1];
236
- }
237
- }
238
- }
239
- const baseError = core.BaseError.fromError(error);
240
- if (baseError.name === "Base" && !core.Is.stringValue(baseError.source)) {
241
- baseError.name = "IOTA";
242
- baseError.source = Iota._CLASS_NAME;
243
- }
244
- return baseError;
245
- }
246
- /**
247
- * Get the key for storing the mnemonic.
248
- * @param identity The identity to use.
249
- * @param vaultMnemonicId The mnemonic ID to use.
250
- * @returns The mnemonic key.
251
- */
252
- static buildMnemonicKey(identity, vaultMnemonicId) {
253
- return `${identity}/${vaultMnemonicId ?? Iota.DEFAULT_MNEMONIC_SECRET_NAME}`;
254
- }
255
- /**
256
- * Get the key for storing the seed.
257
- * @param identity The identity to use.
258
- * @param vaultSeedId The seed ID to use.
259
- * @returns The seed key.
260
- */
261
- static buildSeedKey(identity, vaultSeedId) {
262
- return `${identity}/${vaultSeedId ?? Iota.DEFAULT_SEED_SECRET_NAME}`;
263
- }
264
- /**
265
- * Check if the package exists on the network.
266
- * @param client The client to use.
267
- * @param packageId The package ID to check.
268
- * @returns True if the package exists, false otherwise.
269
- */
270
- static async packageExistsOnNetwork(client, packageId) {
271
- try {
272
- const packageObject = await client.getObject({
273
- id: packageId,
274
- options: {
275
- showType: true
276
- }
277
- });
278
- if ("error" in packageObject) {
279
- if (packageObject?.error?.code === "notExists") {
280
- return false;
281
- }
282
- throw new core.GeneralError(Iota._CLASS_NAME, "packageObjectError", {
283
- packageId,
284
- error: packageObject.error
285
- });
286
- }
287
- return true;
288
- }
289
- catch (error) {
290
- throw new core.GeneralError(Iota._CLASS_NAME, "packageNotFoundOnNetwork", {
291
- packageId,
292
- error: Iota.extractPayloadError(error)
293
- });
294
- }
295
- }
296
- /**
297
- * Dry run a transaction and log the results.
298
- * @param client The IOTA client.
299
- * @param logging The logging component.
300
- * @param txb The transaction to dry run.
301
- * @param sender The sender address.
302
- * @param operation The operation to log.
303
- * @returns void.
304
- */
305
- static async dryRunTransaction(client, logging, txb, sender, operation) {
306
- try {
307
- txb.setSender(sender);
308
- const builtTx = await txb.build({
309
- client,
310
- onlyTransactionKind: false
311
- });
312
- const dryRunResult = await client.dryRunTransactionBlock({
313
- transactionBlock: builtTx
314
- });
315
- if (dryRunResult.effects.status?.status !== "success") {
316
- throw new core.GeneralError(this._CLASS_NAME, "dryRunFailed", {
317
- error: dryRunResult.effects?.status?.error
318
- });
319
- }
320
- const result = {
321
- status: dryRunResult.effects.status.status,
322
- costs: {
323
- computationCost: dryRunResult.effects.gasUsed.computationCost,
324
- computationCostBurned: dryRunResult.effects.gasUsed.computationCostBurned,
325
- storageCost: dryRunResult.effects.gasUsed.storageCost,
326
- storageRebate: dryRunResult.effects.gasUsed.storageRebate,
327
- nonRefundableStorageFee: dryRunResult.effects.gasUsed.nonRefundableStorageFee
328
- },
329
- events: dryRunResult.events ?? [],
330
- balanceChanges: dryRunResult.balanceChanges ?? [],
331
- objectChanges: dryRunResult.objectChanges ?? []
332
- };
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
- }
345
- return result;
346
- }
347
- catch (error) {
348
- if (error instanceof core.GeneralError) {
349
- throw error;
350
- }
351
- throw new core.GeneralError(Iota._CLASS_NAME, "dryRunFailed", undefined, Iota.extractPayloadError(error));
352
- }
353
- }
354
- /**
355
- * Wait for a transaction to be indexed and available over the API.
356
- * @param client The IOTA client instance.
357
- * @param digest The digest of the transaction to wait for.
358
- * @param config The IOTA configuration.
359
- * @param options Additional options for the transaction query.
360
- * @param options.showEffects Whether to show effects.
361
- * @param options.showEvents Whether to show events.
362
- * @param options.showObjectChanges Whether to show object changes.
363
- * @returns The confirmed transaction response.
364
- */
365
- static async waitForTransactionConfirmation(client, digest, config, options) {
366
- const timeoutMs = (config.inclusionTimeoutSeconds ?? Iota.DEFAULT_INCLUSION_TIMEOUT) * 1000;
367
- return client.waitForTransaction({
368
- digest,
369
- timeout: timeoutMs,
370
- options: {
371
- showEffects: options?.showEffects ?? true,
372
- showEvents: options?.showEvents ?? true,
373
- showObjectChanges: options?.showObjectChanges ?? true
374
- }
375
- });
376
- }
377
- /**
378
- * Check if the error is an abort error.
379
- * @param error The error to check.
380
- * @param code The error code to check for.
381
- * @returns True if the error is an abort error, false otherwise.
382
- */
383
- static isAbortError(error, code) {
384
- const err = core.BaseError.fromError(error);
385
- if (core.Is.stringValue(err.properties?.error) && err.properties.error.startsWith("MoveAbort")) {
386
- if (core.Is.number(code)) {
387
- return err.properties.error.includes(code.toString());
388
- }
389
- return true;
390
- }
391
- return false;
392
- }
393
- /**
394
- * Prepare and post a transaction using gas station sponsoring.
395
- * @param config The configuration.
396
- * @param vaultConnector The vault connector.
397
- * @param identity The identity of the user to access the vault keys.
398
- * @param client The client instance.
399
- * @param owner The owner of the address.
400
- * @param transaction The transaction to execute.
401
- * @param options Response options including confirmation behavior.
402
- * @returns The transaction response.
403
- */
404
- static async prepareAndPostGasStationTransaction(config, vaultConnector, identity, client, owner, transaction, options) {
405
- core.Guards.object(this._CLASS_NAME, "config.gasStation", config.gasStation);
406
- try {
407
- // Reserve gas from the gas station
408
- const gasBudget = config.gasBudget ?? 50000000;
409
- const gasReservation = await this.reserveGas(config, gasBudget);
410
- // Set transaction parameters for sponsoring
411
- transaction.setSender(owner);
412
- transaction.setGasOwner(gasReservation.sponsorAddress);
413
- transaction.setGasPayment(gasReservation.gasCoins);
414
- transaction.setGasBudget(gasBudget);
415
- // Build and sign transaction
416
- const unsignedTxBytes = await transaction.build({ client });
417
- // Sign the transaction with the user's private key
418
- const seed = await this.getSeed(config, vaultConnector, identity);
419
- const addressKeyPair = Iota.findAddress(config.maxAddressScanRange ?? Iota.DEFAULT_SCAN_RANGE, config.coinType ?? Iota.DEFAULT_COIN_TYPE, seed, owner);
420
- const keypair = ed25519.Ed25519Keypair.fromSecretKey(addressKeyPair.privateKey);
421
- const signature = await keypair.signTransaction(unsignedTxBytes);
422
- return await this.executeAndConfirmGasStationTransaction(config, client, gasReservation.reservationId, unsignedTxBytes, signature.signature, options);
423
- }
424
- catch (error) {
425
- throw new core.GeneralError(Iota._CLASS_NAME, "gasStationTransactionFailed", undefined, Iota.extractPayloadError(error));
426
- }
427
- }
428
- /**
429
- * Reserve gas from the gas station.
430
- * @param config The configuration containing gas station settings.
431
- * @param gasBudget The gas budget to reserve.
432
- * @returns The gas reservation result.
433
- */
434
- static async reserveGas(config, gasBudget) {
435
- core.Guards.object(this._CLASS_NAME, "config.gasStation", config.gasStation);
436
- const requestData = {
437
- // eslint-disable-next-line camelcase
438
- gas_budget: gasBudget,
439
- // eslint-disable-next-line camelcase
440
- reserve_duration_secs: 30
441
- };
442
- 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, {
444
- headers: {
445
- Authorization: `Bearer ${config.gasStation.gasStationAuthToken}`
446
- }
447
- });
448
- const apiResponse = result.result;
449
- return {
450
- sponsorAddress: apiResponse.sponsor_address,
451
- reservationId: apiResponse.reservation_id,
452
- gasCoins: apiResponse.gas_coins
453
- };
454
- }
455
- /**
456
- * Execute a sponsored transaction through the gas station.
457
- * @param config The configuration containing gas station settings.
458
- * @param reservationId The reservation ID from gas reservation.
459
- * @param transactionBytes The unsigned transaction bytes.
460
- * @param userSignature The user's signature.
461
- * @returns The transaction response.
462
- */
463
- static async executeGasStationTransaction(config, reservationId, transactionBytes, userSignature) {
464
- core.Guards.object(this._CLASS_NAME, "config.gasStation", config.gasStation);
465
- const requestData = {
466
- // eslint-disable-next-line camelcase
467
- reservation_id: reservationId,
468
- // eslint-disable-next-line camelcase
469
- tx_bytes: bcs.toB64(transactionBytes),
470
- // eslint-disable-next-line camelcase
471
- user_sig: userSignature
472
- };
473
- 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, {
475
- headers: {
476
- Authorization: `Bearer ${config.gasStation.gasStationAuthToken}`
477
- }
478
- });
479
- const effectsData = result.effects;
480
- // Match IotaTransactionBlockResponse format
481
- return {
482
- digest: effectsData.transactionDigest,
483
- effects: effectsData,
484
- events: [],
485
- objectChanges: [],
486
- confirmedLocalExecution: true
487
- };
488
- }
489
- /**
490
- * Execute and confirm a gas station transaction.
491
- * @param config The configuration containing gas station settings.
492
- * @param client The IOTA client for confirmation.
493
- * @param reservationId The reservation ID from gas reservation.
494
- * @param transactionBytes The unsigned transaction bytes.
495
- * @param userSignature The user's signature.
496
- * @param options Response options including confirmation behavior.
497
- * @returns The transaction response (confirmed if waitForConfirmation is true).
498
- */
499
- 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);
502
- if (options?.waitForConfirmation ?? true) {
503
- const confirmedTransaction = await Iota.waitForTransactionConfirmation(client, response.digest, config, {
504
- showEffects: options?.showEffects ?? true,
505
- showEvents: options?.showEvents ?? true,
506
- showObjectChanges: options?.showObjectChanges ?? true
507
- });
508
- return confirmedTransaction;
509
- }
510
- return response;
511
- }
512
- }
513
-
514
- // Copyright 2024 IOTA Stiftung.
515
- // SPDX-License-Identifier: Apache-2.0.
516
- /**
517
- * Utility class providing common smart contract operations for IOTA-based contracts.
518
- * This class uses composition pattern to provide shared functionality without inheritance complexity.
519
- */
520
- class IotaSmartContractUtils {
521
- /**
522
- * Runtime name for the class.
523
- */
524
- static CLASS_NAME = "IotaSmartContractUtils";
525
- /**
526
- * Migrate a smart contract object to the current version using admin privileges.
527
- * This is a generic migration method that works with any IOTA smart contract.
528
- * @param config The IOTA configuration.
529
- * @param client The IOTA client instance.
530
- * @param vaultConnector The vault connector for key management.
531
- * @param walletConnector The wallet connector for address generation.
532
- * @param logging Optional logging component.
533
- * @param gasBudget The gas budget for the transaction.
534
- * @param identity The identity of the controller with admin privileges.
535
- * @param objectId The ID of the object to migrate.
536
- * @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
537
- * @param packageId The deployed package ID for the contract.
538
- * @param deploymentConfig The deployment configuration containing object IDs.
539
- * @param walletAddressIndex Optional wallet address index for the controller.
540
- * @returns Promise that resolves when migration is complete.
541
- */
542
- static async migrateSmartContract(config, client, vaultConnector, walletConnector, logging, gasBudget, identity, objectId, namespace, packageId, deploymentConfig, walletAddressIndex) {
543
- try {
544
- const txb = new transactions.Transaction();
545
- txb.setGasBudget(gasBudget);
546
- const moduleName = this.getModuleName(namespace);
547
- // Get admin address for the transaction
548
- const adminAddress = await this.getPackageControllerAddress(walletConnector, identity, walletAddressIndex);
549
- // Get the required object IDs from deployment config
550
- const { adminCapId, migrationStateId } = await this.getContractObjectIds(client, namespace, config.network, deploymentConfig, packageId, adminAddress);
551
- txb.moveCall({
552
- target: `${packageId}::${moduleName}::migrate_${moduleName}`,
553
- arguments: [txb.object(adminCapId), txb.object(migrationStateId), txb.object(objectId)]
554
- });
555
- const result = await Iota.prepareAndPostTransaction(config, vaultConnector, logging, identity, client, adminAddress, txb, {
556
- dryRunLabel: config.enableCostLogging ? "migrate_object" : undefined
557
- });
558
- if (result.effects?.status?.status !== "success") {
559
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "migrationFailed", {
560
- error: result.effects?.status?.error,
561
- objectId
562
- });
563
- }
564
- }
565
- catch (error) {
566
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "migrateSmartContractFailed", { objectId }, error);
567
- }
568
- }
569
- /**
570
- * Enable migration operations using admin privileges.
571
- * @param config The IOTA configuration.
572
- * @param client The IOTA client instance.
573
- * @param vaultConnector The vault connector for key management.
574
- * @param walletConnector The wallet connector for address generation.
575
- * @param logging Optional logging component.
576
- * @param gasBudget The gas budget for the transaction.
577
- * @param identity The identity of the controller with admin privileges.
578
- * @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
579
- * @param packageId The deployed package ID for the contract.
580
- * @param deploymentConfig The deployment configuration containing object IDs.
581
- * @param walletAddressIndex Optional wallet address index for the controller.
582
- * @returns Promise that resolves when migration is enabled.
583
- */
584
- static async enableMigration(config, client, vaultConnector, walletConnector, logging, gasBudget, identity, namespace, packageId, deploymentConfig, walletAddressIndex) {
585
- try {
586
- const txb = new transactions.Transaction();
587
- txb.setGasBudget(gasBudget);
588
- const moduleName = this.getModuleName(namespace);
589
- // Get admin address for the transaction
590
- const adminAddress = await this.getPackageControllerAddress(walletConnector, identity, walletAddressIndex);
591
- // Get the required object IDs from deployment config
592
- const { adminCapId, migrationStateId } = await this.getContractObjectIds(client, namespace, config.network, deploymentConfig, packageId, adminAddress);
593
- txb.moveCall({
594
- target: `${packageId}::${moduleName}::enable_migration`,
595
- arguments: [txb.object(adminCapId), txb.object(migrationStateId)]
596
- });
597
- const result = await Iota.prepareAndPostTransaction(config, vaultConnector, logging, identity, client, adminAddress, txb, {
598
- dryRunLabel: config.enableCostLogging ? "enable_migration" : undefined
599
- });
600
- if (result.effects?.status?.status !== "success") {
601
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "enableMigrationFailed", {
602
- error: result.effects?.status?.error
603
- });
604
- }
605
- }
606
- catch (error) {
607
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "enableMigrationFailed", undefined, error);
608
- }
609
- }
610
- /**
611
- * Disable migration operations using admin privileges.
612
- * @param config The IOTA configuration.
613
- * @param client The IOTA client instance.
614
- * @param vaultConnector The vault connector for key management.
615
- * @param walletConnector The wallet connector for address generation.
616
- * @param logging Optional logging component.
617
- * @param gasBudget The gas budget for the transaction.
618
- * @param identity The identity of the controller with admin privileges.
619
- * @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
620
- * @param packageId The deployed package ID for the contract.
621
- * @param deploymentConfig The deployment configuration containing object IDs.
622
- * @param walletAddressIndex Optional wallet address index for the controller.
623
- * @returns Promise that resolves when migration is disabled.
624
- */
625
- static async disableMigration(config, client, vaultConnector, walletConnector, logging, gasBudget, identity, namespace, packageId, deploymentConfig, walletAddressIndex) {
626
- try {
627
- const txb = new transactions.Transaction();
628
- txb.setGasBudget(gasBudget);
629
- const moduleName = this.getModuleName(namespace);
630
- // Get admin address for the transaction
631
- const adminAddress = await this.getPackageControllerAddress(walletConnector, identity, walletAddressIndex);
632
- // Get the required object IDs from deployment config
633
- const { adminCapId, migrationStateId } = await this.getContractObjectIds(client, namespace, config.network, deploymentConfig, packageId, adminAddress);
634
- txb.moveCall({
635
- target: `${packageId}::${moduleName}::disable_migration`,
636
- arguments: [txb.object(adminCapId), txb.object(migrationStateId)]
637
- });
638
- const result = await Iota.prepareAndPostTransaction(config, vaultConnector, logging, identity, client, adminAddress, txb, {
639
- dryRunLabel: config.enableCostLogging ? "disable_migration" : undefined
640
- });
641
- if (result.effects?.status?.status !== "success") {
642
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "disableMigrationFailed", {
643
- error: result.effects?.status?.error
644
- });
645
- }
646
- }
647
- catch (error) {
648
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "disableMigrationFailed", undefined, error);
649
- }
650
- }
651
- /**
652
- * Check if migration is currently active for a smart contract.
653
- * @param config The IOTA configuration.
654
- * @param client The IOTA client instance.
655
- * @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
656
- * @param packageId The deployed package ID for the contract.
657
- * @param deploymentConfig The deployment configuration containing object IDs.
658
- * @param identity The identity for MigrationState discovery.
659
- * @param walletConnector The wallet connector for address generation.
660
- * @param walletAddressIndex Optional wallet address index.
661
- * @returns True if migration is enabled, false otherwise.
662
- */
663
- static async isMigrationActive(config, client, namespace, packageId, deploymentConfig, identity, walletConnector, walletAddressIndex) {
664
- try {
665
- // Get admin address for discovery
666
- const adminAddress = await this.getPackageControllerAddress(walletConnector, identity, walletAddressIndex);
667
- // Get the migration state ID
668
- const { migrationStateId } = await this.getContractObjectIds(client, namespace, config.network, deploymentConfig, packageId, adminAddress);
669
- const migrationStateResponse = await client.getObject({
670
- id: migrationStateId,
671
- options: {
672
- showContent: true,
673
- showType: true
674
- }
675
- });
676
- if (!migrationStateResponse.data?.content) {
677
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "migrationStateNotReadable", {
678
- migrationStateId
679
- });
680
- }
681
- const content = migrationStateResponse.data.content;
682
- if (content.dataType === "moveObject" && core.Is.objectValue(content.fields)) {
683
- const fields = content.fields;
684
- return core.Is.boolean(fields.enabled) ? fields.enabled : false;
685
- }
686
- return false;
687
- }
688
- catch (error) {
689
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "isMigrationActiveFailed", undefined, error);
690
- }
691
- }
692
- /**
693
- * Get the current contract version from the deployed smart contract.
694
- * @param config The IOTA configuration.
695
- * @param client The IOTA client instance.
696
- * @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
697
- * @param packageId The deployed package ID for the contract.
698
- * @param identity The identity for package controller address.
699
- * @param walletConnector The wallet connector for address generation.
700
- * @param walletAddressIndex Optional wallet address index.
701
- * @returns The current version number of the contract.
702
- */
703
- static async getCurrentContractVersion(config, client, namespace, packageId, identity, walletConnector, walletAddressIndex) {
704
- try {
705
- const tx = new transactions.Transaction();
706
- const moduleName = this.getModuleName(namespace);
707
- tx.moveCall({
708
- target: `${packageId}::${moduleName}::get_current_version`,
709
- arguments: []
710
- });
711
- const controllerAddress = await this.getPackageControllerAddress(walletConnector, identity, walletAddressIndex);
712
- const result = await client.devInspectTransactionBlock({
713
- sender: controllerAddress,
714
- transactionBlock: tx
715
- });
716
- if (core.Is.arrayValue(result.results) &&
717
- core.Is.object(result.results[0]) &&
718
- core.Is.arrayValue(result.results[0].returnValues)) {
719
- const versionBytes = result.results[0].returnValues[0];
720
- // Convert to Uint8Array if it's a regular array
721
- const byteData = versionBytes[0];
722
- if (!core.Is.arrayValue(byteData) && !core.Is.uint8Array(byteData)) {
723
- throw new core.GeneralError(this.CLASS_NAME, "invalidVersionData");
724
- }
725
- // Convert to Uint8Array for BCS parsing
726
- const uint8Data = core.Is.uint8Array(byteData) ? byteData : new Uint8Array(byteData);
727
- // The version is returned as a u64, decode it from bytes using BCS
728
- // IOTA Move contracts return data in BCS format
729
- const version = Number(bcs.bcs.u64().parse(uint8Data));
730
- return version;
731
- }
732
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "getCurrentContractVersionNoData", {
733
- resultExists: core.Is.arrayValue(result.results),
734
- resultLength: core.Is.arrayValue(result.results) ? result.results.length : 0,
735
- hasReturnValues: core.Is.arrayValue(result.results?.[0]?.returnValues)
736
- });
737
- }
738
- catch (error) {
739
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "getCurrentContractVersionFailed", undefined, error);
740
- }
741
- }
742
- /**
743
- * Validate that an object version is compatible with the current contract.
744
- * @param config The IOTA configuration.
745
- * @param client The IOTA client instance.
746
- * @param namespace The contract namespace (e.g., "nft", "verifiable_storage").
747
- * @param packageId The deployed package ID for the contract.
748
- * @param identity The identity for version checking.
749
- * @param objectId The object ID to validate.
750
- * @param walletConnector The wallet connector for address generation.
751
- * @param versionExtractor Function to extract version from object content.
752
- * @param walletAddressIndex Optional wallet address index.
753
- * @returns True if the object version is compatible, false otherwise.
754
- */
755
- static async validateObjectVersion(config, client, namespace, packageId, identity, objectId, walletConnector, versionExtractor, walletAddressIndex) {
756
- try {
757
- // Get current contract version
758
- const currentVersion = await this.getCurrentContractVersion(config, client, namespace, packageId, identity, walletConnector, walletAddressIndex);
759
- // Get object version
760
- const objectResponse = await client.getObject({
761
- id: objectId,
762
- options: {
763
- showContent: true,
764
- showType: true
765
- }
766
- });
767
- if (!objectResponse.data?.content) {
768
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "objectNotReadable", {
769
- objectId
770
- });
771
- }
772
- const content = objectResponse.data.content;
773
- if (content.dataType === "moveObject" && core.Is.objectValue(content.fields)) {
774
- const objectVersion = versionExtractor(content);
775
- return objectVersion <= currentVersion;
776
- }
777
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "objectInvalidFormat", {
778
- objectId,
779
- content
780
- });
781
- }
782
- catch (error) {
783
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "validateObjectVersionFailed", { objectId }, error);
784
- }
785
- }
786
- /**
787
- * Get the module name for a given namespace.
788
- * @param namespace The contract namespace.
789
- * @returns The module name in snake_case format.
790
- * @internal
791
- */
792
- static getModuleName(namespace) {
793
- // Convert namespace to snake_case for module name
794
- return core.StringHelper.snakeCase(namespace);
795
- }
796
- /**
797
- * Get the package controller address for transactions.
798
- * @param walletConnector The wallet connector for address generation.
799
- * @param identity The identity to use.
800
- * @param addressIndex Optional address index to use.
801
- * @returns The controller address.
802
- * @internal
803
- */
804
- static async getPackageControllerAddress(walletConnector, identity, addressIndex = 0) {
805
- const addresses = await walletConnector.getAddresses(identity, 0, addressIndex, 1);
806
- return addresses[0];
807
- }
808
- /**
809
- * Get contract object IDs (AdminCap and MigrationState) from deployment config with fallback discovery.
810
- * @param client The IOTA client instance.
811
- * @param namespace The contract namespace.
812
- * @param network The network name.
813
- * @param deploymentConfig The deployment configuration.
814
- * @param packageId The package ID.
815
- * @param adminAddress The admin address for object discovery.
816
- * @returns Object containing adminCapId and migrationStateId.
817
- * @internal
818
- */
819
- static async getContractObjectIds(client, namespace, network, deploymentConfig, packageId, adminAddress) {
820
- try {
821
- // First try to load from deployment JSON
822
- const networkConfig = deploymentConfig[network];
823
- if (core.Is.objectValue(networkConfig)) {
824
- const migrationStateId = networkConfig.migrationStateId;
825
- // AdminCap must be discovered from blockchain (not stored in JSON)
826
- const adminCapId = await this.discoverAdminCap(client, packageId, namespace, adminAddress);
827
- if (core.Is.stringValue(migrationStateId) && core.Is.stringValue(adminCapId)) {
828
- return { adminCapId, migrationStateId };
829
- }
830
- }
831
- // Fallback: discover both from blockchain
832
- return await this.discoverContractObjectsFromBlockchain(client, packageId, namespace, adminAddress);
833
- }
834
- catch (error) {
835
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "getContractObjectIdsFailed", { namespace, network, packageId }, error);
836
- }
837
- }
838
- /**
839
- * Discover AdminCap object from the blockchain.
840
- * @param client The IOTA client instance.
841
- * @param packageId The package ID.
842
- * @param namespace The contract namespace.
843
- * @param adminAddress The admin address.
844
- * @returns The AdminCap object ID.
845
- * @internal
846
- */
847
- static async discoverAdminCap(client, packageId, namespace, adminAddress) {
848
- const adminCapType = `${packageId}::${this.getModuleName(namespace)}::AdminCap`;
849
- const adminCapObjects = await client.getOwnedObjects({
850
- owner: adminAddress,
851
- filter: {
852
- StructType: adminCapType
853
- },
854
- options: {
855
- showContent: true,
856
- showType: true
857
- }
858
- });
859
- if (core.Is.arrayValue(adminCapObjects.data) && adminCapObjects.data.length > 0) {
860
- const adminCapObject = adminCapObjects.data[0];
861
- if (core.Is.stringValue(adminCapObject.data?.objectId)) {
862
- return adminCapObject.data.objectId;
863
- }
864
- }
865
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "adminCapNotFound", {
866
- adminCapType,
867
- adminAddress
868
- });
869
- }
870
- /**
871
- * Discover contract objects from blockchain as fallback.
872
- * @param client The IOTA client instance.
873
- * @param packageId The package ID.
874
- * @param namespace The contract namespace.
875
- * @param adminAddress The admin address.
876
- * @returns Object containing adminCapId and migrationStateId.
877
- * @internal
878
- */
879
- static async discoverContractObjectsFromBlockchain(client, packageId, namespace, adminAddress) {
880
- // Discover AdminCap
881
- const adminCapId = await this.discoverAdminCap(client, packageId, namespace, adminAddress);
882
- // Discover MigrationState through transaction history
883
- const migrationStateType = `${packageId}::${this.getModuleName(namespace)}::MigrationState`;
884
- const transactions = await client.queryTransactionBlocks({
885
- filter: {
886
- FromAddress: adminAddress
887
- },
888
- options: {
889
- showObjectChanges: true,
890
- showEffects: true
891
- },
892
- limit: 20,
893
- order: "descending"
894
- });
895
- // Look for MigrationState object creation in transaction history
896
- let migrationStateId;
897
- for (const tx of transactions.data) {
898
- const objectChanges = tx.objectChanges;
899
- if (core.Is.arrayValue(objectChanges)) {
900
- for (const change of objectChanges) {
901
- if ((change.type === "created" || change.type === "mutated") &&
902
- "objectType" in change &&
903
- change.objectType === migrationStateType) {
904
- migrationStateId = change.objectId;
905
- break;
906
- }
907
- }
908
- if (migrationStateId) {
909
- break;
910
- }
911
- }
912
- }
913
- if (!migrationStateId) {
914
- throw new core.GeneralError(IotaSmartContractUtils.CLASS_NAME, "migrationStateNotFound", {
915
- migrationStateType,
916
- adminAddress
917
- });
918
- }
919
- return { adminCapId, migrationStateId };
920
- }
921
- }
922
-
923
- // Copyright 2024 IOTA Stiftung.
924
- // SPDX-License-Identifier: Apache-2.0.
925
- /**
926
- * Network types supported for deployment
927
- */
928
- // eslint-disable-next-line @typescript-eslint/naming-convention
929
- const NetworkTypes = {
930
- /**
931
- * Testnet.
932
- */
933
- Testnet: "testnet",
934
- /**
935
- * Devnet.
936
- */
937
- Devnet: "devnet",
938
- /**
939
- * Mainnet.
940
- */
941
- Mainnet: "mainnet"
942
- };
943
-
944
- exports.Iota = Iota;
945
- exports.IotaSmartContractUtils = IotaSmartContractUtils;
946
- exports.NetworkTypes = NetworkTypes;