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,539 @@
1
+ import { deriveAddressFromPublicKey, BtcService, getAddressType, getInputVBytes, getOutputVBytes, getDustThreshold } from './chunk-AW2JZIHR.mjs';
2
+ import { BaseConnector } from './chunk-5Z5Q2Y75.mjs';
3
+
4
+ // src/trezor/TrezorConnector.ts
5
+ var TREZOR_ICON = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF83MDM3XzExNSkiPgo8cGF0aCBkPSJNMjAwIDBIMFYyMDBIMjAwVjBaIiBmaWxsPSIjMDYyRDE2Ii8+CjxwYXRoIGQ9Ik0xMzMuMjYgNjMuMDUzNkMxMzMuMjYgNDUuNTM0IDExOC4wNjIgMzEgOTkuNTYyNyAzMUM4MS4wNjMxIDMxIDY1Ljg2NTYgNDUuNTQxMyA2NS44NjU2IDYzLjA1MzZWNzMuMjk5SDUyVjE0Ni45OTFMOTkuNTYyNyAxNjkuMTE3TDE0Ny4xMzYgMTQ2Ljk3NlY3My42MTI5SDEzMy4yNzFMMTMzLjI2IDYzLjA1MzZaTTgzLjA0NDQgNjMuMDUzNkM4My4wNDQ0IDU0Ljc5MzggOTAuMzEyOSA0OC4xODM4IDk5LjU2MjcgNDguMTgzOEMxMDguODEzIDQ4LjE4MzggMTE2LjA4MSA1NC43OTM4IDExNi4wODEgNjMuMDUzNlY3My4yOTlIODMuMDQ0NFY2My4wNTM2Wk0xMjcuOTczIDEzNS4wOTJMOTkuNTYyNyAxNDguMzEyTDcxLjE1MjggMTM1LjA5MlY5MC44MTEzSDEyNy45NzNWMTM1LjA5MloiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNzAzN18xMTUiPgo8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgcng9IjM1LjU1NTYiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==";
6
+ var ADDRESS_TYPE_TO_PATH = {
7
+ legacy: 44,
8
+ "nested-segwit": 49,
9
+ segwit: 84,
10
+ taproot: 86
11
+ };
12
+ var ADDRESS_TYPE_TO_SCRIPT_TYPE = {
13
+ legacy: "SPENDADDRESS",
14
+ "nested-segwit": "SPENDP2SHWITNESS",
15
+ segwit: "SPENDWITNESS",
16
+ taproot: "SPENDTAPROOT"
17
+ };
18
+ var ADDRESS_TYPE_TO_OUTPUT_SCRIPT_TYPE = {
19
+ legacy: "PAYTOADDRESS",
20
+ "nested-segwit": "PAYTOP2SHWITNESS",
21
+ segwit: "PAYTOWITNESS",
22
+ taproot: "PAYTOTAPROOT"
23
+ };
24
+ var TrezorConnector = class extends BaseConnector {
25
+ constructor(options = {}) {
26
+ super();
27
+ this.id = "trezor";
28
+ this.name = "Trezor";
29
+ this.icon = TREZOR_ICON;
30
+ this._trezorConnect = null;
31
+ this._account = null;
32
+ this._network = "mainnet";
33
+ this._derivationPath = "";
34
+ this._initialized = false;
35
+ this._options = {
36
+ addressType: options.addressType ?? "segwit",
37
+ accountIndex: options.accountIndex ?? 0,
38
+ addressIndex: options.addressIndex ?? 0,
39
+ manifestEmail: options.manifestEmail ?? "support@optimex.com",
40
+ manifestAppUrl: options.manifestAppUrl ?? (typeof window !== "undefined" ? window.location.origin : "https://optimex.com")
41
+ };
42
+ }
43
+ /**
44
+ * Override checkReady - Trezor is always "ready" since it doesn't require browser extension
45
+ * The actual device connection happens when connect() is called
46
+ */
47
+ checkReady() {
48
+ this.ready = true;
49
+ }
50
+ /**
51
+ * Get the provider - Trezor doesn't use window injection
52
+ */
53
+ getProvider() {
54
+ return null;
55
+ }
56
+ /**
57
+ * Get current address type
58
+ */
59
+ getAddressType() {
60
+ return this._options.addressType;
61
+ }
62
+ /**
63
+ * Set address type options before connecting
64
+ * Call this before connect() to change the address derivation path
65
+ */
66
+ setOptions(options) {
67
+ this._options = {
68
+ ...this._options,
69
+ addressType: options.addressType ?? this._options.addressType,
70
+ accountIndex: options.accountIndex ?? this._options.accountIndex,
71
+ addressIndex: options.addressIndex ?? this._options.addressIndex
72
+ };
73
+ }
74
+ /**
75
+ * Get available address types for UI selection
76
+ */
77
+ static getAvailableAddressTypes() {
78
+ return [
79
+ { type: "legacy", label: "Legacy (P2PKH)", description: "Starts with '1' or 'm/n'" },
80
+ { type: "nested-segwit", label: "Nested SegWit (P2SH-P2WPKH)", description: "Starts with '3' or '2'" },
81
+ { type: "segwit", label: "Native SegWit (P2WPKH)", description: "Starts with 'bc1q' or 'tb1q'" },
82
+ { type: "taproot", label: "Taproot (P2TR)", description: "Starts with 'bc1p' or 'tb1p'" }
83
+ ];
84
+ }
85
+ /**
86
+ * Initialize Trezor Connect
87
+ */
88
+ async initTrezorConnect() {
89
+ if (this._trezorConnect && this._initialized) {
90
+ return this._trezorConnect;
91
+ }
92
+ try {
93
+ const TrezorConnectModule = await import('@trezor/connect-web');
94
+ const TrezorConnect = TrezorConnectModule.default;
95
+ await TrezorConnect.init({
96
+ manifest: {
97
+ email: this._options.manifestEmail,
98
+ appUrl: this._options.manifestAppUrl
99
+ },
100
+ popup: true,
101
+ lazyLoad: false,
102
+ // Don't lazy load - init immediately
103
+ coreMode: "popup",
104
+ // Force popup mode for better compatibility
105
+ transports: ["BridgeTransport", "WebUsbTransport"],
106
+ // Try both Bridge and WebUSB
107
+ debug: false
108
+ });
109
+ this._trezorConnect = TrezorConnect;
110
+ this._initialized = true;
111
+ return this._trezorConnect;
112
+ } catch (error) {
113
+ console.error("Trezor Connect init error:", error);
114
+ throw new Error(
115
+ "Failed to initialize Trezor Connect. Make sure @trezor/connect-web is installed and Trezor Bridge or Trezor Suite is running."
116
+ );
117
+ }
118
+ }
119
+ /**
120
+ * Build BIP32 derivation path
121
+ */
122
+ buildDerivationPath(addressType, network, accountIndex, addressIndex, isChange = false) {
123
+ const coinType = network === "mainnet" ? 0 : 1;
124
+ const purpose = ADDRESS_TYPE_TO_PATH[addressType];
125
+ const change = isChange ? 1 : 0;
126
+ return `m/${purpose}'/${coinType}'/${accountIndex}'/${change}/${addressIndex}`;
127
+ }
128
+ /**
129
+ * Parse derivation path string to array of numbers
130
+ */
131
+ parseDerivationPath(path) {
132
+ return path.split("/").slice(1).map((segment) => {
133
+ const isHardened = segment.endsWith("'");
134
+ const num = parseInt(segment, 10);
135
+ return isHardened ? num + 2147483648 : num;
136
+ });
137
+ }
138
+ /**
139
+ * Connect to Trezor device
140
+ * IMPORTANT: This must be called within a user gesture (click event)
141
+ *
142
+ * This method only calls getPublicKey() once and derives the address locally
143
+ * to avoid opening the Trezor popup twice.
144
+ */
145
+ async connect(network = "mainnet") {
146
+ try {
147
+ const trezor = await this.initTrezorConnect();
148
+ this._network = network;
149
+ const coin = network === "mainnet" ? "btc" : "test";
150
+ this._derivationPath = this.buildDerivationPath(
151
+ this._options.addressType,
152
+ network,
153
+ this._options.accountIndex,
154
+ this._options.addressIndex
155
+ );
156
+ const pubKeyResult = await trezor.getPublicKey({
157
+ path: this._derivationPath,
158
+ coin,
159
+ suppressBackupWarning: true
160
+ });
161
+ if (!pubKeyResult.success) {
162
+ const error = pubKeyResult.payload;
163
+ throw new Error(error.error);
164
+ }
165
+ const pubKeyPayload = pubKeyResult.payload;
166
+ const address = deriveAddressFromPublicKey(
167
+ pubKeyPayload.publicKey,
168
+ this.mapTrezorAddressType(this._options.addressType),
169
+ network
170
+ );
171
+ this._account = {
172
+ address,
173
+ publicKey: pubKeyPayload.publicKey,
174
+ type: this.mapTrezorAddressType(this._options.addressType)
175
+ };
176
+ return this._account;
177
+ } catch (error) {
178
+ this.handleTrezorError(error);
179
+ }
180
+ }
181
+ /**
182
+ * Get address with verification on device
183
+ *
184
+ * Note: This method calls getAddress() with showOnTrezor=true to display
185
+ * the address on the Trezor device for user verification. The public key
186
+ * is already available from the initial connect() call.
187
+ */
188
+ async getAddressWithVerification() {
189
+ if (!this._account) {
190
+ throw new Error("Not connected to Trezor device. Call connect() first.");
191
+ }
192
+ try {
193
+ const trezor = await this.initTrezorConnect();
194
+ const coin = this._network === "mainnet" ? "btc" : "test";
195
+ const addressResult = await trezor.getAddress({
196
+ path: this._derivationPath,
197
+ coin,
198
+ showOnTrezor: true
199
+ });
200
+ if (!addressResult.success) {
201
+ const error = addressResult.payload;
202
+ throw new Error(error.error);
203
+ }
204
+ return this._account;
205
+ } catch (error) {
206
+ this.handleTrezorError(error);
207
+ }
208
+ }
209
+ async disconnect() {
210
+ if (this._trezorConnect) {
211
+ try {
212
+ this._trezorConnect.dispose();
213
+ } catch {
214
+ }
215
+ this._trezorConnect = null;
216
+ this._initialized = false;
217
+ }
218
+ this._account = null;
219
+ this.cleanup();
220
+ }
221
+ async getAccounts() {
222
+ if (!this._account) {
223
+ return [];
224
+ }
225
+ return [this._account];
226
+ }
227
+ async signMessage(message) {
228
+ try {
229
+ const trezor = await this.initTrezorConnect();
230
+ const result = await trezor.signMessage({
231
+ path: this._derivationPath,
232
+ message,
233
+ coin: this._network === "mainnet" ? "btc" : "test"
234
+ });
235
+ if (!result.success) {
236
+ const error = result.payload;
237
+ throw new Error(error.error);
238
+ }
239
+ const payload = result.payload;
240
+ return payload.signature;
241
+ } catch (error) {
242
+ this.handleTrezorError(error);
243
+ }
244
+ }
245
+ /**
246
+ * Sign a PSBT with Trezor
247
+ *
248
+ * Note: Trezor Connect doesn't directly support PSBT format.
249
+ * Use signTransaction method or sendBitcoin instead.
250
+ */
251
+ async signPsbt(_psbtHex, _options) {
252
+ throw new Error(
253
+ "Direct PSBT signing is not supported by Trezor Connect. Please use sendBitcoin() method or manually parse the PSBT and use signTransaction()."
254
+ );
255
+ }
256
+ /**
257
+ * Sign multiple PSBTs
258
+ */
259
+ async signPsbts(psbtHexs, options) {
260
+ const results = [];
261
+ for (const psbtHex of psbtHexs) {
262
+ const signed = await this.signPsbt(psbtHex, options);
263
+ results.push(signed);
264
+ }
265
+ return results;
266
+ }
267
+ /**
268
+ * Send a Bitcoin transaction
269
+ *
270
+ * @param to - Recipient address
271
+ * @param satoshis - Amount to send in satoshis
272
+ * @param options - Send options (feeRate, etc.)
273
+ * @returns Transaction ID after broadcast
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * const txid = await trezor.sendTransaction('bc1q...', 50000);
278
+ * // With custom fee rate
279
+ * const txid = await trezor.sendTransaction('bc1q...', 50000, { feeRate: 10 });
280
+ * ```
281
+ */
282
+ async sendTransaction(to, satoshis, options) {
283
+ if (!this._account) {
284
+ throw new Error("Not connected to Trezor device");
285
+ }
286
+ const trezor = await this.initTrezorConnect();
287
+ const btcService = new BtcService(this._network);
288
+ const utxos = await btcService.getUtxosWithTxHex(this._account.address);
289
+ if (utxos.length === 0) {
290
+ throw new Error("No UTXOs available for spending");
291
+ }
292
+ let feeRate = options?.feeRate;
293
+ if (!feeRate) {
294
+ const feeRates = await btcService.getFeeRates();
295
+ feeRate = feeRates.hour;
296
+ }
297
+ const fromAddressType = getAddressType(this._account.address);
298
+ const toAddressType = getAddressType(to);
299
+ const inputVBytes = getInputVBytes(fromAddressType);
300
+ const outputVBytes = getOutputVBytes(toAddressType);
301
+ const changeOutputVBytes = getOutputVBytes(fromAddressType);
302
+ const baseVBytes = 10.5;
303
+ const sortedUtxos = [...utxos].sort((a, b) => b.value - a.value);
304
+ const selectedUtxos = [];
305
+ let totalInputValue = 0;
306
+ for (const utxo of sortedUtxos) {
307
+ selectedUtxos.push(utxo);
308
+ totalInputValue += utxo.value;
309
+ const estimatedVBytes2 = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes + changeOutputVBytes;
310
+ const estimatedFee2 = Math.ceil(estimatedVBytes2 * feeRate);
311
+ if (totalInputValue >= satoshis + estimatedFee2) {
312
+ break;
313
+ }
314
+ }
315
+ let estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes + changeOutputVBytes;
316
+ let estimatedFee = Math.ceil(estimatedVBytes * feeRate);
317
+ if (totalInputValue < satoshis + estimatedFee) {
318
+ throw new Error(
319
+ `Insufficient funds. Available: ${totalInputValue} sats, Required: ${satoshis + estimatedFee} sats (including fee)`
320
+ );
321
+ }
322
+ let changeAmount = totalInputValue - satoshis - estimatedFee;
323
+ const trezorInputs = selectedUtxos.map((utxo) => ({
324
+ address_n: this.parseDerivationPath(this._derivationPath),
325
+ prev_hash: utxo.txid,
326
+ prev_index: utxo.vout,
327
+ amount: utxo.value.toString(),
328
+ script_type: ADDRESS_TYPE_TO_SCRIPT_TYPE[this._options.addressType]
329
+ }));
330
+ const trezorOutputs = [
331
+ {
332
+ address: to,
333
+ amount: satoshis.toString(),
334
+ script_type: "PAYTOADDRESS"
335
+ }
336
+ ];
337
+ const dustThreshold = getDustThreshold(fromAddressType);
338
+ if (changeAmount > dustThreshold) {
339
+ const changePath = this.buildDerivationPath(
340
+ this._options.addressType,
341
+ this._network,
342
+ this._options.accountIndex,
343
+ 0,
344
+ true
345
+ // isChange = true
346
+ );
347
+ trezorOutputs.push({
348
+ address_n: this.parseDerivationPath(changePath),
349
+ amount: changeAmount.toString(),
350
+ script_type: ADDRESS_TYPE_TO_OUTPUT_SCRIPT_TYPE[this._options.addressType]
351
+ });
352
+ } else {
353
+ estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes;
354
+ estimatedFee = Math.ceil(estimatedVBytes * feeRate);
355
+ changeAmount = 0;
356
+ if (totalInputValue < satoshis + estimatedFee) {
357
+ throw new Error(
358
+ `Insufficient funds after fee adjustment. Available: ${totalInputValue} sats, Required: ${satoshis + estimatedFee} sats`
359
+ );
360
+ }
361
+ }
362
+ const coin = this._network === "mainnet" ? "Bitcoin" : "Testnet";
363
+ if (this._network === "mainnet") {
364
+ const result = await trezor.signTransaction({
365
+ inputs: trezorInputs,
366
+ outputs: trezorOutputs,
367
+ coin,
368
+ push: false
369
+ });
370
+ if (!result.success) {
371
+ const error = result.payload;
372
+ throw new Error(error.error);
373
+ }
374
+ const payload = result.payload;
375
+ const txid = await btcService.broadcastTransaction(payload.serializedTx);
376
+ return txid;
377
+ } else {
378
+ const refTxs = [];
379
+ for (const utxo of selectedUtxos) {
380
+ try {
381
+ const rawTx = await btcService.getFullTransaction(utxo.txid);
382
+ const inputRef = rawTx.vin.map((vin) => ({
383
+ prev_hash: vin.txid,
384
+ prev_index: vin.vout,
385
+ sequence: vin.sequence,
386
+ script_sig: vin.scriptsig || ""
387
+ }));
388
+ const binOutputs = rawTx.vout.map((vout) => ({
389
+ amount: vout.value,
390
+ script_pubkey: vout.scriptpubkey
391
+ }));
392
+ refTxs.push({
393
+ hash: utxo.txid,
394
+ version: rawTx.version,
395
+ inputs: inputRef,
396
+ bin_outputs: binOutputs,
397
+ lock_time: rawTx.locktime
398
+ });
399
+ } catch (error) {
400
+ console.error(`Failed to fetch ref tx for ${utxo.txid}:`, error);
401
+ throw new Error(`Failed to fetch reference transaction: ${utxo.txid}`);
402
+ }
403
+ }
404
+ const result = await trezor.signTransaction({
405
+ inputs: trezorInputs,
406
+ outputs: trezorOutputs,
407
+ coin,
408
+ refTxs
409
+ });
410
+ if (!result.success) {
411
+ const error = result.payload;
412
+ throw new Error(error.error);
413
+ }
414
+ const payload = result.payload;
415
+ const txid = await btcService.broadcastTransaction(payload.serializedTx);
416
+ return txid;
417
+ }
418
+ }
419
+ async getNetwork() {
420
+ return this._network;
421
+ }
422
+ /**
423
+ * Get extended public key (xpub/ypub/zpub) for account
424
+ */
425
+ async getExtendedPublicKey() {
426
+ try {
427
+ const trezor = await this.initTrezorConnect();
428
+ const coinType = this._network === "mainnet" ? 0 : 1;
429
+ const purpose = ADDRESS_TYPE_TO_PATH[this._options.addressType];
430
+ const accountPath = `m/${purpose}'/${coinType}'/${this._options.accountIndex}'`;
431
+ const result = await trezor.getPublicKey({
432
+ path: accountPath,
433
+ coin: this._network === "mainnet" ? "btc" : "test"
434
+ });
435
+ if (!result.success) {
436
+ const error = result.payload;
437
+ throw new Error(error.error);
438
+ }
439
+ const payload = result.payload;
440
+ return payload.xpub;
441
+ } catch (error) {
442
+ this.handleTrezorError(error);
443
+ }
444
+ }
445
+ /**
446
+ * Get multiple addresses for the account
447
+ *
448
+ * This method only calls getPublicKey() for each address and derives
449
+ * addresses locally to minimize Trezor popup interactions.
450
+ */
451
+ async getAddresses(startIndex, count) {
452
+ const trezor = await this.initTrezorConnect();
453
+ const accounts = [];
454
+ const coin = this._network === "mainnet" ? "btc" : "test";
455
+ const addressType = this.mapTrezorAddressType(this._options.addressType);
456
+ for (let i = startIndex; i < startIndex + count; i++) {
457
+ const path = this.buildDerivationPath(
458
+ this._options.addressType,
459
+ this._network,
460
+ this._options.accountIndex,
461
+ i
462
+ );
463
+ const isLast = i === startIndex + count - 1;
464
+ const pubKeyResult = await trezor.getPublicKey({
465
+ path,
466
+ coin,
467
+ suppressBackupWarning: true,
468
+ keepSession: !isLast
469
+ // Close session on last iteration
470
+ });
471
+ if (!pubKeyResult.success) {
472
+ continue;
473
+ }
474
+ const pubKeyPayload = pubKeyResult.payload;
475
+ const address = deriveAddressFromPublicKey(
476
+ pubKeyPayload.publicKey,
477
+ addressType,
478
+ this._network
479
+ );
480
+ accounts.push({
481
+ address,
482
+ publicKey: pubKeyPayload.publicKey,
483
+ type: addressType
484
+ });
485
+ }
486
+ return accounts;
487
+ }
488
+ /**
489
+ * Get current derivation path
490
+ */
491
+ getDerivationPath() {
492
+ return this._derivationPath;
493
+ }
494
+ /**
495
+ * Check if device is connected
496
+ */
497
+ isConnected() {
498
+ return this._account !== null && this._initialized;
499
+ }
500
+ mapTrezorAddressType(addressType) {
501
+ switch (addressType) {
502
+ case "legacy":
503
+ return "legacy";
504
+ case "nested-segwit":
505
+ return "nested-segwit";
506
+ case "segwit":
507
+ return "segwit";
508
+ case "taproot":
509
+ return "taproot";
510
+ default:
511
+ return "segwit";
512
+ }
513
+ }
514
+ handleTrezorError(error) {
515
+ if (error instanceof Error) {
516
+ const message = error.message.toLowerCase();
517
+ if (message.includes("cancelled") || message.includes("canceled")) {
518
+ throw new Error("User cancelled the operation on Trezor device.");
519
+ }
520
+ if (message.includes("device disconnected")) {
521
+ throw new Error("Trezor device disconnected. Please reconnect and try again.");
522
+ }
523
+ if (message.includes("permissions")) {
524
+ throw new Error("Permission denied. Please allow access to your Trezor device.");
525
+ }
526
+ if (message.includes("popup")) {
527
+ throw new Error("Trezor popup was closed. Please try again.");
528
+ }
529
+ if (message.includes("device not found")) {
530
+ throw new Error("No Trezor device found. Please connect your device and try again.");
531
+ }
532
+ }
533
+ this.handleError(error);
534
+ }
535
+ };
536
+
537
+ export { TrezorConnector };
538
+ //# sourceMappingURL=out.js.map
539
+ //# sourceMappingURL=chunk-EWRXLZO4.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/trezor/TrezorConnector.ts"],"names":["estimatedVBytes","estimatedFee"],"mappings":"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","sourcesContent":["import type {\n WalletAccount,\n BitcoinNetwork,\n SignPsbtOptions,\n AddressType,\n} from 'otx-btc-wallet-core';\nimport { BaseConnector } from '../base';\nimport { BtcService } from '../utils';\nimport {\n getAddressType,\n getInputVBytes,\n getOutputVBytes,\n getDustThreshold,\n deriveAddressFromPublicKey,\n} from '../utils';\n\n// Trezor wallet icon\nconst TREZOR_ICON =\n 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF83MDM3XzExNSkiPgo8cGF0aCBkPSJNMjAwIDBIMFYyMDBIMjAwVjBaIiBmaWxsPSIjMDYyRDE2Ii8+CjxwYXRoIGQ9Ik0xMzMuMjYgNjMuMDUzNkMxMzMuMjYgNDUuNTM0IDExOC4wNjIgMzEgOTkuNTYyNyAzMUM4MS4wNjMxIDMxIDY1Ljg2NTYgNDUuNTQxMyA2NS44NjU2IDYzLjA1MzZWNzMuMjk5SDUyVjE0Ni45OTFMOTkuNTYyNyAxNjkuMTE3TDE0Ny4xMzYgMTQ2Ljk3NlY3My42MTI5SDEzMy4yNzFMMTMzLjI2IDYzLjA1MzZaTTgzLjA0NDQgNjMuMDUzNkM4My4wNDQ0IDU0Ljc5MzggOTAuMzEyOSA0OC4xODM4IDk5LjU2MjcgNDguMTgzOEMxMDguODEzIDQ4LjE4MzggMTE2LjA4MSA1NC43OTM4IDExNi4wODEgNjMuMDUzNlY3My4yOTlIODMuMDQ0NFY2My4wNTM2Wk0xMjcuOTczIDEzNS4wOTJMOTkuNTYyNyAxNDguMzEyTDcxLjE1MjggMTM1LjA5MlY5MC44MTEzSDEyNy45NzNWMTM1LjA5MloiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNzAzN18xMTUiPgo8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgcng9IjM1LjU1NTYiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==';\n\n/**\n * Trezor address type configuration\n */\nexport type TrezorAddressType = 'legacy' | 'nested-segwit' | 'segwit' | 'taproot';\n\n/**\n * Trezor connector options\n */\nexport interface TrezorConnectorOptions {\n /** Address type for derivation (default: 'segwit') */\n addressType?: TrezorAddressType;\n /** Account index (default: 0) */\n accountIndex?: number;\n /** Address index (default: 0) */\n addressIndex?: number;\n /** Manifest email for Trezor Connect */\n manifestEmail?: string;\n /** Manifest app URL for Trezor Connect */\n manifestAppUrl?: string;\n}\n\n/**\n * Options for sendBitcoin method\n */\nexport interface TrezorSendBitcoinOptions {\n /** Fee rate in sat/vB (optional, defaults to \"hour\" priority) */\n feeRate?: number;\n}\n\n/**\n * Address type to BIP path mapping\n */\nconst ADDRESS_TYPE_TO_PATH: Record<TrezorAddressType, number> = {\n legacy: 44,\n 'nested-segwit': 49,\n segwit: 84,\n taproot: 86,\n};\n\n/**\n * Address type to Trezor script type mapping\n */\nconst ADDRESS_TYPE_TO_SCRIPT_TYPE: Record<TrezorAddressType, string> = {\n legacy: 'SPENDADDRESS',\n 'nested-segwit': 'SPENDP2SHWITNESS',\n segwit: 'SPENDWITNESS',\n taproot: 'SPENDTAPROOT',\n};\n\n/**\n * Address type to output script type mapping\n */\nconst ADDRESS_TYPE_TO_OUTPUT_SCRIPT_TYPE: Record<TrezorAddressType, string> = {\n legacy: 'PAYTOADDRESS',\n 'nested-segwit': 'PAYTOP2SHWITNESS',\n segwit: 'PAYTOWITNESS',\n taproot: 'PAYTOTAPROOT',\n};\n\n// Common params for Trezor Connect methods\ninterface TrezorCommonParams {\n keepSession?: boolean;\n}\n\n// Trezor Connect types (minimal interface to avoid importing full types)\ninterface TrezorConnect {\n init(settings: {\n manifest: { email: string; appUrl: string };\n popup?: boolean;\n lazyLoad?: boolean;\n transports?: string[];\n coreMode?: 'auto' | 'iframe' | 'popup';\n debug?: boolean;\n }): Promise<void>;\n getAddress(params: TrezorCommonParams & {\n path: string;\n coin: string;\n showOnTrezor?: boolean;\n }): Promise<TrezorResponse<{ address: string; path: number[] }>>;\n signMessage(params: TrezorCommonParams & {\n path: string;\n message: string;\n coin: string;\n }): Promise<TrezorResponse<{ signature: string; address: string }>>;\n signTransaction(params: TrezorCommonParams & {\n inputs: TrezorInput[];\n outputs: TrezorOutput[];\n coin: string;\n push?: boolean;\n refTxs?: RefTransaction[];\n }): Promise<TrezorResponse<{ serializedTx: string; signatures: string[] }>>;\n pushTransaction(params: TrezorCommonParams & {\n tx: string;\n coin: string;\n }): Promise<TrezorResponse<{ txid: string }>>;\n getPublicKey(params: TrezorCommonParams & {\n path: string;\n coin: string;\n suppressBackupWarning?: boolean;\n }): Promise<TrezorResponse<{ publicKey: string; chainCode: string; xpub: string }>>;\n dispose(): void;\n}\n\n/**\n * Reference transaction for Trezor (needed for testnet)\n */\ninterface RefTransaction {\n hash: string;\n version: number;\n inputs: Array<{\n prev_hash: string;\n prev_index: number;\n sequence: number;\n script_sig: string;\n }>;\n bin_outputs: Array<{\n amount: number;\n script_pubkey: string;\n }>;\n lock_time: number;\n}\n\ninterface TrezorResponse<T> {\n success: boolean;\n payload: T | { error: string; code?: string };\n}\n\ninterface TrezorInput {\n address_n: number[];\n prev_hash: string;\n prev_index: number;\n amount: string;\n script_type?: string;\n}\n\ninterface TrezorOutput {\n address?: string;\n address_n?: number[];\n amount: string;\n script_type?: string;\n}\n\n/**\n * Trezor Hardware Wallet Connector\n *\n * @see https://docs.trezor.io/trezor-suite/packages/connect/index.html\n * @see https://github.com/trezor/trezor-suite/tree/develop/packages/connect\n */\nexport class TrezorConnector extends BaseConnector {\n readonly id = 'trezor';\n readonly name = 'Trezor';\n readonly icon = TREZOR_ICON;\n\n private _trezorConnect: TrezorConnect | null = null;\n private _account: WalletAccount | null = null;\n private _network: BitcoinNetwork = 'mainnet';\n private _options: Required<TrezorConnectorOptions>;\n private _derivationPath: string = '';\n private _initialized: boolean = false;\n\n constructor(options: TrezorConnectorOptions = {}) {\n super();\n this._options = {\n addressType: options.addressType ?? 'segwit',\n accountIndex: options.accountIndex ?? 0,\n addressIndex: options.addressIndex ?? 0,\n manifestEmail: options.manifestEmail ?? 'support@optimex.com',\n manifestAppUrl:\n options.manifestAppUrl ??\n (typeof window !== 'undefined' ? window.location.origin : 'https://optimex.com'),\n };\n }\n\n /**\n * Override checkReady - Trezor is always \"ready\" since it doesn't require browser extension\n * The actual device connection happens when connect() is called\n */\n protected checkReady(): void {\n this.ready = true;\n }\n\n /**\n * Get the provider - Trezor doesn't use window injection\n */\n protected getProvider(): null {\n return null;\n }\n\n /**\n * Get current address type\n */\n getAddressType(): TrezorAddressType {\n return this._options.addressType;\n }\n\n /**\n * Set address type options before connecting\n * Call this before connect() to change the address derivation path\n */\n setOptions(options: Omit<TrezorConnectorOptions, 'manifestEmail' | 'manifestAppUrl'>): void {\n this._options = {\n ...this._options,\n addressType: options.addressType ?? this._options.addressType,\n accountIndex: options.accountIndex ?? this._options.accountIndex,\n addressIndex: options.addressIndex ?? this._options.addressIndex,\n };\n }\n\n /**\n * Get available address types for UI selection\n */\n static getAvailableAddressTypes(): Array<{ type: TrezorAddressType; label: string; description: string }> {\n return [\n { type: 'legacy', label: 'Legacy (P2PKH)', description: \"Starts with '1' or 'm/n'\" },\n { type: 'nested-segwit', label: 'Nested SegWit (P2SH-P2WPKH)', description: \"Starts with '3' or '2'\" },\n { type: 'segwit', label: 'Native SegWit (P2WPKH)', description: \"Starts with 'bc1q' or 'tb1q'\" },\n { type: 'taproot', label: 'Taproot (P2TR)', description: \"Starts with 'bc1p' or 'tb1p'\" },\n ];\n }\n\n /**\n * Initialize Trezor Connect\n */\n private async initTrezorConnect(): Promise<TrezorConnect> {\n if (this._trezorConnect && this._initialized) {\n return this._trezorConnect;\n }\n\n try {\n const TrezorConnectModule = await import('@trezor/connect-web');\n const TrezorConnect = TrezorConnectModule.default;\n\n // Initialize with proper v9 settings\n // coreMode: 'popup' forces popup mode which works without Trezor Suite\n // transports: specify which transports to use\n await TrezorConnect.init({\n manifest: {\n email: this._options.manifestEmail,\n appUrl: this._options.manifestAppUrl,\n },\n popup: true,\n lazyLoad: false, // Don't lazy load - init immediately\n coreMode: 'popup', // Force popup mode for better compatibility\n transports: ['BridgeTransport', 'WebUsbTransport'], // Try both Bridge and WebUSB\n debug: false,\n });\n\n this._trezorConnect = TrezorConnect as unknown as TrezorConnect;\n this._initialized = true;\n\n return this._trezorConnect;\n } catch (error) {\n console.error('Trezor Connect init error:', error);\n throw new Error(\n 'Failed to initialize Trezor Connect. Make sure @trezor/connect-web is installed and Trezor Bridge or Trezor Suite is running.'\n );\n }\n }\n\n /**\n * Build BIP32 derivation path\n */\n private buildDerivationPath(\n addressType: TrezorAddressType,\n network: BitcoinNetwork,\n accountIndex: number,\n addressIndex: number,\n isChange: boolean = false\n ): string {\n const coinType = network === 'mainnet' ? 0 : 1;\n const purpose = ADDRESS_TYPE_TO_PATH[addressType];\n const change = isChange ? 1 : 0;\n return `m/${purpose}'/${coinType}'/${accountIndex}'/${change}/${addressIndex}`;\n }\n\n /**\n * Parse derivation path string to array of numbers\n */\n private parseDerivationPath(path: string): number[] {\n return path\n .split('/')\n .slice(1) // Remove 'm'\n .map((segment) => {\n const isHardened = segment.endsWith(\"'\");\n const num = parseInt(segment, 10);\n return isHardened ? num + 0x80000000 : num;\n });\n }\n\n /**\n * Connect to Trezor device\n * IMPORTANT: This must be called within a user gesture (click event)\n *\n * This method only calls getPublicKey() once and derives the address locally\n * to avoid opening the Trezor popup twice.\n */\n async connect(network: BitcoinNetwork = 'mainnet'): Promise<WalletAccount> {\n try {\n const trezor = await this.initTrezorConnect();\n\n this._network = network;\n const coin = network === 'mainnet' ? 'btc' : 'test';\n\n // Build derivation path\n this._derivationPath = this.buildDerivationPath(\n this._options.addressType,\n network,\n this._options.accountIndex,\n this._options.addressIndex\n );\n\n // Only call getPublicKey() - address will be derived locally from the public key\n // This avoids opening the Trezor popup twice\n const pubKeyResult = await trezor.getPublicKey({\n path: this._derivationPath,\n coin,\n suppressBackupWarning: true,\n });\n\n if (!pubKeyResult.success) {\n const error = pubKeyResult.payload as { error: string };\n throw new Error(error.error);\n }\n\n const pubKeyPayload = pubKeyResult.payload as { publicKey: string };\n\n // Derive address from public key locally\n const address = deriveAddressFromPublicKey(\n pubKeyPayload.publicKey,\n this.mapTrezorAddressType(this._options.addressType),\n network\n );\n\n this._account = {\n address,\n publicKey: pubKeyPayload.publicKey,\n type: this.mapTrezorAddressType(this._options.addressType),\n };\n\n return this._account;\n } catch (error) {\n this.handleTrezorError(error);\n }\n }\n\n /**\n * Get address with verification on device\n *\n * Note: This method calls getAddress() with showOnTrezor=true to display\n * the address on the Trezor device for user verification. The public key\n * is already available from the initial connect() call.\n */\n async getAddressWithVerification(): Promise<WalletAccount> {\n if (!this._account) {\n throw new Error('Not connected to Trezor device. Call connect() first.');\n }\n\n try {\n const trezor = await this.initTrezorConnect();\n const coin = this._network === 'mainnet' ? 'btc' : 'test';\n\n // Show address on Trezor for verification\n const addressResult = await trezor.getAddress({\n path: this._derivationPath,\n coin,\n showOnTrezor: true,\n });\n\n if (!addressResult.success) {\n const error = addressResult.payload as { error: string };\n throw new Error(error.error);\n }\n\n // Return the existing account (publicKey already available from connect())\n return this._account;\n } catch (error) {\n this.handleTrezorError(error);\n }\n }\n\n async disconnect(): Promise<void> {\n if (this._trezorConnect) {\n try {\n this._trezorConnect.dispose();\n } catch {\n // Ignore dispose errors\n }\n this._trezorConnect = null;\n this._initialized = false;\n }\n this._account = null;\n this.cleanup();\n }\n\n async getAccounts(): Promise<WalletAccount[]> {\n if (!this._account) {\n return [];\n }\n return [this._account];\n }\n\n async signMessage(message: string): Promise<string> {\n try {\n const trezor = await this.initTrezorConnect();\n\n const result = await trezor.signMessage({\n path: this._derivationPath,\n message,\n coin: this._network === 'mainnet' ? 'btc' : 'test',\n });\n\n if (!result.success) {\n const error = result.payload as { error: string };\n throw new Error(error.error);\n }\n\n const payload = result.payload as { signature: string };\n return payload.signature;\n } catch (error) {\n this.handleTrezorError(error);\n }\n }\n\n /**\n * Sign a PSBT with Trezor\n *\n * Note: Trezor Connect doesn't directly support PSBT format.\n * Use signTransaction method or sendBitcoin instead.\n */\n async signPsbt(_psbtHex: string, _options?: SignPsbtOptions): Promise<string> {\n void _psbtHex;\n void _options;\n\n throw new Error(\n 'Direct PSBT signing is not supported by Trezor Connect. ' +\n 'Please use sendBitcoin() method or manually parse the PSBT and use signTransaction().'\n );\n }\n\n /**\n * Sign multiple PSBTs\n */\n async signPsbts(psbtHexs: string[], options?: SignPsbtOptions): Promise<string[]> {\n const results: string[] = [];\n for (const psbtHex of psbtHexs) {\n const signed = await this.signPsbt(psbtHex, options);\n results.push(signed);\n }\n return results;\n }\n\n /**\n * Send a Bitcoin transaction\n *\n * @param to - Recipient address\n * @param satoshis - Amount to send in satoshis\n * @param options - Send options (feeRate, etc.)\n * @returns Transaction ID after broadcast\n *\n * @example\n * ```typescript\n * const txid = await trezor.sendTransaction('bc1q...', 50000);\n * // With custom fee rate\n * const txid = await trezor.sendTransaction('bc1q...', 50000, { feeRate: 10 });\n * ```\n */\n async sendTransaction(\n to: string,\n satoshis: number,\n options?: TrezorSendBitcoinOptions\n ): Promise<string> {\n if (!this._account) {\n throw new Error('Not connected to Trezor device');\n }\n\n const trezor = await this.initTrezorConnect();\n const btcService = new BtcService(this._network);\n\n // 1. Get UTXOs with tx hex (needed for testnet refTxs)\n const utxos = await btcService.getUtxosWithTxHex(this._account.address);\n if (utxos.length === 0) {\n throw new Error('No UTXOs available for spending');\n }\n\n // 2. Get fee rate if not provided\n let feeRate = options?.feeRate;\n if (!feeRate) {\n const feeRates = await btcService.getFeeRates();\n feeRate = feeRates.hour;\n }\n\n // 3. Get address types using utility functions\n const fromAddressType = getAddressType(this._account.address);\n const toAddressType = getAddressType(to);\n\n // 4. Get vBytes estimates using utility functions\n const inputVBytes = getInputVBytes(fromAddressType);\n const outputVBytes = getOutputVBytes(toAddressType);\n const changeOutputVBytes = getOutputVBytes(fromAddressType);\n const baseVBytes = 10.5; // Base transaction overhead\n\n // 5. Select UTXOs using greedy algorithm (sorted by value descending)\n const sortedUtxos = [...utxos].sort((a, b) => b.value - a.value);\n const selectedUtxos: typeof utxos = [];\n let totalInputValue = 0;\n\n for (const utxo of sortedUtxos) {\n selectedUtxos.push(utxo);\n totalInputValue += utxo.value;\n\n // Calculate fee with 2 outputs (recipient + change)\n const estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes + changeOutputVBytes;\n const estimatedFee = Math.ceil(estimatedVBytes * feeRate);\n\n if (totalInputValue >= satoshis + estimatedFee) {\n break;\n }\n }\n\n // 6. Calculate final fee\n let estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes + changeOutputVBytes;\n let estimatedFee = Math.ceil(estimatedVBytes * feeRate);\n\n // Check if we have enough funds\n if (totalInputValue < satoshis + estimatedFee) {\n throw new Error(\n `Insufficient funds. Available: ${totalInputValue} sats, Required: ${satoshis + estimatedFee} sats (including fee)`\n );\n }\n\n // 7. Calculate change\n let changeAmount = totalInputValue - satoshis - estimatedFee;\n\n // 8. Build Trezor inputs\n const trezorInputs: TrezorInput[] = selectedUtxos.map((utxo) => ({\n address_n: this.parseDerivationPath(this._derivationPath),\n prev_hash: utxo.txid,\n prev_index: utxo.vout,\n amount: utxo.value.toString(),\n script_type: ADDRESS_TYPE_TO_SCRIPT_TYPE[this._options.addressType],\n }));\n\n // 9. Build Trezor outputs\n const trezorOutputs: TrezorOutput[] = [\n {\n address: to,\n amount: satoshis.toString(),\n script_type: 'PAYTOADDRESS',\n },\n ];\n\n // Add change output if above dust threshold\n const dustThreshold = getDustThreshold(fromAddressType);\n\n if (changeAmount > dustThreshold) {\n // Build change path\n const changePath = this.buildDerivationPath(\n this._options.addressType,\n this._network,\n this._options.accountIndex,\n 0,\n true // isChange = true\n );\n\n trezorOutputs.push({\n address_n: this.parseDerivationPath(changePath),\n amount: changeAmount.toString(),\n script_type: ADDRESS_TYPE_TO_OUTPUT_SCRIPT_TYPE[this._options.addressType],\n });\n } else {\n // No change output - recalculate fee without change output\n estimatedVBytes = baseVBytes + selectedUtxos.length * inputVBytes + outputVBytes;\n estimatedFee = Math.ceil(estimatedVBytes * feeRate);\n changeAmount = 0;\n\n // Verify we still have enough\n if (totalInputValue < satoshis + estimatedFee) {\n throw new Error(\n `Insufficient funds after fee adjustment. Available: ${totalInputValue} sats, Required: ${satoshis + estimatedFee} sats`\n );\n }\n }\n\n // 10. Sign and broadcast transaction\n const coin = this._network === 'mainnet' ? 'Bitcoin' : 'Testnet';\n\n if (this._network === 'mainnet') {\n // Mainnet: use Trezor's default blockbook backend\n const result = await trezor.signTransaction({\n inputs: trezorInputs,\n outputs: trezorOutputs,\n coin,\n push: false,\n });\n\n if (!result.success) {\n const error = result.payload as { error: string };\n throw new Error(error.error);\n }\n\n const payload = result.payload as { serializedTx: string };\n\n // Broadcast via our service\n const txid = await btcService.broadcastTransaction(payload.serializedTx);\n return txid;\n } else {\n // Testnet: build refTxs manually because Trezor's default blockbook API\n // may not work properly with testnet\n const refTxs: RefTransaction[] = [];\n\n for (const utxo of selectedUtxos) {\n try {\n // Fetch full transaction data using BtcService\n const rawTx = await btcService.getFullTransaction(utxo.txid);\n\n // Build reference transaction\n const inputRef = rawTx.vin.map((vin) => ({\n prev_hash: vin.txid,\n prev_index: vin.vout,\n sequence: vin.sequence,\n script_sig: vin.scriptsig || '',\n }));\n\n const binOutputs = rawTx.vout.map((vout) => ({\n amount: vout.value,\n script_pubkey: vout.scriptpubkey,\n }));\n\n refTxs.push({\n hash: utxo.txid,\n version: rawTx.version,\n inputs: inputRef,\n bin_outputs: binOutputs,\n lock_time: rawTx.locktime,\n });\n } catch (error) {\n console.error(`Failed to fetch ref tx for ${utxo.txid}:`, error);\n throw new Error(`Failed to fetch reference transaction: ${utxo.txid}`);\n }\n }\n\n // Sign with refTxs\n const result = await trezor.signTransaction({\n inputs: trezorInputs,\n outputs: trezorOutputs,\n coin,\n refTxs,\n });\n\n if (!result.success) {\n const error = result.payload as { error: string };\n throw new Error(error.error);\n }\n\n const payload = result.payload as { serializedTx: string };\n\n // Broadcast via our service\n const txid = await btcService.broadcastTransaction(payload.serializedTx);\n return txid;\n }\n }\n\n async getNetwork(): Promise<BitcoinNetwork> {\n return this._network;\n }\n\n /**\n * Get extended public key (xpub/ypub/zpub) for account\n */\n async getExtendedPublicKey(): Promise<string> {\n try {\n const trezor = await this.initTrezorConnect();\n\n const coinType = this._network === 'mainnet' ? 0 : 1;\n const purpose = ADDRESS_TYPE_TO_PATH[this._options.addressType];\n const accountPath = `m/${purpose}'/${coinType}'/${this._options.accountIndex}'`;\n\n const result = await trezor.getPublicKey({\n path: accountPath,\n coin: this._network === 'mainnet' ? 'btc' : 'test',\n });\n\n if (!result.success) {\n const error = result.payload as { error: string };\n throw new Error(error.error);\n }\n\n const payload = result.payload as { xpub: string };\n return payload.xpub;\n } catch (error) {\n this.handleTrezorError(error);\n }\n }\n\n /**\n * Get multiple addresses for the account\n *\n * This method only calls getPublicKey() for each address and derives\n * addresses locally to minimize Trezor popup interactions.\n */\n async getAddresses(startIndex: number, count: number): Promise<WalletAccount[]> {\n const trezor = await this.initTrezorConnect();\n const accounts: WalletAccount[] = [];\n const coin = this._network === 'mainnet' ? 'btc' : 'test';\n const addressType = this.mapTrezorAddressType(this._options.addressType);\n\n for (let i = startIndex; i < startIndex + count; i++) {\n const path = this.buildDerivationPath(\n this._options.addressType,\n this._network,\n this._options.accountIndex,\n i\n );\n\n const isLast = i === startIndex + count - 1;\n\n // Only get public key - address will be derived locally\n const pubKeyResult = await trezor.getPublicKey({\n path,\n coin,\n suppressBackupWarning: true,\n keepSession: !isLast, // Close session on last iteration\n });\n\n if (!pubKeyResult.success) {\n continue;\n }\n\n const pubKeyPayload = pubKeyResult.payload as { publicKey: string };\n\n // Derive address from public key locally\n const address = deriveAddressFromPublicKey(\n pubKeyPayload.publicKey,\n addressType,\n this._network\n );\n\n accounts.push({\n address,\n publicKey: pubKeyPayload.publicKey,\n type: addressType,\n });\n }\n\n return accounts;\n }\n\n /**\n * Get current derivation path\n */\n getDerivationPath(): string {\n return this._derivationPath;\n }\n\n /**\n * Check if device is connected\n */\n isConnected(): boolean {\n return this._account !== null && this._initialized;\n }\n\n private mapTrezorAddressType(addressType: TrezorAddressType): AddressType {\n switch (addressType) {\n case 'legacy':\n return 'legacy';\n case 'nested-segwit':\n return 'nested-segwit';\n case 'segwit':\n return 'segwit';\n case 'taproot':\n return 'taproot';\n default:\n return 'segwit';\n }\n }\n\n private handleTrezorError(error: unknown): never {\n if (error instanceof Error) {\n const message = error.message.toLowerCase();\n\n if (message.includes('cancelled') || message.includes('canceled')) {\n throw new Error('User cancelled the operation on Trezor device.');\n }\n\n if (message.includes('device disconnected')) {\n throw new Error('Trezor device disconnected. Please reconnect and try again.');\n }\n\n if (message.includes('permissions')) {\n throw new Error('Permission denied. Please allow access to your Trezor device.');\n }\n\n if (message.includes('popup')) {\n throw new Error('Trezor popup was closed. Please try again.');\n }\n\n if (message.includes('device not found')) {\n throw new Error('No Trezor device found. Please connect your device and try again.');\n }\n }\n\n this.handleError(error);\n }\n}\n"]}