otx-btc-wallet-connectors 0.1.0

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 (142) hide show
  1. package/README.md +554 -0
  2. package/dist/base-IAFq7sd8.d.mts +53 -0
  3. package/dist/base-IAFq7sd8.d.ts +53 -0
  4. package/dist/binance/index.d.mts +81 -0
  5. package/dist/binance/index.d.ts +81 -0
  6. package/dist/binance/index.js +13 -0
  7. package/dist/binance/index.js.map +1 -0
  8. package/dist/binance/index.mjs +4 -0
  9. package/dist/binance/index.mjs.map +1 -0
  10. package/dist/bitget/index.d.mts +84 -0
  11. package/dist/bitget/index.d.ts +84 -0
  12. package/dist/bitget/index.js +13 -0
  13. package/dist/bitget/index.js.map +1 -0
  14. package/dist/bitget/index.mjs +4 -0
  15. package/dist/bitget/index.mjs.map +1 -0
  16. package/dist/chunk-5Z5Q2Y75.mjs +91 -0
  17. package/dist/chunk-5Z5Q2Y75.mjs.map +1 -0
  18. package/dist/chunk-7KK2LZLZ.mjs +208 -0
  19. package/dist/chunk-7KK2LZLZ.mjs.map +1 -0
  20. package/dist/chunk-AW2JZIHR.mjs +753 -0
  21. package/dist/chunk-AW2JZIHR.mjs.map +1 -0
  22. package/dist/chunk-EIJOSZXZ.js +331 -0
  23. package/dist/chunk-EIJOSZXZ.js.map +1 -0
  24. package/dist/chunk-EQHR7P7G.js +541 -0
  25. package/dist/chunk-EQHR7P7G.js.map +1 -0
  26. package/dist/chunk-EWRXLZO4.mjs +539 -0
  27. package/dist/chunk-EWRXLZO4.mjs.map +1 -0
  28. package/dist/chunk-FISNQZZ7.js +802 -0
  29. package/dist/chunk-FISNQZZ7.js.map +1 -0
  30. package/dist/chunk-HL4WDMGS.js +200 -0
  31. package/dist/chunk-HL4WDMGS.js.map +1 -0
  32. package/dist/chunk-IPYWR76I.js +314 -0
  33. package/dist/chunk-IPYWR76I.js.map +1 -0
  34. package/dist/chunk-JYYNWR5G.js +142 -0
  35. package/dist/chunk-JYYNWR5G.js.map +1 -0
  36. package/dist/chunk-LNKTYZJM.js +701 -0
  37. package/dist/chunk-LNKTYZJM.js.map +1 -0
  38. package/dist/chunk-LVZMONQL.mjs +699 -0
  39. package/dist/chunk-LVZMONQL.mjs.map +1 -0
  40. package/dist/chunk-MFXLQWOE.js +93 -0
  41. package/dist/chunk-MFXLQWOE.js.map +1 -0
  42. package/dist/chunk-NBIA4TTE.mjs +204 -0
  43. package/dist/chunk-NBIA4TTE.mjs.map +1 -0
  44. package/dist/chunk-O4DD2XJ2.js +206 -0
  45. package/dist/chunk-O4DD2XJ2.js.map +1 -0
  46. package/dist/chunk-P7HVBU2B.mjs +140 -0
  47. package/dist/chunk-P7HVBU2B.mjs.map +1 -0
  48. package/dist/chunk-Q7QVQYEB.js +210 -0
  49. package/dist/chunk-Q7QVQYEB.js.map +1 -0
  50. package/dist/chunk-RLZEG6KL.mjs +329 -0
  51. package/dist/chunk-RLZEG6KL.mjs.map +1 -0
  52. package/dist/chunk-SYLDBJ75.mjs +246 -0
  53. package/dist/chunk-SYLDBJ75.mjs.map +1 -0
  54. package/dist/chunk-TTEUU3CI.mjs +198 -0
  55. package/dist/chunk-TTEUU3CI.mjs.map +1 -0
  56. package/dist/chunk-V66BXDTR.mjs +292 -0
  57. package/dist/chunk-V66BXDTR.mjs.map +1 -0
  58. package/dist/chunk-X77ZT4OI.js +268 -0
  59. package/dist/chunk-X77ZT4OI.js.map +1 -0
  60. package/dist/imtoken/index.d.mts +116 -0
  61. package/dist/imtoken/index.d.ts +116 -0
  62. package/dist/imtoken/index.js +14 -0
  63. package/dist/imtoken/index.js.map +1 -0
  64. package/dist/imtoken/index.mjs +5 -0
  65. package/dist/imtoken/index.mjs.map +1 -0
  66. package/dist/index.d.mts +14 -0
  67. package/dist/index.d.ts +14 -0
  68. package/dist/index.js +170 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/index.mjs +13 -0
  71. package/dist/index.mjs.map +1 -0
  72. package/dist/ledger/index.d.mts +290 -0
  73. package/dist/ledger/index.d.ts +290 -0
  74. package/dist/ledger/index.js +14 -0
  75. package/dist/ledger/index.js.map +1 -0
  76. package/dist/ledger/index.mjs +5 -0
  77. package/dist/ledger/index.mjs.map +1 -0
  78. package/dist/okx/index.d.mts +88 -0
  79. package/dist/okx/index.d.ts +88 -0
  80. package/dist/okx/index.js +13 -0
  81. package/dist/okx/index.js.map +1 -0
  82. package/dist/okx/index.mjs +4 -0
  83. package/dist/okx/index.mjs.map +1 -0
  84. package/dist/phantom/index.d.mts +96 -0
  85. package/dist/phantom/index.d.ts +96 -0
  86. package/dist/phantom/index.js +14 -0
  87. package/dist/phantom/index.js.map +1 -0
  88. package/dist/phantom/index.mjs +5 -0
  89. package/dist/phantom/index.mjs.map +1 -0
  90. package/dist/psbt-builder-CFOs69Z5.d.mts +131 -0
  91. package/dist/psbt-builder-CFOs69Z5.d.ts +131 -0
  92. package/dist/trezor/index.d.mts +155 -0
  93. package/dist/trezor/index.d.ts +155 -0
  94. package/dist/trezor/index.js +14 -0
  95. package/dist/trezor/index.js.map +1 -0
  96. package/dist/trezor/index.mjs +5 -0
  97. package/dist/trezor/index.mjs.map +1 -0
  98. package/dist/unisat/index.d.mts +75 -0
  99. package/dist/unisat/index.d.ts +75 -0
  100. package/dist/unisat/index.js +13 -0
  101. package/dist/unisat/index.js.map +1 -0
  102. package/dist/unisat/index.mjs +4 -0
  103. package/dist/unisat/index.mjs.map +1 -0
  104. package/dist/utils/index.d.mts +398 -0
  105. package/dist/utils/index.d.ts +398 -0
  106. package/dist/utils/index.js +120 -0
  107. package/dist/utils/index.js.map +1 -0
  108. package/dist/utils/index.mjs +3 -0
  109. package/dist/utils/index.mjs.map +1 -0
  110. package/dist/xverse/index.d.mts +79 -0
  111. package/dist/xverse/index.d.ts +79 -0
  112. package/dist/xverse/index.js +13 -0
  113. package/dist/xverse/index.js.map +1 -0
  114. package/dist/xverse/index.mjs +4 -0
  115. package/dist/xverse/index.mjs.map +1 -0
  116. package/package.json +108 -0
  117. package/src/base.ts +132 -0
  118. package/src/binance/BinanceConnector.ts +307 -0
  119. package/src/binance/index.ts +1 -0
  120. package/src/bitget/BitgetConnector.ts +301 -0
  121. package/src/bitget/index.ts +1 -0
  122. package/src/imtoken/ImTokenConnector.ts +420 -0
  123. package/src/imtoken/index.ts +2 -0
  124. package/src/index.ts +78 -0
  125. package/src/ledger/LedgerConnector.ts +1019 -0
  126. package/src/ledger/index.ts +8 -0
  127. package/src/okx/OKXConnector.ts +230 -0
  128. package/src/okx/index.ts +1 -0
  129. package/src/phantom/PhantomConnector.ts +381 -0
  130. package/src/phantom/index.ts +2 -0
  131. package/src/trezor/TrezorConnector.ts +824 -0
  132. package/src/trezor/index.ts +6 -0
  133. package/src/unisat/UnisatConnector.ts +312 -0
  134. package/src/unisat/index.ts +1 -0
  135. package/src/utils/blockstream.ts +230 -0
  136. package/src/utils/btc-service.ts +364 -0
  137. package/src/utils/index.ts +56 -0
  138. package/src/utils/mempool.ts +232 -0
  139. package/src/utils/psbt-builder.ts +492 -0
  140. package/src/utils/types.ts +183 -0
  141. package/src/xverse/XverseConnector.ts +479 -0
  142. package/src/xverse/index.ts +1 -0
@@ -0,0 +1,1019 @@
1
+ import type {
2
+ WalletAccount,
3
+ BitcoinNetwork,
4
+ SignPsbtOptions,
5
+ AddressType,
6
+ } from 'otx-btc-wallet-core';
7
+ import { BaseConnector } from '../base';
8
+ import {
9
+ BtcService,
10
+ getAddressType,
11
+ getDustThreshold,
12
+ getInputVBytes,
13
+ getOutputVBytes,
14
+ } from '../utils';
15
+
16
+ // Ledger wallet icon
17
+ const LEDGER_ICON =
18
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF83MDM1XzUxMTApIj4KPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHJ4PSIzMiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE0Mi4yMzkgMzcuNUg4NS4wOTY0VjExNC41ODJIMTYyLjIzOVY1Ny40MzkzQzE2Mi4yMzggNTQuODE2IDE2MS43MiA1Mi4yMTg2IDE2MC43MTQgNDkuNzk1OUMxNTkuNzA4IDQ3LjM3MzIgMTU4LjIzMyA0NS4xNzI4IDE1Ni4zNzYgNDMuMzIwN0MxNTQuNTE4IDQxLjQ2ODUgMTUyLjMxMyA0MC4wMDExIDE0OS44ODcgMzkuMDAyM0MxNDcuNDYyIDM4LjAwMzUgMTQ0Ljg2MyAzNy40OTMgMTQyLjIzOSAzNy41WiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTY3LjI5MjkgMzcuNUg1Ny40NDY0QzUyLjE1NjMgMzcuNSA0Ny4wODI5IDM5LjYwMTUgNDMuMzQyMiA0My4zNDIyQzM5LjYwMTUgNDcuMDgyOSAzNy41IDUyLjE1NjMgMzcuNSA1Ny40NDY0TDM3LjUgNjcuMjkyOUg2Ny4yOTI5VjM3LjVaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMzcuNSA4NS4xMDM2SDY3LjI5MjlWMTE0Ljg5NkgzNy41Vjg1LjEwMzZaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTMyLjcwNyAxNjIuMjQ2SDE0Mi41NTNDMTQ1LjE3NyAxNjIuMjQ2IDE0Ny43NzYgMTYxLjcyOSAxNTAuMTk5IDE2MC43MjNDMTUyLjYyMyAxNTkuNzE3IDE1NC44MjQgMTU4LjI0MyAxNTYuNjc3IDE1Ni4zODVDMTU4LjUzIDE1NC41MjcgMTU5Ljk5OCAxNTIuMzIyIDE2MC45OTcgMTQ5Ljg5NkMxNjEuOTk2IDE0Ny40NyAxNjIuNTA3IDE0NC44NyAxNjIuNSAxNDIuMjQ2VjEzMi43MDdIMTMyLjcwN1YxNjIuMjQ2WiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTg1LjEwMzUgMTMyLjcwN0gxMTQuODk2VjE2Mi41SDg1LjEwMzVWMTMyLjcwN1oiIGZpbGw9ImJsYWNrIi8+CjxwYXRoIGQ9Ik0zNy41IDEzMi43MDdWMTQyLjU1NEMzNy41IDE0NS4xNzMgMzguMDE1OSAxNDcuNzY3IDM5LjAxODMgMTUwLjE4N0M0MC4wMjA3IDE1Mi42MDcgNDEuNDkgMTU0LjgwNiA0My4zNDIyIDE1Ni42NThDNDUuMTk0NCAxNTguNTEgNDcuMzkzMiAxNTkuOTc5IDQ5LjgxMzMgMTYwLjk4MkM1Mi4yMzMzIDE2MS45ODQgNTQuODI3IDE2Mi41IDU3LjQ0NjQgMTYyLjVINjcuMjkyOVYxMzIuNzA3SDM3LjVaIiBmaWxsPSJibGFjayIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzcwMzVfNTExMCI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=';
19
+
20
+ /**
21
+ * Ledger address type configuration
22
+ */
23
+ export type LedgerAddressType = 'legacy' | 'nested-segwit' | 'segwit' | 'taproot';
24
+
25
+ /**
26
+ * Ledger connector options
27
+ */
28
+ export interface LedgerConnectorOptions {
29
+ /** Address type for derivation (default: 'segwit') */
30
+ addressType?: LedgerAddressType;
31
+ /** Account index (default: 0) */
32
+ accountIndex?: number;
33
+ /** Address index (default: 0) */
34
+ addressIndex?: number;
35
+ }
36
+
37
+ /**
38
+ * Address type to BIP path mapping
39
+ */
40
+ const ADDRESS_TYPE_TO_PATH: Record<LedgerAddressType, number> = {
41
+ 'legacy': 44,
42
+ 'nested-segwit': 49,
43
+ 'segwit': 84,
44
+ 'taproot': 86,
45
+ };
46
+
47
+ /**
48
+ * Address type to Ledger format mapping
49
+ */
50
+ const ADDRESS_TYPE_TO_FORMAT: Record<LedgerAddressType, 'legacy' | 'p2sh' | 'bech32' | 'bech32m'> = {
51
+ 'legacy': 'legacy',
52
+ 'nested-segwit': 'p2sh',
53
+ 'segwit': 'bech32',
54
+ 'taproot': 'bech32m',
55
+ };
56
+
57
+ // Define minimal types for Ledger libraries to avoid type issues
58
+ interface LedgerTransport {
59
+ close(): Promise<void>;
60
+ on(event: 'disconnect', callback: () => void): void;
61
+ send(cla: number, ins: number, p1: number, p2: number, data?: Buffer): Promise<Buffer>;
62
+ }
63
+
64
+ interface LedgerBtcApp {
65
+ getWalletPublicKey(
66
+ path: string,
67
+ opts?: { verify?: boolean; format?: 'legacy' | 'p2sh' | 'bech32' | 'bech32m' }
68
+ ): Promise<{ publicKey: string; bitcoinAddress: string; chainCode: string }>;
69
+ getWalletXpub(arg: { path: string; xpubVersion: number }): Promise<string>;
70
+ signMessage(path: string, messageHex: string): Promise<{ v: number; r: string; s: string }>;
71
+ splitTransaction(
72
+ transactionHex: string,
73
+ isSegwitSupported?: boolean | null,
74
+ hasExtraData?: boolean,
75
+ additionals?: string[]
76
+ ): LedgerTransaction;
77
+ createPaymentTransaction(arg: {
78
+ inputs: Array<[LedgerTransaction, number, string | null | undefined, number?]>;
79
+ associatedKeysets: string[];
80
+ changePath?: string;
81
+ outputScriptHex: string;
82
+ lockTime?: number;
83
+ sigHashType?: number;
84
+ segwit?: boolean;
85
+ additionals?: string[];
86
+ expiryHeight?: Buffer;
87
+ useTrustedInputForSegwit?: boolean;
88
+ }): Promise<string>;
89
+ serializeTransactionOutputs(tx: LedgerTransaction): Buffer;
90
+ }
91
+
92
+ /**
93
+ * Ledger Transaction type (returned by splitTransaction)
94
+ */
95
+ interface LedgerTransaction {
96
+ version: Buffer;
97
+ inputs: Array<{
98
+ prevout: Buffer;
99
+ script: Buffer;
100
+ sequence: Buffer;
101
+ }>;
102
+ outputs?: Array<{
103
+ amount: Buffer;
104
+ script: Buffer;
105
+ }>;
106
+ locktime?: Buffer;
107
+ witness?: Buffer;
108
+ }
109
+
110
+ /**
111
+ * UTXO input for createPaymentTransaction
112
+ */
113
+ export interface LedgerUtxoInput {
114
+ /** Raw transaction hex that created this UTXO */
115
+ txHex: string;
116
+ /** Output index in the transaction */
117
+ outputIndex: number;
118
+ /** BIP32 derivation path for signing (e.g., "84'/0'/0'/0/0") */
119
+ derivationPath: string;
120
+ /** Optional redeem script for P2SH inputs */
121
+ redeemScript?: string;
122
+ /** Optional sequence number (default: 0xffffffff) */
123
+ sequence?: number;
124
+ }
125
+
126
+ /**
127
+ * Output for createPaymentTransaction
128
+ */
129
+ export interface LedgerTxOutput {
130
+ /** Recipient address */
131
+ address: string;
132
+ /** Amount in satoshis */
133
+ satoshis: number;
134
+ }
135
+
136
+ /**
137
+ * Options for sendBitcoin method
138
+ */
139
+ export interface SendBitcoinOptions {
140
+ /** Fee rate in sat/vB (optional, defaults to "hour" priority) */
141
+ feeRate?: number;
142
+ }
143
+
144
+ /**
145
+ * Ledger Hardware Wallet Connector
146
+ *
147
+ * @see https://developers.ledger.com/docs/transport/overview
148
+ * @see https://github.com/LedgerHQ/ledger-live/tree/develop/libs/ledgerjs/packages/hw-app-btc
149
+ */
150
+ export class LedgerConnector extends BaseConnector {
151
+ readonly id = 'ledger';
152
+ readonly name = 'Ledger';
153
+ readonly icon = LEDGER_ICON;
154
+
155
+ private _transport: LedgerTransport | null = null;
156
+ private _btcApp: LedgerBtcApp | null = null;
157
+ private _account: WalletAccount | null = null;
158
+ private _network: BitcoinNetwork = 'mainnet';
159
+ private _options: Required<LedgerConnectorOptions>;
160
+ private _derivationPath: string = '';
161
+
162
+ // Connection timeout in milliseconds
163
+ private static readonly CONNECTION_TIMEOUT = 30000;
164
+
165
+ constructor(options: LedgerConnectorOptions = {}) {
166
+ super();
167
+ this._options = {
168
+ addressType: options.addressType ?? 'segwit',
169
+ accountIndex: options.accountIndex ?? 0,
170
+ addressIndex: options.addressIndex ?? 0,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Override checkReady - Ledger is always "ready" since it doesn't require browser extension
176
+ * The actual device connection happens when connect() is called
177
+ */
178
+ protected checkReady(): void {
179
+ this.ready = true;
180
+ }
181
+
182
+ /**
183
+ * Get the provider - for Ledger this is the Bitcoin app instance
184
+ */
185
+ protected getProvider(): LedgerBtcApp | null {
186
+ return this._btcApp;
187
+ }
188
+
189
+ /**
190
+ * Get current address type
191
+ */
192
+ getAddressType(): LedgerAddressType {
193
+ return this._options.addressType;
194
+ }
195
+
196
+ /**
197
+ * Set address type options before connecting
198
+ * Call this before connect() to change the address derivation path
199
+ */
200
+ setOptions(options: LedgerConnectorOptions): void {
201
+ this._options = {
202
+ addressType: options.addressType ?? this._options.addressType,
203
+ accountIndex: options.accountIndex ?? this._options.accountIndex,
204
+ addressIndex: options.addressIndex ?? this._options.addressIndex,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Get available address types for UI selection
210
+ */
211
+ static getAvailableAddressTypes(): Array<{ type: LedgerAddressType; label: string; description: string }> {
212
+ return [
213
+ { type: 'legacy', label: 'Legacy (P2PKH)', description: "Starts with '1' or 'm/n'" },
214
+ { type: 'nested-segwit', label: 'Nested SegWit (P2SH-P2WPKH)', description: "Starts with '3' or '2'" },
215
+ { type: 'segwit', label: 'Native SegWit (P2WPKH)', description: "Starts with 'bc1q' or 'tb1q'" },
216
+ { type: 'taproot', label: 'Taproot (P2TR)', description: "Starts with 'bc1p' or 'tb1p'" },
217
+ ];
218
+ }
219
+
220
+ /**
221
+ * Check if WebUSB is supported in the current browser
222
+ */
223
+ async isSupported(): Promise<boolean> {
224
+ if (typeof window === 'undefined') return false;
225
+ try {
226
+ const TransportWebUSB = (await import('@ledgerhq/hw-transport-webusb')).default;
227
+ return await TransportWebUSB.isSupported();
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Initialize transport with timeout
235
+ * Uses request() instead of create() to always show device selection popup
236
+ */
237
+ private async initTransport(): Promise<LedgerTransport> {
238
+ console.log('[LedgerConnector] initTransport() called');
239
+
240
+ // Close existing transport if any
241
+ await this.closeTransport();
242
+
243
+ // Import WebUSB transport
244
+ console.log('[LedgerConnector] Importing TransportWebUSB...');
245
+ const TransportWebUSB = (await import('@ledgerhq/hw-transport-webusb')).default;
246
+ console.log('[LedgerConnector] TransportWebUSB imported');
247
+
248
+ // Use request() to always show device selection popup
249
+ // create() would auto-connect to previously paired device without showing popup
250
+ console.log('[LedgerConnector] Requesting device selection...');
251
+ const transportPromise = TransportWebUSB.request();
252
+ const timeoutPromise = new Promise<never>((_, reject) => {
253
+ setTimeout(() => {
254
+ reject(new Error('Connection timeout. Please make sure your Ledger is connected and unlocked.'));
255
+ }, LedgerConnector.CONNECTION_TIMEOUT);
256
+ });
257
+
258
+ const transport = await Promise.race([transportPromise, timeoutPromise]);
259
+ console.log('[LedgerConnector] Transport created successfully');
260
+ return transport as unknown as LedgerTransport;
261
+ }
262
+
263
+ /**
264
+ * Get BTC app instance, initializing if needed
265
+ */
266
+ private async getBtcAppInstance(): Promise<LedgerBtcApp> {
267
+ console.log('[LedgerConnector] getBtcAppInstance() called');
268
+
269
+ if (this._btcApp) {
270
+ console.log('[LedgerConnector] Returning existing BTC app instance');
271
+ return this._btcApp;
272
+ }
273
+
274
+ const transport = await this.initTransport();
275
+ this._transport = transport;
276
+
277
+ // Check if Bitcoin app is open by sending a test command
278
+ console.log('[LedgerConnector] Checking if Bitcoin app is open...');
279
+ try {
280
+ // CLA=0xB0 INS=0x01 is get app version - works as a ping
281
+ await transport.send(0xb0, 0x01, 0x00, 0x00);
282
+ console.log('[LedgerConnector] Bitcoin app is open');
283
+ } catch (error) {
284
+ console.error('[LedgerConnector] Bitcoin app check failed:', error);
285
+ await this.closeTransport();
286
+ throw new Error('Please open the Bitcoin app on your Ledger device');
287
+ }
288
+
289
+ // Import and initialize BTC app
290
+ console.log('[LedgerConnector] Importing AppBtc...');
291
+ const AppBtc = (await import('@ledgerhq/hw-app-btc')).default;
292
+ console.log('[LedgerConnector] AppBtc imported');
293
+
294
+ const currency = this._network === 'mainnet' ? 'bitcoin' : 'bitcoin_testnet';
295
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
296
+ this._btcApp = new AppBtc({ transport: transport as any, currency }) as unknown as LedgerBtcApp;
297
+ console.log('[LedgerConnector] BTC app instance created');
298
+
299
+ return this._btcApp;
300
+ }
301
+
302
+ /**
303
+ * Connect to Ledger device
304
+ * IMPORTANT: This must be called within a user gesture (click event)
305
+ */
306
+ async connect(network: BitcoinNetwork = 'mainnet'): Promise<WalletAccount> {
307
+ console.log('[LedgerConnector] connect() called with network:', network);
308
+
309
+ try {
310
+ // Check WebUSB support
311
+ console.log('[LedgerConnector] Checking WebUSB support...');
312
+ const supported = await this.isSupported();
313
+ console.log('[LedgerConnector] WebUSB supported:', supported);
314
+
315
+ if (!supported) {
316
+ throw new Error('WebUSB is not supported in this browser. Please use Chrome or a Chromium-based browser.');
317
+ }
318
+
319
+ // Set network before initializing app
320
+ this._network = network;
321
+
322
+ // Initialize BTC app (this also creates transport)
323
+ console.log('[LedgerConnector] Initializing BTC app...');
324
+ const btcApp = await this.getBtcAppInstance();
325
+ console.log('[LedgerConnector] BTC app initialized');
326
+
327
+ // Build derivation path
328
+ const coinType = network === 'mainnet' ? 0 : 1;
329
+ const purpose = ADDRESS_TYPE_TO_PATH[this._options.addressType];
330
+ this._derivationPath = `${purpose}'/${coinType}'/${this._options.accountIndex}'/0/${this._options.addressIndex}`;
331
+ console.log('[LedgerConnector] Derivation path:', this._derivationPath);
332
+
333
+ // Get address and public key from device
334
+ const format = ADDRESS_TYPE_TO_FORMAT[this._options.addressType];
335
+ console.log('[LedgerConnector] Getting wallet public key...');
336
+ const result = await btcApp.getWalletPublicKey(this._derivationPath, {
337
+ verify: false,
338
+ format,
339
+ });
340
+ console.log('[LedgerConnector] Got wallet public key:', result.bitcoinAddress);
341
+
342
+ this._account = {
343
+ address: result.bitcoinAddress,
344
+ publicKey: result.publicKey,
345
+ type: this.mapLedgerAddressType(this._options.addressType),
346
+ };
347
+
348
+ // Setup disconnect listener
349
+ this.setupDisconnectListener();
350
+
351
+ console.log('[LedgerConnector] Connection successful!');
352
+ return this._account;
353
+ } catch (error) {
354
+ console.error('[LedgerConnector] Connection error:', error);
355
+ await this.closeTransport();
356
+ this.handleLedgerError(error);
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Get address with verification on device
362
+ */
363
+ async getAddressWithVerification(): Promise<WalletAccount> {
364
+ if (!this._btcApp) {
365
+ throw new Error('Not connected to Ledger device');
366
+ }
367
+
368
+ try {
369
+ const format = ADDRESS_TYPE_TO_FORMAT[this._options.addressType];
370
+ const result = await this._btcApp.getWalletPublicKey(this._derivationPath, {
371
+ verify: true, // Show address on device for verification
372
+ format,
373
+ });
374
+
375
+ return {
376
+ address: result.bitcoinAddress,
377
+ publicKey: result.publicKey,
378
+ type: this.mapLedgerAddressType(this._options.addressType),
379
+ };
380
+ } catch (error) {
381
+ this.handleLedgerError(error);
382
+ }
383
+ }
384
+
385
+ private setupDisconnectListener(): void {
386
+ if (!this._transport) return;
387
+
388
+ this._transport.on('disconnect', () => {
389
+ this._transport = null;
390
+ this._btcApp = null;
391
+ this._account = null;
392
+ this.emitAccountsChanged([]);
393
+ });
394
+ }
395
+
396
+ private async closeTransport(): Promise<void> {
397
+ if (this._transport) {
398
+ try {
399
+ await this._transport.close();
400
+ } catch {
401
+ // Ignore close errors
402
+ }
403
+ this._transport = null;
404
+ this._btcApp = null;
405
+ }
406
+ }
407
+
408
+ async disconnect(): Promise<void> {
409
+ await this.closeTransport();
410
+ this._account = null;
411
+ this.cleanup();
412
+ }
413
+
414
+ async getAccounts(): Promise<WalletAccount[]> {
415
+ if (!this._account) {
416
+ return [];
417
+ }
418
+ return [this._account];
419
+ }
420
+
421
+ async signMessage(message: string): Promise<string> {
422
+ if (!this._btcApp) {
423
+ throw new Error('Not connected to Ledger device');
424
+ }
425
+
426
+ try {
427
+ // Convert message to hex
428
+ const messageHex = Buffer.from(message, 'utf8').toString('hex');
429
+
430
+ // Sign message on device
431
+ const result = await this._btcApp.signMessage(this._derivationPath, messageHex);
432
+
433
+ // Construct signature (v || r || s) and encode as base64
434
+ const v = result.v;
435
+ const r = result.r;
436
+ const s = result.s;
437
+
438
+ // Format: 1 byte v + 32 bytes r + 32 bytes s
439
+ const vHex = v.toString(16).padStart(2, '0');
440
+ const signature = Buffer.from(vHex + r + s, 'hex');
441
+
442
+ return signature.toString('base64');
443
+ } catch (error) {
444
+ this.handleLedgerError(error);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Sign a PSBT with Ledger
450
+ *
451
+ * Note: Ledger hardware wallets don't support direct PSBT signing through the standard API.
452
+ * Use the createTransaction method or sendTransaction instead.
453
+ */
454
+ async signPsbt(_psbtHex: string, _options?: SignPsbtOptions): Promise<string> {
455
+ void _psbtHex;
456
+ void _options;
457
+
458
+ throw new Error('SignPsbt is not supported for Ledger Wallet. Use createTransaction or sendTransaction instead.');
459
+ }
460
+
461
+ /**
462
+ * Sign multiple PSBTs
463
+ */
464
+ async signPsbts(psbtHexs: string[], options?: SignPsbtOptions): Promise<string[]> {
465
+ const results: string[] = [];
466
+ for (const psbtHex of psbtHexs) {
467
+ const signed = await this.signPsbt(psbtHex, options);
468
+ results.push(signed);
469
+ }
470
+ return results;
471
+ }
472
+
473
+ /**
474
+ * Send a Bitcoin transaction
475
+ *
476
+ * @param to - Recipient address
477
+ * @param satoshis - Amount to send in satoshis
478
+ * @param options - Send options (feeRate, etc.)
479
+ * @returns Transaction ID after broadcast
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * const txid = await ledger.sendTransaction('bc1q...', 50000);
484
+ * // With custom fee rate
485
+ * const txid = await ledger.sendTransaction('bc1q...', 50000, { feeRate: 10 });
486
+ * ```
487
+ */
488
+ async sendTransaction(to: string, satoshis: number, options?: SendBitcoinOptions): Promise<string> {
489
+ if (!this._btcApp || !this._account) {
490
+ throw new Error('Not connected to Ledger device');
491
+ }
492
+
493
+ const btcService = new BtcService(this._network);
494
+
495
+ // 1. Get UTXOs with tx hex (Ledger requires raw tx hex for each UTXO)
496
+ const utxos = await btcService.getUtxosWithTxHex(this._account.address);
497
+ if (utxos.length === 0) {
498
+ throw new Error('No UTXOs available for spending');
499
+ }
500
+
501
+ // 2. Get fee rate if not provided
502
+ let feeRate = options?.feeRate;
503
+ if (!feeRate) {
504
+ const feeRates = await btcService.getFeeRates();
505
+ feeRate = feeRates.hour;
506
+ }
507
+
508
+ // 3. Get address types using utility functions
509
+ const fromAddressType = getAddressType(this._account.address);
510
+ const toAddressType = getAddressType(to);
511
+
512
+ // 4. Get vBytes estimates using utility functions
513
+ const inputVBytes = getInputVBytes(fromAddressType);
514
+ const outputVBytes = getOutputVBytes(toAddressType);
515
+ const changeOutputVBytes = getOutputVBytes(fromAddressType);
516
+ const baseVBytes = 10.5; // Base transaction overhead
517
+
518
+ // 5. Select UTXOs using greedy algorithm (sorted by value descending)
519
+ const sortedUtxos = [...utxos].sort((a, b) => b.value - a.value);
520
+ const selectedUtxos: typeof utxos = [];
521
+ let totalInputValue = 0;
522
+
523
+ for (const utxo of sortedUtxos) {
524
+ selectedUtxos.push(utxo);
525
+ totalInputValue += utxo.value;
526
+
527
+ // Calculate fee with 2 outputs (recipient + change)
528
+ const estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes + changeOutputVBytes;
529
+ const estimatedFee = Math.ceil(estimatedVBytes * feeRate);
530
+
531
+ if (totalInputValue >= satoshis + estimatedFee) {
532
+ break;
533
+ }
534
+ }
535
+
536
+ // 6. Calculate final fee
537
+ let estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes + changeOutputVBytes;
538
+ let estimatedFee = Math.ceil(estimatedVBytes * feeRate);
539
+
540
+ // Check if we have enough funds
541
+ if (totalInputValue < satoshis + estimatedFee) {
542
+ throw new Error(
543
+ `Insufficient funds. Available: ${totalInputValue} sats, Required: ${satoshis + estimatedFee} sats (including fee)`
544
+ );
545
+ }
546
+
547
+ // 7. Calculate change
548
+ let changeAmount = totalInputValue - satoshis - estimatedFee;
549
+
550
+ // 8. Build outputs
551
+ const outputs: LedgerTxOutput[] = [{ address: to, satoshis }];
552
+
553
+ // Add change output if above dust threshold
554
+ const dustThreshold = getDustThreshold(fromAddressType);
555
+
556
+ if (changeAmount > dustThreshold) {
557
+ outputs.push({ address: this._account.address, satoshis: changeAmount });
558
+ } else {
559
+ // No change output - recalculate fee without change output
560
+ estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes;
561
+ estimatedFee = Math.ceil(estimatedVBytes * feeRate);
562
+ changeAmount = 0;
563
+
564
+ // Verify we still have enough
565
+ if (totalInputValue < satoshis + estimatedFee) {
566
+ throw new Error(
567
+ `Insufficient funds after fee adjustment. Available: ${totalInputValue} sats, Required: ${satoshis + estimatedFee} sats`
568
+ );
569
+ }
570
+ }
571
+
572
+ // 9. Build inputs for createTransaction
573
+ const inputs: LedgerUtxoInput[] = selectedUtxos.map((utxo) => ({
574
+ txHex: utxo.txHex,
575
+ outputIndex: utxo.vout,
576
+ derivationPath: this._derivationPath,
577
+ }));
578
+
579
+ // 10. Create and sign transaction using Ledger's native API
580
+ const signedTxHex = await this.createTransaction(inputs, outputs);
581
+
582
+ // 11. Broadcast transaction
583
+ const txid = await btcService.broadcastTransaction(signedTxHex);
584
+
585
+ return txid;
586
+ }
587
+
588
+ /**
589
+ * Create and sign a Bitcoin transaction using Ledger
590
+ *
591
+ * This method requires you to provide UTXOs (unspent transaction outputs) which
592
+ * must be fetched from a blockchain API (e.g., Blockstream, Mempool.space, etc.)
593
+ *
594
+ * @param inputs - Array of UTXO inputs to spend
595
+ * @param outputs - Array of outputs (recipients)
596
+ * @param changePath - Optional BIP32 path for change output
597
+ * @returns Signed transaction hex ready for broadcast
598
+ *
599
+ * @example
600
+ * ```typescript
601
+ * const signedTx = await ledger.createTransaction(
602
+ * [{
603
+ * txHex: '0100000001...', // Raw tx hex that created the UTXO
604
+ * outputIndex: 0,
605
+ * derivationPath: "84'/0'/0'/0/0",
606
+ * }],
607
+ * [{
608
+ * address: 'bc1q...',
609
+ * satoshis: 50000,
610
+ * }],
611
+ * "84'/0'/0'/1/0" // Change address path
612
+ * );
613
+ * ```
614
+ */
615
+ async createTransaction(
616
+ inputs: LedgerUtxoInput[],
617
+ outputs: LedgerTxOutput[],
618
+ changePath?: string
619
+ ): Promise<string> {
620
+ if (!this._btcApp) {
621
+ throw new Error('Not connected to Ledger device');
622
+ }
623
+
624
+ if (inputs.length === 0) {
625
+ throw new Error('At least one input is required');
626
+ }
627
+
628
+ if (outputs.length === 0) {
629
+ throw new Error('At least one output is required');
630
+ }
631
+
632
+ try {
633
+ // Determine if we're using segwit based on address type
634
+ const isSegwit = this._options.addressType !== 'legacy';
635
+ const additionals = this.getAdditionals();
636
+
637
+ // Parse input transactions
638
+ const parsedInputs: Array<[LedgerTransaction, number, string | null | undefined, number?]> = [];
639
+ const associatedKeysets: string[] = [];
640
+
641
+ for (const input of inputs) {
642
+ const tx = this._btcApp.splitTransaction(input.txHex, isSegwit, false, additionals);
643
+ parsedInputs.push([
644
+ tx,
645
+ input.outputIndex,
646
+ input.redeemScript ?? null,
647
+ input.sequence ?? 0xffffffff,
648
+ ]);
649
+ associatedKeysets.push(input.derivationPath);
650
+ }
651
+
652
+ // Build output script
653
+ const outputScriptHex = this.buildOutputScript(outputs);
654
+
655
+ // Create and sign the transaction
656
+ const txArgs: Parameters<LedgerBtcApp['createPaymentTransaction']>[0] = {
657
+ inputs: parsedInputs,
658
+ associatedKeysets,
659
+ outputScriptHex,
660
+ segwit: isSegwit,
661
+ additionals,
662
+ useTrustedInputForSegwit: isSegwit,
663
+ };
664
+
665
+ if (changePath) {
666
+ txArgs.changePath = changePath;
667
+ }
668
+
669
+ const signedTx = await this._btcApp.createPaymentTransaction(txArgs);
670
+
671
+ return signedTx;
672
+ } catch (error) {
673
+ this.handleLedgerError(error);
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Build output script hex from outputs array
679
+ * Format: varint(output_count) + [amount(8 bytes LE) + varint(script_len) + script]...
680
+ */
681
+ private buildOutputScript(outputs: LedgerTxOutput[]): string {
682
+ let script = '';
683
+
684
+ // Output count as varint
685
+ script += this.encodeVarint(outputs.length);
686
+
687
+ for (const output of outputs) {
688
+ // Amount as 8-byte little-endian
689
+ const amountHex = this.uint64ToLittleEndian(output.satoshis);
690
+ script += amountHex;
691
+
692
+ // Script pubkey
693
+ const scriptPubKey = this.addressToScriptPubKey(output.address);
694
+ script += this.encodeVarint(scriptPubKey.length / 2);
695
+ script += scriptPubKey;
696
+ }
697
+
698
+ return script;
699
+ }
700
+
701
+ /**
702
+ * Convert address to scriptPubKey hex
703
+ */
704
+ private addressToScriptPubKey(address: string): string {
705
+ console.log('[LedgerConnector] addressToScriptPubKey called with:', address);
706
+
707
+ // Detect address type and convert to scriptPubKey
708
+ if (address.startsWith('bc1q') || address.startsWith('tb1q')) {
709
+ // Native SegWit (P2WPKH) - bech32
710
+ const decoded = this.decodeBech32(address);
711
+ console.log('[LedgerConnector] P2WPKH decoded length:', decoded.length, 'hex:', decoded);
712
+ // decoded is hex string, so 20 bytes = 40 hex chars
713
+ if (decoded.length !== 40) {
714
+ throw new Error(`Invalid P2WPKH address: expected 40 hex chars, got ${decoded.length}`);
715
+ }
716
+ // OP_0 <20-byte-hash>
717
+ return '0014' + decoded;
718
+ } else if (address.startsWith('bc1p') || address.startsWith('tb1p')) {
719
+ // Taproot (P2TR) - bech32m
720
+ const decoded = this.decodeBech32(address);
721
+ console.log('[LedgerConnector] P2TR decoded length:', decoded.length, 'hex:', decoded);
722
+ // decoded is hex string, so 32 bytes = 64 hex chars
723
+ if (decoded.length !== 64) {
724
+ throw new Error(`Invalid P2TR address: expected 64 hex chars, got ${decoded.length}`);
725
+ }
726
+ // OP_1 <32-byte-pubkey>
727
+ return '5120' + decoded;
728
+ } else if (address.startsWith('3') || address.startsWith('2')) {
729
+ // P2SH
730
+ const decoded = this.decodeBase58Check(address);
731
+ // OP_HASH160 <20-byte-hash> OP_EQUAL
732
+ return 'a914' + decoded + '87';
733
+ } else if (address.startsWith('1') || address.startsWith('m') || address.startsWith('n')) {
734
+ // P2PKH (Legacy)
735
+ const decoded = this.decodeBase58Check(address);
736
+ // OP_DUP OP_HASH160 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG
737
+ return '76a914' + decoded + '88ac';
738
+ } else {
739
+ throw new Error(`Unsupported address format: ${address}`);
740
+ }
741
+ }
742
+
743
+ /**
744
+ * Decode bech32/bech32m address to hex
745
+ */
746
+ private decodeBech32(address: string): string {
747
+ const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
748
+
749
+ // Find separator
750
+ const pos = address.lastIndexOf('1');
751
+ if (pos < 1) throw new Error('Invalid bech32 address');
752
+
753
+ const data = address.slice(pos + 1);
754
+
755
+ // Decode data characters
756
+ const values: number[] = [];
757
+ for (const char of data) {
758
+ const idx = CHARSET.indexOf(char.toLowerCase());
759
+ if (idx === -1) throw new Error('Invalid bech32 character');
760
+ values.push(idx);
761
+ }
762
+
763
+ // Remove checksum (last 6 characters)
764
+ const dataValues = values.slice(0, -6);
765
+
766
+ // Skip witness version (first value)
767
+ const witnessData = dataValues.slice(1);
768
+
769
+ // Convert from 5-bit to 8-bit
770
+ let acc = 0;
771
+ let bits = 0;
772
+ const result: number[] = [];
773
+
774
+ for (const value of witnessData) {
775
+ acc = (acc << 5) | value;
776
+ bits += 5;
777
+ while (bits >= 8) {
778
+ bits -= 8;
779
+ result.push((acc >> bits) & 0xff);
780
+ }
781
+ }
782
+
783
+ return Buffer.from(result).toString('hex');
784
+ }
785
+
786
+ /**
787
+ * Decode base58check address to hex (without version byte)
788
+ */
789
+ private decodeBase58Check(address: string): string {
790
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
791
+
792
+ let num = BigInt(0);
793
+ for (const char of address) {
794
+ const idx = ALPHABET.indexOf(char);
795
+ if (idx === -1) throw new Error('Invalid base58 character');
796
+ num = num * BigInt(58) + BigInt(idx);
797
+ }
798
+
799
+ // Convert to hex and pad
800
+ let hex = num.toString(16);
801
+ if (hex.length % 2 !== 0) hex = '0' + hex;
802
+
803
+ // Handle leading zeros
804
+ let leadingZeros = 0;
805
+ for (const char of address) {
806
+ if (char === '1') leadingZeros++;
807
+ else break;
808
+ }
809
+
810
+ hex = '00'.repeat(leadingZeros) + hex;
811
+
812
+ // Remove version byte (1 byte) and checksum (4 bytes)
813
+ // Result is the 20-byte hash
814
+ return hex.slice(2, -8);
815
+ }
816
+
817
+ /**
818
+ * Encode number as varint hex
819
+ */
820
+ private encodeVarint(n: number): string {
821
+ if (n < 0xfd) {
822
+ return n.toString(16).padStart(2, '0');
823
+ } else if (n <= 0xffff) {
824
+ return 'fd' + this.uint16ToLittleEndian(n);
825
+ } else if (n <= 0xffffffff) {
826
+ return 'fe' + this.uint32ToLittleEndian(n);
827
+ } else {
828
+ return 'ff' + this.uint64ToLittleEndian(n);
829
+ }
830
+ }
831
+
832
+ /**
833
+ * Convert uint16 to little-endian hex
834
+ */
835
+ private uint16ToLittleEndian(n: number): string {
836
+ const buf = Buffer.alloc(2);
837
+ buf.writeUInt16LE(n);
838
+ return buf.toString('hex');
839
+ }
840
+
841
+ /**
842
+ * Convert uint32 to little-endian hex
843
+ */
844
+ private uint32ToLittleEndian(n: number): string {
845
+ const buf = Buffer.alloc(4);
846
+ buf.writeUInt32LE(n);
847
+ return buf.toString('hex');
848
+ }
849
+
850
+ /**
851
+ * Convert uint64 to little-endian hex
852
+ */
853
+ private uint64ToLittleEndian(n: number): string {
854
+ const buf = Buffer.alloc(8);
855
+ buf.writeBigUInt64LE(BigInt(n));
856
+ return buf.toString('hex');
857
+ }
858
+
859
+ /**
860
+ * Get additionals array based on address type
861
+ */
862
+ private getAdditionals(): string[] {
863
+ switch (this._options.addressType) {
864
+ case 'segwit':
865
+ return ['bech32'];
866
+ case 'taproot':
867
+ return ['bech32m'];
868
+ case 'nested-segwit':
869
+ return [];
870
+ case 'legacy':
871
+ default:
872
+ return [];
873
+ }
874
+ }
875
+
876
+ async getNetwork(): Promise<BitcoinNetwork> {
877
+ return this._network;
878
+ }
879
+
880
+ /**
881
+ * Get extended public key (xpub/ypub/zpub) for account
882
+ */
883
+ async getExtendedPublicKey(): Promise<string> {
884
+ if (!this._btcApp) {
885
+ throw new Error('Not connected to Ledger device');
886
+ }
887
+
888
+ try {
889
+ const coinType = this._network === 'mainnet' ? 0 : 1;
890
+ const purpose = ADDRESS_TYPE_TO_PATH[this._options.addressType];
891
+ const accountPath = `${purpose}'/${coinType}'/${this._options.accountIndex}'`;
892
+
893
+ // Get appropriate xpub version bytes based on address type and network
894
+ const xpubVersion = this.getXpubVersion();
895
+
896
+ const xpub = await this._btcApp.getWalletXpub({
897
+ path: accountPath,
898
+ xpubVersion,
899
+ });
900
+
901
+ return xpub;
902
+ } catch (error) {
903
+ this.handleLedgerError(error);
904
+ }
905
+ }
906
+
907
+ /**
908
+ * Get multiple addresses for the account
909
+ */
910
+ async getAddresses(startIndex: number, count: number): Promise<WalletAccount[]> {
911
+ if (!this._btcApp) {
912
+ throw new Error('Not connected to Ledger device');
913
+ }
914
+
915
+ const accounts: WalletAccount[] = [];
916
+ const coinType = this._network === 'mainnet' ? 0 : 1;
917
+ const purpose = ADDRESS_TYPE_TO_PATH[this._options.addressType];
918
+ const format = ADDRESS_TYPE_TO_FORMAT[this._options.addressType];
919
+
920
+ for (let i = startIndex; i < startIndex + count; i++) {
921
+ const path = `${purpose}'/${coinType}'/${this._options.accountIndex}'/0/${i}`;
922
+ const result = await this._btcApp.getWalletPublicKey(path, {
923
+ verify: false,
924
+ format,
925
+ });
926
+
927
+ accounts.push({
928
+ address: result.bitcoinAddress,
929
+ publicKey: result.publicKey,
930
+ type: this.mapLedgerAddressType(this._options.addressType),
931
+ });
932
+ }
933
+
934
+ return accounts;
935
+ }
936
+
937
+ /**
938
+ * Get current derivation path
939
+ */
940
+ getDerivationPath(): string {
941
+ return this._derivationPath;
942
+ }
943
+
944
+ /**
945
+ * Check if device is connected
946
+ */
947
+ isConnected(): boolean {
948
+ return this._transport !== null && this._btcApp !== null;
949
+ }
950
+
951
+ /**
952
+ * Get the Ledger Bitcoin app instance for advanced operations
953
+ * This allows users to call createPaymentTransaction directly
954
+ */
955
+ getBtcApp(): LedgerBtcApp | null {
956
+ return this._btcApp;
957
+ }
958
+
959
+ private mapLedgerAddressType(addressType: LedgerAddressType): AddressType {
960
+ switch (addressType) {
961
+ case 'legacy':
962
+ return 'legacy';
963
+ case 'nested-segwit':
964
+ return 'nested-segwit';
965
+ case 'segwit':
966
+ return 'segwit';
967
+ case 'taproot':
968
+ return 'taproot';
969
+ default:
970
+ return 'segwit';
971
+ }
972
+ }
973
+
974
+ private getXpubVersion(): number {
975
+ const isMainnet = this._network === 'mainnet';
976
+
977
+ switch (this._options.addressType) {
978
+ case 'legacy':
979
+ return isMainnet ? 0x0488b21e : 0x043587cf; // xpub / tpub
980
+ case 'nested-segwit':
981
+ return isMainnet ? 0x049d7cb2 : 0x044a5262; // ypub / upub
982
+ case 'segwit':
983
+ return isMainnet ? 0x04b24746 : 0x045f1cf6; // zpub / vpub
984
+ case 'taproot':
985
+ return isMainnet ? 0x0488b21e : 0x043587cf; // xpub / tpub (taproot uses standard)
986
+ default:
987
+ return isMainnet ? 0x04b24746 : 0x045f1cf6;
988
+ }
989
+ }
990
+
991
+ private handleLedgerError(error: unknown): never {
992
+ // Handle specific Ledger errors
993
+ if (error instanceof Error) {
994
+ const message = error.message.toLowerCase();
995
+
996
+ if (message.includes('locked') || message.includes('0x6982')) {
997
+ throw new Error('Ledger device is locked. Please unlock it and try again.');
998
+ }
999
+
1000
+ if (message.includes('denied') || message.includes('rejected') || message.includes('0x6985')) {
1001
+ throw new Error('User rejected the request on Ledger device.');
1002
+ }
1003
+
1004
+ if (message.includes('app') && message.includes('open')) {
1005
+ throw new Error('Please open the Bitcoin app on your Ledger device.');
1006
+ }
1007
+
1008
+ if (message.includes('transportopenusercancelled')) {
1009
+ throw new Error('Connection cancelled. Please try again.');
1010
+ }
1011
+
1012
+ if (message.includes('no device selected')) {
1013
+ throw new Error('No Ledger device selected. Please connect your device and try again.');
1014
+ }
1015
+ }
1016
+
1017
+ this.handleError(error);
1018
+ }
1019
+ }