@twin.org/dlt-iota 0.0.3-next.1 → 0.0.3-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.
Files changed (52) hide show
  1. package/README.md +4 -6
  2. package/dist/es/index.js +5 -0
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/es/iota.js +336 -81
  5. package/dist/es/iota.js.map +1 -1
  6. package/dist/es/iotaIdentityUtils.js +84 -0
  7. package/dist/es/iotaIdentityUtils.js.map +1 -0
  8. package/dist/es/iotaSmartContractUtils.js +29 -26
  9. package/dist/es/iotaSmartContractUtils.js.map +1 -1
  10. package/dist/es/models/IIotaClient.js +2 -0
  11. package/dist/es/models/IIotaClient.js.map +1 -0
  12. package/dist/es/models/IIotaConfig.js.map +1 -1
  13. package/dist/es/models/IIotaControllerCapInfo.js +4 -0
  14. package/dist/es/models/IIotaControllerCapInfo.js.map +1 -0
  15. package/dist/es/models/IIotaTransaction.js +2 -0
  16. package/dist/es/models/IIotaTransaction.js.map +1 -0
  17. package/dist/es/models/IIotaTransactionBlockResponse.js +2 -0
  18. package/dist/es/models/IIotaTransactionBlockResponse.js.map +1 -0
  19. package/dist/types/index.d.ts +5 -0
  20. package/dist/types/iota.d.ts +99 -57
  21. package/dist/types/iotaIdentityUtils.d.ts +23 -0
  22. package/dist/types/iotaSmartContractUtils.d.ts +18 -16
  23. package/dist/types/models/IIotaClient.d.ts +5 -0
  24. package/dist/types/models/IIotaConfig.d.ts +5 -0
  25. package/dist/types/models/IIotaControllerCapInfo.d.ts +16 -0
  26. package/dist/types/models/IIotaTransaction.d.ts +5 -0
  27. package/dist/types/models/IIotaTransactionBlockResponse.d.ts +5 -0
  28. package/docs/changelog.md +185 -83
  29. package/docs/examples.md +413 -1
  30. package/docs/reference/classes/Iota.md +358 -172
  31. package/docs/reference/classes/IotaIdentityUtils.md +65 -0
  32. package/docs/reference/classes/IotaSmartContractUtils.md +70 -52
  33. package/docs/reference/index.md +5 -0
  34. package/docs/reference/interfaces/IAdminCapFields.md +1 -1
  35. package/docs/reference/interfaces/IContractData.md +10 -10
  36. package/docs/reference/interfaces/IGasReservationResult.md +3 -3
  37. package/docs/reference/interfaces/IGasStationConfig.md +2 -2
  38. package/docs/reference/interfaces/IGasStationExecuteResponse.md +3 -3
  39. package/docs/reference/interfaces/IGasStationReserveGasResponse.md +3 -3
  40. package/docs/reference/interfaces/IGasStationReserveGasResult.md +3 -3
  41. package/docs/reference/interfaces/IIotaConfig.md +32 -18
  42. package/docs/reference/interfaces/IIotaControllerCapInfo.md +22 -0
  43. package/docs/reference/interfaces/IIotaDryRun.md +5 -5
  44. package/docs/reference/interfaces/IIotaResponseOptions.md +4 -4
  45. package/docs/reference/interfaces/IMigrationStateFields.md +2 -2
  46. package/docs/reference/interfaces/ISmartContractObject.md +2 -2
  47. package/docs/reference/type-aliases/IIotaClient.md +5 -0
  48. package/docs/reference/type-aliases/IIotaTransaction.md +5 -0
  49. package/docs/reference/type-aliases/IIotaTransactionBlockResponse.md +5 -0
  50. package/docs/reference/variables/NetworkTypes.md +3 -3
  51. package/locales/en.json +9 -1
  52. package/package.json +7 -6
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TWIN DLT IOTA
2
2
 
3
- DLT helpers for use with IOTA.
3
+ This package provides utilities for integrating applications with IOTA distributed ledger capabilities, including client construction, transaction submission, sponsored transaction support, and smart contract migration helpers. It is intended for services that need a reliable and repeatable approach to ledger operations without rebuilding common primitives.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,14 +8,12 @@ DLT helpers for use with IOTA.
8
8
  npm install @twin.org/dlt-iota
9
9
  ```
10
10
 
11
- ## Testing
11
+ ## Docker
12
12
 
13
- The tests developed are functional tests and need an instance of the IOTA Gas Station and Redis up and running.
14
-
15
- The simplest way to set up the testing environment using our unified container:
13
+ To perform testing of this component it may be necessary to launch a local instance of the gas station to communicate with.
16
14
 
17
15
  ```shell
18
- docker run -d --name twin-gas-station-test -p 9527:9527 -p 6379:6379 -p 9184:9184 twinfoundation/twin-gas-station-test:latest
16
+ docker run -d --name twin-gas-station-test -p 6379:6379 -p 9527:9527 -p 9184:9184 -e IOTA_NODE_URL="https://api.testnet.iota.cafe" -e GAS_STATION_AUTH="qEyCL6d9BKKFl/tfDGAKeGFkhUlf7FkqiGV7Xw4JUsI=" twinfoundation/twin-gas-station-test:latest
19
17
  ```
20
18
 
21
19
  ## Examples
package/dist/es/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Copyright 2024 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
3
  export * from "./iota.js";
4
+ export * from "./iotaIdentityUtils.js";
4
5
  export * from "./iotaSmartContractUtils.js";
5
6
  export * from "./models/IAdminCapFields.js";
6
7
  export * from "./models/IContractData.js";
@@ -9,6 +10,10 @@ export * from "./models/IGasStationConfig.js";
9
10
  export * from "./models/IGasStationExecuteResponse.js";
10
11
  export * from "./models/IGasStationReserveGasResponse.js";
11
12
  export * from "./models/IGasStationReserveGasResult.js";
13
+ export * from "./models/IIotaClient.js";
14
+ export * from "./models/IIotaControllerCapInfo.js";
15
+ export * from "./models/IIotaTransaction.js";
16
+ export * from "./models/IIotaTransactionBlockResponse.js";
12
17
  export * from "./models/IIotaConfig.js";
13
18
  export * from "./models/IIotaDryRun.js";
14
19
  export * from "./models/IIotaResponseOptions.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,WAAW,CAAC;AAC1B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wCAAwC,CAAC;AACvD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,yCAAyC,CAAC;AACxD,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,uCAAuC,CAAC;AACtD,cAAc,kCAAkC,CAAC;AACjD,cAAc,0BAA0B,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./iota.js\";\nexport * from \"./iotaSmartContractUtils.js\";\nexport * from \"./models/IAdminCapFields.js\";\nexport * from \"./models/IContractData.js\";\nexport * from \"./models/IGasReservationResult.js\";\nexport * from \"./models/IGasStationConfig.js\";\nexport * from \"./models/IGasStationExecuteResponse.js\";\nexport * from \"./models/IGasStationReserveGasResponse.js\";\nexport * from \"./models/IGasStationReserveGasResult.js\";\nexport * from \"./models/IIotaConfig.js\";\nexport * from \"./models/IIotaDryRun.js\";\nexport * from \"./models/IIotaResponseOptions.js\";\nexport * from \"./models/IMigrationStateFields.js\";\nexport * from \"./models/ISmartContractDeployments.js\";\nexport * from \"./models/ISmartContractObject.js\";\nexport * from \"./models/networkTypes.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,WAAW,CAAC;AAC1B,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wCAAwC,CAAC;AACvD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,yCAAyC,CAAC;AACxD,cAAc,yBAAyB,CAAC;AACxC,cAAc,oCAAoC,CAAC;AACnD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2CAA2C,CAAC;AAC1D,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,uCAAuC,CAAC;AACtD,cAAc,kCAAkC,CAAC;AACjD,cAAc,0BAA0B,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./iota.js\";\nexport * from \"./iotaIdentityUtils.js\";\nexport * from \"./iotaSmartContractUtils.js\";\nexport * from \"./models/IAdminCapFields.js\";\nexport * from \"./models/IContractData.js\";\nexport * from \"./models/IGasReservationResult.js\";\nexport * from \"./models/IGasStationConfig.js\";\nexport * from \"./models/IGasStationExecuteResponse.js\";\nexport * from \"./models/IGasStationReserveGasResponse.js\";\nexport * from \"./models/IGasStationReserveGasResult.js\";\nexport * from \"./models/IIotaClient.js\";\nexport * from \"./models/IIotaControllerCapInfo.js\";\nexport * from \"./models/IIotaTransaction.js\";\nexport * from \"./models/IIotaTransactionBlockResponse.js\";\nexport * from \"./models/IIotaConfig.js\";\nexport * from \"./models/IIotaDryRun.js\";\nexport * from \"./models/IIotaResponseOptions.js\";\nexport * from \"./models/IMigrationStateFields.js\";\nexport * from \"./models/ISmartContractDeployments.js\";\nexport * from \"./models/ISmartContractObject.js\";\nexport * from \"./models/networkTypes.js\";\n"]}
package/dist/es/iota.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // Copyright 2024 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
3
  import { IotaClient } from "@iota/iota-sdk/client";
4
+ import { requestIotaFromFaucetV0 } from "@iota/iota-sdk/faucet";
4
5
  import { Ed25519Keypair } from "@iota/iota-sdk/keypairs/ed25519";
5
6
  import { Transaction } from "@iota/iota-sdk/transactions";
6
- import { BaseError, Converter, GeneralError, Guards, Is, StringHelper } from "@twin.org/core";
7
- import { Bip39, Bip44, KeyType } from "@twin.org/crypto";
7
+ import { BaseError, Coerce, Converter, GeneralError, Guards, Is, StringHelper } from "@twin.org/core";
8
+ import { Bip39, Bip44, Blake2b, KeyType } from "@twin.org/crypto";
8
9
  import { FetchHelper, HttpMethod } from "@twin.org/web";
9
10
  /**
10
11
  * Class for performing operations on IOTA.
@@ -22,18 +23,35 @@ export class Iota {
22
23
  * Default coin type.
23
24
  */
24
25
  static DEFAULT_COIN_TYPE = 4218;
26
+ /**
27
+ * Runtime name for the class.
28
+ */
29
+ static CLASS_NAME = "Iota";
25
30
  /**
26
31
  * Default scan range.
32
+ * @internal
33
+ */
34
+ static _DEFAULT_SCAN_RANGE = 1000;
35
+ /**
36
+ * Default pre-calculation chunk size.
37
+ * @internal
27
38
  */
28
- static DEFAULT_SCAN_RANGE = 1000;
39
+ static _PRE_CALC_CHUNK_SIZE = 25;
29
40
  /**
30
41
  * Default inclusion timeout.
42
+ * @internal
31
43
  */
32
- static DEFAULT_INCLUSION_TIMEOUT = 60;
44
+ static _DEFAULT_INCLUSION_TIMEOUT = 60;
33
45
  /**
34
- * Runtime name for the class.
46
+ * Default gas budget for all transactions (including sponsored and direct).
47
+ * @internal
35
48
  */
36
- static CLASS_NAME = "Iota";
49
+ static _DEFAULT_GAS_BUDGET = 50000000;
50
+ /**
51
+ * Default gas reservation duration.
52
+ * @internal
53
+ */
54
+ static _DEFAULT_GAS_RESERVATION_DURATION = 60;
37
55
  /**
38
56
  * Create a new IOTA client.
39
57
  * @param config The configuration.
@@ -54,47 +72,115 @@ export class Iota {
54
72
  config.vaultMnemonicId ??= Iota.DEFAULT_MNEMONIC_SECRET_NAME;
55
73
  config.vaultSeedId ??= Iota.DEFAULT_SEED_SECRET_NAME;
56
74
  config.coinType ??= Iota.DEFAULT_COIN_TYPE;
57
- config.inclusionTimeoutSeconds ??= Iota.DEFAULT_INCLUSION_TIMEOUT;
75
+ config.inclusionTimeoutSeconds ??= Iota._DEFAULT_INCLUSION_TIMEOUT;
76
+ }
77
+ /**
78
+ * Store a mnemonic in the vault, derive and store the seed, and pre-cache the first keypair chunk.
79
+ * @param vaultConnector The vault connector.
80
+ * @param config The configuration.
81
+ * @param identity The identity of the user to access the vault keys.
82
+ * @param mnemonic The mnemonic to store, if undefined a new one will be generated and returned.
83
+ * @param accountIndex The account index to pre-cache.
84
+ * @returns The mnemonic that was stored.
85
+ */
86
+ static async storeMnemonic(vaultConnector, config, identity, mnemonic, accountIndex) {
87
+ Guards.object(Iota.CLASS_NAME, "vaultConnector", vaultConnector);
88
+ Guards.object(Iota.CLASS_NAME, "config", config);
89
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
90
+ Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
91
+ const mnemonicToStore = mnemonic ?? Bip39.randomMnemonic();
92
+ await vaultConnector.setSecret(Iota.buildMnemonicKey(identity, config.vaultMnemonicId), mnemonicToStore);
93
+ const seed = Bip39.mnemonicToSeed(mnemonicToStore);
94
+ await vaultConnector.setSecret(Iota.buildSeedKey(identity, config.vaultSeedId), Converter.bytesToBase64(seed));
95
+ await Iota.buildKeyPairRange(vaultConnector, config, identity, accountIndex, false, 0);
96
+ return mnemonicToStore;
97
+ }
98
+ /**
99
+ * Derive an address from a public key.
100
+ * @param publicKey The public key to derive the address from.
101
+ * @returns The derived address.
102
+ */
103
+ static publicKeyToAddress(publicKey) {
104
+ return Converter.bytesToHex(Blake2b.sum256(publicKey), true);
105
+ }
106
+ /**
107
+ * Get address for the identity.
108
+ * @param vaultConnector The vault connector.
109
+ * @param config The configuration.
110
+ * @param identity The identity of the user to access the vault keys.
111
+ * @param accountIndex The account index to get the addresses for.
112
+ * @param startAddressIndex The start index for the addresses.
113
+ * @param isInternal Whether the addresses are internal.
114
+ * @returns The address.
115
+ */
116
+ static async getAddress(vaultConnector, config, identity, accountIndex, startAddressIndex, isInternal) {
117
+ const addresses = await Iota.getAddresses(vaultConnector, config, identity, accountIndex, startAddressIndex, 1, isInternal);
118
+ return addresses[0];
58
119
  }
59
120
  /**
60
121
  * Get addresses for the identity.
61
- * @param seed The seed to use for generating addresses.
62
- * @param coinType The coin type to use.
122
+ * @param vaultConnector The vault connector.
123
+ * @param config The configuration.
124
+ * @param identity The identity of the user to access the vault keys.
63
125
  * @param accountIndex The account index to get the addresses for.
64
126
  * @param startAddressIndex The start index for the addresses.
65
127
  * @param count The number of addresses to generate.
66
128
  * @param isInternal Whether the addresses are internal.
67
129
  * @returns The list of addresses.
68
130
  */
69
- static getAddresses(seed, coinType, accountIndex, startAddressIndex, count, isInternal) {
70
- Guards.integer(Iota.CLASS_NAME, "coinType", coinType);
131
+ static async getAddresses(vaultConnector, config, identity, accountIndex, startAddressIndex, count, isInternal) {
132
+ Guards.object(Iota.CLASS_NAME, "vaultConnector", vaultConnector);
133
+ Guards.object(Iota.CLASS_NAME, "config", config);
134
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
71
135
  Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
72
136
  Guards.integer(Iota.CLASS_NAME, "startAddressIndex", startAddressIndex);
73
137
  Guards.integer(Iota.CLASS_NAME, "count", count);
74
138
  const addresses = [];
75
- for (let i = startAddressIndex; i < startAddressIndex + count; i++) {
76
- // Derive the keypair using the seed
77
- const keyPair = Bip44.keyPair(seed, KeyType.Ed25519, coinType ?? Iota.DEFAULT_COIN_TYPE, accountIndex, isInternal ?? false, i);
78
- const keypair = Ed25519Keypair.fromSecretKey(keyPair.privateKey);
79
- addresses.push(keypair.getPublicKey().toIotaAddress());
139
+ const internal = isInternal ?? false;
140
+ let currentIndex = startAddressIndex;
141
+ while (addresses.length < count) {
142
+ const chunkStart = Math.floor(currentIndex / Iota._PRE_CALC_CHUNK_SIZE) * Iota._PRE_CALC_CHUNK_SIZE;
143
+ const keyPairs = await Iota.buildKeyPairRange(vaultConnector, config, identity, accountIndex, internal, chunkStart);
144
+ const chunk = keyPairs.map(kp => Iota.publicKeyToAddress(Converter.base64ToBytes(kp.publicKey)));
145
+ const offsetInChunk = currentIndex - chunkStart;
146
+ const remaining = count - addresses.length;
147
+ addresses.push(...chunk.slice(offsetInChunk, offsetInChunk + remaining));
148
+ currentIndex = chunkStart + Iota._PRE_CALC_CHUNK_SIZE;
80
149
  }
81
150
  return addresses;
82
151
  }
83
152
  /**
84
153
  * Get a key pair for the specified index.
85
- * @param seed The seed to use for generating the key pair.
86
- * @param coinType The coin type to use.
154
+ * @param vaultConnector The vault connector.
155
+ * @param config The configuration.
156
+ * @param identity The identity of the user to access the vault keys.
87
157
  * @param accountIndex The account index to get the key pair for.
88
158
  * @param addressIndex The address index to get the key pair for.
89
159
  * @param isInternal Whether the address is internal.
90
160
  * @returns The key pair containing private key and public key.
91
161
  */
92
- static getKeyPair(seed, coinType, accountIndex, addressIndex, isInternal) {
93
- Guards.integer(Iota.CLASS_NAME, "coinType", coinType);
162
+ static async getKeyPair(vaultConnector, config, identity, accountIndex, addressIndex, isInternal) {
163
+ Guards.object(Iota.CLASS_NAME, "vaultConnector", vaultConnector);
164
+ Guards.object(Iota.CLASS_NAME, "config", config);
165
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
94
166
  Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
95
167
  Guards.integer(Iota.CLASS_NAME, "addressIndex", addressIndex);
96
- const keyPair = Bip44.keyPair(seed, KeyType.Ed25519, coinType ?? Iota.DEFAULT_COIN_TYPE, accountIndex, isInternal ?? false, addressIndex);
97
- return keyPair;
168
+ const internal = isInternal ?? false;
169
+ const chunkStart = Math.floor(addressIndex / Iota._PRE_CALC_CHUNK_SIZE) * Iota._PRE_CALC_CHUNK_SIZE;
170
+ const keyPairs = await Iota.buildKeyPairRange(vaultConnector, config, identity, accountIndex, internal, chunkStart);
171
+ const offsetInChunk = addressIndex - chunkStart;
172
+ const entry = keyPairs[offsetInChunk];
173
+ return {
174
+ privateKey: Converter.base64ToBytes(entry.privateKey),
175
+ publicKey: Converter.base64ToBytes(entry.publicKey)
176
+ };
177
+ }
178
+ /**
179
+ * Create a new transaction instance.
180
+ * @returns A new transaction instance.
181
+ */
182
+ static createTransaction() {
183
+ return new Transaction();
98
184
  }
99
185
  /**
100
186
  * Prepare and post a transaction.
@@ -147,8 +233,7 @@ export class Iota {
147
233
  if (Is.stringValue(options?.dryRunLabel)) {
148
234
  await Iota.dryRunTransaction(client, logging, transaction, owner, options.dryRunLabel);
149
235
  }
150
- const seed = await Iota.getSeed(config, vaultConnector, identity);
151
- const addressKeyPair = Iota.findAddress(config.maxAddressScanRange ?? Iota.DEFAULT_SCAN_RANGE, config.coinType ?? Iota.DEFAULT_COIN_TYPE, seed, owner);
236
+ const addressKeyPair = await Iota.findAddress(vaultConnector, config, identity, owner, 0);
152
237
  const keypair = Ed25519Keypair.fromSecretKey(addressKeyPair.privateKey);
153
238
  try {
154
239
  const response = await client.signAndExecuteTransaction({
@@ -177,35 +262,32 @@ export class Iota {
177
262
  }
178
263
  }
179
264
  /**
180
- * Get the seed from the vault.
181
- * @param config The configuration to use.
265
+ * Find the address in the seed.
182
266
  * @param vaultConnector The vault connector to use.
267
+ * @param config The configuration to use.
183
268
  * @param identity The identity of the user to access the vault keys.
184
- * @returns The seed.
185
- */
186
- static async getSeed(config, vaultConnector, identity) {
187
- try {
188
- const seedBase64 = await vaultConnector.getSecret(Iota.buildSeedKey(identity, config.vaultSeedId));
189
- return Converter.base64ToBytes(seedBase64);
190
- }
191
- catch { }
192
- const mnemonic = await vaultConnector.getSecret(Iota.buildMnemonicKey(identity, config.vaultMnemonicId));
193
- return Bip39.mnemonicToSeed(mnemonic);
194
- }
195
- /**
196
- * Find the address in the seed.
197
- * @param maxScanRange The maximum range to scan.
198
- * @param coinType The coin type to use.
199
- * @param seed The seed to use.
200
269
  * @param address The address to find.
270
+ * @param accountIndex The account index to search.
271
+ * @param isInternal Whether to search internal addresses.
272
+ * @param startScanIndex The address index to start scanning from.
273
+ * @param maxScanRange The maximum range to scan.
201
274
  * @returns The address key pair.
202
275
  * @throws Error if the address is not found.
203
276
  */
204
- static findAddress(maxScanRange, coinType, seed, address) {
205
- for (let i = 0; i < maxScanRange; i++) {
206
- const addressKeyPair = Bip44.address(seed, KeyType.Ed25519, coinType, 0, false, i);
207
- if (addressKeyPair.address === address) {
208
- return addressKeyPair;
277
+ static async findAddress(vaultConnector, config, identity, address, accountIndex, isInternal, startScanIndex, maxScanRange) {
278
+ const internal = isInternal ?? false;
279
+ const startIndex = startScanIndex ?? 0;
280
+ const scanRange = maxScanRange ?? config.maxAddressScanRange ?? Iota._DEFAULT_SCAN_RANGE;
281
+ for (let chunkStart = startIndex; chunkStart < startIndex + scanRange; chunkStart += Iota._PRE_CALC_CHUNK_SIZE) {
282
+ const keyPairs = await Iota.buildKeyPairRange(vaultConnector, config, identity, accountIndex, internal, chunkStart);
283
+ const offsetInChunk = keyPairs.findIndex(kp => Iota.publicKeyToAddress(Converter.base64ToBytes(kp.publicKey)) === address);
284
+ if (offsetInChunk !== -1) {
285
+ const entry = keyPairs[offsetInChunk];
286
+ return {
287
+ address,
288
+ privateKey: Converter.base64ToBytes(entry.privateKey),
289
+ publicKey: Converter.base64ToBytes(entry.publicKey)
290
+ };
209
291
  }
210
292
  }
211
293
  throw new GeneralError(Iota.CLASS_NAME, "addressNotFound", { address });
@@ -241,24 +323,6 @@ export class Iota {
241
323
  }
242
324
  return baseError;
243
325
  }
244
- /**
245
- * Get the key for storing the mnemonic.
246
- * @param identity The identity to use.
247
- * @param vaultMnemonicId The mnemonic ID to use.
248
- * @returns The mnemonic key.
249
- */
250
- static buildMnemonicKey(identity, vaultMnemonicId) {
251
- return `${identity}/${vaultMnemonicId ?? Iota.DEFAULT_MNEMONIC_SECRET_NAME}`;
252
- }
253
- /**
254
- * Get the key for storing the seed.
255
- * @param identity The identity to use.
256
- * @param vaultSeedId The seed ID to use.
257
- * @returns The seed key.
258
- */
259
- static buildSeedKey(identity, vaultSeedId) {
260
- return `${identity}/${vaultSeedId ?? Iota.DEFAULT_SEED_SECRET_NAME}`;
261
- }
262
326
  /**
263
327
  * Check if the package exists on the network.
264
328
  * @param client The client to use.
@@ -358,7 +422,7 @@ export class Iota {
358
422
  * @returns The confirmed transaction response.
359
423
  */
360
424
  static async waitForTransactionConfirmation(client, digest, config, options) {
361
- const timeoutMs = (config.inclusionTimeoutSeconds ?? Iota.DEFAULT_INCLUSION_TIMEOUT) * 1000;
425
+ const timeoutMs = (config.inclusionTimeoutSeconds ?? Iota._DEFAULT_INCLUSION_TIMEOUT) * 1000;
362
426
  return client.waitForTransaction({
363
427
  digest,
364
428
  timeout: timeoutMs,
@@ -370,21 +434,33 @@ export class Iota {
370
434
  });
371
435
  }
372
436
  /**
373
- * Check if the error is an abort error.
437
+ * Check if the error is an abort error with a specific code.
374
438
  * @param error The error to check.
375
439
  * @param code The error code to check for.
376
440
  * @returns True if the error is an abort error, false otherwise.
377
441
  */
378
442
  static isAbortError(error, code) {
379
443
  const err = BaseError.fromError(error);
380
- if (Is.stringValue(err.properties?.error) && err.properties.error.startsWith("MoveAbort")) {
381
- if (Is.number(code)) {
382
- return err.properties.error.includes(code.toString());
383
- }
384
- return true;
444
+ if (Is.stringValue(err.properties?.error)) {
445
+ const abortCodeMatch = /abort code\s*:\s*(\d+)/i.exec(err.properties.error);
446
+ return abortCodeMatch?.[1] === code.toString();
385
447
  }
386
448
  return false;
387
449
  }
450
+ /**
451
+ * Extracts the abort code from a transaction result if the transaction was aborted.
452
+ * @param response The transaction result to extract the abort code from.
453
+ * @returns The abort code if the transaction was aborted, or undefined if it was not an abort error or the code could not be extracted.
454
+ */
455
+ static extractAbortCode(response) {
456
+ if (response.effects?.status?.status === "failure" &&
457
+ Is.stringValue(response.effects.status.error)) {
458
+ const match = /abort code: (\d+)/.exec(response.effects.status.error);
459
+ if (match) {
460
+ return Coerce.integer(match[1]);
461
+ }
462
+ }
463
+ }
388
464
  /**
389
465
  * Prepare and post a transaction using gas station sponsoring.
390
466
  * @param config The configuration.
@@ -400,18 +476,16 @@ export class Iota {
400
476
  Guards.object(Iota.CLASS_NAME, "config.gasStation", config.gasStation);
401
477
  try {
402
478
  // Reserve gas from the gas station
403
- const gasBudget = config.gasBudget ?? 50000000;
404
- const gasReservation = await Iota.reserveGas(config, gasBudget);
479
+ const gasReservation = await Iota.reserveGas(config);
405
480
  // Set transaction parameters for sponsoring
406
481
  transaction.setSender(owner);
407
482
  transaction.setGasOwner(gasReservation.sponsorAddress);
408
483
  transaction.setGasPayment(gasReservation.gasCoins);
409
- transaction.setGasBudget(gasBudget);
484
+ transaction.setGasBudget(config.gasBudget ?? Iota._DEFAULT_GAS_BUDGET);
410
485
  // Build and sign transaction
411
486
  const unsignedTxBytes = await transaction.build({ client });
412
487
  // Sign the transaction with the user's private key
413
- const seed = await Iota.getSeed(config, vaultConnector, identity);
414
- const addressKeyPair = Iota.findAddress(config.maxAddressScanRange ?? Iota.DEFAULT_SCAN_RANGE, config.coinType ?? Iota.DEFAULT_COIN_TYPE, seed, owner);
488
+ const addressKeyPair = await Iota.findAddress(vaultConnector, config, identity, owner, 0);
415
489
  const keypair = Ed25519Keypair.fromSecretKey(addressKeyPair.privateKey);
416
490
  const signature = await keypair.signTransaction(unsignedTxBytes);
417
491
  return await Iota.executeAndConfirmGasStationTransaction(config, client, gasReservation.reservationId, unsignedTxBytes, signature.signature, options);
@@ -423,16 +497,15 @@ export class Iota {
423
497
  /**
424
498
  * Reserve gas from the gas station.
425
499
  * @param config The configuration containing gas station settings.
426
- * @param gasBudget The gas budget to reserve.
427
500
  * @returns The gas reservation result.
428
501
  */
429
- static async reserveGas(config, gasBudget) {
502
+ static async reserveGas(config) {
430
503
  Guards.object(Iota.CLASS_NAME, "config.gasStation", config.gasStation);
431
504
  const requestData = {
432
505
  // eslint-disable-next-line camelcase
433
- gas_budget: gasBudget,
506
+ gas_budget: config.gasBudget ?? Iota._DEFAULT_GAS_BUDGET,
434
507
  // eslint-disable-next-line camelcase
435
- reserve_duration_secs: 30
508
+ reserve_duration_secs: config.gasReservationDuration ?? Iota._DEFAULT_GAS_RESERVATION_DURATION
436
509
  };
437
510
  const baseUrl = StringHelper.trimTrailingSlashes(config.gasStation.gasStationUrl);
438
511
  const result = await FetchHelper.fetchJson(Iota.CLASS_NAME, `${baseUrl}/v1/reserve_gas`, HttpMethod.POST, requestData, {
@@ -504,5 +577,187 @@ export class Iota {
504
577
  }
505
578
  return response;
506
579
  }
580
+ /**
581
+ * Fund an address with IOTA from the faucet.
582
+ * @param config The configuration containing endpoint information.
583
+ * @param faucetUrl The URL of the faucet to request funds from.
584
+ * @param identity The identity of the user to access the vault keys.
585
+ * @param address The address to fund.
586
+ * @param timeoutInSeconds The timeout in seconds to wait for the funding to complete.
587
+ * @returns The amount funded.
588
+ */
589
+ static async fundAddress(config, faucetUrl, identity, address, timeoutInSeconds = 60) {
590
+ Guards.objectValue(Iota.CLASS_NAME, "config", config);
591
+ Guards.stringValue(Iota.CLASS_NAME, "config.clientOptions.url", config.clientOptions.url);
592
+ Guards.stringValue(Iota.CLASS_NAME, "faucetUrl", faucetUrl);
593
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
594
+ Guards.stringValue(Iota.CLASS_NAME, "address", address);
595
+ try {
596
+ const initialBalance = await Iota.getBalance(config, address);
597
+ const response = await requestIotaFromFaucetV0({
598
+ host: faucetUrl,
599
+ recipient: address
600
+ });
601
+ if (response?.error) {
602
+ throw new GeneralError(Iota.CLASS_NAME, "fundingFailed", undefined, response.error);
603
+ }
604
+ // Poll for balance change
605
+ const numTries = Math.ceil(timeoutInSeconds / 5);
606
+ for (let i = 0; i < numTries; i++) {
607
+ const newBalance = await Iota.getBalance(config, address);
608
+ if (newBalance > initialBalance) {
609
+ return newBalance - initialBalance;
610
+ }
611
+ if (i < numTries - 1) {
612
+ await new Promise(resolve => setTimeout(resolve, 5000));
613
+ }
614
+ }
615
+ }
616
+ catch (error) {
617
+ const payloadError = Iota.extractPayloadError(error);
618
+ if (payloadError.message.includes("Too many requests from this client have been sent to the faucet")) {
619
+ throw new GeneralError(Iota.CLASS_NAME, "faucetRateLimit", undefined, Iota.extractPayloadError(error));
620
+ }
621
+ throw new GeneralError(Iota.CLASS_NAME, "fundingFailed", undefined, Iota.extractPayloadError(error));
622
+ }
623
+ return 0n;
624
+ }
625
+ /**
626
+ * Ensure the balance for the given address is at least the given amount.
627
+ * @param config The configuration containing endpoint information.
628
+ * @param faucetUrl The URL of the faucet to request funds from.
629
+ * @param identity The identity of the user to access the vault keys.
630
+ * @param address The address to ensure the balance for.
631
+ * @param ensureBalance The minimum balance to ensure.
632
+ * @param timeoutInSeconds Optional timeout in seconds, defaults to 10 seconds.
633
+ * @returns True if the balance is at least the given amount, false otherwise.
634
+ */
635
+ static async ensureBalance(config, faucetUrl, identity, address, ensureBalance, timeoutInSeconds) {
636
+ Guards.objectValue(Iota.CLASS_NAME, "config", config);
637
+ Guards.stringValue(Iota.CLASS_NAME, "config.clientOptions.url", config.clientOptions.url);
638
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
639
+ Guards.stringValue(Iota.CLASS_NAME, "address", address);
640
+ Guards.bigint(Iota.CLASS_NAME, "ensureBalance", ensureBalance);
641
+ let currentBalance = await Iota.getBalance(config, address);
642
+ if (Is.stringValue(faucetUrl)) {
643
+ let retryCount = 10;
644
+ while (currentBalance < ensureBalance && retryCount > 0) {
645
+ const addedBalance = await Iota.fundAddress(config, faucetUrl, identity, address, timeoutInSeconds);
646
+ if (addedBalance === 0n) {
647
+ return false;
648
+ }
649
+ currentBalance += addedBalance;
650
+ if (currentBalance < ensureBalance) {
651
+ await new Promise(resolve => setTimeout(resolve, 1000));
652
+ retryCount--;
653
+ }
654
+ }
655
+ }
656
+ return currentBalance >= ensureBalance;
657
+ }
658
+ /**
659
+ * Get the balance for the given address.
660
+ * @param config The configuration containing endpoint information.
661
+ * @param address The address to get the balance for.
662
+ * @returns The balance of the given address.
663
+ */
664
+ static async getBalance(config, address) {
665
+ Guards.objectValue(Iota.CLASS_NAME, "config", config);
666
+ Guards.stringValue(Iota.CLASS_NAME, "config.clientOptions.url", config.clientOptions.url);
667
+ Guards.stringValue(Iota.CLASS_NAME, "address", address);
668
+ const client = Iota.createClient(config);
669
+ const balance = await client.getBalance({
670
+ owner: address
671
+ });
672
+ return BigInt(balance.totalBalance);
673
+ }
674
+ /**
675
+ * Get the seed from the vault.
676
+ * @param vaultConnector The vault connector to use.
677
+ * @param config The configuration to use.
678
+ * @param identity The identity of the user to access the vault keys.
679
+ * @returns The seed.
680
+ * @internal
681
+ */
682
+ static async getSeed(vaultConnector, config, identity) {
683
+ const seedKey = Iota.buildSeedKey(identity, config.vaultSeedId);
684
+ try {
685
+ const seedBase64 = await vaultConnector.getSecret(seedKey);
686
+ return Converter.base64ToBytes(seedBase64);
687
+ }
688
+ catch { }
689
+ const mnemonic = await vaultConnector.getSecret(Iota.buildMnemonicKey(identity, config.vaultMnemonicId));
690
+ // If the seed is not found but the mnemonic exists, derive the seed and store it for future use
691
+ const seed = Bip39.mnemonicToSeed(mnemonic);
692
+ await vaultConnector.setSecret(seedKey, Converter.bytesToBase64(seed));
693
+ return seed;
694
+ }
695
+ /**
696
+ * Get the key for storing the mnemonic.
697
+ * @param identity The identity to use.
698
+ * @param vaultMnemonicId The mnemonic ID to use.
699
+ * @returns The mnemonic key.
700
+ * @internal
701
+ */
702
+ static buildMnemonicKey(identity, vaultMnemonicId) {
703
+ return `${identity}/${vaultMnemonicId ?? Iota.DEFAULT_MNEMONIC_SECRET_NAME}`;
704
+ }
705
+ /**
706
+ * Get the key for storing the seed.
707
+ * @param identity The identity to use.
708
+ * @param vaultSeedId The seed ID to use.
709
+ * @returns The seed key.
710
+ * @internal
711
+ */
712
+ static buildSeedKey(identity, vaultSeedId) {
713
+ return `${identity}/${vaultSeedId ?? Iota.DEFAULT_SEED_SECRET_NAME}`;
714
+ }
715
+ /**
716
+ * Build a chunk of key pairs from vault cache or derived from seed.
717
+ * @param vaultConnector The vault connector to use.
718
+ * @param config The configuration to use.
719
+ * @param identity The identity of the user to access the vault keys.
720
+ * @param accountIndex The account index.
721
+ * @param internal Whether the addresses are internal or external.
722
+ * @param addressIndex The starting address index.
723
+ * @returns The key pairs for the chunk.
724
+ * @internal
725
+ */
726
+ static async buildKeyPairRange(vaultConnector, config, identity, accountIndex, internal, addressIndex) {
727
+ const keyPairChunkKey = Iota.buildKeyPairChunkKey(identity, accountIndex, internal, addressIndex);
728
+ let keyPairChunk;
729
+ try {
730
+ keyPairChunk =
731
+ await vaultConnector.getSecret(keyPairChunkKey);
732
+ }
733
+ catch { }
734
+ if (Is.empty(keyPairChunk)) {
735
+ const startChunkIndex = addressIndex % Iota._PRE_CALC_CHUNK_SIZE;
736
+ const endChunkIndex = startChunkIndex + Iota._PRE_CALC_CHUNK_SIZE;
737
+ const seed = await Iota.getSeed(vaultConnector, config, identity);
738
+ keyPairChunk = [];
739
+ for (let i = startChunkIndex; i < endChunkIndex; i++) {
740
+ const keyPair = Bip44.keyPair(seed, KeyType.Ed25519, config.coinType ?? Iota.DEFAULT_COIN_TYPE, accountIndex, internal, i);
741
+ keyPairChunk.push({
742
+ privateKey: Converter.bytesToBase64(keyPair.privateKey),
743
+ publicKey: Converter.bytesToBase64(keyPair.publicKey)
744
+ });
745
+ }
746
+ await vaultConnector.setSecret(keyPairChunkKey, keyPairChunk);
747
+ }
748
+ return keyPairChunk;
749
+ }
750
+ /**
751
+ * Get the key for storing a keypair chunk.
752
+ * @param identity The identity to use.
753
+ * @param accountIndex The account index.
754
+ * @param internal Whether the address is internal or external.
755
+ * @param addressIndex The address index to determine the chunk.
756
+ * @returns The keypair chunk key.
757
+ * @internal
758
+ */
759
+ static buildKeyPairChunkKey(identity, accountIndex, internal, addressIndex) {
760
+ return `${identity}/keypair/${accountIndex}/${internal ? "internal" : "external"}/${addressIndex % Iota._PRE_CALC_CHUNK_SIZE}`;
761
+ }
507
762
  }
508
763
  //# sourceMappingURL=iota.js.map