{"version":3,"file":"qr-keyring.mjs","sourceRoot":"","sources":["../src/qr-keyring.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TypedTransaction, TypedTxData } from '@ethereumjs/tx';\nimport type { TypedMessage, MessageTypes } from '@metamask/eth-sig-util';\nimport type { Keyring } from '@metamask/keyring-utils';\nimport { add0x, assert, getChecksumAddress } from '@metamask/utils';\nimport type { Hex } from '@metamask/utils';\n\nimport { Device, DeviceMode } from './device';\nimport type { DeviceDetails, IndexedAddress } from './device';\n\nexport const QR_KEYRING_TYPE = 'QR Hardware Wallet Device';\n\nexport enum QrScanRequestType {\n  /**\n   * Request a scan for a QR code containing a UR\n   * with information related to a hardware wallet.\n   */\n  PAIR = 'pair',\n  /**\n   * Request a scan for a QR code containing a\n   * UR-encoded transaction signature.\n   */\n  SIGN = 'sign',\n}\n\nexport type QrSignatureRequest = {\n  requestId: string;\n  payload: SerializedUR;\n  requestTitle?: string;\n  requestDescription?: string;\n};\n\nexport type QrScanRequest = {\n  type: QrScanRequestType;\n  request?: QrSignatureRequest;\n};\n\nexport type SerializedUR = {\n  type: string;\n  cbor: string;\n};\n\nexport type QrKeyringBridge = {\n  requestScan: (request: QrScanRequest) => Promise<SerializedUR>;\n};\n\nexport type QrKeyringOptions = {\n  ur?: string;\n  bridge: QrKeyringBridge;\n};\n\n/**\n * The state of the QrKeyring\n *\n * @property accounts - The accounts in the QrKeyring\n */\nexport type SerializedQrKeyringState = {\n  version?: number;\n  accounts?: string[];\n  currentAccount?: number;\n} & (\n  | {\n      initialized?: false;\n    }\n  | ({\n      initialized: true;\n    } & DeviceDetails)\n);\n\n/**\n * Returns the default serialized state of the QrKeyring.\n *\n * @returns The default serialized state.\n */\nexport const getDefaultSerializedQrKeyringState =\n  (): SerializedQrKeyringState => ({\n    initialized: false,\n    accounts: [],\n  });\n\n/**\n * Normalizes an address to a 0x-prefixed checksum address.\n *\n * @param address - The address to normalize.\n * @returns The normalized address as a Hex string.\n */\nfunction normalizeAddress(address: string): Hex {\n  return getChecksumAddress(add0x(address));\n}\n\nexport class QrKeyring implements Keyring {\n  static type = QR_KEYRING_TYPE;\n\n  readonly type = QR_KEYRING_TYPE;\n\n  readonly bridge: QrKeyringBridge;\n\n  #device?: Device | undefined;\n\n  #accounts: Hex[] = [];\n\n  #accountToUnlock?: number | undefined;\n\n  #currentPage: number = 0;\n\n  constructor(options: QrKeyringOptions) {\n    this.bridge = options.bridge;\n\n    if (options?.ur) {\n      this.pairDevice(options.ur);\n    }\n  }\n\n  /**\n   * Serializes the QrKeyring state\n   *\n   * @returns The serialized state\n   */\n  async serialize(): Promise<SerializedQrKeyringState> {\n    const deviceDetails = this.#device\n      ? this.#device.getDeviceDetails()\n      : undefined;\n\n    if (\n      !deviceDetails ||\n      ![DeviceMode.HD, DeviceMode.ACCOUNT].includes(deviceDetails.keyringMode)\n    ) {\n      // the keyring has not initialized with a deviceDetails device yet\n      return getDefaultSerializedQrKeyringState();\n    }\n\n    return {\n      ...deviceDetails,\n      initialized: true,\n      accounts: this.#accounts.slice(),\n    };\n  }\n\n  /**\n   * Deserializes the QrKeyring state\n   *\n   * @param state - The serialized state to deserialize\n   */\n  async deserialize(state: SerializedQrKeyringState): Promise<void> {\n    if (!state.initialized) {\n      this.#accounts = [];\n      this.#device = undefined;\n      return;\n    }\n\n    this.#device = new Device({\n      requestScan: this.bridge.requestScan.bind(this.bridge),\n      source: state,\n    });\n    this.#accounts = (state.accounts ?? []).map(normalizeAddress);\n  }\n\n  /**\n   * Get the derivation path for a given address.\n   *\n   * @param address - The address to get the derivation path for.\n   * @returns The derivation path for the address, or undefined if the device\n   * is not paired or the address is not found.\n   */\n  getPathFromAddress(address: Hex): string | undefined {\n    return this.#device?.pathFromAddress(address);\n  }\n\n  /**\n   * Adds accounts to the QrKeyring\n   *\n   * @param accountsToAdd - The number of accounts to add\n   * @returns The accounts added\n   */\n  async addAccounts(accountsToAdd: number): Promise<Hex[]> {\n    if (!this.#device) {\n      throw new Error('No device paired.');\n    }\n\n    const newAccounts: Hex[] = [];\n\n    for (let i = 0; i < accountsToAdd; i++) {\n      const index = this.#accountToUnlock ?? this.#accounts.length + i;\n      const address = this.#device.addressFromIndex(index);\n      this.setAccountToUnlock(index + 1);\n\n      if (this.#accounts.includes(address)) {\n        continue;\n      }\n\n      this.#accounts.push(address);\n      newAccounts.push(address);\n    }\n\n    return newAccounts;\n  }\n\n  /**\n   * Gets the accounts in the QrKeyring\n   *\n   * @returns The accounts in the QrKeyring\n   */\n  async getAccounts(): Promise<Hex[]> {\n    return this.#accounts.slice();\n  }\n\n  /**\n   * Remove an account from the keyring\n   *\n   * @param address - The address of the account to remove\n   */\n  removeAccount(address: Hex): void {\n    const normalizedAddress = normalizeAddress(address);\n    this.#accounts = this.#accounts.filter(\n      (account) => account !== normalizedAddress,\n    );\n  }\n\n  /**\n   * Pair a QR-based Hardware Device from a CBOR encoded UR to the QrKeyring\n   *\n   * @param ur - The CBOR encoded UR\n   */\n  pairDevice(ur: string | SerializedUR): void {\n    this.#device = new Device({\n      requestScan: this.bridge.requestScan.bind(this.bridge),\n      source: ur,\n    });\n  }\n\n  /**\n   * Sets the next account index to unlock\n   *\n   * @param index - The index of the account to unlock\n   */\n  setAccountToUnlock(index: number): void {\n    this.#accountToUnlock = index;\n  }\n\n  /**\n   * Get the name of the paired device or the keyring type\n   * if unavailable.\n   *\n   * @returns The name of the paired device or the keyring type.\n   */\n  getName(): string {\n    if (!this.#device) {\n      return QR_KEYRING_TYPE;\n    }\n    const source = this.#device.getDeviceDetails();\n    return source.name;\n  }\n\n  /**\n   * Get the mode of the paired device.\n   *\n   * @returns The device mode, or undefined if no device is paired.\n   */\n  getMode(): DeviceMode | undefined {\n    return this.#device?.getDeviceDetails().keyringMode;\n  }\n\n  /**\n   * Fetch the first page of accounts. If the keyring is not currently initialized,\n   * it will trigger a scan request to initialize it.\n   *\n   * @returns The first page of accounts as an array of Hex strings.\n   */\n  async getFirstPage(): Promise<IndexedAddress[]> {\n    this.#currentPage = 0;\n    return this.getCurrentPage();\n  }\n\n  /**\n   * Fetch the next page of accounts. If the keyring is not currently initialized,\n   * it will trigger a scan request to initialize it.\n   *\n   * @returns The next page of accounts as an array of IndexedAddress objects.\n   */\n  async getNextPage(): Promise<IndexedAddress[]> {\n    this.#currentPage += 1;\n    return this.getCurrentPage();\n  }\n\n  /**\n   * Fetch the previous page of accounts. If the keyring is not currently initialized,\n   * it will trigger a scan request to initialize it.\n   *\n   * @returns The previous page of accounts as an array of IndexedAddress objects.\n   */\n  async getPreviousPage(): Promise<IndexedAddress[]> {\n    if (this.#currentPage > 0) {\n      this.#currentPage -= 1;\n    } else {\n      this.#currentPage = 0;\n    }\n    return this.getCurrentPage();\n  }\n\n  /**\n   * Fetch the current page of accounts. If the keyring is not currently initialized,\n   * it will trigger a scan request to initialize it.\n   *\n   * @returns The current page of accounts as an array of IndexedAddress objects.\n   */\n  async getCurrentPage(): Promise<IndexedAddress[]> {\n    if (!this.#device) {\n      await this.#scanAndInitialize();\n    }\n    assert(\n      this.#device,\n      'A device is expected to be paired before fetching accounts.',\n    );\n    return this.#device.getAddressesPage(this.#currentPage);\n  }\n\n  /**\n   * Clear the keyring state and forget any paired device or accounts.\n   */\n  async forgetDevice(): Promise<void> {\n    this.#device = undefined;\n    this.#accounts = [];\n    this.#accountToUnlock = undefined;\n    this.#currentPage = 0;\n  }\n\n  /**\n   * Sign a transaction. This is equivalent to the `eth_signTransaction`\n   * Ethereum JSON-RPC method. See the Ethereum JSON-RPC API documentation for\n   * more details.\n   *\n   * @param address - The address of the account to use for signing.\n   * @param transaction - The transaction to sign.\n   * @returns The signed transaction.\n   */\n  async signTransaction(\n    address: Hex,\n    transaction: TypedTransaction,\n  ): Promise<TypedTxData> {\n    if (!this.#device) {\n      throw new Error('No device paired.');\n    }\n    return this.#device.signTransaction(address, transaction);\n  }\n\n  /**\n   * Sign a message. This is equivalent to the `eth_signTypedData` v4 Ethereum\n   * JSON-RPC method.\n   *\n   * @param address - The address of the account to use for signing.\n   * @param data - The data to sign.\n   * @returns The signed message.\n   */\n  async signTypedData<Types extends MessageTypes>(\n    address: Hex,\n    data: TypedMessage<Types>,\n  ): Promise<string> {\n    if (!this.#device) {\n      throw new Error('No device paired.');\n    }\n    return this.#device.signTypedData(address, data);\n  }\n\n  /**\n   * Sign a message. This is equivalent to the `eth_sign` Ethereum JSON-RPC\n   * method, which is exposed by MetaMask as the method `personal_sign`. See\n   * the Ethereum JSON-RPC API documentation for more details.\n   *\n   * For more information about this method and why we call it `personal_sign`,\n   * see the {@link https://docs.metamask.io/guide/signing-data.html|MetaMask Docs}.\n   *\n   * @param address - The address of the account to use for signing.\n   * @param message - The message to sign.\n   * @returns The signed message.\n   */\n  async signPersonalMessage(address: Hex, message: Hex): Promise<string> {\n    if (!this.#device) {\n      throw new Error('No device paired.');\n    }\n    return this.#device.signPersonalMessage(address, message);\n  }\n\n  /**\n   * Scan for a QR code and initialize the keyring with the\n   * scanned UR.\n   */\n  async #scanAndInitialize(): Promise<void> {\n    this.pairDevice(\n      await this.bridge.requestScan({ type: QrScanRequestType.PAIR }),\n    );\n  }\n}\n"]}