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,824 @@
1
+ import type {
2
+ WalletAccount,
3
+ BitcoinNetwork,
4
+ SignPsbtOptions,
5
+ AddressType,
6
+ } from 'otx-btc-wallet-core';
7
+ import { BaseConnector } from '../base';
8
+ import { BtcService } from '../utils';
9
+ import {
10
+ getAddressType,
11
+ getInputVBytes,
12
+ getOutputVBytes,
13
+ getDustThreshold,
14
+ deriveAddressFromPublicKey,
15
+ } from '../utils';
16
+
17
+ // Trezor wallet icon
18
+ const TREZOR_ICON =
19
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF83MDM3XzExNSkiPgo8cGF0aCBkPSJNMjAwIDBIMFYyMDBIMjAwVjBaIiBmaWxsPSIjMDYyRDE2Ii8+CjxwYXRoIGQ9Ik0xMzMuMjYgNjMuMDUzNkMxMzMuMjYgNDUuNTM0IDExOC4wNjIgMzEgOTkuNTYyNyAzMUM4MS4wNjMxIDMxIDY1Ljg2NTYgNDUuNTQxMyA2NS44NjU2IDYzLjA1MzZWNzMuMjk5SDUyVjE0Ni45OTFMOTkuNTYyNyAxNjkuMTE3TDE0Ny4xMzYgMTQ2Ljk3NlY3My42MTI5SDEzMy4yNzFMMTMzLjI2IDYzLjA1MzZaTTgzLjA0NDQgNjMuMDUzNkM4My4wNDQ0IDU0Ljc5MzggOTAuMzEyOSA0OC4xODM4IDk5LjU2MjcgNDguMTgzOEMxMDguODEzIDQ4LjE4MzggMTE2LjA4MSA1NC43OTM4IDExNi4wODEgNjMuMDUzNlY3My4yOTlIODMuMDQ0NFY2My4wNTM2Wk0xMjcuOTczIDEzNS4wOTJMOTkuNTYyNyAxNDguMzEyTDcxLjE1MjggMTM1LjA5MlY5MC44MTEzSDEyNy45NzNWMTM1LjA5MloiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNzAzN18xMTUiPgo8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgcng9IjM1LjU1NTYiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==';
20
+
21
+ /**
22
+ * Trezor address type configuration
23
+ */
24
+ export type TrezorAddressType = 'legacy' | 'nested-segwit' | 'segwit' | 'taproot';
25
+
26
+ /**
27
+ * Trezor connector options
28
+ */
29
+ export interface TrezorConnectorOptions {
30
+ /** Address type for derivation (default: 'segwit') */
31
+ addressType?: TrezorAddressType;
32
+ /** Account index (default: 0) */
33
+ accountIndex?: number;
34
+ /** Address index (default: 0) */
35
+ addressIndex?: number;
36
+ /** Manifest email for Trezor Connect */
37
+ manifestEmail?: string;
38
+ /** Manifest app URL for Trezor Connect */
39
+ manifestAppUrl?: string;
40
+ }
41
+
42
+ /**
43
+ * Options for sendBitcoin method
44
+ */
45
+ export interface TrezorSendBitcoinOptions {
46
+ /** Fee rate in sat/vB (optional, defaults to "hour" priority) */
47
+ feeRate?: number;
48
+ }
49
+
50
+ /**
51
+ * Address type to BIP path mapping
52
+ */
53
+ const ADDRESS_TYPE_TO_PATH: Record<TrezorAddressType, number> = {
54
+ legacy: 44,
55
+ 'nested-segwit': 49,
56
+ segwit: 84,
57
+ taproot: 86,
58
+ };
59
+
60
+ /**
61
+ * Address type to Trezor script type mapping
62
+ */
63
+ const ADDRESS_TYPE_TO_SCRIPT_TYPE: Record<TrezorAddressType, string> = {
64
+ legacy: 'SPENDADDRESS',
65
+ 'nested-segwit': 'SPENDP2SHWITNESS',
66
+ segwit: 'SPENDWITNESS',
67
+ taproot: 'SPENDTAPROOT',
68
+ };
69
+
70
+ /**
71
+ * Address type to output script type mapping
72
+ */
73
+ const ADDRESS_TYPE_TO_OUTPUT_SCRIPT_TYPE: Record<TrezorAddressType, string> = {
74
+ legacy: 'PAYTOADDRESS',
75
+ 'nested-segwit': 'PAYTOP2SHWITNESS',
76
+ segwit: 'PAYTOWITNESS',
77
+ taproot: 'PAYTOTAPROOT',
78
+ };
79
+
80
+ // Common params for Trezor Connect methods
81
+ interface TrezorCommonParams {
82
+ keepSession?: boolean;
83
+ }
84
+
85
+ // Trezor Connect types (minimal interface to avoid importing full types)
86
+ interface TrezorConnect {
87
+ init(settings: {
88
+ manifest: { email: string; appUrl: string };
89
+ popup?: boolean;
90
+ lazyLoad?: boolean;
91
+ transports?: string[];
92
+ coreMode?: 'auto' | 'iframe' | 'popup';
93
+ debug?: boolean;
94
+ }): Promise<void>;
95
+ getAddress(params: TrezorCommonParams & {
96
+ path: string;
97
+ coin: string;
98
+ showOnTrezor?: boolean;
99
+ }): Promise<TrezorResponse<{ address: string; path: number[] }>>;
100
+ signMessage(params: TrezorCommonParams & {
101
+ path: string;
102
+ message: string;
103
+ coin: string;
104
+ }): Promise<TrezorResponse<{ signature: string; address: string }>>;
105
+ signTransaction(params: TrezorCommonParams & {
106
+ inputs: TrezorInput[];
107
+ outputs: TrezorOutput[];
108
+ coin: string;
109
+ push?: boolean;
110
+ refTxs?: RefTransaction[];
111
+ }): Promise<TrezorResponse<{ serializedTx: string; signatures: string[] }>>;
112
+ pushTransaction(params: TrezorCommonParams & {
113
+ tx: string;
114
+ coin: string;
115
+ }): Promise<TrezorResponse<{ txid: string }>>;
116
+ getPublicKey(params: TrezorCommonParams & {
117
+ path: string;
118
+ coin: string;
119
+ suppressBackupWarning?: boolean;
120
+ }): Promise<TrezorResponse<{ publicKey: string; chainCode: string; xpub: string }>>;
121
+ dispose(): void;
122
+ }
123
+
124
+ /**
125
+ * Reference transaction for Trezor (needed for testnet)
126
+ */
127
+ interface RefTransaction {
128
+ hash: string;
129
+ version: number;
130
+ inputs: Array<{
131
+ prev_hash: string;
132
+ prev_index: number;
133
+ sequence: number;
134
+ script_sig: string;
135
+ }>;
136
+ bin_outputs: Array<{
137
+ amount: number;
138
+ script_pubkey: string;
139
+ }>;
140
+ lock_time: number;
141
+ }
142
+
143
+ interface TrezorResponse<T> {
144
+ success: boolean;
145
+ payload: T | { error: string; code?: string };
146
+ }
147
+
148
+ interface TrezorInput {
149
+ address_n: number[];
150
+ prev_hash: string;
151
+ prev_index: number;
152
+ amount: string;
153
+ script_type?: string;
154
+ }
155
+
156
+ interface TrezorOutput {
157
+ address?: string;
158
+ address_n?: number[];
159
+ amount: string;
160
+ script_type?: string;
161
+ }
162
+
163
+ /**
164
+ * Trezor Hardware Wallet Connector
165
+ *
166
+ * @see https://docs.trezor.io/trezor-suite/packages/connect/index.html
167
+ * @see https://github.com/trezor/trezor-suite/tree/develop/packages/connect
168
+ */
169
+ export class TrezorConnector extends BaseConnector {
170
+ readonly id = 'trezor';
171
+ readonly name = 'Trezor';
172
+ readonly icon = TREZOR_ICON;
173
+
174
+ private _trezorConnect: TrezorConnect | null = null;
175
+ private _account: WalletAccount | null = null;
176
+ private _network: BitcoinNetwork = 'mainnet';
177
+ private _options: Required<TrezorConnectorOptions>;
178
+ private _derivationPath: string = '';
179
+ private _initialized: boolean = false;
180
+
181
+ constructor(options: TrezorConnectorOptions = {}) {
182
+ super();
183
+ this._options = {
184
+ addressType: options.addressType ?? 'segwit',
185
+ accountIndex: options.accountIndex ?? 0,
186
+ addressIndex: options.addressIndex ?? 0,
187
+ manifestEmail: options.manifestEmail ?? 'support@optimex.com',
188
+ manifestAppUrl:
189
+ options.manifestAppUrl ??
190
+ (typeof window !== 'undefined' ? window.location.origin : 'https://optimex.com'),
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Override checkReady - Trezor is always "ready" since it doesn't require browser extension
196
+ * The actual device connection happens when connect() is called
197
+ */
198
+ protected checkReady(): void {
199
+ this.ready = true;
200
+ }
201
+
202
+ /**
203
+ * Get the provider - Trezor doesn't use window injection
204
+ */
205
+ protected getProvider(): null {
206
+ return null;
207
+ }
208
+
209
+ /**
210
+ * Get current address type
211
+ */
212
+ getAddressType(): TrezorAddressType {
213
+ return this._options.addressType;
214
+ }
215
+
216
+ /**
217
+ * Set address type options before connecting
218
+ * Call this before connect() to change the address derivation path
219
+ */
220
+ setOptions(options: Omit<TrezorConnectorOptions, 'manifestEmail' | 'manifestAppUrl'>): void {
221
+ this._options = {
222
+ ...this._options,
223
+ addressType: options.addressType ?? this._options.addressType,
224
+ accountIndex: options.accountIndex ?? this._options.accountIndex,
225
+ addressIndex: options.addressIndex ?? this._options.addressIndex,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Get available address types for UI selection
231
+ */
232
+ static getAvailableAddressTypes(): Array<{ type: TrezorAddressType; label: string; description: string }> {
233
+ return [
234
+ { type: 'legacy', label: 'Legacy (P2PKH)', description: "Starts with '1' or 'm/n'" },
235
+ { type: 'nested-segwit', label: 'Nested SegWit (P2SH-P2WPKH)', description: "Starts with '3' or '2'" },
236
+ { type: 'segwit', label: 'Native SegWit (P2WPKH)', description: "Starts with 'bc1q' or 'tb1q'" },
237
+ { type: 'taproot', label: 'Taproot (P2TR)', description: "Starts with 'bc1p' or 'tb1p'" },
238
+ ];
239
+ }
240
+
241
+ /**
242
+ * Initialize Trezor Connect
243
+ */
244
+ private async initTrezorConnect(): Promise<TrezorConnect> {
245
+ if (this._trezorConnect && this._initialized) {
246
+ return this._trezorConnect;
247
+ }
248
+
249
+ try {
250
+ const TrezorConnectModule = await import('@trezor/connect-web');
251
+ const TrezorConnect = TrezorConnectModule.default;
252
+
253
+ // Initialize with proper v9 settings
254
+ // coreMode: 'popup' forces popup mode which works without Trezor Suite
255
+ // transports: specify which transports to use
256
+ await TrezorConnect.init({
257
+ manifest: {
258
+ email: this._options.manifestEmail,
259
+ appUrl: this._options.manifestAppUrl,
260
+ },
261
+ popup: true,
262
+ lazyLoad: false, // Don't lazy load - init immediately
263
+ coreMode: 'popup', // Force popup mode for better compatibility
264
+ transports: ['BridgeTransport', 'WebUsbTransport'], // Try both Bridge and WebUSB
265
+ debug: false,
266
+ });
267
+
268
+ this._trezorConnect = TrezorConnect as unknown as TrezorConnect;
269
+ this._initialized = true;
270
+
271
+ return this._trezorConnect;
272
+ } catch (error) {
273
+ console.error('Trezor Connect init error:', error);
274
+ throw new Error(
275
+ 'Failed to initialize Trezor Connect. Make sure @trezor/connect-web is installed and Trezor Bridge or Trezor Suite is running.'
276
+ );
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Build BIP32 derivation path
282
+ */
283
+ private buildDerivationPath(
284
+ addressType: TrezorAddressType,
285
+ network: BitcoinNetwork,
286
+ accountIndex: number,
287
+ addressIndex: number,
288
+ isChange: boolean = false
289
+ ): string {
290
+ const coinType = network === 'mainnet' ? 0 : 1;
291
+ const purpose = ADDRESS_TYPE_TO_PATH[addressType];
292
+ const change = isChange ? 1 : 0;
293
+ return `m/${purpose}'/${coinType}'/${accountIndex}'/${change}/${addressIndex}`;
294
+ }
295
+
296
+ /**
297
+ * Parse derivation path string to array of numbers
298
+ */
299
+ private parseDerivationPath(path: string): number[] {
300
+ return path
301
+ .split('/')
302
+ .slice(1) // Remove 'm'
303
+ .map((segment) => {
304
+ const isHardened = segment.endsWith("'");
305
+ const num = parseInt(segment, 10);
306
+ return isHardened ? num + 0x80000000 : num;
307
+ });
308
+ }
309
+
310
+ /**
311
+ * Connect to Trezor device
312
+ * IMPORTANT: This must be called within a user gesture (click event)
313
+ *
314
+ * This method only calls getPublicKey() once and derives the address locally
315
+ * to avoid opening the Trezor popup twice.
316
+ */
317
+ async connect(network: BitcoinNetwork = 'mainnet'): Promise<WalletAccount> {
318
+ try {
319
+ const trezor = await this.initTrezorConnect();
320
+
321
+ this._network = network;
322
+ const coin = network === 'mainnet' ? 'btc' : 'test';
323
+
324
+ // Build derivation path
325
+ this._derivationPath = this.buildDerivationPath(
326
+ this._options.addressType,
327
+ network,
328
+ this._options.accountIndex,
329
+ this._options.addressIndex
330
+ );
331
+
332
+ // Only call getPublicKey() - address will be derived locally from the public key
333
+ // This avoids opening the Trezor popup twice
334
+ const pubKeyResult = await trezor.getPublicKey({
335
+ path: this._derivationPath,
336
+ coin,
337
+ suppressBackupWarning: true,
338
+ });
339
+
340
+ if (!pubKeyResult.success) {
341
+ const error = pubKeyResult.payload as { error: string };
342
+ throw new Error(error.error);
343
+ }
344
+
345
+ const pubKeyPayload = pubKeyResult.payload as { publicKey: string };
346
+
347
+ // Derive address from public key locally
348
+ const address = deriveAddressFromPublicKey(
349
+ pubKeyPayload.publicKey,
350
+ this.mapTrezorAddressType(this._options.addressType),
351
+ network
352
+ );
353
+
354
+ this._account = {
355
+ address,
356
+ publicKey: pubKeyPayload.publicKey,
357
+ type: this.mapTrezorAddressType(this._options.addressType),
358
+ };
359
+
360
+ return this._account;
361
+ } catch (error) {
362
+ this.handleTrezorError(error);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Get address with verification on device
368
+ *
369
+ * Note: This method calls getAddress() with showOnTrezor=true to display
370
+ * the address on the Trezor device for user verification. The public key
371
+ * is already available from the initial connect() call.
372
+ */
373
+ async getAddressWithVerification(): Promise<WalletAccount> {
374
+ if (!this._account) {
375
+ throw new Error('Not connected to Trezor device. Call connect() first.');
376
+ }
377
+
378
+ try {
379
+ const trezor = await this.initTrezorConnect();
380
+ const coin = this._network === 'mainnet' ? 'btc' : 'test';
381
+
382
+ // Show address on Trezor for verification
383
+ const addressResult = await trezor.getAddress({
384
+ path: this._derivationPath,
385
+ coin,
386
+ showOnTrezor: true,
387
+ });
388
+
389
+ if (!addressResult.success) {
390
+ const error = addressResult.payload as { error: string };
391
+ throw new Error(error.error);
392
+ }
393
+
394
+ // Return the existing account (publicKey already available from connect())
395
+ return this._account;
396
+ } catch (error) {
397
+ this.handleTrezorError(error);
398
+ }
399
+ }
400
+
401
+ async disconnect(): Promise<void> {
402
+ if (this._trezorConnect) {
403
+ try {
404
+ this._trezorConnect.dispose();
405
+ } catch {
406
+ // Ignore dispose errors
407
+ }
408
+ this._trezorConnect = null;
409
+ this._initialized = false;
410
+ }
411
+ this._account = null;
412
+ this.cleanup();
413
+ }
414
+
415
+ async getAccounts(): Promise<WalletAccount[]> {
416
+ if (!this._account) {
417
+ return [];
418
+ }
419
+ return [this._account];
420
+ }
421
+
422
+ async signMessage(message: string): Promise<string> {
423
+ try {
424
+ const trezor = await this.initTrezorConnect();
425
+
426
+ const result = await trezor.signMessage({
427
+ path: this._derivationPath,
428
+ message,
429
+ coin: this._network === 'mainnet' ? 'btc' : 'test',
430
+ });
431
+
432
+ if (!result.success) {
433
+ const error = result.payload as { error: string };
434
+ throw new Error(error.error);
435
+ }
436
+
437
+ const payload = result.payload as { signature: string };
438
+ return payload.signature;
439
+ } catch (error) {
440
+ this.handleTrezorError(error);
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Sign a PSBT with Trezor
446
+ *
447
+ * Note: Trezor Connect doesn't directly support PSBT format.
448
+ * Use signTransaction method or sendBitcoin instead.
449
+ */
450
+ async signPsbt(_psbtHex: string, _options?: SignPsbtOptions): Promise<string> {
451
+ void _psbtHex;
452
+ void _options;
453
+
454
+ throw new Error(
455
+ 'Direct PSBT signing is not supported by Trezor Connect. ' +
456
+ 'Please use sendBitcoin() method or manually parse the PSBT and use signTransaction().'
457
+ );
458
+ }
459
+
460
+ /**
461
+ * Sign multiple PSBTs
462
+ */
463
+ async signPsbts(psbtHexs: string[], options?: SignPsbtOptions): Promise<string[]> {
464
+ const results: string[] = [];
465
+ for (const psbtHex of psbtHexs) {
466
+ const signed = await this.signPsbt(psbtHex, options);
467
+ results.push(signed);
468
+ }
469
+ return results;
470
+ }
471
+
472
+ /**
473
+ * Send a Bitcoin transaction
474
+ *
475
+ * @param to - Recipient address
476
+ * @param satoshis - Amount to send in satoshis
477
+ * @param options - Send options (feeRate, etc.)
478
+ * @returns Transaction ID after broadcast
479
+ *
480
+ * @example
481
+ * ```typescript
482
+ * const txid = await trezor.sendTransaction('bc1q...', 50000);
483
+ * // With custom fee rate
484
+ * const txid = await trezor.sendTransaction('bc1q...', 50000, { feeRate: 10 });
485
+ * ```
486
+ */
487
+ async sendTransaction(
488
+ to: string,
489
+ satoshis: number,
490
+ options?: TrezorSendBitcoinOptions
491
+ ): Promise<string> {
492
+ if (!this._account) {
493
+ throw new Error('Not connected to Trezor device');
494
+ }
495
+
496
+ const trezor = await this.initTrezorConnect();
497
+ const btcService = new BtcService(this._network);
498
+
499
+ // 1. Get UTXOs with tx hex (needed for testnet refTxs)
500
+ const utxos = await btcService.getUtxosWithTxHex(this._account.address);
501
+ if (utxos.length === 0) {
502
+ throw new Error('No UTXOs available for spending');
503
+ }
504
+
505
+ // 2. Get fee rate if not provided
506
+ let feeRate = options?.feeRate;
507
+ if (!feeRate) {
508
+ const feeRates = await btcService.getFeeRates();
509
+ feeRate = feeRates.hour;
510
+ }
511
+
512
+ // 3. Get address types using utility functions
513
+ const fromAddressType = getAddressType(this._account.address);
514
+ const toAddressType = getAddressType(to);
515
+
516
+ // 4. Get vBytes estimates using utility functions
517
+ const inputVBytes = getInputVBytes(fromAddressType);
518
+ const outputVBytes = getOutputVBytes(toAddressType);
519
+ const changeOutputVBytes = getOutputVBytes(fromAddressType);
520
+ const baseVBytes = 10.5; // Base transaction overhead
521
+
522
+ // 5. Select UTXOs using greedy algorithm (sorted by value descending)
523
+ const sortedUtxos = [...utxos].sort((a, b) => b.value - a.value);
524
+ const selectedUtxos: typeof utxos = [];
525
+ let totalInputValue = 0;
526
+
527
+ for (const utxo of sortedUtxos) {
528
+ selectedUtxos.push(utxo);
529
+ totalInputValue += utxo.value;
530
+
531
+ // Calculate fee with 2 outputs (recipient + change)
532
+ const estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes + changeOutputVBytes;
533
+ const estimatedFee = Math.ceil(estimatedVBytes * feeRate);
534
+
535
+ if (totalInputValue >= satoshis + estimatedFee) {
536
+ break;
537
+ }
538
+ }
539
+
540
+ // 6. Calculate final fee
541
+ let estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes + changeOutputVBytes;
542
+ let estimatedFee = Math.ceil(estimatedVBytes * feeRate);
543
+
544
+ // Check if we have enough funds
545
+ if (totalInputValue < satoshis + estimatedFee) {
546
+ throw new Error(
547
+ `Insufficient funds. Available: ${totalInputValue} sats, Required: ${satoshis + estimatedFee} sats (including fee)`
548
+ );
549
+ }
550
+
551
+ // 7. Calculate change
552
+ let changeAmount = totalInputValue - satoshis - estimatedFee;
553
+
554
+ // 8. Build Trezor inputs
555
+ const trezorInputs: TrezorInput[] = selectedUtxos.map((utxo) => ({
556
+ address_n: this.parseDerivationPath(this._derivationPath),
557
+ prev_hash: utxo.txid,
558
+ prev_index: utxo.vout,
559
+ amount: utxo.value.toString(),
560
+ script_type: ADDRESS_TYPE_TO_SCRIPT_TYPE[this._options.addressType],
561
+ }));
562
+
563
+ // 9. Build Trezor outputs
564
+ const trezorOutputs: TrezorOutput[] = [
565
+ {
566
+ address: to,
567
+ amount: satoshis.toString(),
568
+ script_type: 'PAYTOADDRESS',
569
+ },
570
+ ];
571
+
572
+ // Add change output if above dust threshold
573
+ const dustThreshold = getDustThreshold(fromAddressType);
574
+
575
+ if (changeAmount > dustThreshold) {
576
+ // Build change path
577
+ const changePath = this.buildDerivationPath(
578
+ this._options.addressType,
579
+ this._network,
580
+ this._options.accountIndex,
581
+ 0,
582
+ true // isChange = true
583
+ );
584
+
585
+ trezorOutputs.push({
586
+ address_n: this.parseDerivationPath(changePath),
587
+ amount: changeAmount.toString(),
588
+ script_type: ADDRESS_TYPE_TO_OUTPUT_SCRIPT_TYPE[this._options.addressType],
589
+ });
590
+ } else {
591
+ // No change output - recalculate fee without change output
592
+ estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes;
593
+ estimatedFee = Math.ceil(estimatedVBytes * feeRate);
594
+ changeAmount = 0;
595
+
596
+ // Verify we still have enough
597
+ if (totalInputValue < satoshis + estimatedFee) {
598
+ throw new Error(
599
+ `Insufficient funds after fee adjustment. Available: ${totalInputValue} sats, Required: ${satoshis + estimatedFee} sats`
600
+ );
601
+ }
602
+ }
603
+
604
+ // 10. Sign and broadcast transaction
605
+ const coin = this._network === 'mainnet' ? 'Bitcoin' : 'Testnet';
606
+
607
+ if (this._network === 'mainnet') {
608
+ // Mainnet: use Trezor's default blockbook backend
609
+ const result = await trezor.signTransaction({
610
+ inputs: trezorInputs,
611
+ outputs: trezorOutputs,
612
+ coin,
613
+ push: false,
614
+ });
615
+
616
+ if (!result.success) {
617
+ const error = result.payload as { error: string };
618
+ throw new Error(error.error);
619
+ }
620
+
621
+ const payload = result.payload as { serializedTx: string };
622
+
623
+ // Broadcast via our service
624
+ const txid = await btcService.broadcastTransaction(payload.serializedTx);
625
+ return txid;
626
+ } else {
627
+ // Testnet: build refTxs manually because Trezor's default blockbook API
628
+ // may not work properly with testnet
629
+ const refTxs: RefTransaction[] = [];
630
+
631
+ for (const utxo of selectedUtxos) {
632
+ try {
633
+ // Fetch full transaction data using BtcService
634
+ const rawTx = await btcService.getFullTransaction(utxo.txid);
635
+
636
+ // Build reference transaction
637
+ const inputRef = rawTx.vin.map((vin) => ({
638
+ prev_hash: vin.txid,
639
+ prev_index: vin.vout,
640
+ sequence: vin.sequence,
641
+ script_sig: vin.scriptsig || '',
642
+ }));
643
+
644
+ const binOutputs = rawTx.vout.map((vout) => ({
645
+ amount: vout.value,
646
+ script_pubkey: vout.scriptpubkey,
647
+ }));
648
+
649
+ refTxs.push({
650
+ hash: utxo.txid,
651
+ version: rawTx.version,
652
+ inputs: inputRef,
653
+ bin_outputs: binOutputs,
654
+ lock_time: rawTx.locktime,
655
+ });
656
+ } catch (error) {
657
+ console.error(`Failed to fetch ref tx for ${utxo.txid}:`, error);
658
+ throw new Error(`Failed to fetch reference transaction: ${utxo.txid}`);
659
+ }
660
+ }
661
+
662
+ // Sign with refTxs
663
+ const result = await trezor.signTransaction({
664
+ inputs: trezorInputs,
665
+ outputs: trezorOutputs,
666
+ coin,
667
+ refTxs,
668
+ });
669
+
670
+ if (!result.success) {
671
+ const error = result.payload as { error: string };
672
+ throw new Error(error.error);
673
+ }
674
+
675
+ const payload = result.payload as { serializedTx: string };
676
+
677
+ // Broadcast via our service
678
+ const txid = await btcService.broadcastTransaction(payload.serializedTx);
679
+ return txid;
680
+ }
681
+ }
682
+
683
+ async getNetwork(): Promise<BitcoinNetwork> {
684
+ return this._network;
685
+ }
686
+
687
+ /**
688
+ * Get extended public key (xpub/ypub/zpub) for account
689
+ */
690
+ async getExtendedPublicKey(): Promise<string> {
691
+ try {
692
+ const trezor = await this.initTrezorConnect();
693
+
694
+ const coinType = this._network === 'mainnet' ? 0 : 1;
695
+ const purpose = ADDRESS_TYPE_TO_PATH[this._options.addressType];
696
+ const accountPath = `m/${purpose}'/${coinType}'/${this._options.accountIndex}'`;
697
+
698
+ const result = await trezor.getPublicKey({
699
+ path: accountPath,
700
+ coin: this._network === 'mainnet' ? 'btc' : 'test',
701
+ });
702
+
703
+ if (!result.success) {
704
+ const error = result.payload as { error: string };
705
+ throw new Error(error.error);
706
+ }
707
+
708
+ const payload = result.payload as { xpub: string };
709
+ return payload.xpub;
710
+ } catch (error) {
711
+ this.handleTrezorError(error);
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Get multiple addresses for the account
717
+ *
718
+ * This method only calls getPublicKey() for each address and derives
719
+ * addresses locally to minimize Trezor popup interactions.
720
+ */
721
+ async getAddresses(startIndex: number, count: number): Promise<WalletAccount[]> {
722
+ const trezor = await this.initTrezorConnect();
723
+ const accounts: WalletAccount[] = [];
724
+ const coin = this._network === 'mainnet' ? 'btc' : 'test';
725
+ const addressType = this.mapTrezorAddressType(this._options.addressType);
726
+
727
+ for (let i = startIndex; i < startIndex + count; i++) {
728
+ const path = this.buildDerivationPath(
729
+ this._options.addressType,
730
+ this._network,
731
+ this._options.accountIndex,
732
+ i
733
+ );
734
+
735
+ const isLast = i === startIndex + count - 1;
736
+
737
+ // Only get public key - address will be derived locally
738
+ const pubKeyResult = await trezor.getPublicKey({
739
+ path,
740
+ coin,
741
+ suppressBackupWarning: true,
742
+ keepSession: !isLast, // Close session on last iteration
743
+ });
744
+
745
+ if (!pubKeyResult.success) {
746
+ continue;
747
+ }
748
+
749
+ const pubKeyPayload = pubKeyResult.payload as { publicKey: string };
750
+
751
+ // Derive address from public key locally
752
+ const address = deriveAddressFromPublicKey(
753
+ pubKeyPayload.publicKey,
754
+ addressType,
755
+ this._network
756
+ );
757
+
758
+ accounts.push({
759
+ address,
760
+ publicKey: pubKeyPayload.publicKey,
761
+ type: addressType,
762
+ });
763
+ }
764
+
765
+ return accounts;
766
+ }
767
+
768
+ /**
769
+ * Get current derivation path
770
+ */
771
+ getDerivationPath(): string {
772
+ return this._derivationPath;
773
+ }
774
+
775
+ /**
776
+ * Check if device is connected
777
+ */
778
+ isConnected(): boolean {
779
+ return this._account !== null && this._initialized;
780
+ }
781
+
782
+ private mapTrezorAddressType(addressType: TrezorAddressType): AddressType {
783
+ switch (addressType) {
784
+ case 'legacy':
785
+ return 'legacy';
786
+ case 'nested-segwit':
787
+ return 'nested-segwit';
788
+ case 'segwit':
789
+ return 'segwit';
790
+ case 'taproot':
791
+ return 'taproot';
792
+ default:
793
+ return 'segwit';
794
+ }
795
+ }
796
+
797
+ private handleTrezorError(error: unknown): never {
798
+ if (error instanceof Error) {
799
+ const message = error.message.toLowerCase();
800
+
801
+ if (message.includes('cancelled') || message.includes('canceled')) {
802
+ throw new Error('User cancelled the operation on Trezor device.');
803
+ }
804
+
805
+ if (message.includes('device disconnected')) {
806
+ throw new Error('Trezor device disconnected. Please reconnect and try again.');
807
+ }
808
+
809
+ if (message.includes('permissions')) {
810
+ throw new Error('Permission denied. Please allow access to your Trezor device.');
811
+ }
812
+
813
+ if (message.includes('popup')) {
814
+ throw new Error('Trezor popup was closed. Please try again.');
815
+ }
816
+
817
+ if (message.includes('device not found')) {
818
+ throw new Error('No Trezor device found. Please connect your device and try again.');
819
+ }
820
+ }
821
+
822
+ this.handleError(error);
823
+ }
824
+ }