-
Notifications
You must be signed in to change notification settings - Fork 46
/
Bitcore.ts
232 lines (207 loc) · 7.91 KB
/
Bitcore.ts
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
import coininfo from 'coininfo'
import {
WorkerDiscovery, Blockchain, BitcoreBlockchain, AccountLoadStatus,
UtxoInfo as BaseUtxoInfo, AccountInfo as BaseAccountInfo,
} from 'hd-wallet'
import { TransactionBuilder, Network } from 'bitcoinjs-lib'
import { omit } from 'lodash'
// @ts-ignore
import xpubWasmFile from 'hd-wallet/lib/fastxpub/fastxpub.wasm?file'
// @ts-ignore
import XpubWorker from 'hd-wallet/lib/fastxpub/fastxpub?worker'
// @ts-ignore
import SocketWorker from 'hd-wallet/lib/socketio-worker/inside?worker'
// @ts-ignore
import DiscoveryWorker from 'hd-wallet/lib/discovery/worker/inside?worker'
import log from 'Utilities/log'
import { ypubToXpub, estimateTxFee } from 'Utilities/bitcoin'
// setting up workers
const xpubWorker = new XpubWorker()
const xpubWasmFilePromise = fetch(xpubWasmFile)
.then((response) => response.ok ? response.arrayBuffer() : Promise.reject('failed to load fastxpub.wasm'))
const socketWorkerFactory = () => new SocketWorker()
const discoveryWorkerFactory = () => new DiscoveryWorker()
export type UtxoInfo = BaseUtxoInfo & {
confirmations: number,
}
export type AccountInfo = BaseAccountInfo & {
utxos: UtxoInfo[],
}
export type TxOutput = {
address: string,
amount: number,
}
export type PaymentTx = {
inputUtxos: UtxoInfo[]
outputs: TxOutput[]
outputScript: string,
fee: number,
change: number,
changePath: number[],
changeAddress: string,
isSegwit: boolean,
}
/**
* Sort the utxos for input selection
*/
function sortUtxos(utxoList: UtxoInfo[]): UtxoInfo[] {
const matureList: UtxoInfo[] = []
const immatureList: UtxoInfo[] = []
utxoList.forEach((utxo) => {
if (utxo.confirmations >= 6) {
matureList.push(utxo)
} else {
immatureList.push(utxo)
}
})
matureList.sort((a, b) => a.value - b.value) // Ascending order by value
immatureList.sort((a, b) => b.confirmations - a.confirmations) // Descending order by confirmations
return matureList.concat(immatureList)
}
export class Bitcore extends BitcoreBlockchain {
network: Network
discovery: WorkerDiscovery
constructor(public assetSymbol: string, bitcoreUrls: string[]) {
super(bitcoreUrls, socketWorkerFactory)
this.network = coininfo(assetSymbol).toBitcoinJS()
this.discovery = new WorkerDiscovery(discoveryWorkerFactory, xpubWorker, xpubWasmFilePromise, this)
}
toJSON() {
return Object.assign({}, this, {
discovery: omit(this.discovery, 'chain'), // Avoid circular reference
})
}
/**
* Discover the balance, transactions, unused addresses, etc of an xpub.
*
* @param xpub - The xpub or ypub to discover
* @param [onUpdate] - Callback for partial updates to discover result
* @returns Account info promise
*/
discoverAccount(xpub: string, onUpdate?: (status: AccountLoadStatus) => void): Promise<AccountInfo> {
return Promise.resolve()
.then(() => {
let segwit = 'off'
if (xpub.startsWith('ypub')) {
segwit = 'p2sh'
xpub = ypubToXpub(xpub)
}
const process = this.discovery.discoverAccount(null, xpub, this.network, segwit)
if (onUpdate) {
process.stream.values.attach(onUpdate)
}
return process.ending.then((result: BaseAccountInfo) => ({
...result,
utxos: result.utxos.map((utxo: BaseUtxoInfo) => ({
...utxo,
confirmations: utxo.height ? result.lastBlock.height - utxo.height : 0,
})),
}))
})
}
/**
* Build a simple payment transaction.
* Note: fee will be subtracted from first output when attempting to send entire account balance
*
* @param {Object} account - The result of calling discoverAccount
* @param {Number} account.changeIndex - The index of the next unused changeAddress
* @param {String[]} account.changeAddresses - An array of all change addresses
* @param {Object[]} account.utxos - The unspent transaction outputs for the account
* @param {Number} account.utxos[].value - The value of the utxo (unit: satoshi)
* @param {Number} account.utxos[].confirmations - The confirmations of the utxo
* @param {String} account.utxos[].transactionHash - The hash of the transaction this utxo is in
* @param {Number} account.utxos[].index - The index of this utxo in the transaction
* @param {Number[]} account.utxos[].addressPath - The bip44 address path of the utxo
* @param {Object[]} desiredOutputs - Outputs for the transaction (excluding change)
* @param {String} desiredOutputs[].address - address to send to
* @param {Number} desiredOutputs[].amount - amount to send (unit: satoshi)
* @param {Number} feeRate - desired fee (unit: satoshi per byte)
* @param {Boolean} [isSegwit=true] - True if this is a segwit transaction
* @param {Number} [dustThreshold=546] - A change output will only be included when greater than this value.
* Otherwise it will be included as a fee instead (unit: satoshi)
* @returns {Object}
*/
buildPaymentTx(
account: AccountInfo,
desiredOutputs: Array<{ address: string, amount: number}>,
feeRate: number,
isSegwit = true,
dustThreshold = 546,
): PaymentTx {
const { utxos, changeIndex, changeAddresses } = account
let changeAddress = changeAddresses[changeIndex]
const sortedUtxos = sortUtxos(utxos)
const outputs = desiredOutputs.map(({ address, amount }) => ({ address, amount })) // Clone
const outputCount = outputs.length + 1 // Plus one for change output
let outputTotal = outputs.reduce((total, { amount }) => total + amount, 0)
/* Select inputs and calculate appropriate fee */
let fee = 0 // Total fee is recalculated when adding each input
let amountWithFee = outputTotal + fee
const inputUtxos = []
let inputTotal = 0
for (const utxo of sortedUtxos) {
fee = estimateTxFee(feeRate, inputUtxos.length + 1, outputCount, isSegwit)
amountWithFee = outputTotal + fee
inputTotal = inputTotal + utxo.value
inputUtxos.push(utxo)
if (inputTotal >= amountWithFee) {
break
}
}
if (amountWithFee > inputTotal) {
const amountWithSymbol = `${outputTotal * 1e-8} ${this.assetSymbol}`
if (outputTotal === inputTotal) {
log.debug(`Attempting to send entire ${amountWithSymbol} balance. ` +
`Subtracting fee of ${fee} sat from first output.`)
amountWithFee = outputTotal
outputs[0].amount -= fee
outputTotal -= fee
if (outputs[0].amount <= dustThreshold) {
throw new Error('First output minus fee is below dust threshold')
}
} else {
throw new Error(`You do not have enough UTXOs to send ${amountWithSymbol} with ${feeRate} sat/byte fee`)
}
}
/* Build outputs */
const outputBuilder = new TransactionBuilder(this.network)
outputs.forEach(({ amount, address }) => outputBuilder.addOutput(address, amount))
let change = inputTotal - amountWithFee
let changePath = [1, changeIndex]
if (change > dustThreshold) { // Avoid creating dust outputs
outputBuilder.addOutput(changeAddress, change)
} else {
fee += change
change = 0
changeAddress = null
changePath = null
}
const outputScript = outputBuilder.buildIncomplete().toHex().slice(10, -8) // required by ledgerjs api
return {
inputUtxos,
outputs,
outputScript,
fee,
change,
changePath,
changeAddress,
isSegwit,
}
}
}
const assetToBitcore: { [symbol: string]: Bitcore } = {
BTC: new Bitcore('BTC', ['https://blockexplorer.com', 'https://bitcore1.trezor.io', 'https://bitcore3.trezor.io']),
LTC: new Bitcore('LTC', ['https://ltc-bitcore3.trezor.io']),
}
/** Get the Bitcore service for the specified asset */
export function getNetwork(assetSymbol: string): Bitcore {
const bitcore = assetToBitcore[assetSymbol]
if (!bitcore) {
throw new Error(`Asset ${assetSymbol} has no Bitcore configuration`)
}
return bitcore
}
export default {
getNetwork,
Bitcore,
}