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.
- package/README.md +554 -0
- package/dist/base-IAFq7sd8.d.mts +53 -0
- package/dist/base-IAFq7sd8.d.ts +53 -0
- package/dist/binance/index.d.mts +81 -0
- package/dist/binance/index.d.ts +81 -0
- package/dist/binance/index.js +13 -0
- package/dist/binance/index.js.map +1 -0
- package/dist/binance/index.mjs +4 -0
- package/dist/binance/index.mjs.map +1 -0
- package/dist/bitget/index.d.mts +84 -0
- package/dist/bitget/index.d.ts +84 -0
- package/dist/bitget/index.js +13 -0
- package/dist/bitget/index.js.map +1 -0
- package/dist/bitget/index.mjs +4 -0
- package/dist/bitget/index.mjs.map +1 -0
- package/dist/chunk-5Z5Q2Y75.mjs +91 -0
- package/dist/chunk-5Z5Q2Y75.mjs.map +1 -0
- package/dist/chunk-7KK2LZLZ.mjs +208 -0
- package/dist/chunk-7KK2LZLZ.mjs.map +1 -0
- package/dist/chunk-AW2JZIHR.mjs +753 -0
- package/dist/chunk-AW2JZIHR.mjs.map +1 -0
- package/dist/chunk-EIJOSZXZ.js +331 -0
- package/dist/chunk-EIJOSZXZ.js.map +1 -0
- package/dist/chunk-EQHR7P7G.js +541 -0
- package/dist/chunk-EQHR7P7G.js.map +1 -0
- package/dist/chunk-EWRXLZO4.mjs +539 -0
- package/dist/chunk-EWRXLZO4.mjs.map +1 -0
- package/dist/chunk-FISNQZZ7.js +802 -0
- package/dist/chunk-FISNQZZ7.js.map +1 -0
- package/dist/chunk-HL4WDMGS.js +200 -0
- package/dist/chunk-HL4WDMGS.js.map +1 -0
- package/dist/chunk-IPYWR76I.js +314 -0
- package/dist/chunk-IPYWR76I.js.map +1 -0
- package/dist/chunk-JYYNWR5G.js +142 -0
- package/dist/chunk-JYYNWR5G.js.map +1 -0
- package/dist/chunk-LNKTYZJM.js +701 -0
- package/dist/chunk-LNKTYZJM.js.map +1 -0
- package/dist/chunk-LVZMONQL.mjs +699 -0
- package/dist/chunk-LVZMONQL.mjs.map +1 -0
- package/dist/chunk-MFXLQWOE.js +93 -0
- package/dist/chunk-MFXLQWOE.js.map +1 -0
- package/dist/chunk-NBIA4TTE.mjs +204 -0
- package/dist/chunk-NBIA4TTE.mjs.map +1 -0
- package/dist/chunk-O4DD2XJ2.js +206 -0
- package/dist/chunk-O4DD2XJ2.js.map +1 -0
- package/dist/chunk-P7HVBU2B.mjs +140 -0
- package/dist/chunk-P7HVBU2B.mjs.map +1 -0
- package/dist/chunk-Q7QVQYEB.js +210 -0
- package/dist/chunk-Q7QVQYEB.js.map +1 -0
- package/dist/chunk-RLZEG6KL.mjs +329 -0
- package/dist/chunk-RLZEG6KL.mjs.map +1 -0
- package/dist/chunk-SYLDBJ75.mjs +246 -0
- package/dist/chunk-SYLDBJ75.mjs.map +1 -0
- package/dist/chunk-TTEUU3CI.mjs +198 -0
- package/dist/chunk-TTEUU3CI.mjs.map +1 -0
- package/dist/chunk-V66BXDTR.mjs +292 -0
- package/dist/chunk-V66BXDTR.mjs.map +1 -0
- package/dist/chunk-X77ZT4OI.js +268 -0
- package/dist/chunk-X77ZT4OI.js.map +1 -0
- package/dist/imtoken/index.d.mts +116 -0
- package/dist/imtoken/index.d.ts +116 -0
- package/dist/imtoken/index.js +14 -0
- package/dist/imtoken/index.js.map +1 -0
- package/dist/imtoken/index.mjs +5 -0
- package/dist/imtoken/index.mjs.map +1 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +170 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +13 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ledger/index.d.mts +290 -0
- package/dist/ledger/index.d.ts +290 -0
- package/dist/ledger/index.js +14 -0
- package/dist/ledger/index.js.map +1 -0
- package/dist/ledger/index.mjs +5 -0
- package/dist/ledger/index.mjs.map +1 -0
- package/dist/okx/index.d.mts +88 -0
- package/dist/okx/index.d.ts +88 -0
- package/dist/okx/index.js +13 -0
- package/dist/okx/index.js.map +1 -0
- package/dist/okx/index.mjs +4 -0
- package/dist/okx/index.mjs.map +1 -0
- package/dist/phantom/index.d.mts +96 -0
- package/dist/phantom/index.d.ts +96 -0
- package/dist/phantom/index.js +14 -0
- package/dist/phantom/index.js.map +1 -0
- package/dist/phantom/index.mjs +5 -0
- package/dist/phantom/index.mjs.map +1 -0
- package/dist/psbt-builder-CFOs69Z5.d.mts +131 -0
- package/dist/psbt-builder-CFOs69Z5.d.ts +131 -0
- package/dist/trezor/index.d.mts +155 -0
- package/dist/trezor/index.d.ts +155 -0
- package/dist/trezor/index.js +14 -0
- package/dist/trezor/index.js.map +1 -0
- package/dist/trezor/index.mjs +5 -0
- package/dist/trezor/index.mjs.map +1 -0
- package/dist/unisat/index.d.mts +75 -0
- package/dist/unisat/index.d.ts +75 -0
- package/dist/unisat/index.js +13 -0
- package/dist/unisat/index.js.map +1 -0
- package/dist/unisat/index.mjs +4 -0
- package/dist/unisat/index.mjs.map +1 -0
- package/dist/utils/index.d.mts +398 -0
- package/dist/utils/index.d.ts +398 -0
- package/dist/utils/index.js +120 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/index.mjs +3 -0
- package/dist/utils/index.mjs.map +1 -0
- package/dist/xverse/index.d.mts +79 -0
- package/dist/xverse/index.d.ts +79 -0
- package/dist/xverse/index.js +13 -0
- package/dist/xverse/index.js.map +1 -0
- package/dist/xverse/index.mjs +4 -0
- package/dist/xverse/index.mjs.map +1 -0
- package/package.json +108 -0
- package/src/base.ts +132 -0
- package/src/binance/BinanceConnector.ts +307 -0
- package/src/binance/index.ts +1 -0
- package/src/bitget/BitgetConnector.ts +301 -0
- package/src/bitget/index.ts +1 -0
- package/src/imtoken/ImTokenConnector.ts +420 -0
- package/src/imtoken/index.ts +2 -0
- package/src/index.ts +78 -0
- package/src/ledger/LedgerConnector.ts +1019 -0
- package/src/ledger/index.ts +8 -0
- package/src/okx/OKXConnector.ts +230 -0
- package/src/okx/index.ts +1 -0
- package/src/phantom/PhantomConnector.ts +381 -0
- package/src/phantom/index.ts +2 -0
- package/src/trezor/TrezorConnector.ts +824 -0
- package/src/trezor/index.ts +6 -0
- package/src/unisat/UnisatConnector.ts +312 -0
- package/src/unisat/index.ts +1 -0
- package/src/utils/blockstream.ts +230 -0
- package/src/utils/btc-service.ts +364 -0
- package/src/utils/index.ts +56 -0
- package/src/utils/mempool.ts +232 -0
- package/src/utils/psbt-builder.ts +492 -0
- package/src/utils/types.ts +183 -0
- package/src/xverse/XverseConnector.ts +479 -0
- 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"]}
|