-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathwallet.js
304 lines (283 loc) · 11.4 KB
/
wallet.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
const {
listunspent,
walletprocesspsbt,
sendrawtransaction,
finalizepsbt,
listtransactions,
getrawtransaction,
} = require('./bitcoin')
const prompts = require('prompts')
const { decrypt } = require('./encryption')
const axios = require('axios')
const ecc = require('tiny-secp256k1')
const { BIP32Factory } = require('bip32')
const bip32 = BIP32Factory(ecc)
const bitcoin = require('bitcoinjs-lib')
const bip39 = require('bip39')
const { getAddressInfo } = require('bitcoin-address-validation')
const { BTC_to_satoshi } = require('./utils')
const { sign_psbt_with_coldcard, get_hsm_address } = require('./hsm')
const { getMempoolClient } = require('./utils/mempool')
bitcoin.initEccLib(ecc)
const is_hsm_enabled = () => process.env.USE_HSM === 'true'
const get_wallet_type = () => {
if (is_hsm_enabled()) return 'coldcard'
return process.env.BITCOIN_WALLET ? 'core' : 'local'
}
function get_address() {
const wallet_type = get_wallet_type()
if (wallet_type === 'coldcard') {
return get_hsm_address()
}
const address = process.env.LOCAL_WALLET_ADDRESS
if (wallet_type === 'local' && !address) {
throw new Error('LOCAL_WALLET_ADDRESS must be set')
}
return address
}
let local_wallet_type
let child_xonly_pubkey
let tweaked_child_node
let root_hd_node
let child_hd_node
let derivation_path
const toXOnly = (pubKey) => (pubKey.length === 32 ? pubKey : pubKey.slice(1, 33))
const get_min_sat_utxo_limit = () => Number(process.env.IGNORE_UTXOS_BELOW_SATS) || 1001
async function init_wallet() {
if (get_wallet_type() === 'local') {
if ((!process.env.LOCAL_WALLET_SEED && !process.env.LOCAL_WALLET_SEED_ENCRYPTED) || !process.env.LOCAL_WALLET_ADDRESS) {
throw new Error('LOCAL_WALLET_ADDRESS and LOCAL_WALLET_SEED or LOCAL_WALLET_SEED_ENCRYPTED must be set')
}
if (process.env.LOCAL_WALLET_SEED && process.env.LOCAL_WALLET_SEED_ENCRYPTED) {
throw new Error('Cannot set both LOCAL_WALLET_SEED and LOCAL_WALLET_SEED_ENCRYPTED')
}
// Check validity of seed
let seed_phrase
if (process.env.LOCAL_WALLET_SEED_ENCRYPTED) {
const { password } = await prompts({
type: 'password',
name: 'password',
message: 'Enter the password to decrypt the seed phrase:',
})
seed_phrase = decrypt(process.env.LOCAL_WALLET_SEED_ENCRYPTED, password)
} else {
seed_phrase = process.env.LOCAL_WALLET_SEED
}
const seed_buffer = bip39.mnemonicToSeedSync(seed_phrase)
root_hd_node = bip32.fromSeed(seed_buffer)
const addresss_info = getAddressInfo(process.env.LOCAL_WALLET_ADDRESS)
local_wallet_type = addresss_info.type
if (local_wallet_type !== 'p2tr' && local_wallet_type !== 'p2wpkh') {
throw new Error('Local wallet address must be p2tr or p2wpkh')
}
derivation_path = process.env.LOCAL_DERIVATION_PATH || `m/8${local_wallet_type === 'p2tr' ? '6' : '4'}'/0'/0'/0/0`
child_hd_node = root_hd_node.derivePath(derivation_path)
child_xonly_pubkey = toXOnly(child_hd_node.publicKey)
let address
if (local_wallet_type === 'p2tr') {
const p2tr_derived_info = bitcoin.payments.p2tr({ internalPubkey: child_xonly_pubkey })
address = p2tr_derived_info.address
} else {
const p2wpkh_derived_info = bitcoin.payments.p2wpkh({ pubkey: child_hd_node.publicKey })
address = p2wpkh_derived_info.address
}
if (address !== process.env.LOCAL_WALLET_ADDRESS) {
throw new Error(
'Local address does not match expected - ensure LOCAL_WALLET_SEED/LOCAL_WALLET_SEED_ENCRYPTED, LOCAL_DERIVATION_PATH, and LOCAL_WALLET_ADDRESS are correct'
)
} else {
console.log(`Local wallet address: ${address}`)
if (local_wallet_type === 'p2tr') {
tweaked_child_node = child_hd_node.tweak(bitcoin.crypto.taggedHash('TapTweak', child_xonly_pubkey))
}
}
}
}
async function get_utxos_from_mempool_space({ address }) {
const url = `/api/address/${address}/utxo`
const { data } = await getMempoolClient()
.get(url)
.catch((err) => {
console.error(err)
return {}
})
return data
}
async function get_utxos() {
const IGNORE_UTXOS_BELOW_SATS = get_min_sat_utxo_limit()
if (get_wallet_type() === 'core') {
const unspents = listunspent()
const filtered_unspents = unspents.filter((it) => it.amount * 100000000 >= IGNORE_UTXOS_BELOW_SATS)
const ignored_num = unspents.length - filtered_unspents.length
if (ignored_num > 0) {
console.log(`Ignored ${ignored_num} dust unspents below ${IGNORE_UTXOS_BELOW_SATS} sats`)
}
return filtered_unspents.map((it) => `${it.txid}:${it.vout}`)
}
const address = get_address()
const unspents = await get_utxos_from_mempool_space({ address })
if (!unspents) {
throw new Error('Error reaching mempool api')
}
const all_unspents_length = unspents.length
const filtered_unspents = unspents.filter((it) => it.value >= IGNORE_UTXOS_BELOW_SATS)
const ignored_num = all_unspents_length - filtered_unspents.length
if (ignored_num > 0) {
console.log(`Ignored ${ignored_num} dust unspents below ${IGNORE_UTXOS_BELOW_SATS} sats`)
}
return filtered_unspents.map((it) => `${it.txid}:${it.vout}`)
}
function sign_and_finalize_transaction({ psbt, witnessUtxo }) {
if (is_hsm_enabled()) {
const signed_psbt = sign_psbt_with_coldcard(psbt)
return signed_psbt
}
if (get_wallet_type() === 'core') {
const processed_psbt = walletprocesspsbt({ psbt }).psbt
return finalizepsbt({ psbt: processed_psbt, extract: false }).psbt
}
let psbt_object = bitcoin.Psbt.fromBase64(psbt)
if (local_wallet_type === 'p2tr') {
psbt_object.updateInput(0, {
tapInternalKey: child_xonly_pubkey,
witnessUtxo,
})
psbt_object = psbt_object.signInput(0, tweaked_child_node, [bitcoin.Transaction.SIGHASH_ALL])
} else {
psbt_object.updateInput(0, {
bip32Derivation: [
{
masterFingerprint: root_hd_node.fingerprint,
path: derivation_path,
pubkey: child_hd_node.publicKey,
},
],
})
psbt_object = psbt_object.signInputHD(0, root_hd_node, [bitcoin.Transaction.SIGHASH_ALL])
}
return psbt_object.finalizeAllInputs().toBase64()
// Note: assumes one input
}
async function broadcast_to_mempool_space({ hex }) {
const url = `/api/tx`
const { data } = await getMempoolClient()
.post(url, hex, { headers: { 'Content-Type': 'text/plain' } })
.catch((err) => {
console.error(err)
return {}
})
return data
}
async function broadcast_to_blockstream({ hex }) {
const url = 'https://blockstream.info/api/tx'
const { data } = await axios.post(url, hex, { headers: { 'Content-Type': 'text/plain' } }).catch((err) => {
console.error(err)
return {}
})
return data
}
async function broadcast_transaction({ hex }) {
if (get_wallet_type() === 'core') {
return sendrawtransaction({ hex })
}
const txid = await broadcast_to_mempool_space({ hex })
if (process.env.BROADCAST_TO_BLOCKSTREAM) {
await broadcast_to_blockstream({ hex })
}
return txid
}
async function get_address_txs({ address }) {
const url = `/api/address/${address}/txs`
const { data } = await getMempoolClient()
.get(url, {
maxContentLength: Math.MAX_SAFE_INTEGER,
})
.catch((err) => {
console.error(err)
return {}
})
if (process.env.DEBUG) {
console.log(data)
}
return data || []
}
async function fetch_most_recent_unconfirmed_send() {
const IGNORE_UTXOS_BELOW_SATS = get_min_sat_utxo_limit()
if (get_wallet_type() === 'core') {
const recent_transactions_all_outputs = listtransactions({ count: 200 })
const all_unconfirmed_sends = recent_transactions_all_outputs.filter(
(it) => it.category === 'send' && it.confirmations === 0
)
// Sort by most negative (largest send first)
const all_unconfirmed_sends_sorted = all_unconfirmed_sends.sort((a, b) => a.amount - b.amount)
// For a send with multiple outputs (common in an extraction tx), listtransactions() will return an entry for each output
// in the same tx. So we have many entries referring to the same txid, and should just grab one of them.
const unique_txids = new Set()
const unique_unconfirmed_sends = all_unconfirmed_sends_sorted.filter((it) => {
if (unique_txids.has(it.txid)) {
// This assumes all_unconfirmed_sends_sorted is sorted by biggest (most negative) amounts first
return false
}
unique_txids.add(it.txid)
return true
})
const unconfirmed_sends = unique_unconfirmed_sends.filter(
(it) => Math.abs(BTC_to_satoshi(it.amount)) >= IGNORE_UTXOS_BELOW_SATS
)
const num_unconfirmed_send_below_limit = unique_unconfirmed_sends.length - unconfirmed_sends.length
if (unconfirmed_sends.length === 0) {
console.log(`Did not find any unconfirmed sends above ${IGNORE_UTXOS_BELOW_SATS} sats`)
if (num_unconfirmed_send_below_limit > 0) {
console.log(`Ignored ${num_unconfirmed_send_below_limit} unconfirmed sends below ${IGNORE_UTXOS_BELOW_SATS} sats`)
}
return {}
}
// sort by fee ascending
const tx = unconfirmed_sends.sort((a, b) => a.fee - b.fee)[0]
const fee = -tx.fee * 100000000
const tx_info = getrawtransaction({ txid: tx.txid, verbose: true })
// Assumes one input
const input = tx_info.vin[0]
const existing_fee_rate = (fee / tx_info.vsize).toFixed(1)
return {
existing_fee_rate,
input_utxo: `${input.txid}:${input.vout}`,
}
}
const address = get_address()
const txs = await get_address_txs({ address })
const unconfirmed_sends = txs.filter((it) => {
// Hacky way to find which ones are ours...
if (it.status.confirmed) return false
if (it.vin.length !== 1) return false
if (it.vin[0].prevout.value < IGNORE_UTXOS_BELOW_SATS) {
console.log(`Ignoring spent dust input of ${it.vin[0].prevout.value} sats`)
return false
}
for (const output of it.vout) {
if (output.scriptpubkey_address === process.env.LOCAL_WALLET_ADDRESS) {
return false
}
}
return true
})
console.log(`Found ${unconfirmed_sends.length} unconfirmed sends`)
if (unconfirmed_sends.length === 0) return {}
// Sort by fee descending
const tx = unconfirmed_sends.sort((a, b) => b.fee - a.fee)[0]
const existing_fee_rate = (tx.fee / (tx.weight / 4)).toFixed(1)
return {
existing_fee_rate,
input_utxo: `${tx.vin[0].txid}:${tx.vin[0].vout}`,
}
}
module.exports = {
get_utxos,
sign_and_finalize_transaction,
broadcast_transaction,
fetch_most_recent_unconfirmed_send,
init_wallet,
get_utxos_from_mempool_space,
broadcast_to_mempool_space,
get_address_txs,
}