This repository has been archived by the owner on Mar 7, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 284
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #446 from blockchain/v3.38-release
v3.38
- Loading branch information
Showing
35 changed files
with
1,576 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* eslint-disable semi */ | ||
const BchSpendable = require('./bch-spendable') | ||
|
||
const ACCOUNT_LABEL_PREFIX = 'Bitcoin Cash - ' | ||
|
||
class BchAccount extends BchSpendable { | ||
constructor (bchWallet, wallet, btcAccount) { | ||
super(bchWallet, wallet) | ||
this._btcAccount = btcAccount | ||
} | ||
|
||
get index () { | ||
return this._btcAccount.index | ||
} | ||
|
||
get xpub () { | ||
return this._btcAccount.extendedPublicKey | ||
} | ||
|
||
get archived () { | ||
return this._btcAccount.archived | ||
} | ||
|
||
get label () { | ||
return ACCOUNT_LABEL_PREFIX + this._btcAccount.label | ||
} | ||
|
||
get balance () { | ||
return super.getAddressBalance(this.xpub) | ||
} | ||
|
||
get receiveAddress () { | ||
let { receive } = this._bchWallet.getAccountIndexes(this.xpub) | ||
return this._btcAccount.receiveAddressAtIndex(receive) | ||
} | ||
|
||
get changeAddress () { | ||
let { change } = this._bchWallet.getAccountIndexes(this.xpub) | ||
return this._btcAccount.changeAddressAtIndex(change) | ||
} | ||
|
||
get coinCode () { | ||
return 'bch' | ||
} | ||
|
||
getAvailableBalance (feePerByte) { | ||
return super.getAvailableBalance(this.index, feePerByte) | ||
} | ||
|
||
createPayment () { | ||
return super.createPayment().from(this.index, this.changeAddress) | ||
} | ||
} | ||
|
||
module.exports = BchAccount |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/* eslint-disable semi */ | ||
const { curry, is, prop, lensProp, compose, assoc, over, map } = require('ramda'); | ||
const { mapped } = require('ramda-lens'); | ||
const API = require('../api'); | ||
const Coin = require('./coin.js'); | ||
const Bitcoin = require('bitcoincashjs-lib'); | ||
const constants = require('../constants'); | ||
const Helpers = require('../helpers'); | ||
|
||
const scriptToAddress = coin => { | ||
const scriptBuffer = Buffer.from(coin.script, 'hex'); | ||
let network = constants.getNetwork(Bitcoin); | ||
const address = Bitcoin.address.fromOutputScript(scriptBuffer, network).toString(); | ||
return assoc('priv', address, coin) | ||
} | ||
|
||
const pushTx = (tx) => { | ||
const format = 'plain' | ||
return fetch(`${API.API_ROOT_URL}bch/pushtx`, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | ||
body: API.encodeFormData({ tx, format }) | ||
}).then(r => | ||
r.status === 200 ? r.text() : r.text().then(e => Promise.reject(e)) | ||
).then(r => | ||
r.indexOf('Transaction Submitted') > -1 ? true : Promise.reject(r) | ||
) | ||
}; | ||
|
||
const apiGetUnspents = (as, conf) => { | ||
const active = as.join('|'); | ||
const confirmations = Helpers.isPositiveNumber(conf) ? conf : -1 | ||
const format = 'json' | ||
return fetch(`${API.API_ROOT_URL}bch/unspent`, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | ||
body: API.encodeFormData({ active, confirmations, format }) | ||
}).then(r => | ||
r.status === 200 ? r.json() : r.json().then(e => Promise.reject(e)) | ||
); | ||
} | ||
|
||
const multiaddr = (addresses, n = 1) => { | ||
const active = Helpers.toArrayFormat(addresses).join('|') | ||
const data = { active, format: 'json', offset: 0, no_compact: true, n, language: 'en', no_buttons: true }; | ||
return fetch(`${API.API_ROOT_URL}bch/multiaddr`, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | ||
body: API.encodeFormData(data) | ||
}).then(r => r.status === 200 ? r.json() : r.json().then(e => Promise.reject(e))); | ||
}; | ||
|
||
// source can be a list of legacy addresses or a single integer for account index | ||
const getUnspents = curry((wallet, source) => { | ||
switch (true) { | ||
case is(Number, source): | ||
const accIdx = wallet.hdwallet.accounts[source].extendedPublicKey | ||
return apiGetUnspents([accIdx]) | ||
.then(prop('unspent_outputs')) | ||
.then(over(compose(mapped, lensProp('xpub')), assoc('index', source))) | ||
.then(map(Coin.fromJS)); | ||
case is(Array, source): | ||
return apiGetUnspents(source) | ||
.then(prop('unspent_outputs')) | ||
.then(over(mapped, scriptToAddress)) | ||
.then(map(Coin.fromJS)); | ||
default: | ||
return Promise.reject('WRONG_SOURCE_FOR_UNSPENTS'); | ||
} | ||
}) | ||
|
||
module.exports = { | ||
getUnspents, | ||
pushTx, | ||
multiaddr | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* eslint-disable semi */ | ||
const BchSpendable = require('./bch-spendable') | ||
const { compose, reduce, filter, add } = require('ramda') | ||
|
||
const sumNonNull = compose(reduce(add, 0), filter(x => x != null)) | ||
|
||
class BchImported extends BchSpendable { | ||
get addresses () { | ||
return this._wallet.spendableActiveAddresses | ||
} | ||
|
||
get label () { | ||
return 'Imported Addresses' | ||
} | ||
|
||
get balance () { | ||
let balances = this.addresses.map(a => super.getAddressBalance(a)) | ||
return balances.every(x => x == null) ? null : sumNonNull(balances) | ||
} | ||
|
||
get coinCode () { | ||
return 'bch' | ||
} | ||
|
||
getAvailableBalance (feePerByte) { | ||
return super.getAvailableBalance(this.addresses, feePerByte) | ||
} | ||
|
||
createPayment () { | ||
return super.createPayment().from(this.addresses, this.addresses[0]) | ||
} | ||
} | ||
|
||
module.exports = BchImported |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
/* eslint-disable semi */ | ||
const { compose, clone, assoc, is, all } = require('ramda') | ||
const Coin = require('./coin') | ||
const BchApi = require('./bch-api') | ||
const { isBitcoinAddress, isPositiveInteger } = require('../helpers') | ||
const { selectAll, descentDraw } = require('./coin-selection') | ||
const { sign } = require('./signer') | ||
|
||
const isValidFrom = (from) => ( | ||
is(Number, from) || | ||
(is(Array, from) && all(isBitcoinAddress, from)) | ||
) | ||
|
||
class PaymentError extends Error { | ||
constructor (message, state) { | ||
super(message) | ||
this.recover = () => Promise.resolve(state) | ||
} | ||
} | ||
|
||
class BchPayment { | ||
constructor (wallet) { | ||
this._wallet = wallet | ||
this._payment = BchPayment.defaultStateP() | ||
} | ||
|
||
map (f) { | ||
this._payment = this._payment.then(f) | ||
return this | ||
} | ||
|
||
handleError (f) { | ||
this._payment = this._payment.catch(paymentError => { | ||
f(paymentError) | ||
return is(Function, paymentError.recover) | ||
? paymentError.recover() | ||
: BchPayment.defaultStateP() | ||
}) | ||
return this | ||
} | ||
|
||
sideEffect (f) { | ||
this._payment.then(clone).then(f) | ||
return this | ||
} | ||
|
||
from (from, change) { | ||
if (!isValidFrom(from)) { | ||
throw new Error('must provide a valid payment source') | ||
} | ||
if (!isBitcoinAddress(change)) { | ||
throw new Error('must provide a valid change address') | ||
} | ||
return this.map(payment => | ||
BchApi.getUnspents(this._wallet, from).then(coins => { | ||
let setData = compose(assoc('coins', coins), assoc('change', change)) | ||
return setData(payment) | ||
}) | ||
) | ||
} | ||
|
||
to (to) { | ||
if (!isBitcoinAddress(to)) { | ||
throw new Error('must provide a valid destination address') | ||
} | ||
return this.clean().map(assoc('to', to)) | ||
} | ||
|
||
amount (amount) { | ||
if (!isPositiveInteger(amount)) { | ||
throw new Error('must provide a valid amount') | ||
} | ||
return this.clean().map(assoc('amount', amount)) | ||
} | ||
|
||
feePerByte (feePerByte) { | ||
if (!isPositiveInteger(feePerByte)) { | ||
throw new Error('must provide a valid fee-per-byte value') | ||
} | ||
return this.clean().map(assoc('feePerByte', feePerByte)) | ||
} | ||
|
||
clean () { | ||
return this.map(compose( | ||
assoc('selection', null), | ||
assoc('hash', null), | ||
assoc('rawTx', null) | ||
)) | ||
} | ||
|
||
build () { | ||
return this.map(payment => { | ||
if (payment.to == null) { | ||
throw new PaymentError('must set a destination address', payment) | ||
} | ||
if (payment.amount == null) { | ||
throw new PaymentError('must set an amount', payment) | ||
} | ||
if (payment.feePerByte == null) { | ||
throw new PaymentError('must set a fee-per-byte value', payment) | ||
} | ||
let targets = [new Coin({ address: payment.to, value: payment.amount })] | ||
let selection = descentDraw(targets, payment.feePerByte, payment.coins, payment.change) | ||
return assoc('selection', selection, payment) | ||
}) | ||
} | ||
|
||
buildSweep () { | ||
return this.map(payment => { | ||
if (payment.to == null) { | ||
throw new PaymentError('must set a destination address', payment) | ||
} | ||
if (payment.feePerByte == null) { | ||
throw new PaymentError('must set a fee-per-byte value', payment) | ||
} | ||
let selection = selectAll(payment.feePerByte, payment.coins, payment.to) | ||
return assoc('selection', selection, payment) | ||
}) | ||
} | ||
|
||
sign (secPass) { | ||
return this.map(payment => { | ||
if (payment.selection == null) { | ||
throw new PaymentError('cannot sign an unbuilt transaction', payment) | ||
} | ||
let tx = sign(secPass, this._wallet, payment.selection) | ||
let setData = compose(assoc('hash', tx.getId()), assoc('rawTx', tx.toHex())) | ||
return setData(payment) | ||
}) | ||
} | ||
|
||
publish () { | ||
/* return Promise, not BchPayment instance */ | ||
return this._payment.then(payment => { | ||
if (payment.rawTx == null) { | ||
throw new PaymentError('cannot publish an unsigned transaction', payment) | ||
} | ||
return BchApi.pushTx(payment.rawTx) | ||
.then(() => ({ hash: payment.hash })) | ||
}) | ||
} | ||
|
||
static defaultStateP () { | ||
return Promise.resolve(BchPayment.defaultState()) | ||
} | ||
|
||
static defaultState () { | ||
return { | ||
coins: [], | ||
to: null, | ||
amount: null, | ||
feePerByte: null, | ||
selection: null, | ||
hash: null, | ||
rawTx: null | ||
} | ||
} | ||
} | ||
|
||
module.exports = BchPayment |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* eslint-disable semi */ | ||
const BchApi = require('./bch-api') | ||
const { selectAll } = require('./coin-selection') | ||
|
||
class BchSpendable { | ||
constructor (bchWallet, wallet) { | ||
this._bchWallet = bchWallet | ||
this._wallet = wallet | ||
} | ||
|
||
getAddressBalance (source) { | ||
return this._bchWallet.getAddressBalance(source) | ||
} | ||
|
||
getAvailableBalance (source, feePerByte) { | ||
return BchApi.getUnspents(this._wallet, source).then(coins => { | ||
let { fee, outputs } = selectAll(feePerByte, coins, null) | ||
return { fee: feePerByte, sweepFee: fee, amount: outputs[0].value } | ||
}); | ||
} | ||
|
||
createPayment () { | ||
return this._bchWallet.createPayment() | ||
} | ||
} | ||
|
||
module.exports = BchSpendable |
Oops, something went wrong.