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,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PSBT Builder Utility
|
|
3
|
+
* Reusable functions for building PSBTs across different wallet connectors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BitcoinNetwork, AddressType } from 'otx-btc-wallet-core';
|
|
7
|
+
import * as bitcoin from 'bitcoinjs-lib';
|
|
8
|
+
import { BtcService } from './btc-service';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* UTXO type
|
|
12
|
+
*/
|
|
13
|
+
export interface Utxo {
|
|
14
|
+
txid: string;
|
|
15
|
+
vout: number;
|
|
16
|
+
value: number;
|
|
17
|
+
status?: {
|
|
18
|
+
confirmed: boolean;
|
|
19
|
+
block_height?: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Options for generating a send PSBT
|
|
25
|
+
*/
|
|
26
|
+
export interface GeneratePsbtOptions {
|
|
27
|
+
/** Fee rate in sat/vB */
|
|
28
|
+
feeRate?: number;
|
|
29
|
+
/** Fee rate multiplier */
|
|
30
|
+
feeRateMultiplier?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Result from generating a send PSBT
|
|
35
|
+
*/
|
|
36
|
+
export interface GeneratePsbtResult {
|
|
37
|
+
/** The constructed PSBT */
|
|
38
|
+
psbt: bitcoin.Psbt;
|
|
39
|
+
/** PSBT as hex string */
|
|
40
|
+
psbtHex: string;
|
|
41
|
+
/** PSBT as base64 string */
|
|
42
|
+
psbtBase64: string;
|
|
43
|
+
/** Bitcoin network used */
|
|
44
|
+
btcNetwork: bitcoin.Network;
|
|
45
|
+
/** Selected UTXOs used in the transaction */
|
|
46
|
+
selectedUtxos: Utxo[];
|
|
47
|
+
/** Total input value in satoshis */
|
|
48
|
+
totalInputValue: number;
|
|
49
|
+
/** Estimated fee in satoshis */
|
|
50
|
+
estimatedFee: number;
|
|
51
|
+
/** Change amount in satoshis (0 if no change output) */
|
|
52
|
+
changeAmount: number;
|
|
53
|
+
/** Inputs to sign (for wallet APIs that require this) */
|
|
54
|
+
inputsToSign: Array<{ address: string; index: number }>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Helper to convert hex string to Uint8Array
|
|
59
|
+
*/
|
|
60
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
61
|
+
const cleanHex = hex.replace(/^0x/, '');
|
|
62
|
+
const bytes = new Uint8Array(cleanHex.length / 2);
|
|
63
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
64
|
+
bytes[i] = parseInt(cleanHex.substring(i * 2, i * 2 + 2), 16);
|
|
65
|
+
}
|
|
66
|
+
return bytes;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Helper to convert Uint8Array to hex string
|
|
71
|
+
*/
|
|
72
|
+
export function bytesToHex(bytes: Uint8Array): string {
|
|
73
|
+
return Array.from(bytes)
|
|
74
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
75
|
+
.join('');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Helper to convert public key to x-only (for Taproot)
|
|
80
|
+
*/
|
|
81
|
+
export function toXOnly(pubKey: Uint8Array): Uint8Array {
|
|
82
|
+
return pubKey.subarray(1, 33);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get address type from address string
|
|
87
|
+
*/
|
|
88
|
+
export function getAddressType(address: string): AddressType {
|
|
89
|
+
if (address.startsWith('bc1q') || address.startsWith('tb1q')) {
|
|
90
|
+
return 'segwit';
|
|
91
|
+
}
|
|
92
|
+
if (address.startsWith('bc1p') || address.startsWith('tb1p')) {
|
|
93
|
+
return 'taproot';
|
|
94
|
+
}
|
|
95
|
+
if (address.startsWith('3') || address.startsWith('2')) {
|
|
96
|
+
return 'nested-segwit';
|
|
97
|
+
}
|
|
98
|
+
return 'legacy';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get bitcoinjs-lib network object from BitcoinNetwork
|
|
103
|
+
*/
|
|
104
|
+
export function getBitcoinJsNetwork(network: BitcoinNetwork): bitcoin.Network {
|
|
105
|
+
switch (network) {
|
|
106
|
+
case 'mainnet':
|
|
107
|
+
return bitcoin.networks.bitcoin;
|
|
108
|
+
case 'testnet':
|
|
109
|
+
case 'testnet4':
|
|
110
|
+
case 'signet':
|
|
111
|
+
return bitcoin.networks.testnet;
|
|
112
|
+
default:
|
|
113
|
+
return bitcoin.networks.bitcoin;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Estimate vBytes per input based on address type
|
|
119
|
+
*/
|
|
120
|
+
export function getInputVBytes(addressType: AddressType): number {
|
|
121
|
+
switch (addressType) {
|
|
122
|
+
case 'taproot':
|
|
123
|
+
return 58; // P2TR input
|
|
124
|
+
case 'segwit':
|
|
125
|
+
return 68; // P2WPKH input
|
|
126
|
+
case 'nested-segwit':
|
|
127
|
+
return 91; // P2SH-P2WPKH input
|
|
128
|
+
default:
|
|
129
|
+
return 148; // P2PKH input
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Estimate vBytes per output based on address type
|
|
135
|
+
*/
|
|
136
|
+
export function getOutputVBytes(addressType: AddressType): number {
|
|
137
|
+
switch (addressType) {
|
|
138
|
+
case 'taproot':
|
|
139
|
+
return 43; // P2TR output
|
|
140
|
+
case 'segwit':
|
|
141
|
+
return 31; // P2WPKH output
|
|
142
|
+
case 'nested-segwit':
|
|
143
|
+
return 32; // P2SH output
|
|
144
|
+
default:
|
|
145
|
+
return 34; // P2PKH output
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get dust threshold for address type
|
|
151
|
+
*/
|
|
152
|
+
export function getDustThreshold(addressType: AddressType): number {
|
|
153
|
+
return addressType === 'legacy' ? 546 : 294;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Select UTXOs for transaction using a simple greedy algorithm
|
|
158
|
+
*/
|
|
159
|
+
export function selectUtxos(
|
|
160
|
+
utxos: Utxo[],
|
|
161
|
+
fromAddressType: AddressType,
|
|
162
|
+
toAddressType: AddressType,
|
|
163
|
+
amount: number,
|
|
164
|
+
feeRate: number
|
|
165
|
+
): { selectedUtxos: Utxo[]; totalValue: number; estimatedFee: number } {
|
|
166
|
+
const inputVBytes = getInputVBytes(fromAddressType);
|
|
167
|
+
const outputVBytes = getOutputVBytes(toAddressType);
|
|
168
|
+
const baseVBytes = 10.5; // Base transaction overhead
|
|
169
|
+
|
|
170
|
+
// Sort UTXOs by value (descending) for better selection
|
|
171
|
+
const sortedUtxos = [...utxos].sort((a, b) => b.value - a.value);
|
|
172
|
+
|
|
173
|
+
const selectedUtxos: Utxo[] = [];
|
|
174
|
+
let totalValue = 0;
|
|
175
|
+
|
|
176
|
+
for (const utxo of sortedUtxos) {
|
|
177
|
+
selectedUtxos.push(utxo);
|
|
178
|
+
totalValue += utxo.value;
|
|
179
|
+
|
|
180
|
+
// Calculate fee with current selection (2 outputs: recipient + change)
|
|
181
|
+
const estimatedVBytes =
|
|
182
|
+
baseVBytes + selectedUtxos.length * inputVBytes + 2 * outputVBytes;
|
|
183
|
+
const estimatedFee = Math.ceil(estimatedVBytes * feeRate);
|
|
184
|
+
|
|
185
|
+
if (totalValue >= amount + estimatedFee) {
|
|
186
|
+
return { selectedUtxos, totalValue, estimatedFee };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// If we get here, we don't have enough funds
|
|
191
|
+
// Return what we have and let the caller handle the error
|
|
192
|
+
const estimatedVBytes =
|
|
193
|
+
baseVBytes + selectedUtxos.length * inputVBytes + 2 * outputVBytes;
|
|
194
|
+
const estimatedFee = Math.ceil(estimatedVBytes * feeRate);
|
|
195
|
+
|
|
196
|
+
return { selectedUtxos, totalValue, estimatedFee };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Generate a PSBT for sending Bitcoin
|
|
201
|
+
*
|
|
202
|
+
* @param toAddress - Recipient address
|
|
203
|
+
* @param amount - Amount to send in satoshis
|
|
204
|
+
* @param fromAddress - Sender address
|
|
205
|
+
* @param publicKey - Sender's public key (hex string)
|
|
206
|
+
* @param network - Bitcoin network
|
|
207
|
+
* @param options - Additional options (feeRate, feeRateMultiplier)
|
|
208
|
+
* @returns GeneratePsbtResult containing the PSBT and related info
|
|
209
|
+
*/
|
|
210
|
+
export async function generatePsbtForSend(
|
|
211
|
+
toAddress: string,
|
|
212
|
+
amount: number,
|
|
213
|
+
fromAddress: string,
|
|
214
|
+
publicKey: string,
|
|
215
|
+
network: BitcoinNetwork,
|
|
216
|
+
options?: GeneratePsbtOptions
|
|
217
|
+
): Promise<GeneratePsbtResult> {
|
|
218
|
+
const btcService = new BtcService(network);
|
|
219
|
+
const btcNetwork = getBitcoinJsNetwork(network);
|
|
220
|
+
|
|
221
|
+
// 1. Get UTXOs (only confirmed)
|
|
222
|
+
const allUtxos = await btcService.getUtxos(fromAddress);
|
|
223
|
+
const confirmedUtxos = allUtxos.filter(
|
|
224
|
+
(utxo) => utxo.status?.confirmed !== false
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (confirmedUtxos.length === 0) {
|
|
228
|
+
throw new Error('No confirmed UTXOs available for spending');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 2. Get fee rate
|
|
232
|
+
let feeRate = options?.feeRate;
|
|
233
|
+
if (!feeRate) {
|
|
234
|
+
const feeRates = await btcService.getFeeRates();
|
|
235
|
+
feeRate = feeRates.hour;
|
|
236
|
+
}
|
|
237
|
+
if (options?.feeRateMultiplier) {
|
|
238
|
+
feeRate = Math.ceil(feeRate * options.feeRateMultiplier);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 3. Get address types
|
|
242
|
+
const fromAddressType = getAddressType(fromAddress);
|
|
243
|
+
const toAddressType = getAddressType(toAddress);
|
|
244
|
+
|
|
245
|
+
// 4. Select UTXOs
|
|
246
|
+
const { selectedUtxos, totalValue, estimatedFee } = selectUtxos(
|
|
247
|
+
confirmedUtxos,
|
|
248
|
+
fromAddressType,
|
|
249
|
+
toAddressType,
|
|
250
|
+
amount,
|
|
251
|
+
feeRate
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (totalValue < amount + estimatedFee) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Insufficient funds. Available: ${totalValue} sats, Required: ${amount + estimatedFee} sats (including fee)`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 5. Calculate change
|
|
261
|
+
const dustThreshold = getDustThreshold(fromAddressType);
|
|
262
|
+
let changeAmount = totalValue - amount - estimatedFee;
|
|
263
|
+
|
|
264
|
+
// If change is below dust threshold, add it to fee
|
|
265
|
+
if (changeAmount > 0 && changeAmount <= dustThreshold) {
|
|
266
|
+
changeAmount = 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 6. Build PSBT
|
|
270
|
+
const psbt = new bitcoin.Psbt({ network: btcNetwork });
|
|
271
|
+
const publicKeyBytes = hexToBytes(publicKey);
|
|
272
|
+
const inputsToSign: Array<{ address: string; index: number }> = [];
|
|
273
|
+
|
|
274
|
+
// Add inputs
|
|
275
|
+
for (let i = 0; i < selectedUtxos.length; i++) {
|
|
276
|
+
const utxo = selectedUtxos[i]!;
|
|
277
|
+
const script = bitcoin.address.toOutputScript(fromAddress, btcNetwork);
|
|
278
|
+
|
|
279
|
+
if (fromAddressType === 'taproot') {
|
|
280
|
+
// P2TR - Taproot
|
|
281
|
+
psbt.addInput({
|
|
282
|
+
hash: utxo.txid,
|
|
283
|
+
index: utxo.vout,
|
|
284
|
+
witnessUtxo: {
|
|
285
|
+
script,
|
|
286
|
+
value: BigInt(utxo.value),
|
|
287
|
+
},
|
|
288
|
+
tapInternalKey: toXOnly(publicKeyBytes),
|
|
289
|
+
});
|
|
290
|
+
} else if (fromAddressType === 'segwit') {
|
|
291
|
+
// P2WPKH - Native SegWit
|
|
292
|
+
psbt.addInput({
|
|
293
|
+
hash: utxo.txid,
|
|
294
|
+
index: utxo.vout,
|
|
295
|
+
witnessUtxo: {
|
|
296
|
+
script,
|
|
297
|
+
value: BigInt(utxo.value),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
} else if (fromAddressType === 'nested-segwit') {
|
|
301
|
+
// P2SH-P2WPKH - Nested SegWit
|
|
302
|
+
const p2wpkh = bitcoin.payments.p2wpkh({
|
|
303
|
+
pubkey: publicKeyBytes,
|
|
304
|
+
network: btcNetwork,
|
|
305
|
+
});
|
|
306
|
+
if (!p2wpkh.output) {
|
|
307
|
+
throw new Error('Failed to generate P2WPKH redeem script');
|
|
308
|
+
}
|
|
309
|
+
psbt.addInput({
|
|
310
|
+
hash: utxo.txid,
|
|
311
|
+
index: utxo.vout,
|
|
312
|
+
witnessUtxo: {
|
|
313
|
+
script,
|
|
314
|
+
value: BigInt(utxo.value),
|
|
315
|
+
},
|
|
316
|
+
redeemScript: p2wpkh.output,
|
|
317
|
+
});
|
|
318
|
+
} else {
|
|
319
|
+
// Legacy P2PKH
|
|
320
|
+
psbt.addInput({
|
|
321
|
+
hash: utxo.txid,
|
|
322
|
+
index: utxo.vout,
|
|
323
|
+
witnessUtxo: {
|
|
324
|
+
script,
|
|
325
|
+
value: BigInt(utxo.value),
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
inputsToSign.push({ address: fromAddress, index: i });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Add recipient output
|
|
334
|
+
psbt.addOutput({
|
|
335
|
+
address: toAddress,
|
|
336
|
+
value: BigInt(amount),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Add change output if needed
|
|
340
|
+
if (changeAmount > 0) {
|
|
341
|
+
psbt.addOutput({
|
|
342
|
+
address: fromAddress,
|
|
343
|
+
value: BigInt(Math.floor(changeAmount)),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
psbt,
|
|
349
|
+
psbtHex: psbt.toHex(),
|
|
350
|
+
psbtBase64: psbt.toBase64(),
|
|
351
|
+
btcNetwork,
|
|
352
|
+
selectedUtxos,
|
|
353
|
+
totalInputValue: totalValue,
|
|
354
|
+
estimatedFee,
|
|
355
|
+
changeAmount,
|
|
356
|
+
inputsToSign,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Finalize all inputs in a signed PSBT
|
|
362
|
+
*/
|
|
363
|
+
export function finalizeAllInputs(
|
|
364
|
+
signedPsbt: bitcoin.Psbt,
|
|
365
|
+
inputCount: number
|
|
366
|
+
): void {
|
|
367
|
+
for (let i = 0; i < inputCount; i++) {
|
|
368
|
+
try {
|
|
369
|
+
signedPsbt.finalizeInput(i);
|
|
370
|
+
} catch (finalizeError) {
|
|
371
|
+
console.warn(
|
|
372
|
+
`Failed to finalize input ${i}, attempting alternative finalization:`,
|
|
373
|
+
finalizeError
|
|
374
|
+
);
|
|
375
|
+
// For complex input types, try finalization with empty witness
|
|
376
|
+
try {
|
|
377
|
+
signedPsbt.finalizeInput(i, () => ({
|
|
378
|
+
finalScriptSig: undefined,
|
|
379
|
+
finalScriptWitness: undefined,
|
|
380
|
+
}));
|
|
381
|
+
} catch {
|
|
382
|
+
// If that also fails, just skip - the transaction might still work
|
|
383
|
+
console.warn(`Could not finalize input ${i}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Extract and broadcast a signed PSBT
|
|
391
|
+
*
|
|
392
|
+
* @param signedPsbtHex - Signed PSBT hex string
|
|
393
|
+
* @param network - Bitcoin network
|
|
394
|
+
* @param inputCount - Number of inputs to finalize
|
|
395
|
+
* @returns Transaction ID
|
|
396
|
+
*/
|
|
397
|
+
export async function finalizeAndBroadcast(
|
|
398
|
+
signedPsbtHex: string,
|
|
399
|
+
network: BitcoinNetwork,
|
|
400
|
+
inputCount: number
|
|
401
|
+
): Promise<string> {
|
|
402
|
+
const btcNetwork = getBitcoinJsNetwork(network);
|
|
403
|
+
const btcService = new BtcService(network);
|
|
404
|
+
|
|
405
|
+
// Parse signed PSBT
|
|
406
|
+
const signedPsbt = bitcoin.Psbt.fromHex(signedPsbtHex, { network: btcNetwork });
|
|
407
|
+
|
|
408
|
+
// Finalize all inputs
|
|
409
|
+
finalizeAllInputs(signedPsbt, inputCount);
|
|
410
|
+
|
|
411
|
+
// Extract transaction
|
|
412
|
+
const tx = signedPsbt.extractTransaction();
|
|
413
|
+
const txHex = tx.toHex();
|
|
414
|
+
|
|
415
|
+
// Broadcast
|
|
416
|
+
return await btcService.broadcastTransaction(txHex);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Derive Bitcoin address from public key based on address type
|
|
421
|
+
*
|
|
422
|
+
* @param publicKeyHex - Public key in hex format (33 bytes compressed)
|
|
423
|
+
* @param addressType - Type of address to derive (legacy, nested-segwit, segwit, taproot)
|
|
424
|
+
* @param network - Bitcoin network
|
|
425
|
+
* @returns Derived Bitcoin address
|
|
426
|
+
*/
|
|
427
|
+
export function deriveAddressFromPublicKey(
|
|
428
|
+
publicKeyHex: string,
|
|
429
|
+
addressType: AddressType,
|
|
430
|
+
network: BitcoinNetwork
|
|
431
|
+
): string {
|
|
432
|
+
const btcNetwork = getBitcoinJsNetwork(network);
|
|
433
|
+
const publicKeyBytes = hexToBytes(publicKeyHex);
|
|
434
|
+
|
|
435
|
+
switch (addressType) {
|
|
436
|
+
case 'legacy': {
|
|
437
|
+
// P2PKH - Legacy address (starts with 1 or m/n)
|
|
438
|
+
const p2pkh = bitcoin.payments.p2pkh({
|
|
439
|
+
pubkey: publicKeyBytes,
|
|
440
|
+
network: btcNetwork,
|
|
441
|
+
});
|
|
442
|
+
if (!p2pkh.address) {
|
|
443
|
+
throw new Error('Failed to derive legacy address from public key');
|
|
444
|
+
}
|
|
445
|
+
return p2pkh.address;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
case 'nested-segwit': {
|
|
449
|
+
// P2SH-P2WPKH - Nested SegWit (starts with 3 or 2)
|
|
450
|
+
const p2wpkh = bitcoin.payments.p2wpkh({
|
|
451
|
+
pubkey: publicKeyBytes,
|
|
452
|
+
network: btcNetwork,
|
|
453
|
+
});
|
|
454
|
+
const p2sh = bitcoin.payments.p2sh({
|
|
455
|
+
redeem: p2wpkh,
|
|
456
|
+
network: btcNetwork,
|
|
457
|
+
});
|
|
458
|
+
if (!p2sh.address) {
|
|
459
|
+
throw new Error('Failed to derive nested-segwit address from public key');
|
|
460
|
+
}
|
|
461
|
+
return p2sh.address;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
case 'segwit': {
|
|
465
|
+
// P2WPKH - Native SegWit (starts with bc1q or tb1q)
|
|
466
|
+
const p2wpkh = bitcoin.payments.p2wpkh({
|
|
467
|
+
pubkey: publicKeyBytes,
|
|
468
|
+
network: btcNetwork,
|
|
469
|
+
});
|
|
470
|
+
if (!p2wpkh.address) {
|
|
471
|
+
throw new Error('Failed to derive segwit address from public key');
|
|
472
|
+
}
|
|
473
|
+
return p2wpkh.address;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
case 'taproot': {
|
|
477
|
+
// P2TR - Taproot (starts with bc1p or tb1p)
|
|
478
|
+
const xOnlyPubKey = toXOnly(publicKeyBytes);
|
|
479
|
+
const p2tr = bitcoin.payments.p2tr({
|
|
480
|
+
internalPubkey: xOnlyPubKey,
|
|
481
|
+
network: btcNetwork,
|
|
482
|
+
});
|
|
483
|
+
if (!p2tr.address) {
|
|
484
|
+
throw new Error('Failed to derive taproot address from public key');
|
|
485
|
+
}
|
|
486
|
+
return p2tr.address;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
default:
|
|
490
|
+
throw new Error(`Unsupported address type: ${addressType}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { BitcoinNetwork } from 'otx-btc-wallet-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UTXO (Unspent Transaction Output) interface
|
|
5
|
+
*/
|
|
6
|
+
export interface Utxo {
|
|
7
|
+
txid: string;
|
|
8
|
+
vout: number;
|
|
9
|
+
value: number; // in satoshis
|
|
10
|
+
status: {
|
|
11
|
+
confirmed: boolean;
|
|
12
|
+
block_height?: number;
|
|
13
|
+
block_hash?: string;
|
|
14
|
+
block_time?: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* UTXO with raw transaction hex (needed for Ledger signing)
|
|
20
|
+
*/
|
|
21
|
+
export interface UtxoWithTx extends Utxo {
|
|
22
|
+
txHex: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fee rates response
|
|
27
|
+
*/
|
|
28
|
+
export interface FeeRates {
|
|
29
|
+
fastest: number;
|
|
30
|
+
halfHour: number;
|
|
31
|
+
hour: number;
|
|
32
|
+
economy: number;
|
|
33
|
+
minimum: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Transaction details
|
|
38
|
+
*/
|
|
39
|
+
export interface Transaction {
|
|
40
|
+
txid: string;
|
|
41
|
+
version: number;
|
|
42
|
+
locktime: number;
|
|
43
|
+
size: number;
|
|
44
|
+
weight: number;
|
|
45
|
+
fee: number;
|
|
46
|
+
status: {
|
|
47
|
+
confirmed: boolean;
|
|
48
|
+
block_height?: number;
|
|
49
|
+
block_hash?: string;
|
|
50
|
+
block_time?: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Full transaction with inputs and outputs (for Trezor refTxs)
|
|
56
|
+
*/
|
|
57
|
+
export interface FullTransaction extends Transaction {
|
|
58
|
+
vin: Array<{
|
|
59
|
+
txid: string;
|
|
60
|
+
vout: number;
|
|
61
|
+
sequence: number;
|
|
62
|
+
scriptsig: string;
|
|
63
|
+
scriptsig_asm?: string;
|
|
64
|
+
witness?: string[];
|
|
65
|
+
prevout?: {
|
|
66
|
+
scriptpubkey: string;
|
|
67
|
+
scriptpubkey_asm?: string;
|
|
68
|
+
scriptpubkey_type?: string;
|
|
69
|
+
scriptpubkey_address?: string;
|
|
70
|
+
value: number;
|
|
71
|
+
};
|
|
72
|
+
}>;
|
|
73
|
+
vout: Array<{
|
|
74
|
+
scriptpubkey: string;
|
|
75
|
+
scriptpubkey_asm?: string;
|
|
76
|
+
scriptpubkey_type?: string;
|
|
77
|
+
scriptpubkey_address?: string;
|
|
78
|
+
value: number;
|
|
79
|
+
}>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Address balance
|
|
84
|
+
*/
|
|
85
|
+
export interface AddressBalance {
|
|
86
|
+
address: string;
|
|
87
|
+
chain_stats: {
|
|
88
|
+
funded_txo_count: number;
|
|
89
|
+
funded_txo_sum: number;
|
|
90
|
+
spent_txo_count: number;
|
|
91
|
+
spent_txo_sum: number;
|
|
92
|
+
tx_count: number;
|
|
93
|
+
};
|
|
94
|
+
mempool_stats: {
|
|
95
|
+
funded_txo_count: number;
|
|
96
|
+
funded_txo_sum: number;
|
|
97
|
+
spent_txo_count: number;
|
|
98
|
+
spent_txo_sum: number;
|
|
99
|
+
tx_count: number;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Bitcoin service interface that both Mempool and Blockstream services implement
|
|
105
|
+
*/
|
|
106
|
+
export interface IBtcService {
|
|
107
|
+
readonly name: string;
|
|
108
|
+
readonly network: BitcoinNetwork;
|
|
109
|
+
|
|
110
|
+
setNetwork(network: BitcoinNetwork): void;
|
|
111
|
+
|
|
112
|
+
// UTXO methods
|
|
113
|
+
getUtxos(address: string): Promise<Utxo[]>;
|
|
114
|
+
getUtxosWithTxHex(address: string): Promise<UtxoWithTx[]>;
|
|
115
|
+
|
|
116
|
+
// Transaction methods
|
|
117
|
+
getTxHex(txid: string): Promise<string>;
|
|
118
|
+
getTransaction(txid: string): Promise<Transaction>;
|
|
119
|
+
getFullTransaction(txid: string): Promise<FullTransaction>;
|
|
120
|
+
broadcastTransaction(txHex: string): Promise<string>;
|
|
121
|
+
|
|
122
|
+
// Address methods
|
|
123
|
+
getBalance(address: string): Promise<number>;
|
|
124
|
+
getConfirmedBalance(address: string): Promise<number>;
|
|
125
|
+
getAddressInfo(address: string): Promise<AddressBalance>;
|
|
126
|
+
|
|
127
|
+
// Fee methods
|
|
128
|
+
getFeeRates(): Promise<FeeRates>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Network-specific endpoint URLs
|
|
133
|
+
*/
|
|
134
|
+
export interface NetworkEndpoints {
|
|
135
|
+
mainnet?: string;
|
|
136
|
+
testnet?: string;
|
|
137
|
+
testnet4?: string;
|
|
138
|
+
signet?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Configuration for BtcService endpoints
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* // Use custom mempool instance
|
|
147
|
+
* const config: BtcServiceConfig = {
|
|
148
|
+
* mempool: {
|
|
149
|
+
* mainnet: 'https://my-mempool.com/api',
|
|
150
|
+
* testnet: 'https://my-mempool.com/testnet/api',
|
|
151
|
+
* }
|
|
152
|
+
* };
|
|
153
|
+
*
|
|
154
|
+
* // Or use only one provider
|
|
155
|
+
* const config: BtcServiceConfig = {
|
|
156
|
+
* preferredProvider: 'mempool',
|
|
157
|
+
* mempool: {
|
|
158
|
+
* mainnet: 'https://custom-mempool.example.com/api',
|
|
159
|
+
* }
|
|
160
|
+
* };
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export interface BtcServiceConfig {
|
|
164
|
+
/**
|
|
165
|
+
* Custom Mempool.space API endpoints
|
|
166
|
+
* Default: https://mempool.space/api (mainnet)
|
|
167
|
+
*/
|
|
168
|
+
mempool?: NetworkEndpoints;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Custom Blockstream.info API endpoints
|
|
172
|
+
* Default: https://blockstream.info/api (mainnet)
|
|
173
|
+
*/
|
|
174
|
+
blockstream?: NetworkEndpoints;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Preferred provider to use ('mempool' | 'blockstream' | 'race')
|
|
178
|
+
* - 'mempool': Only use Mempool API
|
|
179
|
+
* - 'blockstream': Only use Blockstream API
|
|
180
|
+
* - 'race': Use both and return fastest response (default)
|
|
181
|
+
*/
|
|
182
|
+
preferredProvider?: 'mempool' | 'blockstream' | 'race';
|
|
183
|
+
}
|