import { default as TransportBLE } from '@ledgerhq/hw-transport-web-ble'\r
import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb'\r
import { default as TransportHID } from '@ledgerhq/hw-transport-webhid'\r
+import { Account } from '#src/lib/account.js'\r
import { ChangeBlock, ReceiveBlock, SendBlock } from '#src/lib/block.js'\r
import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, LEDGER_ADPU_CODES, LEDGER_STATUS_CODES } from '#src/lib/constants.js'\r
import { bytes, dec, hex, utf8 } from '#src/lib/convert.js'\r
*/\r
export class LedgerWallet extends Wallet {\r
static #isInternal: boolean = false\r
- #ledger: Ledger\r
\r
- get ledger () { return this.#ledger }\r
-\r
- constructor (id: Entropy, ledger: Ledger) {\r
+ constructor (id: Entropy) {\r
if (!LedgerWallet.#isInternal) {\r
throw new Error(`LedgerWallet cannot be instantiated directly. Use 'await LedgerWallet.create()' instead.`)\r
}\r
LedgerWallet.#isInternal = false\r
super(id)\r
- this.#ledger = ledger\r
}\r
\r
/**\r
* @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object\r
*/\r
static async create (): Promise<LedgerWallet> {\r
- const l = await Ledger.init()\r
const id = await Entropy.create(16)\r
LedgerWallet.#isInternal = true\r
- return new this(id, l)\r
+ const wallet = new this(id)\r
+ await wallet.init()\r
+ return wallet\r
}\r
\r
/**\r
if (typeof id !== 'string' || id === '') {\r
throw new TypeError('Wallet ID is required to restore')\r
}\r
- const l = await Ledger.init()\r
LedgerWallet.#isInternal = true\r
- return new this(await Entropy.import(id), l)\r
+ const wallet = new this(await Entropy.import(id))\r
+ await wallet.init()\r
+ return wallet\r
}\r
\r
/**\r
async ckd (indexes: number[]): Promise<KeyPair[]> {\r
const results: KeyPair[] = []\r
for (const index of indexes) {\r
- const { status, publicKey } = await this.ledger.account(index)\r
+ const { status, publicKey } = await this.#account(index)\r
if (status === 'OK' && publicKey != null) {\r
results.push({ publicKey, index })\r
} else {\r
* @returns True if successfully locked\r
*/\r
async lock (): Promise<boolean> {\r
- if (this.ledger == null) {\r
- return false\r
- }\r
- const result = await this.ledger.close()\r
+ const result = await this.close()\r
return result.status === 'OK'\r
}\r
\r
* @returns True if successfully unlocked\r
*/\r
async unlock (): Promise<boolean> {\r
- if (this.ledger == null) {\r
- return false\r
- }\r
- const result = await this.ledger.connect()\r
+ const result = await this.connect()\r
return result === 'OK'\r
}\r
-}\r
\r
-export class Ledger {\r
- static #isInternal: boolean = false\r
#status: 'DISCONNECTED' | 'LOCKED' | 'BUSY' | 'CONNECTED' = 'DISCONNECTED'\r
get status () { return this.#status }\r
openTimeout = 3000\r
transport: Transport | null = null\r
DynamicTransport: typeof TransportBLE | typeof TransportUSB | typeof TransportHID = TransportHID\r
\r
- constructor () {\r
- if (!Ledger.#isInternal) {\r
- throw new Error('Ledger cannot be instantiated directly. Use Ledger.init()')\r
- }\r
- Ledger.#isInternal = false\r
- Buffer\r
- }\r
-\r
- static async init (): Promise<Ledger> {\r
- Ledger.#isInternal = true\r
- const self = new this()\r
- await self.checkBrowserSupport()\r
- await self.listen()\r
- return self\r
+ async init (): Promise<void> {\r
+ await this.checkBrowserSupport()\r
+ await this.listen()\r
}\r
\r
/**\r
const version = await this.version()\r
if (version.status === 'OK') {\r
if (version.name === 'Nano') {\r
- const account = await this.account()\r
- if (account.status === 'OK') {\r
+ const { status } = await this.#account()\r
+ if (status === 'OK') {\r
this.#status = 'CONNECTED'\r
- } else if (account.status === 'SECURITY_STATUS_NOT_SATISFIED') {\r
+ } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') {\r
this.#status = 'LOCKED'\r
} else {\r
this.#status = 'DISCONNECTED'\r
}\r
\r
/**\r
- * Get an account at a specific BIP-44 index.\r
+ * Request an account at a specific BIP-44 index.\r
*\r
- * @returns Response object containing command status, public key, and address\r
+ * @returns Account\r
*/\r
- async account (index: number = 0, show: boolean = false): Promise<LedgerAccountResponse> {\r
- if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {\r
- throw new TypeError('Invalid account index')\r
- }\r
- const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)\r
- const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)\r
- const account = dec.toBytes(index + HARDENED_OFFSET, 4)\r
- const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account])\r
-\r
- const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
- const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.account, show ? 0x01 : 0x00, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
- .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
- await transport.close()\r
-\r
- if (response.length === 2) {\r
- const statusCode = bytes.toDec(response) as number\r
- const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
- return { status, publicKey: null, address: null }\r
- }\r
-\r
- try {\r
- const publicKey = bytes.toHex(response.slice(0, 32))\r
- const addressLength = response[32]\r
- const address = response.slice(33, 33 + addressLength).toString()\r
- const statusCode = bytes.toDec(response.slice(33 + addressLength)) as number\r
- const status = LEDGER_STATUS_CODES[statusCode]\r
- return { status, publicKey, address }\r
- } catch (err) {\r
- return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null }\r
+ async account (index: number = 0, show: boolean = false): Promise<Account> {\r
+ const { status, publicKey } = await this.#account(index, show)\r
+ if (publicKey == null) {\r
+ throw new Error('Failed to get account from device', { cause: status })\r
}\r
+ const account = await Account.fromPublicKey(publicKey)\r
+ return account\r
}\r
\r
/**\r
}\r
return this.cacheBlock(index, input)\r
}\r
+\r
+ /**\r
+ * Request an account at a specific BIP-44 index.\r
+ *\r
+ * @returns Response object containing command status, public key, and address\r
+ */\r
+ async #account (index: number = 0, show: boolean = false): Promise<LedgerAccountResponse> {\r
+ if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {\r
+ throw new TypeError('Invalid account index')\r
+ }\r
+ const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)\r
+ const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)\r
+ const account = dec.toBytes(index + HARDENED_OFFSET, 4)\r
+ const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account])\r
+\r
+ const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
+ const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.account, show ? 0x01 : 0x00, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
+ .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
+ await transport.close()\r
+\r
+ if (response.length === 2) {\r
+ const statusCode = bytes.toDec(response) as number\r
+ const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
+ return { status, publicKey: null, address: null }\r
+ }\r
+\r
+ try {\r
+ const publicKey = bytes.toHex(response.slice(0, 32))\r
+ const addressLength = response[32]\r
+ const address = response.slice(33, 33 + addressLength).toString()\r
+ const statusCode = bytes.toDec(response.slice(33 + addressLength)) as number\r
+ const status = LEDGER_STATUS_CODES[statusCode]\r
+ return { status, publicKey, address }\r
+ } catch (err) {\r
+ return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null }\r
+ }\r
+ }\r
}\r