mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-03 03:59:10 +01:00
OPS: preparing HD wallets
This commit is contained in:
parent
5cb8bf04da
commit
fe34f1c08e
5 changed files with 386 additions and 0 deletions
88
HDWallet.test.js
Normal file
88
HDWallet.test.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/* global it, jasmine */
|
||||
import {
|
||||
SegwitP2SHWallet,
|
||||
SegwitBech32Wallet,
|
||||
HDSegwitP2SHWallet,
|
||||
HDLegacyBreadwalletWallet,
|
||||
} from './class';
|
||||
let assert = require('assert');
|
||||
|
||||
it.only('can convert witness to address', () => {
|
||||
let address = SegwitP2SHWallet.witnessToAddress(
|
||||
'035c618df829af694cb99e664ce1b34f80ad2c3b49bcd0d9c0b1836c66b2d25fd8',
|
||||
);
|
||||
assert.equal(address, '34ZVGb3gT8xMLT6fpqC6dNVqJtJmvdjbD7');
|
||||
|
||||
address = SegwitBech32Wallet.witnessToAddress(
|
||||
'035c618df829af694cb99e664ce1b34f80ad2c3b49bcd0d9c0b1836c66b2d25fd8',
|
||||
);
|
||||
assert.equal(address, 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv');
|
||||
});
|
||||
|
||||
it('can create a BIP49', function() {
|
||||
let bip39 = require('bip39');
|
||||
let bitcoin = require('bitcoinjs-lib');
|
||||
let mnemonic =
|
||||
'honey risk juice trip orient galaxy win situate shoot anchor bounce remind horse traffic exotic since escape mimic ramp skin judge owner topple erode';
|
||||
assert.ok(bip39.validateMnemonic(mnemonic));
|
||||
let seed = bip39.mnemonicToSeed(mnemonic);
|
||||
let root = bitcoin.HDNode.fromSeedBuffer(seed);
|
||||
|
||||
let path = "m/49'/0'/0'/0/0";
|
||||
let child = root.derivePath(path);
|
||||
|
||||
let keyhash = bitcoin.crypto.hash160(child.getPublicKeyBuffer());
|
||||
let scriptSig = bitcoin.script.witnessPubKeyHash.output.encode(keyhash);
|
||||
let addressBytes = bitcoin.crypto.hash160(scriptSig);
|
||||
let outputScript = bitcoin.script.scriptHash.output.encode(addressBytes);
|
||||
let address = bitcoin.address.fromOutputScript(
|
||||
outputScript,
|
||||
bitcoin.networks.bitcoin,
|
||||
);
|
||||
|
||||
assert.equal(address, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
|
||||
|
||||
// checking that WIF from HD corresponds to derived segwit address
|
||||
let Segwit = new SegwitP2SHWallet();
|
||||
Segwit.setSecret(child.keyPair.toWIF());
|
||||
assert.equal(address, Segwit.getAddress());
|
||||
|
||||
// testing our class
|
||||
let hd = new HDSegwitP2SHWallet();
|
||||
hd.setSecret(mnemonic);
|
||||
assert.equal(address, hd._getExternalAddressByIndex(0));
|
||||
assert.equal(true, hd.validateMnemonic());
|
||||
|
||||
assert.equal(child.keyPair.toWIF(), hd._getExternalWIFByIndex(0));
|
||||
});
|
||||
|
||||
it('HD breadwallet works', async function() {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000;
|
||||
let hdBread = new HDLegacyBreadwalletWallet();
|
||||
hdBread.setSecret(
|
||||
'high relief amount witness try remember adult destroy puppy fox giant peace',
|
||||
);
|
||||
console.log(
|
||||
'bread 0/0 = ',
|
||||
hdBread._getExternalAddressByIndex(0),
|
||||
'bread 1/0 = ',
|
||||
hdBread._getInternalAddressByIndex(0),
|
||||
'valid = ',
|
||||
hdBread.validateMnemonic(),
|
||||
);
|
||||
console.log(hdBread.getXpub());
|
||||
await hdBread.fetchBalance();
|
||||
console.log(hdBread.balance);
|
||||
|
||||
await hdBread.fetchTransactions();
|
||||
console.log('tx count = ', hdBread.transactions.length);
|
||||
|
||||
console.log(
|
||||
hdBread.next_free_address_index,
|
||||
hdBread.next_free_change_address_index,
|
||||
);
|
||||
|
||||
// let bitcoin = require('bitcoinjs-lib');
|
||||
// let node = bitcoin.HDNode.fromBase58( hdBread.getXpub() );
|
||||
// console.log( "normal address", node.derive(0).derive(0).getAddress());
|
||||
});
|
62
class/hd-legacy-breadwallet-wallet.js
Normal file
62
class/hd-legacy-breadwallet-wallet.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { HDSegwitP2SHWallet } from './';
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
const bip39 = require('bip39');
|
||||
|
||||
/**
|
||||
* HD Wallet (BIP39).
|
||||
* In particular, BIP49 (P2SH Segwit) https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki
|
||||
*/
|
||||
export class HDLegacyBreadwalletWallet extends HDSegwitP2SHWallet {
|
||||
constructor() {
|
||||
super();
|
||||
this.type = 'HDLegacyBreadwallet';
|
||||
}
|
||||
|
||||
getTypeReadable() {
|
||||
return 'HD Legacy Breadwallet-compatible (P2PKH)';
|
||||
}
|
||||
|
||||
getAddress() {
|
||||
// TODO: derive from hierarchy, return next free address
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/584
|
||||
* @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/914
|
||||
* @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/997
|
||||
*/
|
||||
getXpub() {
|
||||
let mnemonic = this.secret;
|
||||
let seed = bip39.mnemonicToSeed(mnemonic);
|
||||
let root = bitcoin.HDNode.fromSeedBuffer(seed);
|
||||
|
||||
let path = "m/0'";
|
||||
let child = root.derivePath(path).neutered();
|
||||
return child.toBase58();
|
||||
}
|
||||
|
||||
_getExternalAddressByIndex(index) {
|
||||
index = index * 1; // cast to int
|
||||
let mnemonic = this.secret;
|
||||
let seed = bip39.mnemonicToSeed(mnemonic);
|
||||
let root = bitcoin.HDNode.fromSeedBuffer(seed);
|
||||
|
||||
let path = "m/0'/0/" + index;
|
||||
let child = root.derivePath(path);
|
||||
|
||||
return child.getAddress();
|
||||
}
|
||||
|
||||
_getInternalAddressByIndex(index) {
|
||||
index = index * 1; // cast to int
|
||||
|
||||
let mnemonic = this.secret;
|
||||
let seed = bip39.mnemonicToSeed(mnemonic);
|
||||
let root = bitcoin.HDNode.fromSeedBuffer(seed);
|
||||
|
||||
let path = "m/0'/1/" + index;
|
||||
let child = root.derivePath(path);
|
||||
|
||||
return child.getAddress();
|
||||
}
|
||||
}
|
233
class/hd-segwit-p2sh-wallet.js
Normal file
233
class/hd-segwit-p2sh-wallet.js
Normal file
|
@ -0,0 +1,233 @@
|
|||
import { LegacyWallet } from './legacy-wallet';
|
||||
import { SegwitP2SHWallet } from './segwit-p2sh-wallet';
|
||||
import Frisbee from 'frisbee';
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
const bip39 = require('bip39');
|
||||
|
||||
/**
|
||||
* HD Wallet (BIP39).
|
||||
* In particular, BIP49 (P2SH Segwit) https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki
|
||||
*/
|
||||
export class HDSegwitP2SHWallet extends LegacyWallet {
|
||||
constructor() {
|
||||
super();
|
||||
this.type = 'HDsegwitP2SH';
|
||||
this.next_free_address_index = 0;
|
||||
this.next_free_change_address_index = 0;
|
||||
this.internal_addresses_cache = {}; // index => address
|
||||
this.external_addresses_cache = {}; // index => address
|
||||
}
|
||||
|
||||
validateMnemonic() {
|
||||
return bip39.validateMnemonic(this.secret);
|
||||
}
|
||||
|
||||
getTypeReadable() {
|
||||
return 'HD SegWit (P2SH)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives from hierarchy, returns next free address
|
||||
* (the one that has no transactions). Looks for several,
|
||||
* gve ups if none found, and returns the used one
|
||||
*
|
||||
* @return {Promise.<string>}
|
||||
*/
|
||||
async getAddressAsync() {
|
||||
// looking for free external address
|
||||
let freeAddress = '';
|
||||
let c;
|
||||
for (c = -1; c < 5; c++) {
|
||||
let Segwit = new SegwitP2SHWallet();
|
||||
Segwit.setSecret(
|
||||
this._getExternalWIFByIndex(this.next_free_address_index + c),
|
||||
);
|
||||
await Segwit.fetchTransactions();
|
||||
if (Segwit.transactions.length === 0) {
|
||||
// found free address
|
||||
freeAddress = Segwit.getAddress();
|
||||
this.next_free_address_index += c + 1; // now points to the one _after_
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!freeAddress) {
|
||||
// could not find in cycle above, give up
|
||||
freeAddress = this._getExternalAddressByIndex(
|
||||
this.next_free_address_index + c,
|
||||
); // we didnt check this one, maybe its free
|
||||
this.next_free_address_index += c + 1; // now points to the one _after_
|
||||
}
|
||||
|
||||
return freeAddress;
|
||||
}
|
||||
|
||||
_getExternalWIFByIndex(index) {
|
||||
index = index * 1; // cast to int
|
||||
let mnemonic = this.secret;
|
||||
let seed = bip39.mnemonicToSeed(mnemonic);
|
||||
let root = bitcoin.HDNode.fromSeedBuffer(seed);
|
||||
let path = "m/49'/0'/0'/0/" + index;
|
||||
let child = root.derivePath(path);
|
||||
return child.keyPair.toWIF();
|
||||
}
|
||||
|
||||
_getInternalWIFByIndex(index) {
|
||||
index = index * 1; // cast to int
|
||||
let mnemonic = this.secret;
|
||||
let seed = bip39.mnemonicToSeed(mnemonic);
|
||||
let root = bitcoin.HDNode.fromSeedBuffer(seed);
|
||||
let path = "m/49'/0'/0'/1/" + index;
|
||||
let child = root.derivePath(path);
|
||||
return child.keyPair.toWIF();
|
||||
}
|
||||
|
||||
_getExternalAddressByIndex(index) {
|
||||
index = index * 1; // cast to int
|
||||
let mnemonic = this.secret;
|
||||
let seed = bip39.mnemonicToSeed(mnemonic);
|
||||
let root = bitcoin.HDNode.fromSeedBuffer(seed);
|
||||
let path = "m/49'/0'/0'/0/" + index;
|
||||
let child = root.derivePath(path);
|
||||
|
||||
let keyhash = bitcoin.crypto.hash160(child.getPublicKeyBuffer());
|
||||
let scriptSig = bitcoin.script.witnessPubKeyHash.output.encode(keyhash);
|
||||
let addressBytes = bitcoin.crypto.hash160(scriptSig);
|
||||
let outputScript = bitcoin.script.scriptHash.output.encode(addressBytes);
|
||||
let address = bitcoin.address.fromOutputScript(
|
||||
outputScript,
|
||||
bitcoin.networks.bitcoin,
|
||||
);
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
_getInternalAddressByIndex(index) {
|
||||
index = index * 1; // cast to int
|
||||
let mnemonic = this.secret;
|
||||
let seed = bip39.mnemonicToSeed(mnemonic);
|
||||
let root = bitcoin.HDNode.fromSeedBuffer(seed);
|
||||
|
||||
let path = "m/49'/0'/0'/1/" + index;
|
||||
let child = root.derivePath(path);
|
||||
|
||||
let keyhash = bitcoin.crypto.hash160(child.getPublicKeyBuffer());
|
||||
let scriptSig = bitcoin.script.witnessPubKeyHash.output.encode(keyhash);
|
||||
let addressBytes = bitcoin.crypto.hash160(scriptSig);
|
||||
let outputScript = bitcoin.script.scriptHash.output.encode(addressBytes);
|
||||
let address = bitcoin.address.fromOutputScript(
|
||||
outputScript,
|
||||
bitcoin.networks.bitcoin,
|
||||
);
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
async fetchBalance() {
|
||||
const api = new Frisbee({ baseURI: 'https://blockchain.info' });
|
||||
|
||||
let response = await api.get('/balance?active=' + this.getXpub());
|
||||
|
||||
// console.log(response);
|
||||
if (response && response.body) {
|
||||
for (let xpub of Object.keys(response.body)) {
|
||||
this.balance = response.body[xpub].final_balance / 100000000;
|
||||
}
|
||||
} else {
|
||||
throw new Error('Could not fetch balance from API');
|
||||
}
|
||||
}
|
||||
|
||||
async fetchTransactions() {
|
||||
const api = new Frisbee({ baseURI: 'https://blockchain.info' });
|
||||
this.transactions = [];
|
||||
let offset = 0;
|
||||
|
||||
while (1) {
|
||||
console.log('fetching ', offset);
|
||||
let response = await api.get(
|
||||
'/multiaddr?active=' + this.getXpub() + '&n=100&offset=' + offset,
|
||||
);
|
||||
|
||||
if (response && response.body) {
|
||||
if (response.body.txs && response.body.txs.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// processing TXs and adding to internal memory
|
||||
console.log('response.body.txs = ', response.body.txs.length);
|
||||
if (response.body.txs) {
|
||||
for (let tx of response.body.txs) {
|
||||
let value = 0;
|
||||
|
||||
for (let input of tx.inputs) {
|
||||
// ----- INPUTS
|
||||
if (input.prev_out.xpub) {
|
||||
// sent FROM US
|
||||
value -= input.prev_out.value;
|
||||
|
||||
// setting internal caches to help ourselves in future...
|
||||
let path = input.prev_out.xpub.path.split('/');
|
||||
if (path[path.length - 2] === '1') {
|
||||
// change address
|
||||
this.next_free_change_address_index = Math.max(
|
||||
path[path.length - 1] * 1 + 1,
|
||||
this.next_free_change_address_index,
|
||||
);
|
||||
// setting to point to last maximum known change address + 1
|
||||
}
|
||||
if (path[path.length - 2] === '0') {
|
||||
// main (aka external) address
|
||||
this.next_free_address_index = Math.max(
|
||||
path[path.length - 1] * 1 + 1,
|
||||
this.next_free_address_index,
|
||||
);
|
||||
// setting to point to last maximum known main address + 1
|
||||
}
|
||||
// done with cache
|
||||
}
|
||||
}
|
||||
|
||||
for (let output of tx.out) {
|
||||
// ----- OUTPUTS
|
||||
if (output.xpub) {
|
||||
// sent TO US (change)
|
||||
value += output.value;
|
||||
|
||||
// setting internal caches to help ourselves in future...
|
||||
let path = output.xpub.path.split('/');
|
||||
if (path[path.length - 2] === '1') {
|
||||
// change address
|
||||
this.next_free_change_address_index = Math.max(
|
||||
path[path.length - 1] * 1 + 1,
|
||||
this.next_free_change_address_index,
|
||||
);
|
||||
// setting to point to last maximum known change address + 1
|
||||
}
|
||||
if (path[path.length - 2] === '0') {
|
||||
// main (aka external) address
|
||||
this.next_free_address_index = Math.max(
|
||||
path[path.length - 1] * 1 + 1,
|
||||
this.next_free_address_index,
|
||||
);
|
||||
// setting to point to last maximum known main address + 1
|
||||
}
|
||||
// done with cache
|
||||
}
|
||||
}
|
||||
|
||||
tx.value = value / 100000000;
|
||||
|
||||
this.transactions.push(tx);
|
||||
}
|
||||
} else {
|
||||
break; // error ?
|
||||
}
|
||||
} else {
|
||||
throw new Error('Could not fetch balance from API'); // breaks here
|
||||
}
|
||||
|
||||
offset += 100;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,3 +4,5 @@ export * from './constants';
|
|||
export * from './legacy-wallet';
|
||||
export * from './segwit-bech-wallet';
|
||||
export * from './segwit-p2sh-wallet';
|
||||
export * from './hd-segwit-p2sh-wallet';
|
||||
export * from './hd-legacy-breadwallet-wallet';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* global fetch */
|
||||
import { AbstractWallet } from './abstract-wallet';
|
||||
import { SegwitBech32Wallet } from './';
|
||||
import { useBlockcypherTokens } from './constants';
|
||||
import Frisbee from 'frisbee';
|
||||
const isaac = require('isaac');
|
||||
|
|
Loading…
Add table
Reference in a new issue