All files / src/services/AddressDiscovery HDSequentialDiscovery.ts

100% Statements 42/42
100% Branches 9/9
100% Functions 7/7
100% Lines 40/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 15942x     42x   42x               42x 4733x               4732x                       42x                       166x 166x 166x   166x 2327x 2327x   2327x         2326x         2325x 2324x   2324x 2324x   2324x 58x   2266x     2324x     163x                                     42x 85x 85x     85x 85x                     84x 84x 84x 80x     84x       1000x                   82x       3654x                   81x       81x 77x        
import { AccountAddressDerivationPath, AddressType, Bip32Account, GroupedAddress } from '@cardano-sdk/key-management';
import { AddressDiscovery } from '../types';
import { ChainHistoryProvider } from '@cardano-sdk/core';
import uniqBy from 'lodash/uniqBy.js';
 
const STAKE_KEY_INDEX_LOOKAHEAD = 5;
 
/**
 * Gets whether the given address has a transaction history.
 *
 * @param address The address to query.
 * @param chainHistoryProvider The chain history provider where to fetch the history from.
 */
const addressHasTx = async (address: GroupedAddress, chainHistoryProvider: ChainHistoryProvider): Promise<boolean> => {
  const txs = await chainHistoryProvider.transactionsByAddresses({
    addresses: [address.address],
    pagination: {
      limit: 1,
      startAt: 0
    }
  });
 
  return txs.totalResultCount > 0;
};
 
/**
 * Search for all base addresses composed with the given payment and stake credentials.
 *
 * @param account The bip32 account to be used to derive the addresses to be discovered.
 * @param chainHistoryProvider The chain history provider.
 * @param lookAheadCount Number down the derivation chain to be searched for.
 * @param getDeriveAddressArgs Callback that retrieves the derivation path arguments.
 * @returns A promise that will be resolved into a GroupedAddress list containing the discovered addresses.
 */
const discoverAddresses = async (
  account: Bip32Account,
  chainHistoryProvider: ChainHistoryProvider,
  lookAheadCount: number,
  getDeriveAddressArgs: (
    index: number,
    type: AddressType
  ) => {
    paymentKeyDerivationPath: AccountAddressDerivationPath;
    stakeKeyDerivationIndex: number;
  }
): Promise<GroupedAddress[]> => {
  let currentGap = 0;
  let currentIndex = 0;
  const addresses = new Array<GroupedAddress>();
 
  while (currentGap <= lookAheadCount) {
    const externalAddressArgs = getDeriveAddressArgs(currentIndex, AddressType.External);
    const internalAddressArgs = getDeriveAddressArgs(currentIndex, AddressType.Internal);
 
    const externalAddress = await account.deriveAddress(
      externalAddressArgs.paymentKeyDerivationPath,
      externalAddressArgs.stakeKeyDerivationIndex
    );
 
    const internalAddress = await account.deriveAddress(
      internalAddressArgs.paymentKeyDerivationPath,
      internalAddressArgs.stakeKeyDerivationIndex
    );
 
    const externalHasTx = await addressHasTx(externalAddress, chainHistoryProvider);
    const internalHasTx = await addressHasTx(internalAddress, chainHistoryProvider);
 
    if (externalHasTx) addresses.push(externalAddress);
    if (internalHasTx) addresses.push(internalAddress);
 
    if (externalHasTx || internalHasTx) {
      currentGap = 0;
    } else {
      ++currentGap;
    }
 
    ++currentIndex;
  }
 
  return addresses;
};
 
/**
 * Provides a mechanism to discover addresses in Hierarchical Deterministic (HD) wallets
 * by performing a look-ahead search of a specified number of addresses in the following manner:
 *
 * - Derive base addresses with payment credential at index 0 and increasing stake credential until it reaches the given limit.
 * - Derives base addresses with increasing payment credential and stake credential at index 0.
 * - if no transactions are found for both internal and external address type, increase the gap count.
 * - if there are some transactions, increase the payment credential index and set the gap count to 0.
 * - if the gap count reaches the given lookAheadCount stop the discovery process.
 *
 * Please note that the algorithm works with the transaction history, not balances, so you can have an address with 0 total coins
 * and the algorithm will still continue with discovery if the address was previously used.
 *
 * If the wallet hits gap limit of unused addresses in a row, it expects there are
 * no used addresses beyond this point and stops searching the address chain.
 */
export class HDSequentialDiscovery implements AddressDiscovery {
  readonly #chainHistoryProvider: ChainHistoryProvider;
  readonly #lookAheadCount: number;
 
  constructor(chainHistoryProvider: ChainHistoryProvider, lookAheadCount: number) {
    this.#chainHistoryProvider = chainHistoryProvider;
    this.#lookAheadCount = lookAheadCount;
  }
 
  /**
   * This method performs a look-ahead search of 'n' addresses in the HD wallet using the chain history and
   * the given key agent. The discovered addresses are returned as a list.
   *
   * @param manager The address manager be used to derive the addresses to be discovered.
   * @returns A promise that will be resolved into a GroupedAddress list containing the discovered addresses.
   */
  public async discover(manager: Bip32Account): Promise<GroupedAddress[]> {
    const firstAddresses = [await manager.deriveAddress({ index: 0, type: AddressType.External }, 0)];
    const firstInternalAddress = await manager.deriveAddress({ index: 0, type: AddressType.Internal }, 0);
    if (await addressHasTx(firstInternalAddress, this.#chainHistoryProvider)) {
      firstAddresses.push(firstInternalAddress);
    }
 
    const stakeKeyAddresses = await discoverAddresses(
      manager,
      this.#chainHistoryProvider,
      STAKE_KEY_INDEX_LOOKAHEAD,
      (currentIndex, type) => ({
        paymentKeyDerivationPath: {
          index: 0,
          type
        },
        // We are going to offset this by 1, since we already know about the first address.
        stakeKeyDerivationIndex: currentIndex + 1
      })
    );
 
    const paymentKeyAddresses = await discoverAddresses(
      manager,
      this.#chainHistoryProvider,
      this.#lookAheadCount,
      (currentIndex, type) => ({
        paymentKeyDerivationPath: {
          // We are going to offset this by 1, since we already know about the first address.
          index: currentIndex + 1,
          type
        },
        stakeKeyDerivationIndex: 0
      })
    );
 
    const addresses = uniqBy([...firstAddresses, ...stakeKeyAddresses, ...paymentKeyAddresses], 'address');
 
    // We need to make sure the addresses are sorted since the wallet assumes that the first address
    // in the list is the change address (payment cred 0 and stake cred 0).
    return addresses.sort(
      (a, b) => a.index - b.index || a.stakeKeyDerivationPath!.index - b.stakeKeyDerivationPath!.index
    );
  }
}