quais 1.0.0-alpha.13 → 1.0.0-alpha.15

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 (40) hide show
  1. package/dist/quais.js +656 -231
  2. package/dist/quais.js.map +1 -1
  3. package/dist/quais.min.js +1 -1
  4. package/dist/quais.umd.js +656 -231
  5. package/dist/quais.umd.js.map +1 -1
  6. package/dist/quais.umd.min.js +1 -1
  7. package/examples/wallets/qi-send.js +165 -35
  8. package/lib/commonjs/providers/abstract-provider.d.ts +12 -0
  9. package/lib/commonjs/providers/abstract-provider.d.ts.map +1 -1
  10. package/lib/commonjs/providers/abstract-provider.js +36 -3
  11. package/lib/commonjs/providers/abstract-provider.js.map +1 -1
  12. package/lib/commonjs/transaction/abstract-coinselector.d.ts +1 -1
  13. package/lib/commonjs/transaction/abstract-coinselector.d.ts.map +1 -1
  14. package/lib/commonjs/transaction/coinselector-fewest.d.ts +52 -4
  15. package/lib/commonjs/transaction/coinselector-fewest.d.ts.map +1 -1
  16. package/lib/commonjs/transaction/coinselector-fewest.js +132 -84
  17. package/lib/commonjs/transaction/coinselector-fewest.js.map +1 -1
  18. package/lib/commonjs/wallet/qi-hdwallet.d.ts +132 -26
  19. package/lib/commonjs/wallet/qi-hdwallet.d.ts.map +1 -1
  20. package/lib/commonjs/wallet/qi-hdwallet.js +485 -127
  21. package/lib/commonjs/wallet/qi-hdwallet.js.map +1 -1
  22. package/lib/esm/providers/abstract-provider.d.ts +12 -0
  23. package/lib/esm/providers/abstract-provider.d.ts.map +1 -1
  24. package/lib/esm/providers/abstract-provider.js +37 -4
  25. package/lib/esm/providers/abstract-provider.js.map +1 -1
  26. package/lib/esm/transaction/abstract-coinselector.d.ts +1 -1
  27. package/lib/esm/transaction/abstract-coinselector.d.ts.map +1 -1
  28. package/lib/esm/transaction/coinselector-fewest.d.ts +52 -4
  29. package/lib/esm/transaction/coinselector-fewest.d.ts.map +1 -1
  30. package/lib/esm/transaction/coinselector-fewest.js +132 -84
  31. package/lib/esm/transaction/coinselector-fewest.js.map +1 -1
  32. package/lib/esm/wallet/qi-hdwallet.d.ts +132 -26
  33. package/lib/esm/wallet/qi-hdwallet.d.ts.map +1 -1
  34. package/lib/esm/wallet/qi-hdwallet.js +485 -127
  35. package/lib/esm/wallet/qi-hdwallet.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/providers/abstract-provider.ts +38 -3
  38. package/src/transaction/abstract-coinselector.ts +1 -1
  39. package/src/transaction/coinselector-fewest.ts +153 -104
  40. package/src/wallet/qi-hdwallet.ts +586 -141
package/dist/quais.js CHANGED
@@ -782,21 +782,6 @@ function getBigInt(value, name) {
782
782
  }
783
783
  assertArgument(false, 'invalid BigNumberish value', name || 'value', value);
784
784
  }
785
- /**
786
- * Returns absolute value of bigint `value`.
787
- *
788
- * @category Utils
789
- * @param {BigNumberish} value - The value to convert.
790
- * @returns {bigint} The absolute value.
791
- */
792
- function bigIntAbs(value) {
793
- value = getBigInt(value);
794
- // if value is negative (including -0), return -value, else return value
795
- if (value === -BN_0$8 || value < BN_0$8) {
796
- return -value;
797
- }
798
- return value;
799
- }
800
785
  /**
801
786
  * Returns `value` as a bigint, validating it is valid as a bigint value and that it is positive.
802
787
  *
@@ -18286,6 +18271,7 @@ class UTXO {
18286
18271
  }
18287
18272
  }
18288
18273
 
18274
+ // import { bigIntAbs } from '../utils/maths.js';
18289
18275
  /**
18290
18276
  * The FewestCoinSelector class provides a coin selection algorithm that selects the fewest UTXOs required to meet the
18291
18277
  * target amount. This algorithm is useful for minimizing the size of the transaction and the fees associated with it.
@@ -18298,115 +18284,171 @@ class UTXO {
18298
18284
  */
18299
18285
  class FewestCoinSelector extends AbstractCoinSelector {
18300
18286
  /**
18301
- * The coin selection algorithm considering transaction fees.
18287
+ * Performs coin selection to meet the target amount plus fee, using the smallest possible denominations and
18288
+ * minimizing the number of inputs and outputs.
18302
18289
  *
18303
18290
  * @param {bigint} target - The target amount to spend.
18304
- * @returns {SelectedCoinsResult} The selected UTXOs and change outputs.
18291
+ * @param {bigint} fee - The fee amount to include in the selection.
18292
+ * @returns {SelectedCoinsResult} The selected UTXOs and outputs.
18305
18293
  */
18306
- performSelection(target) {
18294
+ performSelection(target, fee = BigInt(0)) {
18307
18295
  if (target <= BigInt(0)) {
18308
18296
  throw new Error('Target amount must be greater than 0');
18309
18297
  }
18298
+ if (fee < BigInt(0)) {
18299
+ throw new Error('Fee amount cannot be negative');
18300
+ }
18310
18301
  this.validateUTXOs();
18311
18302
  this.target = target;
18303
+ const totalRequired = BigInt(target) + BigInt(fee);
18312
18304
  // Initialize selection state
18313
18305
  this.selectedUTXOs = [];
18314
18306
  this.totalInputValue = BigInt(0);
18315
- const sortedUTXOs = this.sortUTXOsByDenomination(this.availableUTXOs, 'desc');
18316
- let totalValue = BigInt(0);
18317
- let selectedUTXOs = [];
18318
- // Get UTXOs that meets or exceeds the target value
18319
- const UTXOsEqualOrGreaterThanTarget = sortedUTXOs.filter((utxo) => utxo.denomination !== null && BigInt(denominations[utxo.denomination]) >= target);
18320
- if (UTXOsEqualOrGreaterThanTarget.length > 0) {
18321
- // Find the smallest UTXO that meets or exceeds the target value
18322
- const optimalUTXO = UTXOsEqualOrGreaterThanTarget.reduce((minDenominationUTXO, currentUTXO) => {
18323
- if (currentUTXO.denomination === null)
18324
- return minDenominationUTXO;
18325
- return BigInt(denominations[currentUTXO.denomination]) <
18326
- BigInt(denominations[minDenominationUTXO.denomination])
18327
- ? currentUTXO
18328
- : minDenominationUTXO;
18329
- }, UTXOsEqualOrGreaterThanTarget[0]);
18330
- selectedUTXOs.push(optimalUTXO);
18331
- totalValue += BigInt(denominations[optimalUTXO.denomination]);
18307
+ // Sort available UTXOs by denomination in ascending order
18308
+ const sortedUTXOs = this.sortUTXOsByDenomination(this.availableUTXOs, 'asc');
18309
+ // Attempt to find a single UTXO that can cover the total required amount
18310
+ const singleUTXO = sortedUTXOs.find((utxo) => BigInt(denominations[utxo.denomination]) >= totalRequired);
18311
+ if (singleUTXO) {
18312
+ // Use the smallest UTXO that can cover the total required amount
18313
+ this.selectedUTXOs.push(singleUTXO);
18314
+ this.totalInputValue = BigInt(denominations[singleUTXO.denomination]);
18332
18315
  }
18333
18316
  else {
18334
- // If no single UTXO meets or exceeds the target, aggregate smaller denominations
18335
- // until the target is met/exceeded or there are no more UTXOs to aggregate
18336
- while (sortedUTXOs.length > 0 && totalValue < target) {
18337
- const nextOptimalUTXO = sortedUTXOs.reduce((closest, utxo) => {
18338
- if (utxo.denomination === null)
18339
- return closest;
18340
- // Prioritize UTXOs that bring totalValue closer to target.value
18341
- const absThisDiff = bigIntAbs(BigInt(target) - (BigInt(totalValue) + BigInt(denominations[utxo.denomination])));
18342
- const currentClosestDiff = closest && closest.denomination !== null
18343
- ? bigIntAbs(BigInt(target) - (BigInt(totalValue) + BigInt(denominations[closest.denomination])))
18344
- : BigInt(Number.MAX_SAFE_INTEGER);
18345
- return absThisDiff < currentClosestDiff ? utxo : closest;
18346
- }, sortedUTXOs[0]);
18347
- // Add the selected UTXO to the selection and update totalValue
18348
- selectedUTXOs.push(nextOptimalUTXO);
18349
- totalValue += BigInt(denominations[nextOptimalUTXO.denomination]);
18350
- // Remove the selected UTXO from the list of available UTXOs
18351
- const index = sortedUTXOs.findIndex((utxo) => utxo.denomination === nextOptimalUTXO.denomination && utxo.address === nextOptimalUTXO.address);
18352
- sortedUTXOs.splice(index, 1);
18353
- }
18354
- }
18355
- // Optimize the selection process
18356
- let optimalSelection = selectedUTXOs;
18357
- let minExcess = BigInt(totalValue) - BigInt(target);
18358
- for (let i = 0; i < selectedUTXOs.length; i++) {
18359
- const subsetUTXOs = selectedUTXOs.slice(0, i).concat(selectedUTXOs.slice(i + 1));
18360
- const subsetTotal = subsetUTXOs.reduce((sum, utxo) => BigInt(sum) + BigInt(denominations[utxo.denomination]), BigInt(0));
18361
- if (subsetTotal >= target) {
18362
- const excess = BigInt(subsetTotal) - BigInt(target);
18363
- if (excess < minExcess) {
18364
- optimalSelection = subsetUTXOs;
18365
- minExcess = excess;
18366
- totalValue = subsetTotal;
18367
- }
18368
- }
18369
- }
18370
- selectedUTXOs = optimalSelection;
18371
- // Find the largest denomination used in the inputs
18372
- // Store the selected UTXOs and total input value
18373
- this.selectedUTXOs = selectedUTXOs;
18374
- this.totalInputValue = totalValue;
18375
- // Check if the selected UTXOs meet or exceed the target amount
18376
- if (totalValue < target) {
18377
- throw new Error('Insufficient funds');
18378
- }
18379
- // Store spendOutputs and changeOutputs
18317
+ // If no single UTXO can cover the total required amount, find the minimal set
18318
+ this.selectedUTXOs = this.findMinimalUTXOSet(sortedUTXOs, totalRequired);
18319
+ if (this.selectedUTXOs.length === 0) {
18320
+ throw new Error('Insufficient funds');
18321
+ }
18322
+ // Calculate total input value
18323
+ this.totalInputValue = this.selectedUTXOs.reduce((sum, utxo) => sum + BigInt(denominations[utxo.denomination]), BigInt(0));
18324
+ }
18325
+ // Create outputs
18326
+ const changeAmount = this.totalInputValue - BigInt(target) - BigInt(fee);
18327
+ // Create spend outputs (to the recipient)
18380
18328
  this.spendOutputs = this.createSpendOutputs(target);
18381
- this.changeOutputs = this.createChangeOutputs(BigInt(totalValue) - BigInt(target));
18329
+ // Create change outputs (to ourselves), if any
18330
+ this.changeOutputs = this.createChangeOutputs(changeAmount);
18331
+ // Verify that sum of outputs does not exceed sum of inputs
18332
+ const totalOutputValue = this.calculateTotalOutputValue();
18333
+ if (totalOutputValue > this.totalInputValue) {
18334
+ throw new Error('Total output value exceeds total input value');
18335
+ }
18336
+ // Ensure largest output denomination ≤ largest input denomination
18337
+ const maxInputDenomination = this.getMaxInputDenomination();
18338
+ const maxOutputDenomination = this.getMaxOutputDenomination();
18339
+ if (maxOutputDenomination > maxInputDenomination) {
18340
+ throw new Error('Largest output denomination exceeds largest input denomination');
18341
+ }
18382
18342
  return {
18383
- inputs: selectedUTXOs,
18343
+ inputs: this.selectedUTXOs,
18384
18344
  spendOutputs: this.spendOutputs,
18385
18345
  changeOutputs: this.changeOutputs,
18386
18346
  };
18387
18347
  }
18388
- // Helper methods to create spend and change outputs
18348
+ /**
18349
+ * Finds the minimal set of UTXOs that can cover the total required amount.
18350
+ *
18351
+ * @param {UTXO[]} sortedUTXOs - Available UTXOs sorted by denomination (ascending).
18352
+ * @param {bigint} totalRequired - The total amount required (target + fee).
18353
+ * @returns {UTXO[]} The minimal set of UTXOs.
18354
+ */
18355
+ findMinimalUTXOSet(sortedUTXOs, totalRequired) {
18356
+ // Use a greedy algorithm to select the fewest UTXOs
18357
+ // Starting from the largest denominations to minimize the number of inputs
18358
+ const utxos = [...sortedUTXOs].reverse(); // Largest to smallest
18359
+ let totalValue = BigInt(0);
18360
+ const selectedUTXOs = [];
18361
+ for (const utxo of utxos) {
18362
+ if (totalValue >= totalRequired) {
18363
+ break;
18364
+ }
18365
+ selectedUTXOs.push(utxo);
18366
+ totalValue += BigInt(denominations[utxo.denomination]);
18367
+ }
18368
+ if (totalValue >= totalRequired) {
18369
+ return selectedUTXOs;
18370
+ }
18371
+ else {
18372
+ return []; // Insufficient funds
18373
+ }
18374
+ }
18375
+ /**
18376
+ * Creates spend outputs based on the target amount and input denominations.
18377
+ *
18378
+ * @param {bigint} amount - The target amount to spend.
18379
+ * @param {UTXO[]} inputs - The selected inputs.
18380
+ * @returns {UTXO[]} The spend outputs.
18381
+ */
18389
18382
  createSpendOutputs(amount) {
18390
- const maxDenomination = this.getMaxInputDenomination();
18391
- const spendDenominations = denominate(amount, maxDenomination);
18392
- return spendDenominations.map((denomination) => {
18383
+ const maxInputDenomination = this.getMaxInputDenomination();
18384
+ // Denominate the amount using available denominations up to the max input denomination
18385
+ const spendDenominations = denominate(amount, maxInputDenomination);
18386
+ return spendDenominations.map((denominationValue) => {
18393
18387
  const utxo = new UTXO();
18394
- utxo.denomination = denominations.indexOf(denomination);
18388
+ utxo.denomination = denominations.indexOf(denominationValue);
18395
18389
  return utxo;
18396
18390
  });
18397
18391
  }
18392
+ /**
18393
+ * Creates change outputs based on the change amount and input denominations.
18394
+ *
18395
+ * @param {bigint} change - The change amount to return.
18396
+ * @param {UTXO[]} inputs - The selected inputs.
18397
+ * @returns {UTXO[]} The change outputs.
18398
+ */
18398
18399
  createChangeOutputs(change) {
18399
18400
  if (change <= BigInt(0)) {
18400
18401
  return [];
18401
18402
  }
18402
- const maxDenomination = this.getMaxInputDenomination();
18403
- const changeDenominations = denominate(change, maxDenomination);
18404
- return changeDenominations.map((denomination) => {
18403
+ const maxInputDenomination = this.getMaxInputDenomination();
18404
+ // Denominate the change amount using available denominations up to the max input denomination
18405
+ const changeDenominations = denominate(change, maxInputDenomination);
18406
+ return changeDenominations.map((denominationValue) => {
18405
18407
  const utxo = new UTXO();
18406
- utxo.denomination = denominations.indexOf(denomination);
18408
+ utxo.denomination = denominations.indexOf(denominationValue);
18407
18409
  return utxo;
18408
18410
  });
18409
18411
  }
18412
+ /**
18413
+ * Calculates the total value of outputs (spend + change).
18414
+ *
18415
+ * @returns {bigint} The total output value.
18416
+ */
18417
+ calculateTotalOutputValue() {
18418
+ const spendValue = this.spendOutputs.reduce((sum, output) => sum + BigInt(denominations[output.denomination]), BigInt(0));
18419
+ const changeValue = this.changeOutputs.reduce((sum, output) => sum + BigInt(denominations[output.denomination]), BigInt(0));
18420
+ return spendValue + changeValue;
18421
+ }
18422
+ /**
18423
+ * Gets the maximum denomination value from the selected UTXOs.
18424
+ *
18425
+ * @returns {bigint} The maximum input denomination value.
18426
+ */
18427
+ getMaxInputDenomination() {
18428
+ const inputs = [...this.selectedUTXOs];
18429
+ return this.getMaxDenomination(inputs);
18430
+ }
18431
+ /**
18432
+ * Gets the maximum denomination value from the spend and change outputs.
18433
+ *
18434
+ * @returns {bigint} The maximum output denomination value.
18435
+ */
18436
+ getMaxOutputDenomination() {
18437
+ const outputs = [...this.spendOutputs, ...this.changeOutputs];
18438
+ return this.getMaxDenomination(outputs);
18439
+ }
18440
+ /**
18441
+ * Gets the maximum denomination value from a list of UTXOs.
18442
+ *
18443
+ * @param {UTXO[]} utxos - The list of UTXOs.
18444
+ * @returns {bigint} The maximum denomination value.
18445
+ */
18446
+ getMaxDenomination(utxos) {
18447
+ return utxos.reduce((max, utxo) => {
18448
+ const denomValue = BigInt(denominations[utxo.denomination]);
18449
+ return denomValue > max ? denomValue : max;
18450
+ }, BigInt(0));
18451
+ }
18410
18452
  /**
18411
18453
  * Increases the total fee by first reducing change outputs, then selecting additional inputs if necessary.
18412
18454
  *
@@ -18439,9 +18481,7 @@ class FewestCoinSelector extends AbstractCoinSelector {
18439
18481
  if (remainingFee <= BigInt(0)) {
18440
18482
  // If we have excess, create a new change output
18441
18483
  if (remainingFee < BigInt(0)) {
18442
- const change = BigInt(this.totalInputValue) -
18443
- BigInt(this.target) -
18444
- (BigInt(additionalFeeNeeded) - BigInt(remainingFee));
18484
+ const change = BigInt(this.totalInputValue) - BigInt(this.target) - BigInt(additionalFeeNeeded);
18445
18485
  this.adjustChangeOutputs(change);
18446
18486
  }
18447
18487
  }
@@ -18485,12 +18525,6 @@ class FewestCoinSelector extends AbstractCoinSelector {
18485
18525
  changeOutputs: this.changeOutputs,
18486
18526
  };
18487
18527
  }
18488
- getMaxInputDenomination() {
18489
- return this.selectedUTXOs.reduce((max, utxo) => {
18490
- const denomValue = BigInt(denominations[utxo.denomination]);
18491
- return denomValue > max ? denomValue : max;
18492
- }, BigInt(0));
18493
- }
18494
18528
  /**
18495
18529
  * Helper method to adjust change outputs.
18496
18530
  *
@@ -27070,6 +27104,15 @@ class HDNodeBIP32Adapter {
27070
27104
  }
27071
27105
  }
27072
27106
 
27107
+ /**
27108
+ * Current known issues:
27109
+ *
27110
+ * - When generating send addresses we are not checking if the address has already been used before
27111
+ * - When syncing is seems like we are adding way too many change addresses
27112
+ * - Bip44 external and change address maps also have gap addresses in them
27113
+ * - It is unclear if we have checked if addresses have been used and if they are used
27114
+ * - We should always check all addresses that were previously included in a transaction to see if they have been used
27115
+ */
27073
27116
  /**
27074
27117
  * The Qi HD wallet is a BIP44-compliant hierarchical deterministic wallet used for managing a set of addresses in the
27075
27118
  * Qi ledger. This is wallet implementation is the primary way to interact with the Qi UTXO ledger on the Quai network.
@@ -27107,7 +27150,7 @@ class QiHDWallet extends AbstractHDWallet {
27107
27150
  * @ignore
27108
27151
  * @type {number}
27109
27152
  */
27110
- static _GAP_LIMIT = 20;
27153
+ static _GAP_LIMIT = 5;
27111
27154
  /**
27112
27155
  * @ignore
27113
27156
  * @type {AllowedCoinType}
@@ -27134,19 +27177,44 @@ class QiHDWallet extends AbstractHDWallet {
27134
27177
  * @type {NeuteredAddressInfo[]}
27135
27178
  */
27136
27179
  _gapAddresses = [];
27180
+ /**
27181
+ * This array is used to keep track of gap addresses that have been included in a transaction, but whose outpoints
27182
+ * have not been imported into the wallet.
27183
+ *
27184
+ * @ignore
27185
+ * @type {NeuteredAddressInfo[]}
27186
+ */
27187
+ _usedGapAddresses = [];
27188
+ /**
27189
+ * This array is used to keep track of gap change addresses that have been included in a transaction, but whose
27190
+ * outpoints have not been imported into the wallet.
27191
+ *
27192
+ * @ignore
27193
+ * @type {NeuteredAddressInfo[]}
27194
+ */
27195
+ _usedGapChangeAddresses = [];
27137
27196
  /**
27138
27197
  * Array of outpoint information.
27139
27198
  *
27140
27199
  * @ignore
27141
27200
  * @type {OutpointInfo[]}
27142
27201
  */
27143
- _outpoints = [];
27202
+ _availableOutpoints = [];
27144
27203
  /**
27145
- * Map of paymentcodes to paymentCodeInfo for the receiver
27204
+ * Map of outpoints that are pending confirmation of being spent.
27205
+ */
27206
+ _pendingOutpoints = [];
27207
+ /**
27208
+ * @ignore
27209
+ * @type {AddressUsageCallback}
27210
+ */
27211
+ _addressUseChecker;
27212
+ /**
27213
+ * Map of paymentcodes to PaymentChannelAddressInfo for the receiver
27146
27214
  */
27147
27215
  _receiverPaymentCodeInfo = new Map();
27148
27216
  /**
27149
- * Map of paymentcodes to paymentCodeInfo for the sender
27217
+ * Map of paymentcodes to PaymentChannelAddressInfo for the sender
27150
27218
  */
27151
27219
  _senderPaymentCodeInfo = new Map();
27152
27220
  /**
@@ -27157,6 +27225,16 @@ class QiHDWallet extends AbstractHDWallet {
27157
27225
  constructor(guard, root, provider) {
27158
27226
  super(guard, root, provider);
27159
27227
  }
27228
+ /**
27229
+ * Sets the address use checker. The provided callback function should accept an address as input and return a
27230
+ * boolean indicating whether the address is in use. If the callback returns true, the address is considered used
27231
+ * and if it returns false, the address is considered unused.
27232
+ *
27233
+ * @param {AddressUsageCallback} checker - The address use checker.
27234
+ */
27235
+ setAddressUseChecker(checker) {
27236
+ this._addressUseChecker = checker;
27237
+ }
27160
27238
  // getters for the payment code info maps
27161
27239
  get receiverPaymentCodeInfo() {
27162
27240
  return Object.fromEntries(this._receiverPaymentCodeInfo);
@@ -27191,7 +27269,7 @@ class QiHDWallet extends AbstractHDWallet {
27191
27269
  */
27192
27270
  importOutpoints(outpoints) {
27193
27271
  this.validateOutpointInfo(outpoints);
27194
- this._outpoints.push(...outpoints);
27272
+ this._availableOutpoints.push(...outpoints);
27195
27273
  }
27196
27274
  /**
27197
27275
  * Gets the outpoints for the specified zone.
@@ -27201,7 +27279,7 @@ class QiHDWallet extends AbstractHDWallet {
27201
27279
  */
27202
27280
  getOutpoints(zone) {
27203
27281
  this.validateZone(zone);
27204
- return this._outpoints.filter((outpoint) => outpoint.zone === zone);
27282
+ return this._availableOutpoints.filter((outpoint) => outpoint.zone === zone);
27205
27283
  }
27206
27284
  /**
27207
27285
  * Signs a Qi transaction and returns the serialized transaction.
@@ -27231,6 +27309,47 @@ class QiHDWallet extends AbstractHDWallet {
27231
27309
  txobj.signature = signature;
27232
27310
  return txobj.serialized;
27233
27311
  }
27312
+ /**
27313
+ * Gets the payment channel address info for a given address.
27314
+ *
27315
+ * @param {string} address - The address to look up.
27316
+ * @returns {PaymentChannelAddressInfo | null} The address info or null if not found.
27317
+ */
27318
+ getPaymentChannelAddressInfo(address) {
27319
+ for (const [paymentCode, pcInfoArray] of this._receiverPaymentCodeInfo.entries()) {
27320
+ const pcInfo = pcInfoArray.find((info) => info.address === address);
27321
+ if (pcInfo) {
27322
+ return { ...pcInfo, counterpartyPaymentCode: paymentCode };
27323
+ }
27324
+ }
27325
+ return null;
27326
+ }
27327
+ /**
27328
+ * Locates the address information for the given address, searching through standard addresses, change addresses,
27329
+ * and payment channel addresses.
27330
+ *
27331
+ * @param {string} address - The address to locate.
27332
+ * @returns {NeuteredAddressInfo | PaymentChannelAddressInfo | null} The address info or null if not found.
27333
+ */
27334
+ locateAddressInfo(address) {
27335
+ // First, try to get standard address info
27336
+ let addressInfo = this.getAddressInfo(address);
27337
+ if (addressInfo) {
27338
+ return addressInfo;
27339
+ }
27340
+ // Next, try to get change address info
27341
+ addressInfo = this.getChangeAddressInfo(address);
27342
+ if (addressInfo) {
27343
+ return addressInfo;
27344
+ }
27345
+ // Finally, try to get payment channel address info
27346
+ const pcAddressInfo = this.getPaymentChannelAddressInfo(address);
27347
+ if (pcAddressInfo) {
27348
+ return pcAddressInfo;
27349
+ }
27350
+ // Address not found
27351
+ return null;
27352
+ }
27234
27353
  /**
27235
27354
  * Gets the balance for the specified zone.
27236
27355
  *
@@ -27239,7 +27358,7 @@ class QiHDWallet extends AbstractHDWallet {
27239
27358
  */
27240
27359
  getBalanceForZone(zone) {
27241
27360
  this.validateZone(zone);
27242
- return this._outpoints
27361
+ return this._availableOutpoints
27243
27362
  .filter((outpoint) => outpoint.zone === zone)
27244
27363
  .reduce((total, outpoint) => {
27245
27364
  const denominationValue = denominations[outpoint.outpoint.denomination];
@@ -27254,7 +27373,7 @@ class QiHDWallet extends AbstractHDWallet {
27254
27373
  */
27255
27374
  outpointsToUTXOs(zone) {
27256
27375
  this.validateZone(zone);
27257
- return this._outpoints
27376
+ return this._availableOutpoints
27258
27377
  .filter((outpointInfo) => outpointInfo.zone === zone)
27259
27378
  .map((outpointInfo) => {
27260
27379
  const utxo = new UTXO();
@@ -27296,17 +27415,32 @@ class QiHDWallet extends AbstractHDWallet {
27296
27415
  let selection = fewestCoinSelector.performSelection(spendTarget);
27297
27416
  // 3. Generate as many unused addresses as required to populate the spend outputs
27298
27417
  const sendAddresses = [];
27299
- for (let i = 0; i < selection.spendOutputs.length; i++) {
27300
- sendAddresses.push(await this.getNextSendAddress(recipientPaymentCode, destinationZone));
27418
+ while (sendAddresses.length < selection.spendOutputs.length) {
27419
+ const address = this.getNextSendAddress(recipientPaymentCode, destinationZone).address;
27420
+ const { isUsed } = await this.checkAddressUse(address);
27421
+ if (!isUsed) {
27422
+ sendAddresses.push(address);
27423
+ }
27301
27424
  }
27302
- // 4. Generate as many addresses as required to populate the change outputs
27425
+ // 4. get known change addresses, then populate with new ones as needed
27303
27426
  const changeAddresses = [];
27304
27427
  for (let i = 0; i < selection.changeOutputs.length; i++) {
27305
- changeAddresses.push((await this.getNextChangeAddress(0, originZone)).address);
27428
+ if (this._gapChangeAddresses.length > 0) {
27429
+ // 1. get next change address from gap addresses array
27430
+ // 2. remove it from the gap change addresses array
27431
+ // 3. add it to the change addresses array
27432
+ // 4. add it to the used gap change addresses array
27433
+ const nextChangeAddressInfo = this._gapChangeAddresses.shift();
27434
+ changeAddresses.push(nextChangeAddressInfo.address);
27435
+ this._usedGapChangeAddresses.push(nextChangeAddressInfo);
27436
+ }
27437
+ else {
27438
+ changeAddresses.push((await this.getNextChangeAddress(0, originZone)).address);
27439
+ }
27306
27440
  }
27307
27441
  // 5. Create the transaction and sign it using the signTransaction method
27308
27442
  // 5.1 Fetch the public keys for the input addresses
27309
- let inputPubKeys = selection.inputs.map((input) => this.getAddressInfo(input.address)?.pubKey);
27443
+ let inputPubKeys = selection.inputs.map((input) => this.locateAddressInfo(input.address)?.pubKey);
27310
27444
  if (inputPubKeys.some((pubkey) => !pubkey)) {
27311
27445
  throw new Error('Missing public key for input address');
27312
27446
  }
@@ -27316,23 +27450,25 @@ class QiHDWallet extends AbstractHDWallet {
27316
27450
  const feeData = await this.provider.getFeeData(originZone, false);
27317
27451
  // 5.6 Calculate total fee for the transaction using the gasLimit, gasPrice, maxFeePerGas and maxPriorityFeePerGas
27318
27452
  const totalFee = gasLimit * (feeData.gasPrice ?? 1n) + (feeData.maxFeePerGas ?? 0n) + (feeData.maxPriorityFeePerGas ?? 0n);
27319
- // Get new selection with increased fee
27320
- selection = fewestCoinSelector.increaseFee(totalFee);
27453
+ // Get new selection with fee
27454
+ selection = fewestCoinSelector.performSelection(spendTarget, totalFee);
27321
27455
  // 5.7 Determine if new addresses are needed for the change outputs
27322
- const changeAddressesNeeded = selection.changeOutputs.length > changeAddresses.length;
27323
- if (changeAddressesNeeded) {
27324
- for (let i = 0; i < selection.changeOutputs.length; i++) {
27456
+ const changeAddressesNeeded = selection.changeOutputs.length - changeAddresses.length;
27457
+ if (changeAddressesNeeded > 0) {
27458
+ for (let i = 0; i < changeAddressesNeeded; i++) {
27325
27459
  changeAddresses.push((await this.getNextChangeAddress(0, originZone)).address);
27326
27460
  }
27327
27461
  }
27328
- const spendAddressesNeeded = selection.spendOutputs.length > sendAddresses.length;
27329
- if (spendAddressesNeeded) {
27330
- for (let i = 0; i < selection.spendOutputs.length; i++) {
27331
- sendAddresses.push(await this.getNextSendAddress(recipientPaymentCode, destinationZone));
27462
+ const spendAddressesNeeded = selection.spendOutputs.length - sendAddresses.length;
27463
+ if (spendAddressesNeeded > 0) {
27464
+ for (let i = 0; i < spendAddressesNeeded; i++) {
27465
+ sendAddresses.push(this.getNextSendAddress(recipientPaymentCode, destinationZone).address);
27332
27466
  }
27333
27467
  }
27334
- inputPubKeys = selection.inputs.map((input) => this.getAddressInfo(input.address)?.pubKey);
27468
+ inputPubKeys = selection.inputs.map((input) => this.locateAddressInfo(input.address)?.pubKey);
27335
27469
  tx = await this.prepareTransaction(selection, inputPubKeys.map((pubkey) => pubkey), sendAddresses, changeAddresses, Number(chainId));
27470
+ // Move used outpoints to pendingOutpoints
27471
+ this.moveOutpointsToPending(tx.txInputs);
27336
27472
  // 5.6 Sign the transaction
27337
27473
  const signedTx = await this.signTransaction(tx);
27338
27474
  // 6. Broadcast the transaction to the network using the provider
@@ -27362,6 +27498,60 @@ class QiHDWallet extends AbstractHDWallet {
27362
27498
  tx.chainId = chainId;
27363
27499
  return tx;
27364
27500
  }
27501
+ /**
27502
+ * Checks the status of pending outpoints and updates the wallet's UTXO set accordingly.
27503
+ *
27504
+ * @param zone The zone in which to check the pending outpoints.
27505
+ */
27506
+ async checkPendingOutpoints(zone) {
27507
+ // Create a copy to iterate over, as we'll be modifying the _pendingOutpoints array
27508
+ const pendingOutpoints = [...this._pendingOutpoints.filter((info) => info.zone === zone)];
27509
+ const uniqueAddresses = new Set(pendingOutpoints.map((info) => info.address));
27510
+ const outpointsByAddress = await Promise.all(Array.from(uniqueAddresses).map((address) => this.getOutpointsByAddress(address)));
27511
+ const allOutpointsByAddress = outpointsByAddress.flat();
27512
+ for (const outpointInfo of pendingOutpoints) {
27513
+ const isSpent = !allOutpointsByAddress.some((outpoint) => outpoint.txhash === outpointInfo.outpoint.txhash && outpoint.index === outpointInfo.outpoint.index);
27514
+ if (isSpent) {
27515
+ // Outpoint has been spent; remove it from pendingOutpoints
27516
+ this.removeOutpointFromPending(outpointInfo.outpoint);
27517
+ }
27518
+ else {
27519
+ // Outpoint is still unspent; move it back to available outpoints
27520
+ this.moveOutpointToAvailable(outpointInfo);
27521
+ }
27522
+ }
27523
+ }
27524
+ /**
27525
+ * Moves specified inputs to pending outpoints.
27526
+ *
27527
+ * @param inputs List of inputs used in the transaction.
27528
+ */
27529
+ moveOutpointsToPending(inputs) {
27530
+ inputs.forEach((input) => {
27531
+ const index = this._availableOutpoints.findIndex((outpointInfo) => outpointInfo.outpoint.txhash === input.txhash && outpointInfo.outpoint.index === input.index);
27532
+ if (index !== -1) {
27533
+ const [outpointInfo] = this._availableOutpoints.splice(index, 1);
27534
+ this._pendingOutpoints.push(outpointInfo);
27535
+ }
27536
+ });
27537
+ }
27538
+ /**
27539
+ * Removes an outpoint from the pending outpoints.
27540
+ *
27541
+ * @param outpoint The outpoint to remove.
27542
+ */
27543
+ removeOutpointFromPending(outpoint) {
27544
+ this._pendingOutpoints = this._pendingOutpoints.filter((info) => !(info.outpoint.txhash === outpoint.txhash && info.outpoint.index === outpoint.index));
27545
+ }
27546
+ /**
27547
+ * Moves an outpoint from pending back to available outpoints.
27548
+ *
27549
+ * @param outpointInfo The outpoint info to move.
27550
+ */
27551
+ moveOutpointToAvailable(outpointInfo) {
27552
+ this.removeOutpointFromPending(outpointInfo.outpoint);
27553
+ this._availableOutpoints.push(outpointInfo);
27554
+ }
27365
27555
  /**
27366
27556
  * Returns a schnorr signature for the given message and private key.
27367
27557
  *
@@ -27414,15 +27604,12 @@ class QiHDWallet extends AbstractHDWallet {
27414
27604
  /**
27415
27605
  * Retrieves the private key for a given transaction input.
27416
27606
  *
27417
- * This method derives the private key for a transaction input by following these steps:
27607
+ * This method derives the private key for a transaction input by locating the address info and then deriving the
27608
+ * private key based on where the address info was found:
27418
27609
  *
27419
- * 1. Ensures the input contains a public key.
27420
- * 2. Computes the address from the public key.
27421
- * 3. Fetches address information associated with the computed address.
27422
- * 4. Derives the hierarchical deterministic (HD) node corresponding to the address.
27423
- * 5. Returns the private key of the derived HD node.
27610
+ * - For BIP44 addresses (standard or change), it uses the HD wallet to derive the private key.
27611
+ * - For payment channel addresses (BIP47), it uses PaymentCodePrivate to derive the private key.
27424
27612
  *
27425
- * @ignore
27426
27613
  * @param {TxInput} input - The transaction input containing the public key.
27427
27614
  * @returns {string} The private key corresponding to the transaction input.
27428
27615
  * @throws {Error} If the input does not contain a public key or if the address information cannot be found.
@@ -27431,21 +27618,42 @@ class QiHDWallet extends AbstractHDWallet {
27431
27618
  if (!input.pubkey)
27432
27619
  throw new Error('Missing public key for input');
27433
27620
  const address = computeAddress(input.pubkey);
27434
- // get address info
27435
- const addressInfo = this.getAddressInfo(address);
27436
- if (!addressInfo)
27621
+ const addressInfo = this.locateAddressInfo(address);
27622
+ if (!addressInfo) {
27437
27623
  throw new Error(`Address not found: ${address}`);
27438
- // derive an HDNode for the address and get the private key
27439
- const changeIndex = addressInfo.change ? 1 : 0;
27440
- const addressNode = this._root
27441
- .deriveChild(addressInfo.account)
27442
- .deriveChild(changeIndex)
27443
- .deriveChild(addressInfo.index);
27444
- return addressNode.privateKey;
27624
+ }
27625
+ if ('change' in addressInfo) {
27626
+ // NeuteredAddressInfo (BIP44 addresses)
27627
+ const changeIndex = addressInfo.change ? 1 : 0;
27628
+ const addressNode = this._root
27629
+ .deriveChild(addressInfo.account)
27630
+ .deriveChild(changeIndex)
27631
+ .deriveChild(addressInfo.index);
27632
+ return addressNode.privateKey;
27633
+ }
27634
+ else {
27635
+ // PaymentChannelAddressInfo (BIP47 addresses)
27636
+ const pcAddressInfo = addressInfo;
27637
+ const account = pcAddressInfo.account;
27638
+ const index = pcAddressInfo.index - 1;
27639
+ const counterpartyPaymentCode = pcAddressInfo.counterpartyPaymentCode;
27640
+ if (!counterpartyPaymentCode) {
27641
+ throw new Error('Counterparty payment code not found for payment channel address');
27642
+ }
27643
+ const bip32 = BIP32Factory(ecc);
27644
+ const buf = bs58check.decode(counterpartyPaymentCode);
27645
+ const version = buf[0];
27646
+ if (version !== PC_VERSION)
27647
+ throw new Error('Invalid payment code version');
27648
+ const counterpartyPCodePublic = new PaymentCodePublic(ecc, bip32, buf.slice(1));
27649
+ const paymentCodePrivate = this._getPaymentCodePrivate(account);
27650
+ const paymentPrivateKey = paymentCodePrivate.derivePaymentPrivateKey(counterpartyPCodePublic, index);
27651
+ return hexlify(paymentPrivateKey);
27652
+ }
27445
27653
  }
27446
27654
  /**
27447
27655
  * Scans the specified zone for addresses with unspent outputs. Starting at index 0, it will generate new addresses
27448
- * until the gap limit is reached for both gap and change addresses.
27656
+ * until the gap limit is reached for external and change BIP44 addresses and payment channel addresses.
27449
27657
  *
27450
27658
  * @param {Zone} zone - The zone in which to scan for addresses.
27451
27659
  * @param {number} [account=0] - The index of the account to scan. Default is `0`
@@ -27459,38 +27667,28 @@ class QiHDWallet extends AbstractHDWallet {
27459
27667
  this._changeAddresses = new Map();
27460
27668
  this._gapAddresses = [];
27461
27669
  this._gapChangeAddresses = [];
27462
- this._outpoints = [];
27670
+ this._availableOutpoints = [];
27671
+ // Reset each map so that all keys have empty array values but keys are preserved
27672
+ const resetSenderPaymentCodeInfo = new Map(Array.from(this._senderPaymentCodeInfo.keys()).map((key) => [key, []]));
27673
+ const resetReceiverPaymentCodeInfo = new Map(Array.from(this._receiverPaymentCodeInfo.keys()).map((key) => [key, []]));
27674
+ this._senderPaymentCodeInfo = resetSenderPaymentCodeInfo;
27675
+ this._receiverPaymentCodeInfo = resetReceiverPaymentCodeInfo;
27463
27676
  await this._scan(zone, account);
27464
27677
  }
27465
27678
  /**
27466
27679
  * Scans the specified zone for addresses with unspent outputs. Starting at the last address index, it will generate
27467
- * new addresses until the gap limit is reached for both gap and change addresses. If no account is specified, it
27468
- * will scan all accounts known to the wallet.
27680
+ * new addresses until the gap limit is reached for external and change BIP44 addresses and payment channel
27681
+ * addresses.
27469
27682
  *
27470
27683
  * @param {Zone} zone - The zone in which to sync addresses.
27471
- * @param {number} [account] - The index of the account to sync. If not specified, all accounts will be scanned.
27684
+ * @param {number} [account=0] - The index of the account to sync. Default is `0`
27472
27685
  * @returns {Promise<void>} A promise that resolves when the sync is complete.
27473
27686
  * @throws {Error} If the zone is invalid.
27474
27687
  */
27475
- async sync(zone, account) {
27688
+ async sync(zone, account = 0) {
27476
27689
  this.validateZone(zone);
27477
- // if no account is specified, scan all accounts.
27478
- if (account === undefined) {
27479
- const addressInfos = Array.from(this._addresses.values());
27480
- const accounts = addressInfos.reduce((unique, info) => {
27481
- if (!unique.includes(info.account)) {
27482
- unique.push(info.account);
27483
- }
27484
- return unique;
27485
- }, []);
27486
- for (const acc of accounts) {
27487
- await this._scan(zone, acc);
27488
- }
27489
- }
27490
- else {
27491
- await this._scan(zone, account);
27492
- }
27493
- return;
27690
+ await this._scan(zone, account);
27691
+ await this.checkPendingOutpoints(zone);
27494
27692
  }
27495
27693
  /**
27496
27694
  * Internal method to scan the specified zone for addresses with unspent outputs. This method handles the actual
@@ -27504,18 +27702,17 @@ class QiHDWallet extends AbstractHDWallet {
27504
27702
  async _scan(zone, account = 0) {
27505
27703
  if (!this.provider)
27506
27704
  throw new Error('Provider not set');
27507
- let gapAddressesCount = 0;
27508
- let changeGapAddressesCount = 0;
27509
- while (gapAddressesCount < QiHDWallet._GAP_LIMIT || changeGapAddressesCount < QiHDWallet._GAP_LIMIT) {
27510
- [gapAddressesCount, changeGapAddressesCount] = await Promise.all([
27511
- gapAddressesCount < QiHDWallet._GAP_LIMIT
27512
- ? this.scanAddress(zone, account, false, gapAddressesCount)
27513
- : gapAddressesCount,
27514
- changeGapAddressesCount < QiHDWallet._GAP_LIMIT
27515
- ? this.scanAddress(zone, account, true, changeGapAddressesCount)
27516
- : changeGapAddressesCount,
27517
- ]);
27705
+ // Start scanning processes for each derivation tree
27706
+ const scans = [
27707
+ this.scanBIP44Addresses(zone, account, false),
27708
+ this.scanBIP44Addresses(zone, account, true), // Change addresses
27709
+ ];
27710
+ // Add scanning processes for each payment channel
27711
+ for (const paymentCode of this._receiverPaymentCodeInfo.keys()) {
27712
+ scans.push(this.scanPaymentChannel(zone, account, paymentCode));
27518
27713
  }
27714
+ // Run all scans in parallel
27715
+ await Promise.all(scans);
27519
27716
  }
27520
27717
  /**
27521
27718
  * Scans for the next address in the specified zone and account, checking for associated outpoints, and updates the
@@ -27524,29 +27721,169 @@ class QiHDWallet extends AbstractHDWallet {
27524
27721
  * @param {Zone} zone - The zone in which the address is being scanned.
27525
27722
  * @param {number} account - The index of the account for which the address is being scanned.
27526
27723
  * @param {boolean} isChange - A flag indicating whether the address is a change address.
27527
- * @param {number} addressesCount - The current count of addresses scanned.
27528
- * @returns {Promise<number>} A promise that resolves to the updated address count.
27724
+ * @returns {Promise<void>} A promise that resolves when the scan is complete.
27529
27725
  * @throws {Error} If an error occurs during the address scanning or outpoints retrieval process.
27530
27726
  */
27531
- async scanAddress(zone, account, isChange, addressesCount) {
27727
+ async scanBIP44Addresses(zone, account, isChange) {
27532
27728
  const addressMap = isChange ? this._changeAddresses : this._addresses;
27533
- const addressInfo = this._getNextAddress(account, zone, isChange, addressMap);
27534
- const outpoints = await this.getOutpointsByAddress(addressInfo.address);
27535
- if (outpoints.length > 0) {
27536
- this.importOutpoints(outpoints.map((outpoint) => ({
27537
- outpoint,
27538
- address: addressInfo.address,
27539
- zone,
27540
- account,
27541
- })));
27542
- addressesCount = 0;
27543
- isChange ? (this._gapChangeAddresses = []) : (this._gapAddresses = []);
27729
+ const gapAddresses = isChange ? this._gapChangeAddresses : this._gapAddresses;
27730
+ const usedGapAddresses = isChange ? this._usedGapChangeAddresses : this._usedGapAddresses;
27731
+ // First, add all used gap addresses to the address map and import their outpoints
27732
+ for (const addressInfo of usedGapAddresses) {
27733
+ this._addAddress(addressMap, account, addressInfo.index, isChange);
27734
+ const outpoints = await this.getOutpointsByAddress(addressInfo.address);
27735
+ if (outpoints.length > 0) {
27736
+ this.importOutpoints(outpoints.map((outpoint) => ({
27737
+ outpoint,
27738
+ address: addressInfo.address,
27739
+ zone,
27740
+ account,
27741
+ })));
27742
+ }
27743
+ }
27744
+ let gapCount = 0;
27745
+ // Second, re-examine existing gap addresses
27746
+ const newlyUsedAddresses = [];
27747
+ for (let i = 0; i < gapAddresses.length;) {
27748
+ const addressInfo = gapAddresses[i];
27749
+ const { isUsed, outpoints } = await this.checkAddressUse(addressInfo.address);
27750
+ if (isUsed) {
27751
+ // Address has been used since last scan
27752
+ this._addAddress(addressMap, account, addressInfo.index, isChange);
27753
+ if (outpoints.length > 0) {
27754
+ this.importOutpoints(outpoints.map((outpoint) => ({
27755
+ outpoint,
27756
+ address: addressInfo.address,
27757
+ zone,
27758
+ account,
27759
+ })));
27760
+ }
27761
+ // Remove from gap addresses
27762
+ newlyUsedAddresses.push(addressInfo);
27763
+ gapCount = 0;
27764
+ }
27765
+ else {
27766
+ gapCount++;
27767
+ i++;
27768
+ }
27769
+ }
27770
+ // remove addresses that have been used from the gap addresses
27771
+ const updatedGapAddresses = gapAddresses.filter((addressInfo) => !newlyUsedAddresses.some((usedAddress) => usedAddress.address === addressInfo.address));
27772
+ // Scan for new gap addresses
27773
+ const newGapAddresses = [];
27774
+ while (gapCount < QiHDWallet._GAP_LIMIT) {
27775
+ const addressInfo = this._getNextAddress(account, zone, isChange, addressMap);
27776
+ const { isUsed, outpoints } = await this.checkAddressUse(addressInfo.address);
27777
+ if (isUsed) {
27778
+ if (outpoints.length > 0) {
27779
+ this.importOutpoints(outpoints.map((outpoint) => ({
27780
+ outpoint,
27781
+ address: addressInfo.address,
27782
+ zone,
27783
+ account,
27784
+ })));
27785
+ }
27786
+ gapCount = 0;
27787
+ }
27788
+ else {
27789
+ gapCount++;
27790
+ // check if the address is already in the updated gap addresses array
27791
+ if (!updatedGapAddresses.some((usedAddress) => usedAddress.address === addressInfo.address)) {
27792
+ newGapAddresses.push(addressInfo);
27793
+ }
27794
+ }
27795
+ }
27796
+ // update the gap addresses
27797
+ if (isChange) {
27798
+ this._gapChangeAddresses = [...updatedGapAddresses, ...newGapAddresses];
27544
27799
  }
27545
27800
  else {
27546
- addressesCount++;
27547
- isChange ? this._gapChangeAddresses.push(addressInfo) : this._gapAddresses.push(addressInfo);
27801
+ this._gapAddresses = [...updatedGapAddresses, ...newGapAddresses];
27802
+ }
27803
+ }
27804
+ /**
27805
+ * Scans the specified payment channel for addresses with unspent outputs. Starting at the last address index, it
27806
+ * will generate new addresses until the gap limit is reached.
27807
+ *
27808
+ * @param {Zone} zone - The zone in which to scan for addresses.
27809
+ * @param {number} account - The index of the account to scan.
27810
+ * @param {string} paymentCode - The payment code to scan.
27811
+ * @returns {Promise<void>} A promise that resolves when the scan is complete.
27812
+ * @throws {Error} If the zone is invalid.
27813
+ */
27814
+ async scanPaymentChannel(zone, account, paymentCode) {
27815
+ let gapCount = 0;
27816
+ const paymentCodeInfoArray = this._receiverPaymentCodeInfo.get(paymentCode);
27817
+ if (!paymentCodeInfoArray) {
27818
+ throw new Error(`Payment code ${paymentCode} not found`);
27819
+ }
27820
+ // first, re-examine existing unused addresses
27821
+ const newlyUsedAddresses = [];
27822
+ const unusedAddresses = paymentCodeInfoArray.filter((info) => !info.isUsed);
27823
+ for (let i = 0; i < unusedAddresses.length;) {
27824
+ const addressInfo = unusedAddresses[i];
27825
+ const { isUsed, outpoints } = await this.checkAddressUse(addressInfo.address);
27826
+ if (outpoints.length > 0 || isUsed) {
27827
+ // Address has been used since last scan
27828
+ addressInfo.isUsed = true;
27829
+ const pcAddressInfoIndex = paymentCodeInfoArray.findIndex((info) => info.index === addressInfo.index);
27830
+ paymentCodeInfoArray[pcAddressInfoIndex] = addressInfo;
27831
+ this.importOutpoints(outpoints.map((outpoint) => ({
27832
+ outpoint,
27833
+ address: addressInfo.address,
27834
+ zone,
27835
+ account,
27836
+ })));
27837
+ // Remove from gap addresses
27838
+ newlyUsedAddresses.push(addressInfo);
27839
+ gapCount = 0;
27840
+ }
27841
+ else {
27842
+ // Address is still unused
27843
+ gapCount++;
27844
+ i++;
27845
+ }
27846
+ }
27847
+ // remove the addresses that have been used from the payment code info array
27848
+ const updatedPaymentCodeInfoArray = paymentCodeInfoArray.filter((addressInfo) => !newlyUsedAddresses.some((usedAddress) => usedAddress.index === addressInfo.index));
27849
+ // Then, scan for new gap addresses
27850
+ while (gapCount < QiHDWallet._GAP_LIMIT) {
27851
+ const pcAddressInfo = this.getNextReceiveAddress(paymentCode, zone, account);
27852
+ const outpoints = await this.getOutpointsByAddress(pcAddressInfo.address);
27853
+ let isUsed = false;
27854
+ if (outpoints.length > 0) {
27855
+ isUsed = true;
27856
+ this.importOutpoints(outpoints.map((outpoint) => ({
27857
+ outpoint,
27858
+ address: pcAddressInfo.address,
27859
+ zone,
27860
+ account,
27861
+ })));
27862
+ gapCount = 0;
27863
+ }
27864
+ else if (this._addressUseChecker !== undefined &&
27865
+ (await this._addressUseChecker(pcAddressInfo.address))) {
27866
+ // address checker returned true, so the address is used
27867
+ isUsed = true;
27868
+ gapCount = 0;
27869
+ }
27870
+ else {
27871
+ gapCount++;
27872
+ }
27873
+ if (isUsed) {
27874
+ // update the payment code info array if the address has been used
27875
+ pcAddressInfo.isUsed = isUsed;
27876
+ const pcAddressInfoIndex = updatedPaymentCodeInfoArray.findIndex((info) => info.index === pcAddressInfo.index);
27877
+ if (pcAddressInfoIndex !== -1) {
27878
+ updatedPaymentCodeInfoArray[pcAddressInfoIndex] = pcAddressInfo;
27879
+ }
27880
+ else {
27881
+ updatedPaymentCodeInfoArray.push(pcAddressInfo);
27882
+ }
27883
+ }
27548
27884
  }
27549
- return addressesCount;
27885
+ // update the payment code info map
27886
+ this._receiverPaymentCodeInfo.set(paymentCode, updatedPaymentCodeInfoArray);
27550
27887
  }
27551
27888
  /**
27552
27889
  * Queries the network node for the outpoints of the specified address.
@@ -27564,6 +27901,24 @@ class QiHDWallet extends AbstractHDWallet {
27564
27901
  throw new Error(`Failed to get outpoints for address: ${address} - error: ${error}`);
27565
27902
  }
27566
27903
  }
27904
+ async checkAddressUse(address) {
27905
+ let isUsed = false;
27906
+ let outpoints = [];
27907
+ try {
27908
+ outpoints = await this.getOutpointsByAddress(address);
27909
+ if (outpoints.length > 0) {
27910
+ isUsed = true;
27911
+ }
27912
+ else if (this._addressUseChecker !== undefined && (await this._addressUseChecker(address))) {
27913
+ // address checker returned true, so the address is used
27914
+ isUsed = true;
27915
+ }
27916
+ }
27917
+ catch (error) {
27918
+ throw new Error(`Failed to get outpoints for address: ${address} - error: ${error}`);
27919
+ }
27920
+ return { isUsed, outpoints };
27921
+ }
27567
27922
  /**
27568
27923
  * Gets the change addresses for the specified zone.
27569
27924
  *
@@ -27621,10 +27976,13 @@ class QiHDWallet extends AbstractHDWallet {
27621
27976
  serialize() {
27622
27977
  const hdwalletSerialized = super.serialize();
27623
27978
  return {
27624
- outpoints: this._outpoints,
27979
+ outpoints: this._availableOutpoints,
27980
+ pendingOutpoints: this._pendingOutpoints,
27625
27981
  changeAddresses: Array.from(this._changeAddresses.values()),
27626
27982
  gapAddresses: this._gapAddresses,
27627
27983
  gapChangeAddresses: this._gapChangeAddresses,
27984
+ usedGapAddresses: this._usedGapAddresses,
27985
+ usedGapChangeAddresses: this._usedGapChangeAddresses,
27628
27986
  receiverPaymentCodeInfo: Object.fromEntries(this._receiverPaymentCodeInfo),
27629
27987
  senderPaymentCodeInfo: Object.fromEntries(this._senderPaymentCodeInfo),
27630
27988
  ...hdwalletSerialized,
@@ -27665,9 +28023,26 @@ class QiHDWallet extends AbstractHDWallet {
27665
28023
  }
27666
28024
  wallet._gapChangeAddresses.push(gapChangeAddressInfo);
27667
28025
  }
27668
- // validate the outpoints and import them
28026
+ // validate the used gap addresses and import them
28027
+ for (const usedGapAddressInfo of serialized.usedGapAddresses) {
28028
+ if (!wallet._addresses.has(usedGapAddressInfo.address)) {
28029
+ throw new Error(`Address ${usedGapAddressInfo.address} not found in wallet`);
28030
+ }
28031
+ wallet._usedGapAddresses.push(usedGapAddressInfo);
28032
+ }
28033
+ // validate the used gap change addresses and import them
28034
+ for (const usedGapChangeAddressInfo of serialized.usedGapChangeAddresses) {
28035
+ if (!wallet._changeAddresses.has(usedGapChangeAddressInfo.address)) {
28036
+ throw new Error(`Address ${usedGapChangeAddressInfo.address} not found in wallet`);
28037
+ }
28038
+ wallet._usedGapChangeAddresses.push(usedGapChangeAddressInfo);
28039
+ }
28040
+ // validate the available outpoints and import them
27669
28041
  wallet.validateOutpointInfo(serialized.outpoints);
27670
- wallet._outpoints.push(...serialized.outpoints);
28042
+ wallet._availableOutpoints.push(...serialized.outpoints);
28043
+ // validate the pending outpoints and import them
28044
+ wallet.validateOutpointInfo(serialized.pendingOutpoints);
28045
+ wallet._pendingOutpoints.push(...serialized.pendingOutpoints);
27671
28046
  // validate and import the payment code info
27672
28047
  wallet.validateAndImportPaymentCodeInfo(serialized.receiverPaymentCodeInfo, 'receiver');
27673
28048
  wallet.validateAndImportPaymentCodeInfo(serialized.senderPaymentCodeInfo, 'sender');
@@ -27676,7 +28051,8 @@ class QiHDWallet extends AbstractHDWallet {
27676
28051
  /**
27677
28052
  * Validates and imports a map of payment code info.
27678
28053
  *
27679
- * @param {Map<string, paymentCodeInfo[]>} paymentCodeInfoMap - The map of payment code info to validate and import.
28054
+ * @param {Map<string, PaymentChannelAddressInfo[]>} paymentCodeInfoMap - The map of payment code info to validate
28055
+ * and import.
27680
28056
  * @param {'receiver' | 'sender'} target - The target map to update ('receiver' or 'sender').
27681
28057
  * @throws {Error} If any of the payment code info is invalid.
27682
28058
  */
@@ -27695,7 +28071,7 @@ class QiHDWallet extends AbstractHDWallet {
27695
28071
  /**
27696
28072
  * Validates a payment code info object.
27697
28073
  *
27698
- * @param {paymentCodeInfo} pcInfo - The payment code info to validate.
28074
+ * @param {PaymentChannelAddressInfo} pcInfo - The payment code info to validate.
27699
28075
  * @throws {Error} If the payment code info is invalid.
27700
28076
  */
27701
28077
  validatePaymentCodeInfo(pcInfo) {
@@ -27733,19 +28109,22 @@ class QiHDWallet extends AbstractHDWallet {
27733
28109
  // validate zone
27734
28110
  this.validateZone(info.zone);
27735
28111
  // validate address and account
27736
- const addressInfo = this.getAddressInfo(info.address);
27737
- if (!addressInfo) {
27738
- throw new Error(`Address ${info.address} not found in wallet`);
27739
- }
27740
- if (info.account !== undefined && info.account !== addressInfo.account) {
27741
- throw new Error(`Account ${info.account} not found for address ${info.address}`);
27742
- }
28112
+ this.validateAddressAndAccount(info.address, info.account);
27743
28113
  // validate Outpoint
27744
28114
  if (info.outpoint.txhash == null || info.outpoint.index == null || info.outpoint.denomination == null) {
27745
28115
  throw new Error(`Invalid Outpoint: ${JSON.stringify(info)} `);
27746
28116
  }
27747
28117
  });
27748
28118
  }
28119
+ validateAddressAndAccount(address, account) {
28120
+ const addressInfo = this.locateAddressInfo(address);
28121
+ if (!addressInfo) {
28122
+ throw new Error(`Address ${address} not found in wallet`);
28123
+ }
28124
+ if (account && account !== addressInfo.account) {
28125
+ throw new Error(`Address ${address} does not match account ${account}`);
28126
+ }
28127
+ }
27749
28128
  /**
27750
28129
  * Creates a new BIP47 payment code for the specified account. The payment code is derived from the account's BIP32
27751
28130
  * root key.
@@ -27758,11 +28137,11 @@ class QiHDWallet extends AbstractHDWallet {
27758
28137
  return privatePcode.toBase58();
27759
28138
  }
27760
28139
  // helper method to get a bip32 API instance
27761
- async _getBIP32API() {
28140
+ _getBIP32API() {
27762
28141
  return BIP32Factory(ecc);
27763
28142
  }
27764
28143
  // helper method to decode a base58 string into a Uint8Array
27765
- async _decodeBase58(base58) {
28144
+ _decodeBase58(base58) {
27766
28145
  return bs58check.decode(base58);
27767
28146
  }
27768
28147
  /**
@@ -27773,8 +28152,8 @@ class QiHDWallet extends AbstractHDWallet {
27773
28152
  * @param {number} account - The account index for which to generate the private payment code.
27774
28153
  * @returns {Promise<PaymentCodePrivate>} A promise that resolves to the PaymentCodePrivate instance.
27775
28154
  */
27776
- async _getPaymentCodePrivate(account) {
27777
- const bip32 = await this._getBIP32API();
28155
+ _getPaymentCodePrivate(account) {
28156
+ const bip32 = this._getBIP32API();
27778
28157
  const accountNode = this._root.deriveChild(account);
27779
28158
  // payment code array
27780
28159
  const pc = new Uint8Array(80);
@@ -27797,15 +28176,15 @@ class QiHDWallet extends AbstractHDWallet {
27797
28176
  * @returns {Promise<string>} A promise that resolves to the payment address for sending funds.
27798
28177
  * @throws {Error} Throws an error if the payment code version is invalid.
27799
28178
  */
27800
- async getNextSendAddress(receiverPaymentCode, zone, account = 0) {
27801
- const bip32 = await this._getBIP32API();
27802
- const buf = await this._decodeBase58(receiverPaymentCode);
28179
+ getNextSendAddress(receiverPaymentCode, zone, account = 0) {
28180
+ const bip32 = this._getBIP32API();
28181
+ const buf = this._decodeBase58(receiverPaymentCode);
27803
28182
  const version = buf[0];
27804
28183
  if (version !== PC_VERSION)
27805
28184
  throw new Error('Invalid payment code version');
27806
- const receiverPCodePrivate = await this._getPaymentCodePrivate(account);
28185
+ const receiverPCodePrivate = this._getPaymentCodePrivate(account);
27807
28186
  const senderPCodePublic = new PaymentCodePublic(ecc, bip32, buf.slice(1));
27808
- const paymentCodeInfoArray = this._receiverPaymentCodeInfo.get(receiverPaymentCode);
28187
+ const paymentCodeInfoArray = this._senderPaymentCodeInfo.get(receiverPaymentCode);
27809
28188
  const lastIndex = paymentCodeInfoArray && paymentCodeInfoArray.length > 0
27810
28189
  ? paymentCodeInfoArray[paymentCodeInfoArray.length - 1].index
27811
28190
  : 0;
@@ -27815,6 +28194,7 @@ class QiHDWallet extends AbstractHDWallet {
27815
28194
  if (this.isValidAddressForZone(address, zone)) {
27816
28195
  const pcInfo = {
27817
28196
  address,
28197
+ pubKey: hexlify(senderPCodePublic.pubKey),
27818
28198
  index: addrIndex,
27819
28199
  account,
27820
28200
  zone,
@@ -27824,9 +28204,9 @@ class QiHDWallet extends AbstractHDWallet {
27824
28204
  paymentCodeInfoArray.push(pcInfo);
27825
28205
  }
27826
28206
  else {
27827
- this._receiverPaymentCodeInfo.set(receiverPaymentCode, [pcInfo]);
28207
+ this._senderPaymentCodeInfo.set(receiverPaymentCode, [pcInfo]);
27828
28208
  }
27829
- return address;
28209
+ return pcInfo;
27830
28210
  }
27831
28211
  }
27832
28212
  throw new Error(`Failed to derive a valid address for the zone ${zone} after ${MAX_ADDRESS_DERIVATION_ATTEMPTS} attempts.`);
@@ -27839,15 +28219,15 @@ class QiHDWallet extends AbstractHDWallet {
27839
28219
  * @returns {Promise<string>} A promise that resolves to the payment address for receiving funds.
27840
28220
  * @throws {Error} Throws an error if the payment code version is invalid.
27841
28221
  */
27842
- async getNextReceiveAddress(senderPaymentCode, zone, account = 0) {
27843
- const bip32 = await this._getBIP32API();
27844
- const buf = await this._decodeBase58(senderPaymentCode);
28222
+ getNextReceiveAddress(senderPaymentCode, zone, account = 0) {
28223
+ const bip32 = this._getBIP32API();
28224
+ const buf = this._decodeBase58(senderPaymentCode);
27845
28225
  const version = buf[0];
27846
28226
  if (version !== PC_VERSION)
27847
28227
  throw new Error('Invalid payment code version');
27848
28228
  const senderPCodePublic = new PaymentCodePublic(ecc, bip32, buf.slice(1));
27849
- const receiverPCodePrivate = await this._getPaymentCodePrivate(account);
27850
- const paymentCodeInfoArray = this._senderPaymentCodeInfo.get(senderPaymentCode);
28229
+ const receiverPCodePrivate = this._getPaymentCodePrivate(account);
28230
+ const paymentCodeInfoArray = this._receiverPaymentCodeInfo.get(senderPaymentCode);
27851
28231
  const lastIndex = paymentCodeInfoArray && paymentCodeInfoArray.length > 0
27852
28232
  ? paymentCodeInfoArray[paymentCodeInfoArray.length - 1].index
27853
28233
  : 0;
@@ -27857,6 +28237,7 @@ class QiHDWallet extends AbstractHDWallet {
27857
28237
  if (this.isValidAddressForZone(address, zone)) {
27858
28238
  const pcInfo = {
27859
28239
  address,
28240
+ pubKey: hexlify(receiverPCodePrivate.pubKey),
27860
28241
  index: addrIndex,
27861
28242
  account,
27862
28243
  zone,
@@ -27866,9 +28247,9 @@ class QiHDWallet extends AbstractHDWallet {
27866
28247
  paymentCodeInfoArray.push(pcInfo);
27867
28248
  }
27868
28249
  else {
27869
- this._senderPaymentCodeInfo.set(senderPaymentCode, [pcInfo]);
28250
+ this._receiverPaymentCodeInfo.set(senderPaymentCode, [pcInfo]);
27870
28251
  }
27871
- return address;
28252
+ return pcInfo;
27872
28253
  }
27873
28254
  }
27874
28255
  throw new Error(`Failed to derive a valid address for the zone ${zone} after ${MAX_ADDRESS_DERIVATION_ATTEMPTS} attempts.`);
@@ -27885,18 +28266,29 @@ class QiHDWallet extends AbstractHDWallet {
27885
28266
  throw new Error(`Invalid payment code: ${paymentCode}`);
27886
28267
  }
27887
28268
  if (type === 'receiver') {
27888
- if (this._receiverPaymentCodeInfo.has(paymentCode)) {
27889
- return;
28269
+ if (!this._receiverPaymentCodeInfo.has(paymentCode)) {
28270
+ this._receiverPaymentCodeInfo.set(paymentCode, []);
27890
28271
  }
27891
- this._receiverPaymentCodeInfo.set(paymentCode, []);
27892
28272
  }
27893
28273
  else {
27894
- if (this._senderPaymentCodeInfo.has(paymentCode)) {
27895
- return;
28274
+ if (!this._senderPaymentCodeInfo.has(paymentCode)) {
28275
+ this._senderPaymentCodeInfo.set(paymentCode, []);
27896
28276
  }
27897
- this._senderPaymentCodeInfo.set(paymentCode, []);
27898
28277
  }
27899
28278
  }
28279
+ /**
28280
+ * Gets the address info for a given address.
28281
+ *
28282
+ * @param {string} address - The address.
28283
+ * @returns {NeuteredAddressInfo | null} The address info or null if not found.
28284
+ */
28285
+ getChangeAddressInfo(address) {
28286
+ const changeAddressInfo = this._changeAddresses.get(address);
28287
+ if (!changeAddressInfo) {
28288
+ return null;
28289
+ }
28290
+ return changeAddressInfo;
28291
+ }
27900
28292
  }
27901
28293
 
27902
28294
  /**
@@ -28848,9 +29240,42 @@ class AbstractProvider {
28848
29240
  * @returns {Promise<number>} A promise that resolves to the protocol expansion number.
28849
29241
  */
28850
29242
  async getProtocolExpansionNumber() {
28851
- return await this.#perform({
28852
- method: 'getProtocolExpansionNumber',
28853
- });
29243
+ return getNumber(await this.#perform({ method: 'getProtocolExpansionNumber' }));
29244
+ }
29245
+ /**
29246
+ * Get the active region shards based on the protocol expansion number.
29247
+ *
29248
+ * @returns {Promise<Shard[]>} A promise that resolves to the active shards.
29249
+ */
29250
+ async getActiveRegions() {
29251
+ const protocolExpansionNumber = await this.getProtocolExpansionNumber();
29252
+ const shards = [Shard.Cyprus];
29253
+ if (protocolExpansionNumber >= 1) {
29254
+ shards.push(Shard.Paxos);
29255
+ }
29256
+ if (protocolExpansionNumber >= 3) {
29257
+ shards.push(Shard.Hydra);
29258
+ }
29259
+ return shards.sort((a, b) => a.localeCompare(b));
29260
+ }
29261
+ /**
29262
+ * Get the active zones for a shard based on the protocol expansion number.
29263
+ *
29264
+ * @returns {Promise<Zone[]>} A promise that resolves to the active zones.
29265
+ */
29266
+ async getActiveZones() {
29267
+ const protocolExpansionNumber = await this.getProtocolExpansionNumber();
29268
+ const zones = [Zone.Cyprus1];
29269
+ if (protocolExpansionNumber >= 1) {
29270
+ zones.push(Zone.Cyprus2);
29271
+ }
29272
+ if (protocolExpansionNumber >= 2) {
29273
+ zones.push(Zone.Paxos1, Zone.Paxos2);
29274
+ }
29275
+ if (protocolExpansionNumber >= 3) {
29276
+ zones.push(Zone.Cyprus3, Zone.Paxos3, Zone.Hydra1, Zone.Hydra2, Zone.Hydra3);
29277
+ }
29278
+ return zones.sort((a, b) => a.localeCompare(b));
28854
29279
  }
28855
29280
  /**
28856
29281
  * Get the latest Qi rate for a zone.