BlueWallet/class.js

565 lines
14 KiB
JavaScript
Raw Normal View History

2018-03-17 19:19:26 +00:00
/* global fetch */
2018-03-17 22:39:21 +02:00
import { AsyncStorage } from 'react-native';
import Frisbee from 'frisbee';
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
let useBlockcypherTokens = false;
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
let bitcoin = require('bitcoinjs-lib');
let signer = require('./models/signer');
let BigNumber = require('bignumber.js');
let isaac = require('isaac');
2018-01-30 22:42:38 +00:00
// alternative https://github.com/pointbiz/bitaddress.org/blob/master/src/securerandom.js
class AbstractWallet {
2018-03-17 22:39:21 +02:00
constructor() {
this.type = 'abstract';
this.label = '';
this.secret = ''; // private key or recovery phrase
this.balance = 0;
this.transactions = [];
this._address = false; // cache
this.utxo = [];
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getTransactions() {
return this.transactions;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getTypeReadable() {
return this.type;
2018-01-30 22:42:38 +00:00
}
/**
*
* @returns {string}
*/
2018-03-17 22:39:21 +02:00
getLabel() {
return this.label;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getBalance() {
return this.balance;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
setLabel(newLabel) {
this.label = newLabel;
return this;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getSecret() {
return this.secret;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
setSecret(newSecret) {
this.secret = newSecret;
return this;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
static fromJson(obj) {
let obj2 = JSON.parse(obj);
let temp = new this();
2018-01-30 22:42:38 +00:00
for (let key2 of Object.keys(obj2)) {
2018-03-17 22:39:21 +02:00
temp[key2] = obj2[key2];
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
return temp;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getAddress() {}
2018-01-30 22:42:38 +00:00
2018-03-17 19:36:30 +02:00
// createTx () { throw Error('not implemented') }
2018-01-30 22:42:38 +00:00
}
/**
* Has private key and address signle like "1ABCD....."
* (legacy P2PKH compressed)
*/
export class LegacyWallet extends AbstractWallet {
2018-03-17 22:39:21 +02:00
constructor() {
super();
this.type = 'legacy';
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
generate() {
function myRng(c) {
let buf = Buffer.alloc(c);
let totalhex = '';
2018-03-17 19:36:30 +02:00
for (let i = 0; i < c; i++) {
2018-03-17 22:39:21 +02:00
let randomNumber = isaac.random();
randomNumber = Math.floor(randomNumber * 255);
let n = new BigNumber(randomNumber);
let hex = n.toString(16);
2018-01-30 22:42:38 +00:00
if (hex.length === 1) {
2018-03-17 22:39:21 +02:00
hex = '0' + hex;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
totalhex += hex;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
totalhex = bitcoin.crypto.sha256('oh hai!' + totalhex).toString('hex');
totalhex = bitcoin.crypto.sha256(totalhex).toString('hex');
buf.fill(totalhex, 0, 'hex');
return buf;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
this.secret = bitcoin.ECPair.makeRandom({ rng: myRng }).toWIF();
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getTypeReadable() {
return 'P2 PKH';
2018-01-30 22:42:38 +00:00
}
/**
*
* @returns {string}
*/
2018-03-17 22:39:21 +02:00
getAddress() {
if (this._address) return this._address;
let address;
2018-01-30 22:42:38 +00:00
try {
2018-03-17 22:39:21 +02:00
let keyPair = bitcoin.ECPair.fromWIF(this.secret);
address = keyPair.getAddress();
2018-01-30 22:42:38 +00:00
} catch (err) {
2018-03-17 22:39:21 +02:00
return false;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
this._address = address;
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
return this._address;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
async fetchBalance() {
let response;
let token = (array => {
2018-01-30 22:42:38 +00:00
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
2018-03-17 22:39:21 +02:00
[array[i], array[j]] = [array[j], array[i]];
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
return array[0];
})([
'0326b7107b4149559d18ce80612ef812',
'a133eb7ccacd4accb80cb1225de4b155',
'7c2b1628d27b4bd3bf8eaee7149c577f',
'f1e5a02b9ec84ec4bc8db2349022e5f5',
'e5926dbeb57145979153adc41305b183',
]);
2018-01-30 22:42:38 +00:00
try {
if (useBlockcypherTokens) {
2018-03-17 22:39:21 +02:00
response = await fetch(
'https://api.blockcypher.com/v1/btc/main/addrs/' +
this.getAddress() +
'/balance?token=' +
token,
);
2018-01-30 22:42:38 +00:00
} else {
2018-03-17 22:39:21 +02:00
response = await fetch(
'https://api.blockcypher.com/v1/btc/main/addrs/' +
this.getAddress() +
'/balance',
);
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
let json = await response.json();
2018-01-30 22:42:38 +00:00
if (typeof json.final_balance === 'undefined') {
2018-03-17 22:39:21 +02:00
throw new Error('Could not fetch balance from API');
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
this.balance = json.final_balance / 100000000;
2018-01-30 22:42:38 +00:00
} catch (err) {
2018-03-17 22:39:21 +02:00
console.warn(err);
2018-01-30 22:42:38 +00:00
}
}
2018-03-17 22:39:21 +02:00
async fetchUtxo() {
let response;
let token = (array => {
2018-01-30 22:42:38 +00:00
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
2018-03-17 22:39:21 +02:00
[array[i], array[j]] = [array[j], array[i]];
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
return array[0];
})([
'0326b7107b4149559d18ce80612ef812',
'a133eb7ccacd4accb80cb1225de4b155',
'7c2b1628d27b4bd3bf8eaee7149c577f',
'f1e5a02b9ec84ec4bc8db2349022e5f5',
'e5926dbeb57145979153adc41305b183',
]);
2018-01-30 22:42:38 +00:00
try {
// TODO: hande case when there's more than 2000 UTXOs (do pagination)
// TODO: (2000 is max UTXOs we can fetch in one call)
if (useBlockcypherTokens) {
2018-03-17 22:39:21 +02:00
response = await fetch(
'https://api.blockcypher.com/v1/btc/main/addrs/' +
this.getAddress() +
'?unspentOnly=true&limit=2000&token=' +
token,
);
2018-01-30 22:42:38 +00:00
} else {
2018-03-17 22:39:21 +02:00
response = await fetch(
'https://api.blockcypher.com/v1/btc/main/addrs/' +
this.getAddress() +
'?unspentOnly=true&limit=2000',
);
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
let json = await response.json();
2018-01-30 22:42:38 +00:00
if (typeof json.final_balance === 'undefined') {
2018-03-17 22:39:21 +02:00
throw new Error('Could not fetch UTXO from API');
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
json.txrefs = json.txrefs || []; // case when source address is empty
this.utxo = json.txrefs;
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
json.unconfirmed_txrefs = json.unconfirmed_txrefs || [];
this.utxo = this.utxo.concat(json.unconfirmed_txrefs);
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
console.log('got utxo: ', this.utxo);
2018-01-30 22:42:38 +00:00
} catch (err) {
2018-03-17 22:39:21 +02:00
console.warn(err);
2018-01-30 22:42:38 +00:00
}
}
2018-03-17 22:39:21 +02:00
async fetchTransactions() {
let response;
let token = (array => {
2018-01-30 22:42:38 +00:00
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
2018-03-17 22:39:21 +02:00
[array[i], array[j]] = [array[j], array[i]];
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
return array[0];
})([
'0326b7107b4149559d18ce80612ef812',
'a133eb7ccacd4accb80cb1225de4b155',
'7c2b1628d27b4bd3bf8eaee7149c577f',
'f1e5a02b9ec84ec4bc8db2349022e5f5',
'e5926dbeb57145979153adc41305b183',
]);
2018-01-30 22:42:38 +00:00
try {
2018-03-17 22:39:21 +02:00
let url;
2018-01-30 22:42:38 +00:00
if (useBlockcypherTokens) {
2018-03-17 22:39:21 +02:00
response = await fetch(
(url =
'https://api.blockcypher.com/v1/btc/main/addrs/' +
this.getAddress() +
'/full?token=' +
token),
);
2018-01-30 22:42:38 +00:00
} else {
2018-03-17 22:39:21 +02:00
response = await fetch(
(url =
'https://api.blockcypher.com/v1/btc/main/addrs/' +
this.getAddress() +
'/full'),
);
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
console.log(url);
let json = await response.json();
2018-01-30 22:42:38 +00:00
if (!json.txs) {
2018-03-17 22:39:21 +02:00
throw new Error('Could not fetch transactions from API');
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
this.transactions = json.txs;
2018-01-30 22:42:38 +00:00
// now, calculating value per each transaction...
for (let tx of this.transactions) {
// how much came in...
2018-03-17 22:39:21 +02:00
let value = 0;
2018-01-30 22:42:38 +00:00
for (let out of tx.outputs) {
2018-03-17 22:39:21 +02:00
if (out.addresses.indexOf(this.getAddress()) !== -1) {
// found our address in outs of this TX
value += out.value;
2018-01-30 22:42:38 +00:00
}
}
2018-03-17 22:39:21 +02:00
tx.value = value;
2018-01-30 22:42:38 +00:00
// end
// how much came out
2018-03-17 22:39:21 +02:00
value = 0;
2018-01-30 22:42:38 +00:00
for (let inp of tx.inputs) {
2018-03-17 22:39:21 +02:00
if (inp.addresses.indexOf(this.getAddress()) !== -1) {
// found our address in outs of this TX
value -= inp.output_value;
2018-01-30 22:42:38 +00:00
}
}
2018-03-17 22:39:21 +02:00
console.log('came out', value);
tx.value += value;
2018-01-30 22:42:38 +00:00
// end
}
} catch (err) {
2018-03-17 22:39:21 +02:00
console.warn(err);
2018-01-30 22:42:38 +00:00
}
}
2018-03-17 22:39:21 +02:00
getShortAddress() {
let a = this.getAddress().split('');
return (
a[0] +
a[1] +
a[2] +
a[3] +
a[4] +
a[5] +
a[6] +
a[7] +
a[8] +
a[9] +
a[10] +
a[11] +
a[12] +
a[13] +
'...' +
a[a.length - 6] +
a[a.length - 5] +
a[a.length - 4] +
a[a.length - 3] +
a[a.length - 2] +
a[a.length - 1]
);
}
async broadcastTx(txhex) {
2018-01-30 22:42:38 +00:00
const api = new Frisbee({
baseURI: 'https://btczen.com',
headers: {
2018-03-17 22:39:21 +02:00
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
let res = await api.get('/broadcast/' + txhex);
console.log('response', res.body);
return res.body;
2018-01-30 22:42:38 +00:00
2018-03-17 19:36:30 +02:00
/* const api = new Frisbee({
2018-01-30 22:42:38 +00:00
baseURI: 'https://api.blockcypher.com',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
;let token = ((array) => {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]]
}
return array[0]
})(['0326b7107b4149559d18ce80612ef812', 'a133eb7ccacd4accb80cb1225de4b155', '7c2b1628d27b4bd3bf8eaee7149c577f', 'f1e5a02b9ec84ec4bc8db2349022e5f5', 'e5926dbeb57145979153adc41305b183'])
console.log('broadcast using token')
let res = await api.post('/v1/btc/main/txs/push?token=' + token, {body: {'tx': txhex}})
console.log('response', res.body)
2018-03-17 19:36:30 +02:00
return res.body */
2018-01-30 22:42:38 +00:00
}
}
export class SegwitBech32Wallet extends LegacyWallet {
2018-03-17 22:39:21 +02:00
constructor() {
super();
this.type = 'segwitBech32';
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getTypeReadable() {
return 'P2 WPKH';
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getAddress() {
if (this._address) return this._address;
let address;
2018-01-30 22:42:38 +00:00
try {
2018-03-17 22:39:21 +02:00
let keyPair = bitcoin.ECPair.fromWIF(this.secret);
let pubKey = keyPair.getPublicKeyBuffer();
let scriptPubKey = bitcoin.script.witnessPubKeyHash.output.encode(
bitcoin.crypto.hash160(pubKey),
);
address = bitcoin.address.fromOutputScript(scriptPubKey);
2018-01-30 22:42:38 +00:00
} catch (err) {
2018-03-17 22:39:21 +02:00
return false;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
this._address = address;
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
return this._address;
2018-01-30 22:42:38 +00:00
}
}
export class SegwitP2SHWallet extends LegacyWallet {
2018-03-17 22:39:21 +02:00
constructor() {
super();
this.type = 'segwitP2SH';
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getTypeReadable() {
return 'SegWit (P2SH)';
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getAddress() {
if (this._address) return this._address;
let address;
2018-01-30 22:42:38 +00:00
try {
2018-03-17 22:39:21 +02:00
let keyPair = bitcoin.ECPair.fromWIF(this.secret);
let pubKey = keyPair.getPublicKeyBuffer();
let witnessScript = bitcoin.script.witnessPubKeyHash.output.encode(
bitcoin.crypto.hash160(pubKey),
);
let scriptPubKey = bitcoin.script.scriptHash.output.encode(
bitcoin.crypto.hash160(witnessScript),
);
address = bitcoin.address.fromOutputScript(scriptPubKey);
2018-01-30 22:42:38 +00:00
} catch (err) {
2018-03-17 22:39:21 +02:00
return false;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
this._address = address;
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
return this._address;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
createTx(utxos, amount, fee, address, memo, sequence) {
2018-01-30 22:42:38 +00:00
if (sequence === undefined) {
2018-03-17 22:39:21 +02:00
sequence = 0;
2018-01-30 22:42:38 +00:00
}
// transforming UTXOs fields to how module expects it
for (let u of utxos) {
2018-03-17 22:39:21 +02:00
u.confirmations = 6; // hack to make module accept 0 confirmations
u.txid = u.tx_hash;
u.vout = u.tx_output_n;
u.amount = new BigNumber(u.value);
u.amount = u.amount.div(100000000);
u.amount = u.amount.toString(10);
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
console.log(
'creating tx ',
amount,
' with fee ',
fee,
'secret=',
this.getSecret(),
'from address',
this.getAddress(),
);
let amountPlusFee = parseFloat(new BigNumber(amount).add(fee).toString(10));
2018-01-30 22:42:38 +00:00
// to compensate that module substracts fee from amount
2018-03-17 22:39:21 +02:00
return signer.createSegwitTransaction(
utxos,
address,
amountPlusFee,
fee,
this.getSecret(),
this.getAddress(),
sequence,
);
2018-01-30 22:42:38 +00:00
}
}
export class AppStorage {
2018-03-17 22:39:21 +02:00
constructor() {
2018-01-30 22:42:38 +00:00
/** {Array.<AbstractWallet>} */
2018-03-17 22:39:21 +02:00
this.wallets = [];
this.tx_metadata = {};
2018-01-30 22:42:38 +00:00
this.settings = {
brandingColor: '#00aced',
buttonBackground: '#00aced',
2018-03-17 22:39:21 +02:00
buttonDangedBackground: '#F40349',
};
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
async loadFromDisk() {
2018-01-30 22:42:38 +00:00
try {
2018-03-17 22:39:21 +02:00
let data = await AsyncStorage.getItem('data');
2018-01-30 22:42:38 +00:00
if (data !== null) {
2018-03-17 22:39:21 +02:00
data = JSON.parse(data);
if (!data.wallets) return false;
let wallets = data.wallets;
2018-01-30 22:42:38 +00:00
for (let key of wallets) {
// deciding which type is wallet and instatiating correct object
2018-03-17 22:39:21 +02:00
let tempObj = JSON.parse(key);
let unserializedWallet;
2018-01-30 22:42:38 +00:00
switch (tempObj.type) {
case 'segwitBech32':
2018-03-17 22:39:21 +02:00
unserializedWallet = SegwitBech32Wallet.fromJson(key);
break;
2018-01-30 22:42:38 +00:00
case 'segwitP2SH':
2018-03-17 22:39:21 +02:00
unserializedWallet = SegwitP2SHWallet.fromJson(key);
break;
2018-01-30 22:42:38 +00:00
case 'legacy':
default:
2018-03-17 22:39:21 +02:00
unserializedWallet = LegacyWallet.fromJson(key);
break;
2018-01-30 22:42:38 +00:00
}
// done
2018-03-17 22:39:21 +02:00
this.wallets.push(unserializedWallet);
this.tx_metadata = data.tx_metadata;
2018-01-30 22:42:38 +00:00
}
}
} catch (error) {
2018-03-17 22:39:21 +02:00
return false;
2018-01-30 22:42:38 +00:00
}
}
/**
*
* @param wallet {AbstractWallet}
*/
2018-03-17 22:39:21 +02:00
deleteWallet(wallet) {
let secret = wallet.getSecret();
let tempWallets = [];
2018-01-30 22:42:38 +00:00
for (let value of this.wallets) {
2018-03-17 22:39:21 +02:00
if (value.getSecret() === secret) {
// the one we should delete
2018-01-30 22:42:38 +00:00
// nop
2018-03-17 22:39:21 +02:00
} else {
// the one we must keep
tempWallets.push(value);
2018-01-30 22:42:38 +00:00
}
}
2018-03-17 22:39:21 +02:00
this.wallets = tempWallets;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
saveToDisk() {
let walletsToSave = [];
2018-01-30 22:42:38 +00:00
for (let key of this.wallets) {
2018-03-17 22:39:21 +02:00
walletsToSave.push(JSON.stringify(key));
2018-01-30 22:42:38 +00:00
}
let data = {
wallets: walletsToSave,
2018-03-17 22:39:21 +02:00
tx_metadata: this.tx_metadata,
};
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
return AsyncStorage.setItem('data', JSON.stringify(data));
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
async fetchWalletBalances() {
2018-01-30 22:42:38 +00:00
// console.warn('app - fetchWalletBalances()')
for (let wallet of this.wallets) {
2018-03-17 22:39:21 +02:00
await wallet.fetchBalance();
2018-01-30 22:42:38 +00:00
}
}
2018-03-17 22:39:21 +02:00
async fetchWalletTransactions() {
2018-01-30 22:42:38 +00:00
// console.warn('app - fetchWalletTransactions()')
for (let wallet of this.wallets) {
2018-03-17 22:39:21 +02:00
await wallet.fetchTransactions();
2018-01-30 22:42:38 +00:00
}
}
/**
*
* @returns {Array.<AbstractWallet>}
*/
2018-03-17 22:39:21 +02:00
getWallets() {
return this.wallets;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
getTransactions() {
let txs = [];
2018-01-30 22:42:38 +00:00
for (let wallet of this.wallets) {
2018-03-17 22:39:21 +02:00
txs = txs.concat(wallet.transactions);
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
return txs;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
saveWallets() {}
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
listTXs() {}
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
listUnconfirmed() {}
2018-01-30 22:42:38 +00:00
2018-03-17 22:39:21 +02:00
getBalance() {
let finalBalance = 0;
2018-01-30 22:42:38 +00:00
for (let wal of this.wallets) {
2018-03-17 22:39:21 +02:00
finalBalance += wal.balance;
2018-01-30 22:42:38 +00:00
}
2018-03-17 22:39:21 +02:00
return finalBalance;
2018-01-30 22:42:38 +00:00
}
2018-03-17 19:36:30 +02:00
}