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

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 +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 +322 -75
  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/IIotaControllerCapInfo.js +4 -0
  13. package/dist/es/models/IIotaControllerCapInfo.js.map +1 -0
  14. package/dist/es/models/IIotaTransaction.js +2 -0
  15. package/dist/es/models/IIotaTransaction.js.map +1 -0
  16. package/dist/es/models/IIotaTransactionBlockResponse.js +2 -0
  17. package/dist/es/models/IIotaTransactionBlockResponse.js.map +1 -0
  18. package/dist/types/index.d.ts +5 -0
  19. package/dist/types/iota.d.ts +98 -55
  20. package/dist/types/iotaIdentityUtils.d.ts +23 -0
  21. package/dist/types/iotaSmartContractUtils.d.ts +18 -16
  22. package/dist/types/models/IIotaClient.d.ts +5 -0
  23. package/dist/types/models/IIotaControllerCapInfo.d.ts +16 -0
  24. package/dist/types/models/IIotaTransaction.d.ts +5 -0
  25. package/dist/types/models/IIotaTransactionBlockResponse.d.ts +5 -0
  26. package/docs/changelog.md +178 -83
  27. package/docs/examples.md +413 -1
  28. package/docs/reference/classes/Iota.md +357 -165
  29. package/docs/reference/classes/IotaIdentityUtils.md +65 -0
  30. package/docs/reference/classes/IotaSmartContractUtils.md +70 -52
  31. package/docs/reference/index.md +5 -0
  32. package/docs/reference/interfaces/IAdminCapFields.md +1 -1
  33. package/docs/reference/interfaces/IContractData.md +10 -10
  34. package/docs/reference/interfaces/IGasReservationResult.md +3 -3
  35. package/docs/reference/interfaces/IGasStationConfig.md +2 -2
  36. package/docs/reference/interfaces/IGasStationExecuteResponse.md +3 -3
  37. package/docs/reference/interfaces/IGasStationReserveGasResponse.md +3 -3
  38. package/docs/reference/interfaces/IGasStationReserveGasResult.md +3 -3
  39. package/docs/reference/interfaces/IIotaConfig.md +18 -18
  40. package/docs/reference/interfaces/IIotaControllerCapInfo.md +22 -0
  41. package/docs/reference/interfaces/IIotaDryRun.md +5 -5
  42. package/docs/reference/interfaces/IIotaResponseOptions.md +4 -4
  43. package/docs/reference/interfaces/IMigrationStateFields.md +2 -2
  44. package/docs/reference/interfaces/ISmartContractObject.md +2 -2
  45. package/docs/reference/type-aliases/IIotaClient.md +5 -0
  46. package/docs/reference/type-aliases/IIotaTransaction.md +5 -0
  47. package/docs/reference/type-aliases/IIotaTransactionBlockResponse.md +5 -0
  48. package/docs/reference/variables/NetworkTypes.md +3 -3
  49. package/locales/en.json +9 -1
  50. 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,25 @@ 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
27
33
  */
28
- static DEFAULT_SCAN_RANGE = 1000;
34
+ static _DEFAULT_SCAN_RANGE = 1000;
29
35
  /**
30
- * Default inclusion timeout.
36
+ * Default pre-calculation chunk size.
37
+ * @internal
31
38
  */
32
- static DEFAULT_INCLUSION_TIMEOUT = 60;
39
+ static _PRE_CALC_CHUNK_SIZE = 25;
33
40
  /**
34
- * Runtime name for the class.
41
+ * Default inclusion timeout.
42
+ * @internal
35
43
  */
36
- static CLASS_NAME = "Iota";
44
+ static _DEFAULT_INCLUSION_TIMEOUT = 60;
37
45
  /**
38
46
  * Create a new IOTA client.
39
47
  * @param config The configuration.
@@ -54,47 +62,115 @@ export class Iota {
54
62
  config.vaultMnemonicId ??= Iota.DEFAULT_MNEMONIC_SECRET_NAME;
55
63
  config.vaultSeedId ??= Iota.DEFAULT_SEED_SECRET_NAME;
56
64
  config.coinType ??= Iota.DEFAULT_COIN_TYPE;
57
- config.inclusionTimeoutSeconds ??= Iota.DEFAULT_INCLUSION_TIMEOUT;
65
+ config.inclusionTimeoutSeconds ??= Iota._DEFAULT_INCLUSION_TIMEOUT;
66
+ }
67
+ /**
68
+ * Store a mnemonic in the vault, derive and store the seed, and pre-cache the first keypair chunk.
69
+ * @param vaultConnector The vault connector.
70
+ * @param config The configuration.
71
+ * @param identity The identity of the user to access the vault keys.
72
+ * @param mnemonic The mnemonic to store, if undefined a new one will be generated and returned.
73
+ * @param accountIndex The account index to pre-cache.
74
+ * @returns The mnemonic that was stored.
75
+ */
76
+ static async storeMnemonic(vaultConnector, config, identity, mnemonic, accountIndex) {
77
+ Guards.object(Iota.CLASS_NAME, "vaultConnector", vaultConnector);
78
+ Guards.object(Iota.CLASS_NAME, "config", config);
79
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
80
+ Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
81
+ const mnemonicToStore = mnemonic ?? Bip39.randomMnemonic();
82
+ await vaultConnector.setSecret(Iota.buildMnemonicKey(identity, config.vaultMnemonicId), mnemonicToStore);
83
+ const seed = Bip39.mnemonicToSeed(mnemonicToStore);
84
+ await vaultConnector.setSecret(Iota.buildSeedKey(identity, config.vaultSeedId), Converter.bytesToBase64(seed));
85
+ await Iota.buildKeyPairRange(vaultConnector, config, identity, accountIndex, false, 0);
86
+ return mnemonicToStore;
87
+ }
88
+ /**
89
+ * Derive an address from a public key.
90
+ * @param publicKey The public key to derive the address from.
91
+ * @returns The derived address.
92
+ */
93
+ static publicKeyToAddress(publicKey) {
94
+ return Converter.bytesToHex(Blake2b.sum256(publicKey), true);
95
+ }
96
+ /**
97
+ * Get address for the identity.
98
+ * @param vaultConnector The vault connector.
99
+ * @param config The configuration.
100
+ * @param identity The identity of the user to access the vault keys.
101
+ * @param accountIndex The account index to get the addresses for.
102
+ * @param startAddressIndex The start index for the addresses.
103
+ * @param isInternal Whether the addresses are internal.
104
+ * @returns The address.
105
+ */
106
+ static async getAddress(vaultConnector, config, identity, accountIndex, startAddressIndex, isInternal) {
107
+ const addresses = await Iota.getAddresses(vaultConnector, config, identity, accountIndex, startAddressIndex, 1, isInternal);
108
+ return addresses[0];
58
109
  }
59
110
  /**
60
111
  * Get addresses for the identity.
61
- * @param seed The seed to use for generating addresses.
62
- * @param coinType The coin type to use.
112
+ * @param vaultConnector The vault connector.
113
+ * @param config The configuration.
114
+ * @param identity The identity of the user to access the vault keys.
63
115
  * @param accountIndex The account index to get the addresses for.
64
116
  * @param startAddressIndex The start index for the addresses.
65
117
  * @param count The number of addresses to generate.
66
118
  * @param isInternal Whether the addresses are internal.
67
119
  * @returns The list of addresses.
68
120
  */
69
- static getAddresses(seed, coinType, accountIndex, startAddressIndex, count, isInternal) {
70
- Guards.integer(Iota.CLASS_NAME, "coinType", coinType);
121
+ static async getAddresses(vaultConnector, config, identity, accountIndex, startAddressIndex, count, isInternal) {
122
+ Guards.object(Iota.CLASS_NAME, "vaultConnector", vaultConnector);
123
+ Guards.object(Iota.CLASS_NAME, "config", config);
124
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
71
125
  Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
72
126
  Guards.integer(Iota.CLASS_NAME, "startAddressIndex", startAddressIndex);
73
127
  Guards.integer(Iota.CLASS_NAME, "count", count);
74
128
  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());
129
+ const internal = isInternal ?? false;
130
+ let currentIndex = startAddressIndex;
131
+ while (addresses.length < count) {
132
+ const chunkStart = Math.floor(currentIndex / Iota._PRE_CALC_CHUNK_SIZE) * Iota._PRE_CALC_CHUNK_SIZE;
133
+ const keyPairs = await Iota.buildKeyPairRange(vaultConnector, config, identity, accountIndex, internal, chunkStart);
134
+ const chunk = keyPairs.map(kp => Iota.publicKeyToAddress(Converter.base64ToBytes(kp.publicKey)));
135
+ const offsetInChunk = currentIndex - chunkStart;
136
+ const remaining = count - addresses.length;
137
+ addresses.push(...chunk.slice(offsetInChunk, offsetInChunk + remaining));
138
+ currentIndex = chunkStart + Iota._PRE_CALC_CHUNK_SIZE;
80
139
  }
81
140
  return addresses;
82
141
  }
83
142
  /**
84
143
  * 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.
144
+ * @param vaultConnector The vault connector.
145
+ * @param config The configuration.
146
+ * @param identity The identity of the user to access the vault keys.
87
147
  * @param accountIndex The account index to get the key pair for.
88
148
  * @param addressIndex The address index to get the key pair for.
89
149
  * @param isInternal Whether the address is internal.
90
150
  * @returns The key pair containing private key and public key.
91
151
  */
92
- static getKeyPair(seed, coinType, accountIndex, addressIndex, isInternal) {
93
- Guards.integer(Iota.CLASS_NAME, "coinType", coinType);
152
+ static async getKeyPair(vaultConnector, config, identity, accountIndex, addressIndex, isInternal) {
153
+ Guards.object(Iota.CLASS_NAME, "vaultConnector", vaultConnector);
154
+ Guards.object(Iota.CLASS_NAME, "config", config);
155
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
94
156
  Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
95
157
  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;
158
+ const internal = isInternal ?? false;
159
+ const chunkStart = Math.floor(addressIndex / Iota._PRE_CALC_CHUNK_SIZE) * Iota._PRE_CALC_CHUNK_SIZE;
160
+ const keyPairs = await Iota.buildKeyPairRange(vaultConnector, config, identity, accountIndex, internal, chunkStart);
161
+ const offsetInChunk = addressIndex - chunkStart;
162
+ const entry = keyPairs[offsetInChunk];
163
+ return {
164
+ privateKey: Converter.base64ToBytes(entry.privateKey),
165
+ publicKey: Converter.base64ToBytes(entry.publicKey)
166
+ };
167
+ }
168
+ /**
169
+ * Create a new transaction instance.
170
+ * @returns A new transaction instance.
171
+ */
172
+ static createTransaction() {
173
+ return new Transaction();
98
174
  }
99
175
  /**
100
176
  * Prepare and post a transaction.
@@ -147,8 +223,7 @@ export class Iota {
147
223
  if (Is.stringValue(options?.dryRunLabel)) {
148
224
  await Iota.dryRunTransaction(client, logging, transaction, owner, options.dryRunLabel);
149
225
  }
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);
226
+ const addressKeyPair = await Iota.findAddress(vaultConnector, config, identity, owner, 0);
152
227
  const keypair = Ed25519Keypair.fromSecretKey(addressKeyPair.privateKey);
153
228
  try {
154
229
  const response = await client.signAndExecuteTransaction({
@@ -177,35 +252,32 @@ export class Iota {
177
252
  }
178
253
  }
179
254
  /**
180
- * Get the seed from the vault.
181
- * @param config The configuration to use.
255
+ * Find the address in the seed.
182
256
  * @param vaultConnector The vault connector to use.
257
+ * @param config The configuration to use.
183
258
  * @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
259
  * @param address The address to find.
260
+ * @param accountIndex The account index to search.
261
+ * @param isInternal Whether to search internal addresses.
262
+ * @param startScanIndex The address index to start scanning from.
263
+ * @param maxScanRange The maximum range to scan.
201
264
  * @returns The address key pair.
202
265
  * @throws Error if the address is not found.
203
266
  */
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;
267
+ static async findAddress(vaultConnector, config, identity, address, accountIndex, isInternal, startScanIndex, maxScanRange) {
268
+ const internal = isInternal ?? false;
269
+ const startIndex = startScanIndex ?? 0;
270
+ const scanRange = maxScanRange ?? config.maxAddressScanRange ?? Iota._DEFAULT_SCAN_RANGE;
271
+ for (let chunkStart = startIndex; chunkStart < startIndex + scanRange; chunkStart += Iota._PRE_CALC_CHUNK_SIZE) {
272
+ const keyPairs = await Iota.buildKeyPairRange(vaultConnector, config, identity, accountIndex, internal, chunkStart);
273
+ const offsetInChunk = keyPairs.findIndex(kp => Iota.publicKeyToAddress(Converter.base64ToBytes(kp.publicKey)) === address);
274
+ if (offsetInChunk !== -1) {
275
+ const entry = keyPairs[offsetInChunk];
276
+ return {
277
+ address,
278
+ privateKey: Converter.base64ToBytes(entry.privateKey),
279
+ publicKey: Converter.base64ToBytes(entry.publicKey)
280
+ };
209
281
  }
210
282
  }
211
283
  throw new GeneralError(Iota.CLASS_NAME, "addressNotFound", { address });
@@ -241,24 +313,6 @@ export class Iota {
241
313
  }
242
314
  return baseError;
243
315
  }
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
316
  /**
263
317
  * Check if the package exists on the network.
264
318
  * @param client The client to use.
@@ -358,7 +412,7 @@ export class Iota {
358
412
  * @returns The confirmed transaction response.
359
413
  */
360
414
  static async waitForTransactionConfirmation(client, digest, config, options) {
361
- const timeoutMs = (config.inclusionTimeoutSeconds ?? Iota.DEFAULT_INCLUSION_TIMEOUT) * 1000;
415
+ const timeoutMs = (config.inclusionTimeoutSeconds ?? Iota._DEFAULT_INCLUSION_TIMEOUT) * 1000;
362
416
  return client.waitForTransaction({
363
417
  digest,
364
418
  timeout: timeoutMs,
@@ -370,21 +424,33 @@ export class Iota {
370
424
  });
371
425
  }
372
426
  /**
373
- * Check if the error is an abort error.
427
+ * Check if the error is an abort error with a specific code.
374
428
  * @param error The error to check.
375
429
  * @param code The error code to check for.
376
430
  * @returns True if the error is an abort error, false otherwise.
377
431
  */
378
432
  static isAbortError(error, code) {
379
433
  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;
434
+ if (Is.stringValue(err.properties?.error)) {
435
+ const abortCodeMatch = /abort code\s*:\s*(\d+)/i.exec(err.properties.error);
436
+ return abortCodeMatch?.[1] === code.toString();
385
437
  }
386
438
  return false;
387
439
  }
440
+ /**
441
+ * Extracts the abort code from a transaction result if the transaction was aborted.
442
+ * @param response The transaction result to extract the abort code from.
443
+ * @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.
444
+ */
445
+ static extractAbortCode(response) {
446
+ if (response.effects?.status?.status === "failure" &&
447
+ Is.stringValue(response.effects.status.error)) {
448
+ const match = /abort code: (\d+)/.exec(response.effects.status.error);
449
+ if (match) {
450
+ return Coerce.integer(match[1]);
451
+ }
452
+ }
453
+ }
388
454
  /**
389
455
  * Prepare and post a transaction using gas station sponsoring.
390
456
  * @param config The configuration.
@@ -410,8 +476,7 @@ export class Iota {
410
476
  // Build and sign transaction
411
477
  const unsignedTxBytes = await transaction.build({ client });
412
478
  // 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);
479
+ const addressKeyPair = await Iota.findAddress(vaultConnector, config, identity, owner, 0);
415
480
  const keypair = Ed25519Keypair.fromSecretKey(addressKeyPair.privateKey);
416
481
  const signature = await keypair.signTransaction(unsignedTxBytes);
417
482
  return await Iota.executeAndConfirmGasStationTransaction(config, client, gasReservation.reservationId, unsignedTxBytes, signature.signature, options);
@@ -504,5 +569,187 @@ export class Iota {
504
569
  }
505
570
  return response;
506
571
  }
572
+ /**
573
+ * Fund an address with IOTA from the faucet.
574
+ * @param config The configuration containing endpoint information.
575
+ * @param faucetUrl The URL of the faucet to request funds from.
576
+ * @param identity The identity of the user to access the vault keys.
577
+ * @param address The address to fund.
578
+ * @param timeoutInSeconds The timeout in seconds to wait for the funding to complete.
579
+ * @returns The amount funded.
580
+ */
581
+ static async fundAddress(config, faucetUrl, identity, address, timeoutInSeconds = 60) {
582
+ Guards.objectValue(Iota.CLASS_NAME, "config", config);
583
+ Guards.stringValue(Iota.CLASS_NAME, "config.clientOptions.url", config.clientOptions.url);
584
+ Guards.stringValue(Iota.CLASS_NAME, "faucetUrl", faucetUrl);
585
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
586
+ Guards.stringValue(Iota.CLASS_NAME, "address", address);
587
+ try {
588
+ const initialBalance = await Iota.getBalance(config, address);
589
+ const response = await requestIotaFromFaucetV0({
590
+ host: faucetUrl,
591
+ recipient: address
592
+ });
593
+ if (response?.error) {
594
+ throw new GeneralError(Iota.CLASS_NAME, "fundingFailed", undefined, response.error);
595
+ }
596
+ // Poll for balance change
597
+ const numTries = Math.ceil(timeoutInSeconds / 5);
598
+ for (let i = 0; i < numTries; i++) {
599
+ const newBalance = await Iota.getBalance(config, address);
600
+ if (newBalance > initialBalance) {
601
+ return newBalance - initialBalance;
602
+ }
603
+ if (i < numTries - 1) {
604
+ await new Promise(resolve => setTimeout(resolve, 5000));
605
+ }
606
+ }
607
+ }
608
+ catch (error) {
609
+ const payloadError = Iota.extractPayloadError(error);
610
+ if (payloadError.message.includes("Too many requests from this client have been sent to the faucet")) {
611
+ throw new GeneralError(Iota.CLASS_NAME, "faucetRateLimit", undefined, Iota.extractPayloadError(error));
612
+ }
613
+ throw new GeneralError(Iota.CLASS_NAME, "fundingFailed", undefined, Iota.extractPayloadError(error));
614
+ }
615
+ return 0n;
616
+ }
617
+ /**
618
+ * Ensure the balance for the given address is at least the given amount.
619
+ * @param config The configuration containing endpoint information.
620
+ * @param faucetUrl The URL of the faucet to request funds from.
621
+ * @param identity The identity of the user to access the vault keys.
622
+ * @param address The address to ensure the balance for.
623
+ * @param ensureBalance The minimum balance to ensure.
624
+ * @param timeoutInSeconds Optional timeout in seconds, defaults to 10 seconds.
625
+ * @returns True if the balance is at least the given amount, false otherwise.
626
+ */
627
+ static async ensureBalance(config, faucetUrl, identity, address, ensureBalance, timeoutInSeconds) {
628
+ Guards.objectValue(Iota.CLASS_NAME, "config", config);
629
+ Guards.stringValue(Iota.CLASS_NAME, "config.clientOptions.url", config.clientOptions.url);
630
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
631
+ Guards.stringValue(Iota.CLASS_NAME, "address", address);
632
+ Guards.bigint(Iota.CLASS_NAME, "ensureBalance", ensureBalance);
633
+ let currentBalance = await Iota.getBalance(config, address);
634
+ if (Is.stringValue(faucetUrl)) {
635
+ let retryCount = 10;
636
+ while (currentBalance < ensureBalance && retryCount > 0) {
637
+ const addedBalance = await Iota.fundAddress(config, faucetUrl, identity, address, timeoutInSeconds);
638
+ if (addedBalance === 0n) {
639
+ return false;
640
+ }
641
+ currentBalance += addedBalance;
642
+ if (currentBalance < ensureBalance) {
643
+ await new Promise(resolve => setTimeout(resolve, 1000));
644
+ retryCount--;
645
+ }
646
+ }
647
+ }
648
+ return currentBalance >= ensureBalance;
649
+ }
650
+ /**
651
+ * Get the balance for the given address.
652
+ * @param config The configuration containing endpoint information.
653
+ * @param address The address to get the balance for.
654
+ * @returns The balance of the given address.
655
+ */
656
+ static async getBalance(config, address) {
657
+ Guards.objectValue(Iota.CLASS_NAME, "config", config);
658
+ Guards.stringValue(Iota.CLASS_NAME, "config.clientOptions.url", config.clientOptions.url);
659
+ Guards.stringValue(Iota.CLASS_NAME, "address", address);
660
+ const client = Iota.createClient(config);
661
+ const balance = await client.getBalance({
662
+ owner: address
663
+ });
664
+ return BigInt(balance.totalBalance);
665
+ }
666
+ /**
667
+ * Get the seed from the vault.
668
+ * @param vaultConnector The vault connector to use.
669
+ * @param config The configuration to use.
670
+ * @param identity The identity of the user to access the vault keys.
671
+ * @returns The seed.
672
+ * @internal
673
+ */
674
+ static async getSeed(vaultConnector, config, identity) {
675
+ const seedKey = Iota.buildSeedKey(identity, config.vaultSeedId);
676
+ try {
677
+ const seedBase64 = await vaultConnector.getSecret(seedKey);
678
+ return Converter.base64ToBytes(seedBase64);
679
+ }
680
+ catch { }
681
+ const mnemonic = await vaultConnector.getSecret(Iota.buildMnemonicKey(identity, config.vaultMnemonicId));
682
+ // If the seed is not found but the mnemonic exists, derive the seed and store it for future use
683
+ const seed = Bip39.mnemonicToSeed(mnemonic);
684
+ await vaultConnector.setSecret(seedKey, Converter.bytesToBase64(seed));
685
+ return seed;
686
+ }
687
+ /**
688
+ * Get the key for storing the mnemonic.
689
+ * @param identity The identity to use.
690
+ * @param vaultMnemonicId The mnemonic ID to use.
691
+ * @returns The mnemonic key.
692
+ * @internal
693
+ */
694
+ static buildMnemonicKey(identity, vaultMnemonicId) {
695
+ return `${identity}/${vaultMnemonicId ?? Iota.DEFAULT_MNEMONIC_SECRET_NAME}`;
696
+ }
697
+ /**
698
+ * Get the key for storing the seed.
699
+ * @param identity The identity to use.
700
+ * @param vaultSeedId The seed ID to use.
701
+ * @returns The seed key.
702
+ * @internal
703
+ */
704
+ static buildSeedKey(identity, vaultSeedId) {
705
+ return `${identity}/${vaultSeedId ?? Iota.DEFAULT_SEED_SECRET_NAME}`;
706
+ }
707
+ /**
708
+ * Build a chunk of key pairs from vault cache or derived from seed.
709
+ * @param vaultConnector The vault connector to use.
710
+ * @param config The configuration to use.
711
+ * @param identity The identity of the user to access the vault keys.
712
+ * @param accountIndex The account index.
713
+ * @param internal Whether the addresses are internal or external.
714
+ * @param addressIndex The starting address index.
715
+ * @returns The key pairs for the chunk.
716
+ * @internal
717
+ */
718
+ static async buildKeyPairRange(vaultConnector, config, identity, accountIndex, internal, addressIndex) {
719
+ const keyPairChunkKey = Iota.buildKeyPairChunkKey(identity, accountIndex, internal, addressIndex);
720
+ let keyPairChunk;
721
+ try {
722
+ keyPairChunk =
723
+ await vaultConnector.getSecret(keyPairChunkKey);
724
+ }
725
+ catch { }
726
+ if (Is.empty(keyPairChunk)) {
727
+ const startChunkIndex = addressIndex % Iota._PRE_CALC_CHUNK_SIZE;
728
+ const endChunkIndex = startChunkIndex + Iota._PRE_CALC_CHUNK_SIZE;
729
+ const seed = await Iota.getSeed(vaultConnector, config, identity);
730
+ keyPairChunk = [];
731
+ for (let i = startChunkIndex; i < endChunkIndex; i++) {
732
+ const keyPair = Bip44.keyPair(seed, KeyType.Ed25519, config.coinType ?? Iota.DEFAULT_COIN_TYPE, accountIndex, internal, i);
733
+ keyPairChunk.push({
734
+ privateKey: Converter.bytesToBase64(keyPair.privateKey),
735
+ publicKey: Converter.bytesToBase64(keyPair.publicKey)
736
+ });
737
+ }
738
+ await vaultConnector.setSecret(keyPairChunkKey, keyPairChunk);
739
+ }
740
+ return keyPairChunk;
741
+ }
742
+ /**
743
+ * Get the key for storing a keypair chunk.
744
+ * @param identity The identity to use.
745
+ * @param accountIndex The account index.
746
+ * @param internal Whether the address is internal or external.
747
+ * @param addressIndex The address index to determine the chunk.
748
+ * @returns The keypair chunk key.
749
+ * @internal
750
+ */
751
+ static buildKeyPairChunkKey(identity, accountIndex, internal, addressIndex) {
752
+ return `${identity}/keypair/${accountIndex}/${internal ? "internal" : "external"}/${addressIndex % Iota._PRE_CALC_CHUNK_SIZE}`;
753
+ }
507
754
  }
508
755
  //# sourceMappingURL=iota.js.map