Skip to content
This repository has been archived by the owner on Mar 7, 2023. It is now read-only.

Commit

Permalink
Merge pull request #446 from blockchain/v3.38-release
Browse files Browse the repository at this point in the history
v3.38
  • Loading branch information
jtormey authored Oct 11, 2017
2 parents 9e29f10 + 6653c7e commit eda1257
Show file tree
Hide file tree
Showing 35 changed files with 1,576 additions and 96 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "blockchain-wallet-client",
"version": "3.37.3",
"version": "3.38.5",
"description": "Blockchain.info JavaScript Wallet",
"homepage": "https://github.com/blockchain/my-wallet-v3",
"bugs": {
Expand Down Expand Up @@ -51,6 +51,7 @@
"bitcoin-exchange-client": "^0.4.6",
"bitcoin-sfox-client": "^0.1.11",
"bitcoin-unocoin-client": "^0.3.4",
"bitcoincashjs-lib": "https://github.com/bitcoinjs/bitcoinjs-lib#9ac221c80dbc3462d7a2392cec2045ae2b590461",
"bitcoinjs-lib": "2.1.*",
"bs58": "2.0.*",
"core-js": "^2.4.1",
Expand All @@ -61,8 +62,8 @@
"isomorphic-fetch": "^2.2.1",
"pbkdf2": "^3.0.12",
"ramda": "^0.22.1",
"ramda-lens": "^0.1.2",
"randombytes": "^2.0.1",
"synk": "^0.0.2",
"unorm": "^1.4.1",
"ws": "2.0.*"
},
Expand Down
55 changes: 55 additions & 0 deletions src/bch/bch-account.js
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
76 changes: 76 additions & 0 deletions src/bch/bch-api.js
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
};
34 changes: 34 additions & 0 deletions src/bch/bch-imported.js
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
160 changes: 160 additions & 0 deletions src/bch/bch-payment.js
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
27 changes: 27 additions & 0 deletions src/bch/bch-spendable.js
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
Loading

0 comments on commit eda1257

Please sign in to comment.