@twin.org/dlt-iota 0.0.3-next.9 → 0.9.0-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 (60) hide show
  1. package/dist/es/index.js +7 -3
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/es/iota.js +356 -93
  4. package/dist/es/iota.js.map +1 -1
  5. package/dist/es/iotaIdentityUtils.js +2 -4
  6. package/dist/es/iotaIdentityUtils.js.map +1 -1
  7. package/dist/es/iotaSmartContractUtils.js +31 -27
  8. package/dist/es/iotaSmartContractUtils.js.map +1 -1
  9. package/dist/es/models/IAdminCapFields.js.map +1 -1
  10. package/dist/es/models/IGasStationExecuteResponse.js.map +1 -1
  11. package/dist/es/models/IIotaConfig.js.map +1 -1
  12. package/dist/es/models/IIotaControllerCapInfo.js.map +1 -1
  13. package/dist/es/models/IIotaResponseOptions.js.map +1 -1
  14. package/dist/es/models/IMigrationStateFields.js.map +1 -1
  15. package/dist/es/models/ISmartContractObject.js.map +1 -1
  16. package/dist/es/models/ITransactionSigner.js +2 -0
  17. package/dist/es/models/ITransactionSigner.js.map +1 -0
  18. package/dist/es/vaultJwkStorage.js +71 -0
  19. package/dist/es/vaultJwkStorage.js.map +1 -0
  20. package/dist/es/vaultJwtSigner.js +49 -0
  21. package/dist/es/vaultJwtSigner.js.map +1 -0
  22. package/dist/es/vaultSigner.js +60 -0
  23. package/dist/es/vaultSigner.js.map +1 -0
  24. package/dist/es/vaultTransactionSigner.js +74 -0
  25. package/dist/es/vaultTransactionSigner.js.map +1 -0
  26. package/dist/types/index.d.ts +7 -3
  27. package/dist/types/iota.d.ts +82 -53
  28. package/dist/types/iotaIdentityUtils.d.ts +2 -4
  29. package/dist/types/iotaSmartContractUtils.d.ts +18 -17
  30. package/dist/types/models/IAdminCapFields.d.ts +2 -2
  31. package/dist/types/models/IGasStationExecuteResponse.d.ts +1 -3
  32. package/dist/types/models/IIotaConfig.d.ts +5 -0
  33. package/dist/types/models/IIotaControllerCapInfo.d.ts +3 -6
  34. package/dist/types/models/IIotaResponseOptions.d.ts +1 -1
  35. package/dist/types/models/IMigrationStateFields.d.ts +2 -2
  36. package/dist/types/models/ISmartContractObject.d.ts +2 -2
  37. package/dist/types/models/ITransactionSigner.d.ts +27 -0
  38. package/dist/types/vaultJwkStorage.d.ts +50 -0
  39. package/dist/types/vaultJwtSigner.d.ts +26 -0
  40. package/dist/types/vaultSigner.d.ts +32 -0
  41. package/dist/types/vaultTransactionSigner.d.ts +39 -0
  42. package/docs/changelog.md +88 -0
  43. package/docs/examples.md +6 -13
  44. package/docs/reference/classes/Iota.md +325 -189
  45. package/docs/reference/classes/IotaIdentityUtils.md +2 -4
  46. package/docs/reference/classes/IotaSmartContractUtils.md +57 -40
  47. package/docs/reference/classes/VaultJwtSigner.md +71 -0
  48. package/docs/reference/classes/VaultSigner.md +106 -0
  49. package/docs/reference/classes/VaultTransactionSigner.md +122 -0
  50. package/docs/reference/index.md +4 -0
  51. package/docs/reference/interfaces/IAdminCapFields.md +2 -2
  52. package/docs/reference/interfaces/IGasStationExecuteResponse.md +1 -3
  53. package/docs/reference/interfaces/IIotaConfig.md +14 -0
  54. package/docs/reference/interfaces/IIotaControllerCapInfo.md +3 -6
  55. package/docs/reference/interfaces/IIotaResponseOptions.md +1 -1
  56. package/docs/reference/interfaces/IMigrationStateFields.md +2 -2
  57. package/docs/reference/interfaces/ISmartContractObject.md +2 -2
  58. package/docs/reference/interfaces/ITransactionSigner.md +67 -0
  59. package/locales/en.json +7 -1
  60. package/package.json +12 -12
package/dist/es/index.js CHANGED
@@ -11,14 +11,18 @@ export * from "./models/IGasStationExecuteResponse.js";
11
11
  export * from "./models/IGasStationReserveGasResponse.js";
12
12
  export * from "./models/IGasStationReserveGasResult.js";
13
13
  export * from "./models/IIotaClient.js";
14
- export * from "./models/IIotaControllerCapInfo.js";
15
- export * from "./models/IIotaTransaction.js";
16
- export * from "./models/IIotaTransactionBlockResponse.js";
17
14
  export * from "./models/IIotaConfig.js";
15
+ export * from "./models/IIotaControllerCapInfo.js";
18
16
  export * from "./models/IIotaDryRun.js";
19
17
  export * from "./models/IIotaResponseOptions.js";
18
+ export * from "./models/IIotaTransaction.js";
19
+ export * from "./models/IIotaTransactionBlockResponse.js";
20
+ export * from "./models/ITransactionSigner.js";
20
21
  export * from "./models/IMigrationStateFields.js";
21
22
  export * from "./models/ISmartContractDeployments.js";
22
23
  export * from "./models/ISmartContractObject.js";
23
24
  export * from "./models/networkTypes.js";
25
+ export * from "./vaultJwtSigner.js";
26
+ export * from "./vaultSigner.js";
27
+ export * from "./vaultTransactionSigner.js";
24
28
  //# sourceMappingURL=index.js.map
@@ -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,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"]}
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,yBAAyB,CAAC;AACxC,cAAc,oCAAoC,CAAC;AACnD,cAAc,yBAAyB,CAAC;AACxC,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2CAA2C,CAAC;AAC1D,cAAc,gCAAgC,CAAC;AAC/C,cAAc,mCAAmC,CAAC;AAClD,cAAc,uCAAuC,CAAC;AACtD,cAAc,kCAAkC,CAAC;AACjD,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,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/IIotaConfig.js\";\nexport * from \"./models/IIotaControllerCapInfo.js\";\nexport * from \"./models/IIotaDryRun.js\";\nexport * from \"./models/IIotaResponseOptions.js\";\nexport * from \"./models/IIotaTransaction.js\";\nexport * from \"./models/IIotaTransactionBlockResponse.js\";\nexport * from \"./models/ITransactionSigner.js\";\nexport * from \"./models/IMigrationStateFields.js\";\nexport * from \"./models/ISmartContractDeployments.js\";\nexport * from \"./models/ISmartContractObject.js\";\nexport * from \"./models/networkTypes.js\";\nexport * from \"./vaultJwtSigner.js\";\nexport * from \"./vaultSigner.js\";\nexport * from \"./vaultTransactionSigner.js\";\n"]}
package/dist/es/iota.js CHANGED
@@ -1,11 +1,14 @@
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 { Ed25519Keypair } from "@iota/iota-sdk/keypairs/ed25519";
4
+ import { requestIotaFromFaucetV0 } from "@iota/iota-sdk/faucet";
5
5
  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";
6
+ import { BaseError, Coerce, Converter, GeneralError, Guards, Is, StringHelper } from "@twin.org/core";
7
+ import { Bip39, Bip44, Blake2b, KeyType } from "@twin.org/crypto";
8
+ import { VaultConnectorHelper, VaultKeyType } from "@twin.org/vault-models";
8
9
  import { FetchHelper, HttpMethod } from "@twin.org/web";
10
+ import { VaultSigner } from "./vaultSigner.js";
11
+ import { VaultTransactionSigner } from "./vaultTransactionSigner.js";
9
12
  /**
10
13
  * Class for performing operations on IOTA.
11
14
  */
@@ -31,11 +34,26 @@ export class Iota {
31
34
  * @internal
32
35
  */
33
36
  static _DEFAULT_SCAN_RANGE = 1000;
37
+ /**
38
+ * Default pre-calculation chunk size.
39
+ * @internal
40
+ */
41
+ static _PRE_CALC_CHUNK_SIZE = 25;
34
42
  /**
35
43
  * Default inclusion timeout.
36
44
  * @internal
37
45
  */
38
46
  static _DEFAULT_INCLUSION_TIMEOUT = 60;
47
+ /**
48
+ * Default gas budget for all transactions (including sponsored and direct).
49
+ * @internal
50
+ */
51
+ static _DEFAULT_GAS_BUDGET = 50000000;
52
+ /**
53
+ * Default gas reservation duration.
54
+ * @internal
55
+ */
56
+ static _DEFAULT_GAS_RESERVATION_DURATION = 60;
39
57
  /**
40
58
  * Create a new IOTA client.
41
59
  * @param config The configuration.
@@ -58,45 +76,107 @@ export class Iota {
58
76
  config.coinType ??= Iota.DEFAULT_COIN_TYPE;
59
77
  config.inclusionTimeoutSeconds ??= Iota._DEFAULT_INCLUSION_TIMEOUT;
60
78
  }
79
+ /**
80
+ * Store a mnemonic in the vault, derive and store the seed, and pre-cache the first keypair chunk.
81
+ * @param vaultConnector The vault connector.
82
+ * @param config The configuration.
83
+ * @param identity The identity of the user to access the vault keys.
84
+ * @param mnemonic The mnemonic to store, if undefined a new one will be generated and returned.
85
+ * @param accountIndex The account index to pre-cache.
86
+ * @returns The mnemonic that was stored.
87
+ */
88
+ static async storeMnemonic(vaultConnector, config, identity, mnemonic, accountIndex) {
89
+ Guards.object(Iota.CLASS_NAME, "vaultConnector", vaultConnector);
90
+ Guards.object(Iota.CLASS_NAME, "config", config);
91
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
92
+ Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
93
+ const mnemonicToStore = mnemonic ?? Bip39.randomMnemonic();
94
+ await vaultConnector.setSecret(Iota.buildMnemonicKey(identity, config.vaultMnemonicId), mnemonicToStore);
95
+ const seed = Bip39.mnemonicToSeed(mnemonicToStore);
96
+ await vaultConnector.setSecret(Iota.buildSeedKey(identity, config.vaultSeedId), Converter.bytesToBase64(seed));
97
+ await Iota.getPublicKeys(vaultConnector, config, identity, accountIndex, false, 0, async () => seed);
98
+ return mnemonicToStore;
99
+ }
100
+ /**
101
+ * Derive an address from a public key.
102
+ * @param publicKey The public key to derive the address from.
103
+ * @returns The derived address.
104
+ */
105
+ static publicKeyToAddress(publicKey) {
106
+ return Converter.bytesToHex(Blake2b.sum256(publicKey), true);
107
+ }
108
+ /**
109
+ * Get address for the identity.
110
+ * @param vaultConnector The vault connector.
111
+ * @param config The configuration.
112
+ * @param identity The identity of the user to access the vault keys.
113
+ * @param accountIndex The account index to get the addresses for.
114
+ * @param startAddressIndex The start index for the addresses.
115
+ * @param isInternal Whether the addresses are internal.
116
+ * @returns The address.
117
+ */
118
+ static async getAddress(vaultConnector, config, identity, accountIndex, startAddressIndex, isInternal) {
119
+ const addresses = await Iota.getAddresses(vaultConnector, config, identity, accountIndex, startAddressIndex, 1, isInternal);
120
+ return addresses[0];
121
+ }
61
122
  /**
62
123
  * Get addresses for the identity.
63
- * @param seed The seed to use for generating addresses.
64
- * @param coinType The coin type to use.
124
+ * @param vaultConnector The vault connector.
125
+ * @param config The configuration.
126
+ * @param identity The identity of the user to access the vault keys.
65
127
  * @param accountIndex The account index to get the addresses for.
66
128
  * @param startAddressIndex The start index for the addresses.
67
129
  * @param count The number of addresses to generate.
68
130
  * @param isInternal Whether the addresses are internal.
69
131
  * @returns The list of addresses.
70
132
  */
71
- static getAddresses(seed, coinType, accountIndex, startAddressIndex, count, isInternal) {
72
- Guards.integer(Iota.CLASS_NAME, "coinType", coinType);
133
+ static async getAddresses(vaultConnector, config, identity, accountIndex, startAddressIndex, count, isInternal) {
134
+ Guards.object(Iota.CLASS_NAME, "vaultConnector", vaultConnector);
135
+ Guards.object(Iota.CLASS_NAME, "config", config);
136
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
73
137
  Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
74
138
  Guards.integer(Iota.CLASS_NAME, "startAddressIndex", startAddressIndex);
75
139
  Guards.integer(Iota.CLASS_NAME, "count", count);
76
140
  const addresses = [];
77
- for (let i = startAddressIndex; i < startAddressIndex + count; i++) {
78
- // Derive the keypair using the seed
79
- const keyPair = Bip44.keyPair(seed, KeyType.Ed25519, coinType ?? Iota.DEFAULT_COIN_TYPE, accountIndex, isInternal ?? false, i);
80
- const keypair = Ed25519Keypair.fromSecretKey(keyPair.privateKey);
81
- addresses.push(keypair.getPublicKey().toIotaAddress());
141
+ const internal = isInternal ?? false;
142
+ let currentIndex = startAddressIndex;
143
+ let cachedSeed;
144
+ const seedProvider = async () => {
145
+ cachedSeed ??= await Iota.getSeed(vaultConnector, config, identity);
146
+ return cachedSeed;
147
+ };
148
+ while (addresses.length < count) {
149
+ const chunkStart = Math.floor(currentIndex / Iota._PRE_CALC_CHUNK_SIZE) * Iota._PRE_CALC_CHUNK_SIZE;
150
+ const publicKeys = await Iota.getPublicKeys(vaultConnector, config, identity, accountIndex, internal, chunkStart, seedProvider);
151
+ const chunk = publicKeys.map((pk) => Iota.publicKeyToAddress(Converter.base64ToBytes(pk)));
152
+ const offsetInChunk = currentIndex - chunkStart;
153
+ const remaining = count - addresses.length;
154
+ addresses.push(...chunk.slice(offsetInChunk, offsetInChunk + remaining));
155
+ currentIndex = chunkStart + Iota._PRE_CALC_CHUNK_SIZE;
82
156
  }
83
157
  return addresses;
84
158
  }
85
159
  /**
86
- * Get a key pair for the specified index.
87
- * @param seed The seed to use for generating the key pair.
88
- * @param coinType The coin type to use.
89
- * @param accountIndex The account index to get the key pair for.
90
- * @param addressIndex The address index to get the key pair for.
91
- * @param isInternal Whether the address is internal.
92
- * @returns The key pair containing private key and public key.
160
+ * Get a vault-backed transaction signer for the given identity and key indices.
161
+ * @param vaultConnector The vault connector.
162
+ * @param config The configuration.
163
+ * @param identity The identity of the user to access the vault keys.
164
+ * @param accountIndex The account index.
165
+ * @param addressIndex The address index within the account.
166
+ * @returns A VaultTransactionSigner for the specified key.
93
167
  */
94
- static getKeyPair(seed, coinType, accountIndex, addressIndex, isInternal) {
95
- Guards.integer(Iota.CLASS_NAME, "coinType", coinType);
168
+ static async getTransactionSigner(vaultConnector, config, identity, accountIndex, addressIndex) {
169
+ Guards.object(Iota.CLASS_NAME, "vaultConnector", vaultConnector);
170
+ Guards.object(Iota.CLASS_NAME, "config", config);
171
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
96
172
  Guards.integer(Iota.CLASS_NAME, "accountIndex", accountIndex);
97
173
  Guards.integer(Iota.CLASS_NAME, "addressIndex", addressIndex);
98
- const keyPair = Bip44.keyPair(seed, KeyType.Ed25519, coinType ?? Iota.DEFAULT_COIN_TYPE, accountIndex, isInternal ?? false, addressIndex);
99
- return keyPair;
174
+ const publicKeys = await Iota.getPublicKeys(vaultConnector, config, identity, accountIndex, false, addressIndex, async () => Iota.getSeed(vaultConnector, config, identity));
175
+ const chunkStart = addressIndex - (addressIndex % Iota._PRE_CALC_CHUNK_SIZE);
176
+ const offsetInChunk = addressIndex - chunkStart;
177
+ const publicKey = Converter.base64ToBytes(publicKeys[offsetInChunk]);
178
+ const keyName = Iota.buildAddressKeyName(identity, accountIndex, false, addressIndex);
179
+ return new VaultTransactionSigner(vaultConnector, keyName, publicKey);
100
180
  }
101
181
  /**
102
182
  * Create a new transaction instance.
@@ -156,13 +236,12 @@ export class Iota {
156
236
  if (Is.stringValue(options?.dryRunLabel)) {
157
237
  await Iota.dryRunTransaction(client, logging, transaction, owner, options.dryRunLabel);
158
238
  }
159
- const seed = await Iota.getSeed(config, vaultConnector, identity);
160
- const addressKeyPair = Iota.findAddress(config.maxAddressScanRange ?? Iota._DEFAULT_SCAN_RANGE, config.coinType ?? Iota.DEFAULT_COIN_TYPE, seed, owner);
161
- const keypair = Ed25519Keypair.fromSecretKey(addressKeyPair.privateKey);
239
+ const { keyName, publicKey } = await Iota.ensureOwnerKey(vaultConnector, config, identity, owner);
240
+ const signer = new VaultSigner(vaultConnector, keyName, publicKey);
162
241
  try {
163
242
  const response = await client.signAndExecuteTransaction({
164
243
  transaction,
165
- signer: keypair,
244
+ signer,
166
245
  requestType: "WaitForLocalExecution",
167
246
  options: {
168
247
  showEffects: options?.showEffects ?? true,
@@ -185,40 +264,6 @@ export class Iota {
185
264
  throw new GeneralError(Iota.CLASS_NAME, "transactionFailed", undefined, Iota.extractPayloadError(error));
186
265
  }
187
266
  }
188
- /**
189
- * Get the seed from the vault.
190
- * @param config The configuration to use.
191
- * @param vaultConnector The vault connector to use.
192
- * @param identity The identity of the user to access the vault keys.
193
- * @returns The seed.
194
- */
195
- static async getSeed(config, vaultConnector, identity) {
196
- try {
197
- const seedBase64 = await vaultConnector.getSecret(Iota.buildSeedKey(identity, config.vaultSeedId));
198
- return Converter.base64ToBytes(seedBase64);
199
- }
200
- catch { }
201
- const mnemonic = await vaultConnector.getSecret(Iota.buildMnemonicKey(identity, config.vaultMnemonicId));
202
- return Bip39.mnemonicToSeed(mnemonic);
203
- }
204
- /**
205
- * Find the address in the seed.
206
- * @param maxScanRange The maximum range to scan.
207
- * @param coinType The coin type to use.
208
- * @param seed The seed to use.
209
- * @param address The address to find.
210
- * @returns The address key pair.
211
- * @throws Error if the address is not found.
212
- */
213
- static findAddress(maxScanRange, coinType, seed, address) {
214
- for (let i = 0; i < maxScanRange; i++) {
215
- const addressKeyPair = Bip44.address(seed, KeyType.Ed25519, coinType, 0, false, i);
216
- if (addressKeyPair.address === address) {
217
- return addressKeyPair;
218
- }
219
- }
220
- throw new GeneralError(Iota.CLASS_NAME, "addressNotFound", { address });
221
- }
222
267
  /**
223
268
  * Extract error from SDK payload.
224
269
  * Errors from the IOTA SDK are usually not JSON strings but objects.
@@ -250,24 +295,6 @@ export class Iota {
250
295
  }
251
296
  return baseError;
252
297
  }
253
- /**
254
- * Get the key for storing the mnemonic.
255
- * @param identity The identity to use.
256
- * @param vaultMnemonicId The mnemonic ID to use.
257
- * @returns The mnemonic key.
258
- */
259
- static buildMnemonicKey(identity, vaultMnemonicId) {
260
- return `${identity}/${vaultMnemonicId ?? Iota.DEFAULT_MNEMONIC_SECRET_NAME}`;
261
- }
262
- /**
263
- * Get the key for storing the seed.
264
- * @param identity The identity to use.
265
- * @param vaultSeedId The seed ID to use.
266
- * @returns The seed key.
267
- */
268
- static buildSeedKey(identity, vaultSeedId) {
269
- return `${identity}/${vaultSeedId ?? Iota.DEFAULT_SEED_SECRET_NAME}`;
270
- }
271
298
  /**
272
299
  * Check if the package exists on the network.
273
300
  * @param client The client to use.
@@ -306,7 +333,7 @@ export class Iota {
306
333
  * @param txb The transaction to dry run.
307
334
  * @param sender The sender address.
308
335
  * @param operation The operation to log.
309
- * @returns void.
336
+ * @returns The dry run result including status, costs, events, and object changes.
310
337
  */
311
338
  static async dryRunTransaction(client, logging, txb, sender, operation) {
312
339
  try {
@@ -387,11 +414,25 @@ export class Iota {
387
414
  static isAbortError(error, code) {
388
415
  const err = BaseError.fromError(error);
389
416
  if (Is.stringValue(err.properties?.error)) {
390
- const abortCodeMatch = /abort\s+code\s*:\s*(\d+)/i.exec(err.properties.error);
417
+ const abortCodeMatch = /abort code\s*:\s*(\d+)/i.exec(err.properties.error);
391
418
  return abortCodeMatch?.[1] === code.toString();
392
419
  }
393
420
  return false;
394
421
  }
422
+ /**
423
+ * Extracts the abort code from a transaction result if the transaction was aborted.
424
+ * @param response The transaction result to extract the abort code from.
425
+ * @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.
426
+ */
427
+ static extractAbortCode(response) {
428
+ if (response.effects?.status?.status === "failure" &&
429
+ Is.stringValue(response.effects.status.error)) {
430
+ const match = /abort code: (\d+)/.exec(response.effects.status.error);
431
+ if (match) {
432
+ return Coerce.integer(match[1]);
433
+ }
434
+ }
435
+ }
395
436
  /**
396
437
  * Prepare and post a transaction using gas station sponsoring.
397
438
  * @param config The configuration.
@@ -407,20 +448,17 @@ export class Iota {
407
448
  Guards.object(Iota.CLASS_NAME, "config.gasStation", config.gasStation);
408
449
  try {
409
450
  // Reserve gas from the gas station
410
- const gasBudget = config.gasBudget ?? 50000000;
411
- const gasReservation = await Iota.reserveGas(config, gasBudget);
451
+ const gasReservation = await Iota.reserveGas(config);
412
452
  // Set transaction parameters for sponsoring
413
453
  transaction.setSender(owner);
414
454
  transaction.setGasOwner(gasReservation.sponsorAddress);
415
455
  transaction.setGasPayment(gasReservation.gasCoins);
416
- transaction.setGasBudget(gasBudget);
456
+ transaction.setGasBudget(config.gasBudget ?? Iota._DEFAULT_GAS_BUDGET);
417
457
  // Build and sign transaction
418
458
  const unsignedTxBytes = await transaction.build({ client });
419
- // Sign the transaction with the user's private key
420
- const seed = await Iota.getSeed(config, vaultConnector, identity);
421
- const addressKeyPair = Iota.findAddress(config.maxAddressScanRange ?? Iota._DEFAULT_SCAN_RANGE, config.coinType ?? Iota.DEFAULT_COIN_TYPE, seed, owner);
422
- const keypair = Ed25519Keypair.fromSecretKey(addressKeyPair.privateKey);
423
- const signature = await keypair.signTransaction(unsignedTxBytes);
459
+ const { keyName, publicKey } = await Iota.ensureOwnerKey(vaultConnector, config, identity, owner);
460
+ const signer = new VaultSigner(vaultConnector, keyName, publicKey);
461
+ const signature = await signer.signTransaction(unsignedTxBytes);
424
462
  return await Iota.executeAndConfirmGasStationTransaction(config, client, gasReservation.reservationId, unsignedTxBytes, signature.signature, options);
425
463
  }
426
464
  catch (error) {
@@ -430,16 +468,15 @@ export class Iota {
430
468
  /**
431
469
  * Reserve gas from the gas station.
432
470
  * @param config The configuration containing gas station settings.
433
- * @param gasBudget The gas budget to reserve.
434
471
  * @returns The gas reservation result.
435
472
  */
436
- static async reserveGas(config, gasBudget) {
473
+ static async reserveGas(config) {
437
474
  Guards.object(Iota.CLASS_NAME, "config.gasStation", config.gasStation);
438
475
  const requestData = {
439
476
  // eslint-disable-next-line camelcase
440
- gas_budget: gasBudget,
477
+ gas_budget: config.gasBudget ?? Iota._DEFAULT_GAS_BUDGET,
441
478
  // eslint-disable-next-line camelcase
442
- reserve_duration_secs: 30
479
+ reserve_duration_secs: config.gasReservationDuration ?? Iota._DEFAULT_GAS_RESERVATION_DURATION
443
480
  };
444
481
  const baseUrl = StringHelper.trimTrailingSlashes(config.gasStation.gasStationUrl);
445
482
  const result = await FetchHelper.fetchJson(Iota.CLASS_NAME, `${baseUrl}/v1/reserve_gas`, HttpMethod.POST, requestData, {
@@ -511,5 +548,231 @@ export class Iota {
511
548
  }
512
549
  return response;
513
550
  }
551
+ /**
552
+ * Fund an address with IOTA from the faucet.
553
+ * @param config The configuration containing endpoint information.
554
+ * @param faucetUrl The URL of the faucet to request funds from.
555
+ * @param identity The identity of the user to access the vault keys.
556
+ * @param address The address to fund.
557
+ * @param timeoutInSeconds The timeout in seconds to wait for the funding to complete.
558
+ * @returns The amount funded.
559
+ */
560
+ static async fundAddress(config, faucetUrl, identity, address, timeoutInSeconds = 60) {
561
+ Guards.objectValue(Iota.CLASS_NAME, "config", config);
562
+ Guards.stringValue(Iota.CLASS_NAME, "config.clientOptions.url", config.clientOptions.url);
563
+ Guards.stringValue(Iota.CLASS_NAME, "faucetUrl", faucetUrl);
564
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
565
+ Guards.stringValue(Iota.CLASS_NAME, "address", address);
566
+ try {
567
+ const initialBalance = await Iota.getBalance(config, address);
568
+ const response = await requestIotaFromFaucetV0({
569
+ host: faucetUrl,
570
+ recipient: address
571
+ });
572
+ if (response?.error) {
573
+ throw new GeneralError(Iota.CLASS_NAME, "fundingFailed", undefined, response.error);
574
+ }
575
+ // Poll for balance change
576
+ const numTries = Math.ceil(timeoutInSeconds / 5);
577
+ for (let i = 0; i < numTries; i++) {
578
+ const newBalance = await Iota.getBalance(config, address);
579
+ if (newBalance > initialBalance) {
580
+ return newBalance - initialBalance;
581
+ }
582
+ if (i < numTries - 1) {
583
+ await new Promise(resolve => setTimeout(resolve, 5000));
584
+ }
585
+ }
586
+ }
587
+ catch (error) {
588
+ const payloadError = Iota.extractPayloadError(error);
589
+ if (payloadError.message.includes("Too many requests from this client have been sent to the faucet")) {
590
+ throw new GeneralError(Iota.CLASS_NAME, "faucetRateLimit", undefined, Iota.extractPayloadError(error));
591
+ }
592
+ throw new GeneralError(Iota.CLASS_NAME, "fundingFailed", undefined, Iota.extractPayloadError(error));
593
+ }
594
+ return 0n;
595
+ }
596
+ /**
597
+ * Ensure the balance for the given address is at least the given amount.
598
+ * @param config The configuration containing endpoint information.
599
+ * @param faucetUrl The URL of the faucet to request funds from.
600
+ * @param identity The identity of the user to access the vault keys.
601
+ * @param address The address to ensure the balance for.
602
+ * @param ensureBalance The minimum balance to ensure.
603
+ * @param timeoutInSeconds Optional timeout in seconds, defaults to 10 seconds.
604
+ * @returns True if the balance is at least the given amount, false otherwise.
605
+ */
606
+ static async ensureBalance(config, faucetUrl, identity, address, ensureBalance, timeoutInSeconds) {
607
+ Guards.objectValue(Iota.CLASS_NAME, "config", config);
608
+ Guards.stringValue(Iota.CLASS_NAME, "config.clientOptions.url", config.clientOptions.url);
609
+ Guards.stringValue(Iota.CLASS_NAME, "identity", identity);
610
+ Guards.stringValue(Iota.CLASS_NAME, "address", address);
611
+ Guards.bigint(Iota.CLASS_NAME, "ensureBalance", ensureBalance);
612
+ let currentBalance = await Iota.getBalance(config, address);
613
+ if (Is.stringValue(faucetUrl)) {
614
+ let retryCount = 10;
615
+ while (currentBalance < ensureBalance && retryCount > 0) {
616
+ const addedBalance = await Iota.fundAddress(config, faucetUrl, identity, address, timeoutInSeconds);
617
+ if (addedBalance === 0n) {
618
+ return false;
619
+ }
620
+ currentBalance += addedBalance;
621
+ if (currentBalance < ensureBalance) {
622
+ await new Promise(resolve => setTimeout(resolve, 1000));
623
+ retryCount--;
624
+ }
625
+ }
626
+ }
627
+ return currentBalance >= ensureBalance;
628
+ }
629
+ /**
630
+ * Get the balance for the given address.
631
+ * @param config The configuration containing endpoint information.
632
+ * @param address The address to get the balance for.
633
+ * @returns The balance of the given address.
634
+ */
635
+ static async getBalance(config, address) {
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, "address", address);
639
+ const client = Iota.createClient(config);
640
+ const balance = await client.getBalance({
641
+ owner: address
642
+ });
643
+ return BigInt(balance.totalBalance);
644
+ }
645
+ /**
646
+ * Create a transaction instance from the given bytes.
647
+ * @param bytes The transaction bytes to create the transaction from.
648
+ * @returns The transaction instance created from the given bytes.
649
+ */
650
+ static transactionFromBytes(bytes) {
651
+ return Transaction.from(bytes);
652
+ }
653
+ /**
654
+ * Find the vault key name and public key for the given owner address.
655
+ * Keys are individually registered in the vault by buildKeyPairRange, so no addKey is needed here.
656
+ * @param vaultConnector The vault connector to use.
657
+ * @param config The configuration to use.
658
+ * @param identity The identity of the user to access the vault keys.
659
+ * @param owner The owner address whose key should be located.
660
+ * @param accountIndex The account index to search.
661
+ * @returns The vault key name and the public key bytes.
662
+ * @internal
663
+ */
664
+ static async ensureOwnerKey(vaultConnector, config, identity, owner, accountIndex = 0) {
665
+ const scanRange = config.maxAddressScanRange ?? Iota._DEFAULT_SCAN_RANGE;
666
+ let cachedSeed;
667
+ const seedProvider = async () => {
668
+ cachedSeed ??= await Iota.getSeed(vaultConnector, config, identity);
669
+ return cachedSeed;
670
+ };
671
+ for (let chunkStart = 0; chunkStart < scanRange; chunkStart += Iota._PRE_CALC_CHUNK_SIZE) {
672
+ const publicKeys = await Iota.getPublicKeys(vaultConnector, config, identity, accountIndex, false, chunkStart, seedProvider);
673
+ const matchIndex = publicKeys.findIndex((pk) => Iota.publicKeyToAddress(Converter.base64ToBytes(pk)) === owner);
674
+ if (matchIndex >= 0) {
675
+ const addressIndex = chunkStart + matchIndex;
676
+ return {
677
+ keyName: Iota.buildAddressKeyName(identity, accountIndex, false, addressIndex),
678
+ publicKey: Converter.base64ToBytes(publicKeys[matchIndex])
679
+ };
680
+ }
681
+ }
682
+ throw new GeneralError(Iota.CLASS_NAME, "addressNotFound", { address: owner });
683
+ }
684
+ /**
685
+ * Get the seed from the vault, deriving it from the mnemonic if necessary.
686
+ * @param vaultConnector The vault connector to use.
687
+ * @param config The configuration to use.
688
+ * @param identity The identity of the user to access the vault keys.
689
+ * @returns The seed bytes.
690
+ * @internal
691
+ */
692
+ static async getSeed(vaultConnector, config, identity) {
693
+ const seedKey = Iota.buildSeedKey(identity, config.vaultSeedId);
694
+ try {
695
+ const seedBase64 = await vaultConnector.getSecret(seedKey);
696
+ return Converter.base64ToBytes(seedBase64);
697
+ }
698
+ catch { }
699
+ const mnemonic = await vaultConnector.getSecret(Iota.buildMnemonicKey(identity, config.vaultMnemonicId));
700
+ // If the seed is not found but the mnemonic exists, derive the seed and store it for future use
701
+ const seed = Bip39.mnemonicToSeed(mnemonic);
702
+ await vaultConnector.setSecret(seedKey, Converter.bytesToBase64(seed));
703
+ return seed;
704
+ }
705
+ /**
706
+ * Get the key for storing the mnemonic.
707
+ * @param identity The identity to use.
708
+ * @param vaultMnemonicId The mnemonic ID to use.
709
+ * @returns The mnemonic key.
710
+ * @internal
711
+ */
712
+ static buildMnemonicKey(identity, vaultMnemonicId) {
713
+ return VaultConnectorHelper.buildKeyName(identity, vaultMnemonicId ?? Iota.DEFAULT_MNEMONIC_SECRET_NAME);
714
+ }
715
+ /**
716
+ * Get the key for storing the seed.
717
+ * @param identity The identity to use.
718
+ * @param vaultSeedId The seed ID to use.
719
+ * @returns The seed key.
720
+ * @internal
721
+ */
722
+ static buildSeedKey(identity, vaultSeedId) {
723
+ return VaultConnectorHelper.buildKeyName(identity, vaultSeedId ?? Iota.DEFAULT_SEED_SECRET_NAME);
724
+ }
725
+ /**
726
+ * Ensure a range of BIP44-derived keys are registered as individual vault keys and return their public keys.
727
+ * If the first key of the range already exists the range is considered registered and only public keys are derived.
728
+ * If not registered, all keys are derived and added to the vault before returning the public keys.
729
+ * @param vaultConnector The vault connector to use.
730
+ * @param config The configuration to use.
731
+ * @param identity The identity of the user to access the vault keys.
732
+ * @param accountIndex The account index.
733
+ * @param internal Whether the addresses are internal or external.
734
+ * @param addressIndex Any address index within the desired chunk; aligned internally.
735
+ * @param seedProvider Callback invoked at most once per call to supply the seed when a chunk is not yet registered.
736
+ * @returns The base64-encoded public keys for each address in the range.
737
+ * @internal
738
+ */
739
+ static async getPublicKeys(vaultConnector, config, identity, accountIndex, internal, addressIndex, seedProvider) {
740
+ const chunkStart = addressIndex - (addressIndex % Iota._PRE_CALC_CHUNK_SIZE);
741
+ const firstKeyName = Iota.buildAddressKeyName(identity, accountIndex, internal, chunkStart);
742
+ const publicKeys = [];
743
+ if (await vaultConnector.keyExists(firstKeyName)) {
744
+ for (let i = chunkStart; i < chunkStart + Iota._PRE_CALC_CHUNK_SIZE; i++) {
745
+ const keyName = Iota.buildAddressKeyName(identity, accountIndex, internal, i);
746
+ const keyData = await vaultConnector.getKey(keyName, "public");
747
+ if (!keyData.publicKey) {
748
+ throw new GeneralError(Iota.CLASS_NAME, "missingPublicKey", { keyName });
749
+ }
750
+ publicKeys.push(Converter.bytesToBase64(keyData.publicKey));
751
+ }
752
+ }
753
+ else {
754
+ const seed = await seedProvider();
755
+ const coinType = config.coinType ?? Iota.DEFAULT_COIN_TYPE;
756
+ for (let i = chunkStart; i < chunkStart + Iota._PRE_CALC_CHUNK_SIZE; i++) {
757
+ const keyName = Iota.buildAddressKeyName(identity, accountIndex, internal, i);
758
+ const keyPair = Bip44.keyPair(seed, KeyType.Ed25519, coinType, accountIndex, internal, i);
759
+ await vaultConnector.addKey(keyName, VaultKeyType.Ed25519, keyPair.privateKey, keyPair.publicKey);
760
+ publicKeys.push(Converter.bytesToBase64(keyPair.publicKey));
761
+ }
762
+ }
763
+ return publicKeys;
764
+ }
765
+ /**
766
+ * Build the vault key name for a specific derived address.
767
+ * @param identity The identity to use.
768
+ * @param accountIndex The account index.
769
+ * @param internal Whether the address is internal or external.
770
+ * @param addressIndex The address index.
771
+ * @returns The vault key name.
772
+ * @internal
773
+ */
774
+ static buildAddressKeyName(identity, accountIndex, internal, addressIndex) {
775
+ return VaultConnectorHelper.buildKeyName(identity, "account", accountIndex.toString(), internal ? "1" : "0", `${addressIndex}`);
776
+ }
514
777
  }
515
778
  //# sourceMappingURL=iota.js.map