chroid 1.0.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 +2 -0
- package/cli/bitcoin.js +207 -0
- package/cli/evm.js +214 -0
- package/cli/tron.js +91 -0
- package/index.js +17 -0
- package/package.json +30 -0
- package/utils/askForPassword.js +23 -0
- package/utils/bitcoin.js +746 -0
- package/utils/const.js +8 -0
- package/utils/evm.js +425 -0
- package/utils/tron.js +163 -0
package/utils/bitcoin.js
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
const bitcoin = require("bitcoinjs-lib");
|
|
2
|
+
const { ECPairFactory } = require("ecpair");
|
|
3
|
+
const ecc = require("tiny-secp256k1");
|
|
4
|
+
bitcoin.initEccLib(ecc)
|
|
5
|
+
const ECPair = ECPairFactory(ecc)
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const {BIP32Factory} = require('bip32')
|
|
8
|
+
const bip39 = require('bip39')
|
|
9
|
+
const bip32 = BIP32Factory(ecc)
|
|
10
|
+
|
|
11
|
+
const network = bitcoin.networks.bitcoin
|
|
12
|
+
const MEMPOOL_URL = `https://mempool.space`
|
|
13
|
+
|
|
14
|
+
// configuration for testnet
|
|
15
|
+
// const network = bitcoin.networks.testnet
|
|
16
|
+
// const MEMPOOL_URL = `https://mempool.space/testnet`
|
|
17
|
+
|
|
18
|
+
const DUST_LIMIT = 546
|
|
19
|
+
const BASE_TX_SIZE = 10
|
|
20
|
+
|
|
21
|
+
const BitcoinAddressType = {
|
|
22
|
+
Legacy: 'legacy',
|
|
23
|
+
NestedSegwit: 'nested-segwit',
|
|
24
|
+
NativeSegwit: 'native-segwit',
|
|
25
|
+
Taproot: 'taproot',
|
|
26
|
+
Invalid: 'invalid'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const LEGACY_TX_INPUT_SIZE = 148
|
|
30
|
+
const LEGACY_TX_OUTPUT_SIZE = 34
|
|
31
|
+
const NESTED_SEGWIT_TX_INPUT_SIZE = 91
|
|
32
|
+
const NESTED_SEGWIT_TX_OUTPUT_SIZE = 31
|
|
33
|
+
const NATIVE_SEGWIT_TX_INPUT_SIZE = 68
|
|
34
|
+
const NATIVE_SEGWIT_TX_OUTPUT_SIZE = 31
|
|
35
|
+
const TAPROOT_TX_INPUT_SIZE = 58
|
|
36
|
+
const TAPROOT_TX_OUTPUT_SIZE = 43
|
|
37
|
+
|
|
38
|
+
// Function to fetch unspent transaction outputs (UTXOs) for the fromAddress
|
|
39
|
+
async function getUTXOs(address) {
|
|
40
|
+
const url = `${MEMPOOL_URL}/api/address/${address}/utxo`
|
|
41
|
+
const response = await fetch(url)
|
|
42
|
+
if (response.ok) {
|
|
43
|
+
const utxo_array = await response.json()
|
|
44
|
+
let confirmed = [], unconfirmed = []
|
|
45
|
+
for (const i in utxo_array)
|
|
46
|
+
utxo_array[i]['status']['confirmed'] ? confirmed.push(utxo_array[i]) : unconfirmed.push(utxo_array[i])
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
confirmed: utxo_array.filter((elem) => elem?.status?.confirmed) || [],
|
|
50
|
+
unconfirmed: utxo_array.filter((elem) => !elem?.status?.confirmed) || []
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
confirmed: [],
|
|
57
|
+
unconfirmed: []
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Function to get confirmed total balance of a bitcoin address
|
|
63
|
+
async function getConfirmedBalanceFromAddress(address) {
|
|
64
|
+
const { confirmed } = await getUTXOs(address)
|
|
65
|
+
let totalBalance = 0
|
|
66
|
+
for (const i in confirmed)
|
|
67
|
+
totalBalance += parseInt(confirmed[i]['value'])
|
|
68
|
+
return totalBalance
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Function to get current mempool status
|
|
72
|
+
async function getSatsbyte() {
|
|
73
|
+
const url = `${MEMPOOL_URL}/api/v1/fees/recommended`
|
|
74
|
+
const response = await fetch(url)
|
|
75
|
+
if (response.ok) {
|
|
76
|
+
const recommendedFees = await response.json()
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
recommendedFees
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
recommendedFees: {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Function to determine what type of address it is among 4 bitcoin address types
|
|
91
|
+
function getBitcoinAddressType(address) {
|
|
92
|
+
// Regular expressions for different Bitcoin address types
|
|
93
|
+
const legacyRegex = network === bitcoin.networks.bitcoin ? /^[1][a-km-zA-HJ-NP-Z1-9]{25,34}$/ : /^[m,n][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
|
|
94
|
+
const nestedSegwitRegex = network === bitcoin.networks.bitcoin ? /^[3][a-km-zA-HJ-NP-Z1-9]{25,34}$/ : /^[2][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
|
|
95
|
+
const nativeSegwitRegex = network === bitcoin.networks.bitcoin ? /^(bc1q)[0-9a-z]{35,59}$/ : /^(tb1q)[0-9a-z]{35,59}$/;
|
|
96
|
+
const taprootRegex = network === bitcoin.networks.bitcoin ? /^(bc1p)[0-9a-z]{39,59}$/ : /^(tb1p)[0-9a-z]{39,59}$/;
|
|
97
|
+
|
|
98
|
+
if (legacyRegex.test(address)) {
|
|
99
|
+
return BitcoinAddressType.Legacy;
|
|
100
|
+
} else if (nestedSegwitRegex.test(address)) {
|
|
101
|
+
return BitcoinAddressType.NestedSegwit;
|
|
102
|
+
} else if (nativeSegwitRegex.test(address)) {
|
|
103
|
+
return BitcoinAddressType.NativeSegwit;
|
|
104
|
+
} else if (taprootRegex.test(address)) {
|
|
105
|
+
return BitcoinAddressType.Taproot;
|
|
106
|
+
} else {
|
|
107
|
+
return BitcoinAddressType.Invalid;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getAddressFromWIFandType(wif, type) {
|
|
112
|
+
const keyPair = ECPair.fromWIF(wif);
|
|
113
|
+
|
|
114
|
+
if (type === BitcoinAddressType.Legacy)
|
|
115
|
+
return bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network }).address
|
|
116
|
+
else if (type === BitcoinAddressType.NestedSegwit)
|
|
117
|
+
return bitcoin.payments.p2sh({
|
|
118
|
+
redeem: bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network }),
|
|
119
|
+
network
|
|
120
|
+
}).address;
|
|
121
|
+
else if (type === BitcoinAddressType.NativeSegwit)
|
|
122
|
+
return bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network }).address
|
|
123
|
+
else if (type === BitcoinAddressType.Taproot)
|
|
124
|
+
return bitcoin.payments.p2tr({
|
|
125
|
+
internalPubkey: toXOnly(keyPair.publicKey),
|
|
126
|
+
network
|
|
127
|
+
}).address;
|
|
128
|
+
else
|
|
129
|
+
return "invalid"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function toXOnly (publicKey) {
|
|
133
|
+
return publicKey.slice(1, 33);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getKeypairInfo (childNode) {
|
|
137
|
+
const childNodeXOnlyPubkey = toXOnly(childNode.publicKey);
|
|
138
|
+
|
|
139
|
+
const { address, output } = bitcoin.payments.p2tr({
|
|
140
|
+
internalPubkey: childNodeXOnlyPubkey,
|
|
141
|
+
network
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const tweakedChildNode = childNode.tweak(
|
|
145
|
+
bitcoin.crypto.taggedHash('TapTweak', childNodeXOnlyPubkey),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
address,
|
|
150
|
+
tweakedChildNode,
|
|
151
|
+
childNodeXOnlyPubkey,
|
|
152
|
+
output,
|
|
153
|
+
childNode
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Function to estimate transaction size from input utxos and output utxos and the address type
|
|
158
|
+
function estimateTransactionSize(numInputs, numOutputs, type) {
|
|
159
|
+
let inputSize, outputSize
|
|
160
|
+
|
|
161
|
+
switch (type) {
|
|
162
|
+
case BitcoinAddressType.Legacy:
|
|
163
|
+
inputSize = LEGACY_TX_INPUT_SIZE;
|
|
164
|
+
outputSize = LEGACY_TX_OUTPUT_SIZE;
|
|
165
|
+
break;
|
|
166
|
+
case BitcoinAddressType.NestedSegwit:
|
|
167
|
+
inputSize = NESTED_SEGWIT_TX_INPUT_SIZE;
|
|
168
|
+
outputSize = NESTED_SEGWIT_TX_OUTPUT_SIZE;
|
|
169
|
+
break;
|
|
170
|
+
case BitcoinAddressType.NativeSegwit:
|
|
171
|
+
inputSize = NATIVE_SEGWIT_TX_INPUT_SIZE;
|
|
172
|
+
outputSize = NATIVE_SEGWIT_TX_OUTPUT_SIZE;
|
|
173
|
+
break;
|
|
174
|
+
case BitcoinAddressType.Taproot:
|
|
175
|
+
inputSize = TAPROOT_TX_INPUT_SIZE;
|
|
176
|
+
outputSize = TAPROOT_TX_OUTPUT_SIZE;
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
throw new Error('Unknown transaction type');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return BASE_TX_SIZE + (numInputs * inputSize) + (numOutputs * outputSize);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function estimateTransactionFee(numInputs, numOutputs, type, feeRate) {
|
|
186
|
+
const txSize = estimateTransactionSize(numInputs, numOutputs, type);
|
|
187
|
+
return txSize * feeRate;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function getTransactionDetailFromTxID(txid) {
|
|
191
|
+
const url = `${MEMPOOL_URL}/api/tx/${txid}/hex`
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetch(url)
|
|
194
|
+
if (response.ok) {
|
|
195
|
+
const hex = await response.text()
|
|
196
|
+
const txDetail = bitcoin.Transaction.fromHex(hex)
|
|
197
|
+
return {
|
|
198
|
+
hex,
|
|
199
|
+
txDetail
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.log(error)
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
hex: "",
|
|
207
|
+
txDetail: {}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Function to send bitcoin from one address to another
|
|
212
|
+
// returns the txid when success, error msg when error
|
|
213
|
+
async function sendBtc(fromAddressPair, toAddress, amountInSats, satsbyte) {
|
|
214
|
+
|
|
215
|
+
// validate address types
|
|
216
|
+
const { address: fromAddress, wif: fromWIF } = fromAddressPair
|
|
217
|
+
const fromAddressType = getBitcoinAddressType(fromAddress)
|
|
218
|
+
if (fromAddressType === BitcoinAddressType.Invalid)
|
|
219
|
+
throw "Invalid Source Address"
|
|
220
|
+
|
|
221
|
+
const toAddressType = getBitcoinAddressType(toAddress)
|
|
222
|
+
if (toAddressType === BitcoinAddressType.Invalid)
|
|
223
|
+
throw "Invalid Destination Address"
|
|
224
|
+
|
|
225
|
+
// first check if that address holds such balance
|
|
226
|
+
const currentBalance = await getConfirmedBalanceFromAddress(fromAddress)
|
|
227
|
+
if (amountInSats >= currentBalance)
|
|
228
|
+
throw "Insufficient balance"
|
|
229
|
+
|
|
230
|
+
// check if fromWIF matches the fromAddress
|
|
231
|
+
const checkingFromAddress = getAddressFromWIFandType(fromWIF, fromAddressType);
|
|
232
|
+
if (fromAddress !== checkingFromAddress)
|
|
233
|
+
throw "Source Address does not match WIF"
|
|
234
|
+
|
|
235
|
+
// now building transactions based on address types
|
|
236
|
+
const keyPair = ECPair.fromWIF(fromAddressPair.wif);
|
|
237
|
+
const keyPairInfo = getKeypairInfo(keyPair)
|
|
238
|
+
const { confirmed } = await getUTXOs(fromAddress)
|
|
239
|
+
const sortedUTXOs = confirmed.sort((a, b) => parseInt(a.value) - parseInt(b.value))
|
|
240
|
+
|
|
241
|
+
const fastestFee = satsbyte
|
|
242
|
+
|
|
243
|
+
// build transaction
|
|
244
|
+
const psbt = new bitcoin.Psbt({ network });
|
|
245
|
+
let totalInputSats = 0, inputUtxoCount = 0
|
|
246
|
+
let estimatedTransactionFee = estimateTransactionFee(1, 1, toAddressType, fastestFee)
|
|
247
|
+
let inputsAreEnough = false
|
|
248
|
+
for (const i in sortedUTXOs) {
|
|
249
|
+
const { txid, vout, value } = sortedUTXOs[i]
|
|
250
|
+
const { hex, txDetail } = await getTransactionDetailFromTxID(txid)
|
|
251
|
+
if (!hex) {
|
|
252
|
+
return {
|
|
253
|
+
success: false,
|
|
254
|
+
result: `cannot find proper hex for transaction ${txid}`
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const input = {
|
|
258
|
+
hash: txid,
|
|
259
|
+
index: vout
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (fromAddressType === BitcoinAddressType.Legacy)
|
|
263
|
+
input.nonWitnessUtxo = Buffer.from(hex, 'hex');
|
|
264
|
+
if (fromAddressType === BitcoinAddressType.NestedSegwit) {
|
|
265
|
+
input.witnessUtxo = {
|
|
266
|
+
script: txDetail.outs[vout].script,
|
|
267
|
+
value: txDetail.outs[vout].value,
|
|
268
|
+
}
|
|
269
|
+
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network });
|
|
270
|
+
input.redeemScript = p2wpkh.output
|
|
271
|
+
}
|
|
272
|
+
if (fromAddressType === BitcoinAddressType.NativeSegwit)
|
|
273
|
+
input.witnessUtxo = {
|
|
274
|
+
script: txDetail.outs[vout].script,
|
|
275
|
+
value: txDetail.outs[vout].value,
|
|
276
|
+
};
|
|
277
|
+
if (fromAddressType === BitcoinAddressType.Taproot) {
|
|
278
|
+
input.witnessUtxo = {
|
|
279
|
+
script: txDetail.outs[vout].script,
|
|
280
|
+
value: txDetail.outs[vout].value,
|
|
281
|
+
};
|
|
282
|
+
input.tapInternalKey = keyPairInfo.childNodeXOnlyPubkey
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
psbt.addInput(input)
|
|
286
|
+
inputUtxoCount ++
|
|
287
|
+
totalInputSats += value
|
|
288
|
+
estimatedTransactionFee = estimateTransactionFee(inputUtxoCount, 2, toAddressType, fastestFee)
|
|
289
|
+
|
|
290
|
+
if (totalInputSats >= amountInSats + estimatedTransactionFee) {
|
|
291
|
+
inputsAreEnough = true
|
|
292
|
+
psbt.addOutput({
|
|
293
|
+
address: toAddress,
|
|
294
|
+
value: amountInSats
|
|
295
|
+
})
|
|
296
|
+
if (totalInputSats - amountInSats - estimatedTransactionFee > DUST_LIMIT) {
|
|
297
|
+
psbt.addOutput({
|
|
298
|
+
address: fromAddress,
|
|
299
|
+
value: totalInputSats - amountInSats - estimatedTransactionFee
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
break
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!inputsAreEnough) {
|
|
307
|
+
return {
|
|
308
|
+
success: false,
|
|
309
|
+
result: "Input UTXOs are not enough to send..."
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (fromAddressType === BitcoinAddressType.Taproot) {
|
|
314
|
+
for (let i = 0; i < inputUtxoCount; i ++)
|
|
315
|
+
psbt.signInput(i, keyPairInfo.tweakedChildNode)
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
for (let i = 0; i < inputUtxoCount; i ++)
|
|
319
|
+
psbt.signInput(i, keyPairInfo.childNode)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
psbt.finalizeAllInputs()
|
|
323
|
+
|
|
324
|
+
const tx = psbt.extractTransaction()
|
|
325
|
+
const txHex = tx.toHex();
|
|
326
|
+
|
|
327
|
+
// broadcast the transaction
|
|
328
|
+
const broadcastAPI = `${MEMPOOL_URL}/api/tx`
|
|
329
|
+
const response = await fetch(broadcastAPI, {
|
|
330
|
+
method: "POST",
|
|
331
|
+
body: txHex,
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
if (response.ok) {
|
|
335
|
+
const transactionId = await response.text()
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
result: transactionId
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
throw 'Error while broadcast...'
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function sendBtcFromMultiSig(fromAddress, pwds, toAddress, amountInSats, satsbyte) {
|
|
346
|
+
const toAddressType = getBitcoinAddressType(toAddress)
|
|
347
|
+
if (toAddressType === BitcoinAddressType.Invalid)
|
|
348
|
+
throw "Invalid Destination Address"
|
|
349
|
+
|
|
350
|
+
// first check if that address holds such balance
|
|
351
|
+
const currentBalance = await getConfirmedBalanceFromAddress(fromAddress)
|
|
352
|
+
if (amountInSats >= currentBalance)
|
|
353
|
+
throw "Insufficient balance"
|
|
354
|
+
|
|
355
|
+
const mnemonic0 = generateMnemonicFromPassword(pwds[0])
|
|
356
|
+
const wif0 = await getWIFFromMnemonic(mnemonic0)
|
|
357
|
+
const keyPair0 = ECPair.fromWIF(wif0);
|
|
358
|
+
const mnemonic1 = generateMnemonicFromPassword(pwds[1])
|
|
359
|
+
const wif1 = await getWIFFromMnemonic(mnemonic1)
|
|
360
|
+
const keyPair1 = ECPair.fromWIF(wif1);
|
|
361
|
+
const mnemonic2 = generateMnemonicFromPassword(pwds[2])
|
|
362
|
+
const wif2 = await getWIFFromMnemonic(mnemonic2)
|
|
363
|
+
const keyPair2 = ECPair.fromWIF(wif2);
|
|
364
|
+
|
|
365
|
+
const keyPairs = [keyPair0, keyPair1, keyPair2]
|
|
366
|
+
const pubkeys = keyPairs.map(kp => kp.publicKey)
|
|
367
|
+
const p2ms = bitcoin.payments.p2ms({m: 3, pubkeys, network})
|
|
368
|
+
const p2sh = bitcoin.payments.p2sh({redeem: p2ms, network})
|
|
369
|
+
|
|
370
|
+
// now building transactions based on address types
|
|
371
|
+
const { confirmed } = await getUTXOs(fromAddress)
|
|
372
|
+
const sortedUTXOs = confirmed.sort((a, b) => parseInt(a.value) - parseInt(b.value))
|
|
373
|
+
|
|
374
|
+
const fastestFee = satsbyte * 4
|
|
375
|
+
|
|
376
|
+
// build transaction
|
|
377
|
+
const psbt = new bitcoin.Psbt({ network });
|
|
378
|
+
let totalInputSats = 0, inputUtxoCount = 0
|
|
379
|
+
let estimatedTransactionFee = estimateTransactionFee(1, 1, toAddressType, fastestFee)
|
|
380
|
+
let inputsAreEnough = false
|
|
381
|
+
for (const i in sortedUTXOs) {
|
|
382
|
+
if (inputsAreEnough)
|
|
383
|
+
break;
|
|
384
|
+
const { txid, vout, value } = sortedUTXOs[i]
|
|
385
|
+
const { hex } = await getTransactionDetailFromTxID(txid)
|
|
386
|
+
const input = {
|
|
387
|
+
hash: txid,
|
|
388
|
+
index: vout,
|
|
389
|
+
nonWitnessUtxo: Buffer.from(hex, 'hex'),
|
|
390
|
+
redeemScript: p2sh.redeem.output
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
psbt.addInput(input)
|
|
394
|
+
inputUtxoCount ++
|
|
395
|
+
totalInputSats += value
|
|
396
|
+
estimatedTransactionFee = estimateTransactionFee(inputUtxoCount, 2, toAddressType, fastestFee)
|
|
397
|
+
|
|
398
|
+
if (totalInputSats >= amountInSats + estimatedTransactionFee) {
|
|
399
|
+
inputsAreEnough = true
|
|
400
|
+
psbt.addOutput({
|
|
401
|
+
address: toAddress,
|
|
402
|
+
value: amountInSats
|
|
403
|
+
})
|
|
404
|
+
if (totalInputSats - amountInSats - estimatedTransactionFee > DUST_LIMIT)
|
|
405
|
+
psbt.addOutput({
|
|
406
|
+
address: fromAddress,
|
|
407
|
+
value: totalInputSats - amountInSats - estimatedTransactionFee
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!inputsAreEnough) {
|
|
413
|
+
return {
|
|
414
|
+
success: false,
|
|
415
|
+
result: "Input UTXOs are not enough to send..."
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (let i = 0; i < inputUtxoCount; i ++) {
|
|
420
|
+
psbt.signInput(i, keyPair0)
|
|
421
|
+
psbt.signInput(i, keyPair1)
|
|
422
|
+
psbt.signInput(i, keyPair2)
|
|
423
|
+
}
|
|
424
|
+
psbt.finalizeAllInputs()
|
|
425
|
+
|
|
426
|
+
const tx = psbt.extractTransaction()
|
|
427
|
+
const txHex = tx.toHex();
|
|
428
|
+
|
|
429
|
+
// broadcast the transaction
|
|
430
|
+
const broadcastAPI = `${MEMPOOL_URL}/api/tx`
|
|
431
|
+
const response = await fetch(broadcastAPI, {
|
|
432
|
+
method: "POST",
|
|
433
|
+
body: txHex,
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
if (response.ok) {
|
|
437
|
+
const transactionId = await response.text()
|
|
438
|
+
return {
|
|
439
|
+
success: true,
|
|
440
|
+
result: transactionId
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
throw 'Error while broadcast...'
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function drainMultiSigBtc(fromAddress, pwds, toAddress, satsbyte) {
|
|
448
|
+
const toAddressType = getBitcoinAddressType(toAddress)
|
|
449
|
+
if (toAddressType === BitcoinAddressType.Invalid)
|
|
450
|
+
throw "Invalid Destination Address"
|
|
451
|
+
|
|
452
|
+
// first check if that address holds such balance
|
|
453
|
+
const currentBalance = await getConfirmedBalanceFromAddress(fromAddress)
|
|
454
|
+
|
|
455
|
+
const mnemonic0 = generateMnemonicFromPassword(pwds[0])
|
|
456
|
+
const wif0 = await getWIFFromMnemonic(mnemonic0)
|
|
457
|
+
const keyPair0 = ECPair.fromWIF(wif0);
|
|
458
|
+
const mnemonic1 = generateMnemonicFromPassword(pwds[1])
|
|
459
|
+
const wif1 = await getWIFFromMnemonic(mnemonic1)
|
|
460
|
+
const keyPair1 = ECPair.fromWIF(wif1);
|
|
461
|
+
const mnemonic2 = generateMnemonicFromPassword(pwds[2])
|
|
462
|
+
const wif2 = await getWIFFromMnemonic(mnemonic2)
|
|
463
|
+
const keyPair2 = ECPair.fromWIF(wif2);
|
|
464
|
+
|
|
465
|
+
const keyPairs = [keyPair0, keyPair1, keyPair2]
|
|
466
|
+
const pubkeys = keyPairs.map(kp => kp.publicKey)
|
|
467
|
+
const p2ms = bitcoin.payments.p2ms({m: 3, pubkeys, network})
|
|
468
|
+
const p2sh = bitcoin.payments.p2sh({redeem: p2ms, network})
|
|
469
|
+
|
|
470
|
+
// now building transactions based on address types
|
|
471
|
+
const { confirmed } = await getUTXOs(fromAddress)
|
|
472
|
+
const sortedUTXOs = confirmed.sort((a, b) => parseInt(a.value) - parseInt(b.value))
|
|
473
|
+
|
|
474
|
+
const fastestFee = satsbyte * 4
|
|
475
|
+
|
|
476
|
+
// build transaction
|
|
477
|
+
const psbt = new bitcoin.Psbt({ network });
|
|
478
|
+
let totalInputSats = 0, inputUtxoCount = 0
|
|
479
|
+
let estimatedTransactionFee = estimateTransactionFee(1, 1, toAddressType, fastestFee)
|
|
480
|
+
let inputsAreEnough = false
|
|
481
|
+
for (const i in sortedUTXOs) {
|
|
482
|
+
const { txid, vout, value } = sortedUTXOs[i]
|
|
483
|
+
const { hex } = await getTransactionDetailFromTxID(txid)
|
|
484
|
+
const input = {
|
|
485
|
+
hash: txid,
|
|
486
|
+
index: vout,
|
|
487
|
+
nonWitnessUtxo: Buffer.from(hex, 'hex'),
|
|
488
|
+
redeemScript: p2sh.redeem.output
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
psbt.addInput(input)
|
|
492
|
+
inputUtxoCount ++
|
|
493
|
+
totalInputSats += value
|
|
494
|
+
estimatedTransactionFee = estimateTransactionFee(inputUtxoCount, 1, toAddressType, fastestFee)
|
|
495
|
+
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
psbt.addOutput({
|
|
499
|
+
address: toAddress,
|
|
500
|
+
value: totalInputSats - estimatedTransactionFee
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
for (let i = 0; i < inputUtxoCount; i ++) {
|
|
504
|
+
psbt.signInput(i, keyPair0)
|
|
505
|
+
psbt.signInput(i, keyPair1)
|
|
506
|
+
psbt.signInput(i, keyPair2)
|
|
507
|
+
}
|
|
508
|
+
psbt.finalizeAllInputs()
|
|
509
|
+
|
|
510
|
+
const tx = psbt.extractTransaction()
|
|
511
|
+
const txHex = tx.toHex();
|
|
512
|
+
|
|
513
|
+
// broadcast the transaction
|
|
514
|
+
const broadcastAPI = `${MEMPOOL_URL}/api/tx`
|
|
515
|
+
const response = await fetch(broadcastAPI, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
body: txHex,
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
if (response.ok) {
|
|
521
|
+
const transactionId = await response.text()
|
|
522
|
+
return {
|
|
523
|
+
success: true,
|
|
524
|
+
result: transactionId
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
throw 'Error while broadcast...'
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function drainBtc(fromAddressPair, toAddress, satsbyte) {
|
|
532
|
+
|
|
533
|
+
// validate address types
|
|
534
|
+
const { address: fromAddress, wif: fromWIF } = fromAddressPair
|
|
535
|
+
const fromAddressType = getBitcoinAddressType(fromAddress)
|
|
536
|
+
if (fromAddressType === BitcoinAddressType.Invalid)
|
|
537
|
+
throw "Invalid Source Address"
|
|
538
|
+
|
|
539
|
+
const toAddressType = getBitcoinAddressType(toAddress)
|
|
540
|
+
if (toAddressType === BitcoinAddressType.Invalid)
|
|
541
|
+
throw "Invalid Destination Address"
|
|
542
|
+
|
|
543
|
+
// check if fromWIF matches the fromAddress
|
|
544
|
+
const checkingFromAddress = getAddressFromWIFandType(fromWIF, fromAddressType);
|
|
545
|
+
if (fromAddress !== checkingFromAddress)
|
|
546
|
+
throw "Source Address does not match WIF"
|
|
547
|
+
|
|
548
|
+
// now building transactions based on address types
|
|
549
|
+
const keyPair = ECPair.fromWIF(fromAddressPair.wif);
|
|
550
|
+
const keyPairInfo = getKeypairInfo(keyPair)
|
|
551
|
+
const { confirmed } = await getUTXOs(fromAddress)
|
|
552
|
+
const sortedUTXOs = confirmed.sort((a, b) => parseInt(a.value) - parseInt(b.value))
|
|
553
|
+
|
|
554
|
+
const fastestFee = satsbyte
|
|
555
|
+
|
|
556
|
+
// build transaction
|
|
557
|
+
const psbt = new bitcoin.Psbt({ network });
|
|
558
|
+
let totalInputSats = 0, inputUtxoCount = 0
|
|
559
|
+
let inputsAreEnough = false
|
|
560
|
+
for (const i in sortedUTXOs) {
|
|
561
|
+
const { txid, vout, value } = sortedUTXOs[i]
|
|
562
|
+
// Eric bro... better to store transaction hex on the database so that you can reduce unnecessary API calls...
|
|
563
|
+
const { hex, txDetail } = await getTransactionDetailFromTxID(txid)
|
|
564
|
+
if (!hex) {
|
|
565
|
+
return {
|
|
566
|
+
success: false,
|
|
567
|
+
result: `cannot find proper hex for transaction ${txid}`
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const input = {
|
|
571
|
+
hash: txid,
|
|
572
|
+
index: vout
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (fromAddressType === BitcoinAddressType.Legacy)
|
|
576
|
+
input.nonWitnessUtxo = Buffer.from(hex, 'hex');
|
|
577
|
+
if (fromAddressType === BitcoinAddressType.NestedSegwit) {
|
|
578
|
+
input.witnessUtxo = {
|
|
579
|
+
script: txDetail.outs[vout].script,
|
|
580
|
+
value: txDetail.outs[vout].value,
|
|
581
|
+
}
|
|
582
|
+
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network });
|
|
583
|
+
input.redeemScript = p2wpkh.output
|
|
584
|
+
}
|
|
585
|
+
if (fromAddressType === BitcoinAddressType.NativeSegwit)
|
|
586
|
+
input.witnessUtxo = {
|
|
587
|
+
script: txDetail.outs[vout].script,
|
|
588
|
+
value: txDetail.outs[vout].value,
|
|
589
|
+
};
|
|
590
|
+
if (fromAddressType === BitcoinAddressType.Taproot) {
|
|
591
|
+
input.witnessUtxo = {
|
|
592
|
+
script: txDetail.outs[vout].script,
|
|
593
|
+
value: txDetail.outs[vout].value,
|
|
594
|
+
};
|
|
595
|
+
input.tapInternalKey = keyPairInfo.childNodeXOnlyPubkey
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
psbt.addInput(input)
|
|
599
|
+
inputUtxoCount ++
|
|
600
|
+
totalInputSats += value
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
let estimatedTransactionFee = estimateTransactionFee(inputUtxoCount, 1, toAddressType, fastestFee)
|
|
604
|
+
|
|
605
|
+
psbt.addOutput({
|
|
606
|
+
address: toAddress,
|
|
607
|
+
value: totalInputSats - estimatedTransactionFee
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
if (totalInputSats <= estimatedTransactionFee + DUST_LIMIT) {
|
|
611
|
+
return {
|
|
612
|
+
success: false,
|
|
613
|
+
result: "Input UTXOs are not enough to send..."
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (fromAddressType === BitcoinAddressType.Taproot) {
|
|
618
|
+
for (let i = 0; i < inputUtxoCount; i ++)
|
|
619
|
+
psbt.signInput(i, keyPairInfo.tweakedChildNode)
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
for (let i = 0; i < inputUtxoCount; i ++)
|
|
623
|
+
psbt.signInput(i, keyPairInfo.childNode)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
psbt.finalizeAllInputs()
|
|
627
|
+
|
|
628
|
+
const tx = psbt.extractTransaction()
|
|
629
|
+
const txHex = tx.toHex();
|
|
630
|
+
// console.log(`raw transaction hex: ${txHex}`)
|
|
631
|
+
|
|
632
|
+
// broadcast the transaction
|
|
633
|
+
const broadcastAPI = `${MEMPOOL_URL}/api/tx`
|
|
634
|
+
const response = await fetch(broadcastAPI, {
|
|
635
|
+
method: "POST",
|
|
636
|
+
body: txHex,
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
if (response.ok) {
|
|
640
|
+
const transactionId = await response.text()
|
|
641
|
+
return {
|
|
642
|
+
success: true,
|
|
643
|
+
result: transactionId
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
throw 'Error while broadcast...'
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function generateHash(input) {
|
|
651
|
+
return crypto.createHash('sha256') // Use SHA-256 algorithm
|
|
652
|
+
.update(input) // Hash the input string
|
|
653
|
+
.digest('hex'); // Output the hash in hex format
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function generateMnemonicFromPassword (pwd) {
|
|
657
|
+
const hash = generateHash(pwd)
|
|
658
|
+
const mnemonic = bip39.entropyToMnemonic(hash)
|
|
659
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
660
|
+
return null
|
|
661
|
+
}
|
|
662
|
+
return mnemonic
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// I have to add address type option so that CLI users can select Legacy/Segwit as well
|
|
666
|
+
// Right now, only Taproot
|
|
667
|
+
async function generateBitcoinAddressFromMnemonic (mnemonic) {
|
|
668
|
+
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
669
|
+
const rootKey = bip32.fromSeed(seed);
|
|
670
|
+
const childNode86 = rootKey.derivePath(`m/86'/1'/9'/9/8`)
|
|
671
|
+
const childNodeXOnlyPubkey86 = toXOnly(childNode86.publicKey);
|
|
672
|
+
const { address: taproot86 } = bitcoin.payments.p2tr({
|
|
673
|
+
internalPubkey: childNodeXOnlyPubkey86,
|
|
674
|
+
network: bitcoin.networks.bitcoin
|
|
675
|
+
});
|
|
676
|
+
return taproot86
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async function getWIFFromMnemonic (mnemonic) {
|
|
680
|
+
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
681
|
+
const rootKey = bip32.fromSeed(seed);
|
|
682
|
+
const childNode86 = rootKey.derivePath(`m/86'/1'/9'/9/8`)
|
|
683
|
+
return childNode86.toWIF()
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function generateWalletFromPassword (pwd) {
|
|
687
|
+
const mnemonic = generateMnemonicFromPassword(pwd)
|
|
688
|
+
const wif = await getWIFFromMnemonic(mnemonic)
|
|
689
|
+
const address = await generateBitcoinAddressFromMnemonic(mnemonic)
|
|
690
|
+
return {
|
|
691
|
+
mnemonic,
|
|
692
|
+
wif,
|
|
693
|
+
address
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function generateMultiSigWalletFromPasswords(pwds) {
|
|
698
|
+
if (!pwds || pwds.length != 3) {
|
|
699
|
+
console.log("Cannot create maddy")
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
const mnemonic0 = generateMnemonicFromPassword(pwds[0])
|
|
703
|
+
const wif0 = await getWIFFromMnemonic(mnemonic0)
|
|
704
|
+
const mnemonic1 = generateMnemonicFromPassword(pwds[1])
|
|
705
|
+
const wif1 = await getWIFFromMnemonic(mnemonic1)
|
|
706
|
+
const mnemonic2 = generateMnemonicFromPassword(pwds[2])
|
|
707
|
+
const wif2 = await getWIFFromMnemonic(mnemonic2)
|
|
708
|
+
const address = await generateMultiSigBitcoinAddressFromWIFs([wif0, wif1, wif2])
|
|
709
|
+
return {
|
|
710
|
+
address
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function generateMultiSigBitcoinAddressFromWIFs(wifs) {
|
|
715
|
+
const keyPair0 = ECPair.fromWIF(wifs[0]);
|
|
716
|
+
const keyPair1 = ECPair.fromWIF(wifs[1]);
|
|
717
|
+
const keyPair2 = ECPair.fromWIF(wifs[2]);
|
|
718
|
+
const keyPairs = [keyPair0, keyPair1, keyPair2]
|
|
719
|
+
const pubkeys = keyPairs.map(kp => kp.publicKey)
|
|
720
|
+
const p2ms = bitcoin.payments.p2ms({
|
|
721
|
+
m: 3,
|
|
722
|
+
pubkeys,
|
|
723
|
+
network
|
|
724
|
+
})
|
|
725
|
+
const p2sh = bitcoin.payments.p2sh({
|
|
726
|
+
redeem: p2ms,
|
|
727
|
+
network
|
|
728
|
+
})
|
|
729
|
+
return p2sh.address
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
module.exports = {
|
|
733
|
+
getUTXOs,
|
|
734
|
+
getBitcoinAddressType,
|
|
735
|
+
getConfirmedBalanceFromAddress,
|
|
736
|
+
generateMnemonicFromPassword,
|
|
737
|
+
generateBitcoinAddressFromMnemonic,
|
|
738
|
+
generateWalletFromPassword,
|
|
739
|
+
getWIFFromMnemonic,
|
|
740
|
+
getSatsbyte,
|
|
741
|
+
sendBtc,
|
|
742
|
+
drainBtc,
|
|
743
|
+
generateMultiSigWalletFromPasswords,
|
|
744
|
+
sendBtcFromMultiSig,
|
|
745
|
+
drainMultiSigBtc
|
|
746
|
+
};
|