forked from oipwg/oip-hdmw
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCoin.js
436 lines (371 loc) · 15.2 KB
/
Coin.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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
import * as bip32 from 'bip32'
import Account from './Account'
import TransactionBuilder from './TransactionBuilder'
const COIN_START = 0x80000000
/**
* Manage Accounts for a specific Coin
*/
class Coin {
/**
* Create a new Coin object to interact with Accounts and Chains for that coin. This spawns a BIP44 compatible wallet.
*
* ##### Examples
* Create a new Coin using a specified seed.
*```
*import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
*```
* Create a new Coin using a specified seed, don't auto discover.
*```
*import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin, false)
*```
* @param {string} node - BIP32 Node already derived to m/44'
* @param {CoinInfo} coin - The CoinInfo containing network & version variables
* @param {Object} [options] - The Options for spawning the Coin
* @param {boolean} [options.discover=true] - Should the Coin auto-discover Accounts and Chains
* @param {Object} [options.serializedData] - The Data to de-serialize from
* @return {Coin}
*/
constructor (node, coin, options) {
if (typeof node === 'string') { this.seed = node } else { this.seed = node.toBase58() }
this.coin = coin
this.discover = true
if (options && options.discover !== undefined) { this.discover = options.discover }
const purposeNode = bip32.fromBase58(this.seed)
purposeNode.network = this.coin.network
let bip44Num = this.coin.network.slip44
// Check if we need to convert the hexa to the index
if (bip44Num >= COIN_START) { bip44Num -= COIN_START }
this.root = purposeNode.derivePath(bip44Num + "'")
this.accounts = {}
if (options && options.serializedData) { this.deserialize(options.serializedData) }
if (this.discover) {
this.discoverAccounts()
}
}
serialize () {
const serializedAccounts = {}
for (const accountNumber in this.accounts) {
serializedAccounts[accountNumber] = this.accounts[accountNumber].serialize()
}
return {
name: this.coin.name,
network: this.coin.network,
seed: this.seed,
accounts: serializedAccounts
}
}
deserialize (serializedData) {
if (serializedData) {
if (serializedData.accounts) {
for (const accountNumber in serializedData.accounts) {
if (!Object.prototype.hasOwnProperty.call(serializedData.accounts, accountNumber)) continue
const accountMaster = bip32.fromBase58(serializedData.accounts[accountNumber].extendedPrivateKey, this.coin.network)
this.accounts[accountNumber] = new Account(accountMaster, this.coin, {
discover: false,
serializedData: serializedData.accounts[accountNumber]
})
}
}
}
}
/**
* Get the balance for the entire coin, or a specific address/array of addresses
* @param {Object} [options] - Specific options defining what balance to get back
* @param {Boolean} [options.discover=true] - Should the Coin discover Accounts
* @param {number|Array.<number>} [options.accounts=All Accounts in Coin] - Get Balance for defined Accounts
* @param {string|Array.<string>} [options.addresses=All Addresses in each Account in Coin] - Get Balance for defined Addresses
* @example <caption> Get Balance for entire Coin</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* bitcoin.getBalance().then((balance) => {
* console.log(balance)
* })
* @return {Promise<number>} A Promise that will resolve to the balance of the entire Coin
*/
async getBalance (options) {
if (!options || (options && options.discover === undefined) || (options && options.discover === true)) {
try {
await this.discoverAccounts()
} catch (e) { throw new Error('Unable to Discover Coin Accounts for getBalance! \n' + e) }
}
const accountsToSearch = []
// Check if we are an array (ex. [0,1,2]) or just a number (ex. 1)
if (options && Array.isArray(options.accounts)) {
for (const accNum of options.accounts) {
if (!isNaN(accNum)) {
accountsToSearch.push(accNum)
}
}
} else if (options && !isNaN(options.accounts)) {
accountsToSearch.push(options.accounts)
} else {
for (const accNum in this.accounts) {
accountsToSearch.push(accNum)
}
}
let totalBalance = 0
let addrsToSearch
if (options && options.addresses && (typeof options.addresses === 'string' || Array.isArray(options.addresses))) {
addrsToSearch = options.addresses
}
for (const accNum of accountsToSearch) {
if (this.accounts[accNum]) {
try {
const balanceRes = await this.accounts[accNum].getBalance({
discover: false,
addresses: addrsToSearch,
id: accNum
})
totalBalance += balanceRes.balance
} catch (e) { throw new Error('Unable to get Coin balance! \n' + e) }
}
}
return totalBalance
}
/**
* Get a specific Address
* @param {number} [accountNumber=0] - Number of the account you wish to get the Address from
* @param {number} [chainNumber=0] - Number of the Chain you wish to get the Address from
* @param {number} [addressIndex=0] - Index of the Address you wish to get
* @return {Address}
*/
getAddress (accountNumber, chainNumber, addressIndex) {
return this.getAccount(accountNumber || 0).getAddress(chainNumber, addressIndex)
}
/**
* Get the Main Address for a specific Account number.
* This is the Address at index 0 on the External Chain of the Account.
* @param {number} [accountNumber=0] - Number of the Account you wish to get
* @example <caption>Get Main Address for Coin</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let mainAddress = bitcoin.getMainAddress()
* @example <caption>Get Main Address for Account #1 on Coin</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let mainAddress = bitcoin.getMainAddress(1)
* @return {Address}
*/
getMainAddress (accountNumber) {
return this.getAccount(accountNumber || 0).getMainAddress()
}
/**
* Send a Payment to specified Addresses and Amounts
* @param {Object} options - the options for the specific transaction being sent
* @param {OutputAddress|Array.<OutputAddress>} options.to - Define outputs for the Payment
* @param {string|Array.<string>} [options.from=All Addresses in Coin] - Define what public address(es) you wish to send from
* @param {number|Array.<number>} [options.fromAccounts=All Accounts in Coin] - Define what Accounts you wish to send from
* @param {Boolean} [options.discover=true] - Should discovery happen before sending payment
* @param {string} [options.floData=""] - Flo data to attach to the transaction
* @return {Promise<string>} - Returns a promise that will resolve to the success TXID
*/
sendPayment (options) {
return new Promise((resolve, reject) => {
if (!options) { reject(new Error('You must define your payment options!')) }
const processPayment = () => {
let sendFrom = []
let allAddresses = []
// Add all Addresses from selected accounts to array
for (const account in this.accounts) {
// Check if we are defining what accounts to send the payment from
if (options.fromAccounts) {
// Check if it is a single account number, or an array of account numbers
if (typeof options.fromAccounts === 'number') {
// If we match the passed account number, set the grabbed addresses
if (options.fromAccounts === parseInt(account)) {
allAddresses = this.accounts[account].getAddresses()
}
} else if (Array.isArray(options.fromAccounts)) {
// If we are an array, iterate through
for (const acs of options.fromAccounts) {
if (acs === parseInt(account)) {
allAddresses = allAddresses.concat(this.accounts[account].getAddresses())
}
}
}
} else {
allAddresses = allAddresses.concat(this.accounts[account].getAddresses())
}
}
// Check if we define what address we wish to send from
if (options.from) {
// Check if it is a single from address or an array
if (typeof options.from === 'string') {
for (const address of allAddresses) {
if (address.getPublicAddress() === options.from) {
sendFrom.push(address)
}
}
} else if (Array.isArray(options.from)) {
for (const adr of options.from) {
for (const address of allAddresses) {
if (address.getPublicAddress() === adr) {
sendFrom.push(address)
}
}
}
}
// else add all the addresses on the Account that have received any txs
} else {
sendFrom = allAddresses
}
if (sendFrom.length === 0) {
reject(new Error('No Addresses match defined options.from Addresses!'))
return
}
const newOpts = options
newOpts.from = sendFrom
const txb = new TransactionBuilder(this.coin, newOpts)
txb.sendTX().then(resolve).catch(reject)
}
if (options.discover === false) {
processPayment()
} else {
this.discoverAccounts().then(processPayment).catch(reject)
}
})
}
/**
* Get the Extended Private Key for the root path. This is derived at m/44'/coinType'
* @example <caption>Get the Extended Private Key for the entire Coin</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let extPrivateKey = bitcoin.getExtendedPrivateKey()
* // extPrivateKey = xprv9x8MQtHNRrGgrnWPkUxjUC57DWKgkjobwAYUFedxVa2FAA5qaQuGqLkJnVcszqomTar51PCR8JiKnGGgzK9eJKGjbpUirKPVHxH2PU2Rc93
* @return {string} The Extended Private Key
*/
getExtendedPrivateKey () {
return this.root.toBase58()
}
/**
* Get the Neutered Extended Public Key for the root path. This is derived at m/44'/coinType'
* @example <caption>Get the Extended Private Key for the entire Coin</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let extPublicKey = bitcoin.getExtendedPrivateKey()
* // extPublicKey = xpub6B7hpPpGGDpz5GarrWVjqL1qmYABACXTJPU5433a3uZE2xQz7xDXP94ndkjrxogjordTDSDaHY4i5G4HqRH6E9FJZk2F4ED4cbnprW2Vm9v
* @return {string} The Extended Public Key
*/
getExtendedPublicKey () {
return this.root.neutered().toBase58()
}
/**
* Get the Account at the specified number
* @param {number} [accountNumber=0]
* @example <caption>Get Default Account</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let account = bitcoin.getAccount()
* @example <caption>Get Account #1</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let account = bitcoin.getAccount(1)
* @return {Account}
*/
getAccount (accountNumber) {
let num = accountNumber || 0
if (typeof accountNumber === 'string' && !isNaN(parseInt(accountNumber))) { num = parseInt(accountNumber) }
if (!this.accounts[num]) { return this.addAccount(num) }
return this.accounts[num]
}
/**
* Get all Accounts on the Coin
* @example
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let accounts = bitcoin.getAccounts()
* // accounts = {
* // 0: Account,
* // 1: Account
* // }
* @return {Object.<number, Account>} Returns a JSON object with accounts
*/
getAccounts () {
return this.accounts
}
/**
* Add the Account at the specified number, if it already exists, it returns the Account.
* If the Account does not exist, it will create it and then return it.
* @param {number} [accountNumber=0]
* @param {Boolean} [discover=discover Set in Coin Constructor] - Should the Account start auto-discovery.
* @example
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let account = bitcoin.addAccount(1)
* @return {Account}
*/
addAccount (accountNumber, discover) {
let num = accountNumber || 0
if (typeof accountNumber === 'string' && !isNaN(parseInt(accountNumber))) { num = parseInt(accountNumber) }
// if the account has already been added, just return
if (this.accounts[num]) { return this.getAccount(num) }
const accountMaster = this.root.deriveHardened(num)
let shouldDiscover
if (discover !== undefined) { shouldDiscover = discover } else { shouldDiscover = this.discover }
this.accounts[num] = new Account(accountMaster, this.coin, { discover: shouldDiscover })
return this.getAccount(num)
}
/**
* Get the CoinInfo for the Coin
* @example
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let coinInfo = bitcoin.getCoinInfo()
* // coinInfo = Networks.bitcoin
* @return {CoinInfo}
*/
getCoinInfo () {
return this.coin
}
getHighestAccountNumber () {
let highestAccountNumber = 0
for (const accNum in this.accounts) {
if (accNum > highestAccountNumber) { highestAccountNumber = accNum }
}
return parseInt(highestAccountNumber)
}
/**
* Discover all Accounts for the Coin
* @example
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin, false)
* bitcoin.discoverAccounts().then((accounts) => {
* console.log(accounts.length)
* })
* @return {Promise<Array.<Account>>} Returns a Promise that will resolve to an Array of Accounts once complete
*/
async discoverAccounts () {
// Reset the internal accounts
this.accounts = {}
// Get the Account #0 and start discovery there.
try {
await this.getAccount(0).discoverChains()
} catch (e) { throw new Error('Unable to discoverAccounts! \n' + e) }
while (this.accounts[this.getHighestAccountNumber()].getUsedAddresses().length > 0) {
try {
await this.getAccount(this.getHighestAccountNumber() + 1).discoverChains()
} catch (e) { throw new Error('Unable to discover account #' + (this.getHighestAccountNumber() + 1) + '\n' + e) }
}
const discoveredAccounts = []
for (const accNum in this.accounts) {
discoveredAccounts.push(this.accounts[accNum])
}
return discoveredAccounts
}
}
module.exports = Coin