'use strict';
// External Dependencies
const BN = require('bn.js');
const EC = require('elliptic').ec;
const merge = require('lodash.merge');
const payments = require('bitcoinjs-lib/src/payments');
// Mnemonics
const ecc = require('tiny-secp256k1');
const BIP32 = require('bip32').default;
const bip32 = new BIP32(ecc);
const bip39 = require('bip39');
// Types
const Key = require('./key');
const Actor = require('./actor');
const Collection = require('./collection');
// const Consensus = require('./consensus');
// const Channel = require('./channel');
const Hash256 = require('./hash256');
const Service = require('./service');
const Secret = require('./secret');
const State = require('./state');
/**
* Manage keys and track their balances.
* @property {String} id Unique identifier for this {@link Wallet}.
* @type {Object}
*/
class Wallet extends Service {
/**
* Create an instance of a {@link Wallet}.
* @param {Object} [settings={}] Configure the wallet.
* @param {Number} [settings.verbosity=2] One of: 0 (none), 1 (error), 2 (warning), 3 (notice), 4 (debug), 5 (audit)
* @param {Object} [settings.key] Key to restore from.
* @param {String} [settings.key.seed] Mnemonic seed for a restored wallet.
* @return {Wallet} Instance of the wallet.
*/
constructor (settings = {}) {
super(settings);
// Create a Marshalling object
this.marshall = {
agents: [],
collections: {
transactions: null, // not yet loaded, seek for Buffer,
orders: null
}
};
this.settings = merge({
name: 'primary',
network: 'regtest',
language: 'english',
locktime: 144,
decimals: 8,
shardsize: 4,
verbosity: 2,
witness: true,
key: null,
version: 1
}, settings);
// bcoin.set(this.settings.network);
this.database = null;
/* this.database = new WalletDB({
network: 'regtest'
}); */
this.account = null;
this.manager = null;
this.wallet = null;
this.master = null;
this.ring = null;
this.seed = null;
// TODO: enable wordlist translations
// this.words = Mnemonic.getWordlist(this.settings.language).words;
this.mnemonic = null;
this.index = 0;
// Storage
this.accounts = new Collection();
this.addresses = new Collection();
this.keys = new Collection();
this.coins = new Collection();
this.transactions = new Collection();
this.txids = new Collection();
this.outputs = new Collection();
// Encrypted Storage
this.secrets = new Collection({
methods: {
create: this._prepareSecret.bind(this)
}
});
// Internals
this.ec = new EC('secp256k1');
this.key = new Key(this.settings.key);
this.entity = new Actor(this.settings);
// this.consensus = new Consensus();
// Internal State
this._state = merge(this._state, {
actors: {},
asset: this.settings.asset || null,
balances: {
confirmed: 0,
unconfirmed: 0
},
content: {
balances: {
spendable: 0
},
keys: {},
transactions: {},
utxos: []
},
labels: [],
space: {}, // tracks addresses in shard
keys: {},
services: {},
status: 'PAUSED',
transactions: {},
orders: {},
outputs: {}
});
// Cleanup log output
// TODO: remove these
Object.defineProperty(this, 'database', { enumerable: false });
Object.defineProperty(this, 'accounts', { enumerable: false });
Object.defineProperty(this, 'addresses', { enumerable: false });
Object.defineProperty(this, 'utxos', { enumerable: false });
Object.defineProperty(this, 'keys', { enumerable: false });
Object.defineProperty(this, 'outputs', { enumerable: false });
Object.defineProperty(this, 'secrets', { enumerable: false });
Object.defineProperty(this, 'swarm', { enumerable: false });
Object.defineProperty(this, 'transactions', { enumerable: false });
Object.defineProperty(this, 'wallet', { enumerable: false });
return this;
}
get balance () {
return this.get('/balances/confirmed');
}
get orders () {
return this.get('/orders');
}
get xprv () {
return this.key.xprv;
}
get xpub () {
return this.key.xpub;
}
get version () {
return this.settings.version;
}
/**
* Create a new seed phrase.
* @param {String} passphrase BIP 39 passphrase for key derivation.
* @returns {FabricSeed} The seed object.
*/
static createSeed (passphrase = '') {
const mnemonic = bip39.generateMnemonic(256);
const interim = bip39.mnemonicToSeedSync(mnemonic, passphrase);
const master = bip32.fromSeed(interim);
return {
phrase: mnemonic.toString(),
master: master.privateKey.toString('hex'),
xprv: master.toBase58()
};
}
/**
* Create a new {@link Wallet} from a seed object.
* @param {FabricSeed} seed Fabric seed.
* @returns {Wallet} Instance of the wallet.
*/
static fromSeed (seed) {
return new Wallet({
key: {
seed: seed.phrase
}
});
}
derive (path = `m/7777'/7777'/0'/0/0`) {
return this.key.master.derivePath(path);
}
export () {
return {
type: 'FabricWallet',
object: {
// TODO: export this.logs
// TODO: reduce to C mem equivalent
logs: [
{ method: 'genesis', params: [JSON.stringify({ id: this.id })] },
{
method: 'transaction',
params: [
JSON.stringify({
changes: [
{ method: 'replace', path: '/status', value: 'PAUSED' }
]
}),
1 // Version
]
}
],
master: {
private: this.key.master.privateKey.toString('hex'),
public: this.key.master.publicKey.toString('hex')
},
seed: this.key.seed,
xprv: this.key.master.toBase58()
},
version: this.version
};
}
/**
* Import a key to the wallet.
* @param {Object} keypair Keypair.
* @param {Buffer} keypair.public Public key.
* @param {Buffer} [keypair.private] Private key.
* @returns {Wallet} Instance of the Wallet.
*/
loadKey (keypair, labels = []) {
if (!keypair) throw new Error('You must provide a keypair.');
if (!keypair.public) throw new Error('The keypair must have a "public" property.');
if (!(keypair.public instanceof Buffer)) throw new Error('The "public" property must be of type Buffer.');
const id = { public: keypair.public.toString('hex') };
const actor = new Actor(id);
// Addresses
// P2PKH: Pay to Public Key Hash
const p2pkh = payments.p2pkh({
pubkey: keypair.public
});
// P2WPKH: Pay to Witness Public Key Hash
const p2wpkh = payments.p2wpkh({
pubkey: keypair.public
});
// P2TR: Pay to Tap Root
// const p2t2 = ...
// TODO: new Key() here, then use key.export()
const key = {
addresses: {
p2pkh: p2pkh.address,
p2wpkh: p2wpkh.address
},
labels: labels.concat(['p2pkh', 'p2wpkh']),
private: (keypair.private) ? keypair.private.toString('hex') : undefined,
public: keypair.public.toString('hex')
};
this._state.content.keys[actor.id] = key;
this._state.labels = this._state.labels.concat(key.labels);
this.commit();
return this;
}
loadTransaction (transaction, labels = []) {
if (!transaction) throw new Error('You must provide a transaction.');
if (!transaction.id) throw new Error('The transaction must have a "id" property.');
const actor = new Actor(transaction);
this._state.content.transactions[transaction.id] = actor.toObject();
if (transaction.spendable) {
this._state.content.utxos.push({
type: 'UnspentTransactionOutput',
object: {
id: transaction.id
}
});
}
this.commit();
return this;
}
/**
* Start the wallet, including listening for transactions.
*/
start () {
this.status = 'STARTING';
this._load();
this.status = 'STARTED';
}
trust (emitter) {
const wallet = this;
const listener = emitter.on('message', this._handleGenericMessage.bind(this));
// Keep track of all event handlers
this.marshall.agents.push(listener);
emitter.on('transaction', async function trustedHandler (msg) {
if (this.settings.verbosity >= 5) console.log('[FABRIC:WALLET]', 'Received transaction from trusted event emitter:', msg);
await wallet.addTransactionToWallet(msg);
});
return this;
}
_handleGenericMessage (msg) {
if (this.settings.verbosity >= 5) console.log('[FABRIC:WALLET]', 'Received message from trusted event emitter:', msg);
if (this.settings.verbosity >= 5) console.log('[AUDIT]', '[FABRIC:WALLET]', 'Trusted emitter gave us:', msg);
// TODO: bind @fabric/core/services/bitcoin to addresses on wallet...
// ATTN: Eric
// TODO: update channels
// TODO: parse as {@link Message}
// TODO: store in this.messages
switch (msg['@type']) {
case 'ServiceMessage':
return this._processServiceMessage(msg['@data']);
default:
return console.warn('[FABRIC:WALLET]', `Unhandled message type: ${msg['@type']}`);
}
}
/**
* Initialize the wallet, including keys and addresses.
* @param {Object} settings Settings to load.
*/
_load (settings = {}) {
if (this.wallet) return this;
this.keypair = this.ec.keyFromPrivate(this.key.master.privateKey);
this.wallet = {
keypair: this.keypair
};
this.loadKey({
private: this.key.master.privateKey,
public: this.key.master.publicKey
});
/* for (let i = 0; i < 20; i++) {
const account = this.derive(`m/44'/0'/0'/0/${i}`);
this.loadKey({
private: account.privateKey.toString('hex'),
public: account.publicKey.toString('hex')
});
} */
return this;
}
async _processServiceMessage (msg) {
switch (msg['@type']) {
case 'BitcoinBlock':
this.processBitcoinBlock(msg['@data']);
break;
case 'BitcoinTransaction':
// TODO: validate destination is this wallet
this.addTransactionToWallet(msg['@data']);
break;
default:
return console.warn('[FABRIC:WALLET]', `Unhandled message type: ${msg['@type']}`);
}
}
async processBitcoinBlock (block) {
if (this.settings.verbosity >= 4) console.log('[FABRIC:WALLET]', 'Processing block:', block);
if (!block.block) return 0;
for (let i = 0; i < block.block.hashes.length; i++) {
const txid = block.block.hashes[i].toString('hex');
// ATTN: Eric
// TODO: process transaction
console.log('found txid in block:', txid);
}
}
async _attachTXID (txid) {
// TODO: check that `txid` is a proper TXID
let txp = await this.txids.create(txid);
if (this.settings.verbosity >= 5) console.log('[AUDIT]', `Attached TXID ${txid} to Wallet ID ${this.id}, result:`, txp);
return txp;
}
async _handleFabricTransaction (tx) {
console.log('[FABRIC:WALLET]', 'Handling Fabric Transaction:', tx);
}
async addTransactionToWallet (transaction) {
if (this.settings.verbosity >= 5) console.log('[AUDIT]', '[FABRIC:WALLET]', 'Adding transaction to Wallet:', transaction);
let entity = new Actor(transaction);
if (!transaction.spent) transaction.spent = false;
if (!transaction.outputs) transaction.outputs = [];
this._state.transactions.push(transaction);
await this.commit();
if (this.settings.verbosity >= 5) console.log('[FABRIC:WALLET]', 'Wallet transactions now:', this._state.transactions);
for (let i = 0; i < transaction.outputs.length; i++) {
let output = transaction.outputs[i].toJSON();
let address = await this._findAddressInCurrentShard(output.address);
// TODO: test these outputs
// console.log('output to parse:', output);
// console.log('address found:', address);
if (address) {
this._state.outputs.push(output);
this._state.utxos.push(new Coin(transaction.outputs[i]));
this.emit('payment', {
'@type': 'WalletPayment',
'@data': {
id: entity.id,
transaction: transaction
}
});
}
/* switch (output.type) {
default:
console.warn('[FABRIC:WALLET]', 'Unhandled output type:', output.type);
break;
case 'pubkeyhash':
let address = await this._findAddressInCurrentShard(output.address);
break;
} */
}
await this.commit();
}
async _findAddressInCurrentShard (address) {
for (let i = 0; i < this.shard.length; i++) {
let slice = this.shard[i];
if (slice.string === address) return slice;
}
return null;
}
async _createMultisigAddress (m, n, keys) {
let result = null;
// Check for required fields
if (!m) throw new Error('Parameter 0 required: m');
if (!n) throw new Error('Parameter 1 required: n');
if (!keys || !keys.length) throw new Error('Parameter 2 required: keys');
try {
// Compose the address
const pubkeys = keys.map(key => Buffer.from(key, 'hex'));
const payment = payments.p2wsh({
redeem: payments.p2ms({ m, pubkeys })
});
// Assign to output
result = payment.address;
} catch (exception) {
console.error('[FABRIC:WALLET]', 'Could not create multisig address:', exception);
}
return result;
}
async _spendToAddress (amount, address) {
const mtx = new MTX();
const change = await this.wallet.receiveAddress();
const coins = await this.wallet.getCoins();
this.emit('log', `Amount to send: ${amount}`);
mtx.addOutput({
address: recipient,
value: parseInt(amount)
});
await mtx.fund(coins, {
rate: 10,
changeAddress: change
});
const sigs = mtx.sign(this.ring);
const tx = mtx.toTX();
const valid = tx.check(mtx.view);
return tx;
}
async _getUnspentOutput (amount) {
if (!this._state.utxos.length) throw new Error('No available funds.');
// TODO: use coin selection
const mtx = new MTX();
// Send 10,000 satoshis to ourself.
mtx.addOutput({
address: this.ring.getAddress(),
value: amount
});
await mtx.fund(this._state.utxos, {
// Use a rate of 10,000 satoshis per kb.
// With the `fullnode` object, you can
// use the fee estimator for this instead
// of blindly guessing.
rate: 10000,
// Send the change back to ourselves.
changeAddress: this.ring.getAddress()
});
// TODO: use the MTX to select outputs
return this._state.utxos[0];
}
/**
* Returns a bech32 address for the provided {@link Script}.
* @param {Script} script
*/
getAddressForScript (script) {
// TODO: use Fabric.Script
let p2wsh = script.forWitness();
let address = p2wsh.getAddress().toBech32(this.settings.network);
return address;
}
/**
* Generate a {@link BitcoinAddress} for the supplied {@link BitcoinScript}.
* @param {BitcoinScript} redeemScript
*/
getAddressFromRedeemScript (redeemScript) {
if (!redeemScript) return null;
return Address.fromScripthash(redeemScript.hash160());
}
/**
* Create a priced order.
* @param {Object} order
* @param {Object} order.asset
* @param {Object} order.amount
*/
async createPricedOrder (order) {
if (!order.asset) throw new Error('Order parameter "asset" is required.');
if (!order.amount) throw new Error('Order parameter "amount" is required.');
let leftover = order.amount % (10 * this.settings.decimals);
let parts = order.amount / (10 * this.settings.decimals);
let partials = [];
// TODO: remove short-circuit
let cb = await this._generateFakeCoinbase(order.amount);
let mtx = new MTX();
let script = new Script();
let secret = await this.generateSecret();
let image = Buffer.from(secret.hash);
console.log('secret generated:', secret);
console.log('image of secret:', image);
let refund = await this.ring.getPublicKey();
console.log('refund:', refund);
script.pushSym('OP_IF');
script.pushSym('OP_SHA256');
script.pushData(image);
script.pushSym('OP_EQUALVERIFY');
script.pushData(order.counterparty);
script.pushSym('OP_ELSE');
script.pushInt(this.settings.locktime);
script.pushSym('OP_CHECKSEQUENCEVERIFY');
script.pushSym('OP_DROP');
script.pushData(refund);
script.pushSym('OP_ENDIF');
script.pushSym('OP_CHECKSIG');
script.compile();
// TODO: complete order construction
for (let i = 0; i < parts; i++) {
// TODO: should be split parts
partials.push(script);
}
let entity = new Actor({
comment: 'List of transactions to validate.',
orders: partials,
transactions: partials
});
return entity;
}
async createHTLC (contract) {
// if (!contract.asset) throw new Error('Contract parameter "asset" is required.');
if (!contract.amount) throw new Error('Contract parameter "amount" is required.');
// TODO: remove short-circuit
if (!contract.counterparty) {
// TODO: replace this with a randomly-generated input
// sha256
// -> pubkey
contract.counterparty = await this.ring.getPublicKey();
console.log('contract counterparty artificially generated:', contract.counterparty);
}
let leftover = contract.amount % this.settings.decimals;
let parts = contract.amount / this.settings.decimals;
let partials = [];
// TODO: remove short-circuit
let cb = await this._generateFakeCoinbase(contract.amount);
let mtx = new MTX();
let script = new Script();
let secret = await this.generateSecret();
let image = Buffer.from(secret.hash);
console.log('secret generated:', secret);
console.log('image of secret:', image);
let refund = await this.ring.getPublicKey();
console.log('refund:', refund);
script.pushSym('OP_IF');
script.pushSym('OP_SHA256');
script.pushData(image);
script.pushSym('OP_EQUALVERIFY');
script.pushData(contract.counterparty);
script.pushSym('OP_ELSE');
script.pushInt(this.settings.locktime);
script.pushSym('OP_CHECKSEQUENCEVERIFY');
script.pushSym('OP_DROP');
script.pushData(refund);
script.pushSym('OP_ENDIF');
script.pushSym('OP_CHECKSIG');
script.compile();
// TODO: complete order construction
for (let i = 0; i < parts; i++) {
// TODO: should be split parts
partials.push(script);
}
console.log('parts:', partials);
console.log('leftover:', leftover);
let entity = new Actor({
comment: 'List of transactions to validate.',
orders: partials,
transactions: partials,
type: 'BitcoinTransaction'
});
return entity;
}
async generateSecret () {
const secret = new Secret();
const entity = await this.secrets.create({
hash: secret.hash
});
console.log('created secret:', entity);
return entity;
}
async generateSignedTransactionTo (address, amount) {
if (!address) throw new Error(`Parameter "address" is required.`);
if (!amount) throw new Error(`Parameter "amount" is required.`);
let bn = new BN(amount + '', 10);
// TODO: labeled keypairs
let clean = await this.generateCleanKeyPair();
let change = await this.generateCleanKeyPair();
let mtx = new MTX();
let cb = await this._generateFakeCoinbase(amount);
mtx.addOutput({
address: address,
amount: amount
});
await mtx.fund(this._state.utxos, {
rate: 10000, // TODO: fee calculation
changeAddress: change.address
});
mtx.sign(this.ring);
// mtx.signInput(0, this.ring);
let tx = mtx.toTX();
let output = Coin.fromTX(mtx, 0, -1);
let raw = mtx.toRaw();
let hash = Hash256.digest(raw.toString('hex'));
return {
type: 'BitcoinTransaction',
data: {
tx: tx,
output: output,
raw: raw.toString('hex'),
hash: hash
}
};
}
async generateOrderRootTo (pubkey, amount) {
if (!pubkey) throw new Error(`Parameter "pubkey" is required.`);
if (!amount) throw new Error(`Parameter "amount" is required.`);
let bn = new BN(amount + '', 10);
// TODO: labeled keypairs
let clean = await this.generateCleanKeyPair();
let change = await this.generateCleanKeyPair();
let mtx = new MTX();
let cb = await this._generateFakeCoinbase(amount);
mtx.addOutput({
address: address,
amount: amount
});
await mtx.fund(this._state.utxos, {
rate: 10000, // TODO: fee calculation
changeAddress: change.address
});
mtx.sign(this.ring);
// mtx.signInput(0, this.ring);
let tx = mtx.toTX();
let output = null;
try {
output = Coin.fromTX(mtx, 0, -1);
} catch (exception) {
console.error('[FABRIC:WALLET]', 'Could not generate output:', exception);
}
let raw = mtx.toRaw();
let hash = Hash256.digest(raw.toString('hex'));
return {
type: 'BitcoinTransaction',
data: {
tx: tx,
output: output,
raw: raw.toString('hex'),
hash: hash
}
};
}
addInputForCrowdfund (coin, inputIndex, mtx, keyring, hashType) {
let sampleCoin = coin instanceof Coin ? coin : Coin.fromJSON(coin);
if (!hashType) hashType = Script.hashType.ANYONECANPAY | Script.hashType.ALL;
mtx.addCoin(sampleCoin);
mtx.scriptInput(inputIndex, sampleCoin, keyring);
mtx.signInput(inputIndex, sampleCoin, keyring, hashType);
console.log('MTX after Input added (and signed):', mtx);
// TODO: return a full object for Fabric
return mtx;
}
balanceFromState (state) {
if (!state.transactions) throw new Error('State does not provide a `transactions` property.');
if (!state.transactions.length) return 0;
return state.transactions.reduce((acc, obj, i) => {
if (!acc.value) acc.value = 0;
acc.value += obj.value;
});
}
getFeeForInput (coin, address, keyring, rate) {
let fundingTarget = 100000000; // 1 BTC (arbitrary for purposes of this function)
let testMTX = new MTX();
// TODO: restore swap code, abstract input types
// this.addInputForCrowdfund(coin, 0, testMTX, this.keyring);
return testMTX.getMinFee(null, rate);
}
async _createAccount (data) {
// console.log('wallet creating account with data:', data);
await this._load();
let existing = await this.wallet.getAccount(data.name);
if (existing) return existing;
let account = await this.wallet.createAccount(data);
return account;
}
async _updateBalance (amount) {
return this.set('/balances/confirmed', amount);
}
_handleWalletTransaction (tx) {
console.log('[BRIDGE:WALLET]', 'incoming transaction:', tx);
}
_getDepositAddress () {
return this.ring.getAddress().toString();
}
_getSeed () {
return this.seed;
}
_getAccountByIndex (index = 0) {
return {
address: this.account.deriveReceive(index).getAddress('string')
};
}
async _splitCoinbase (funderKeyring, coin, targetAmount, txRate) {
// loop through each coinbase coin to split
let coins = [];
const mtx = new MTX();
assert(coin.value > targetAmount, 'coin value is not enough!');
// creating a transaction that will have an output equal to what we want to fund
mtx.addOutput({
address: funderKeyring.getAddress(),
value: targetAmount
});
// the fund method will automatically split
// the remaining funds to the change address
// Note that in a real application these splitting transactions will also
// have to be broadcast to the network
await mtx.fund([coin], {
rate: txRate,
// send change back to an address belonging to the funder
changeAddress: funderKeyring.getAddress()
}).then(() => {
// sign the mtx to finalize split
mtx.sign(funderKeyring);
assert(mtx.verify());
const tx = mtx.toTX();
assert(tx.verify(mtx.view));
const outputs = tx.outputs;
// get coins from tx
outputs.forEach((outputs, index) => {
coins.push(Coin.fromTX(tx, index, -1));
});
}).catch(e => console.log('There was an error: ', e));
return coins;
}
async composeCrowdfund (coins) {
const funderCoins = {};
// Loop through each coinbase
for (let index in coins) {
const coinbase = coins[index][0];
// estimate fee for each coin (assuming their split coins will use same tx type)
const estimatedFee = getFeeForInput(coinbase, fundeeAddress, funders[index], txRate);
const targetPlusFee = amountToFund + estimatedFee;
// split the coinbase with targetAmount plus estimated fee
const splitCoins = await Utils.splitCoinbase(funders[index], coinbase, targetPlusFee, txRate);
// add to funderCoins object with returned coins from splitCoinbase being value,
// and index being the key
funderCoins[index] = splitCoins;
}
// ... we'll keep filling out the rest of the code here
}
async _addOutputToSpendables (coin) {
this._state.utxos.push(coin);
return this;
}
async getUnusedAddress () {
let clean = await this.wallet.receiveAddress();
this.emit('log', `unused address: ${clean}`);
return clean;
}
async getUnspentTransactionOutputs () {
return this._state.transactions.filter(x => {
return (x.spent === 0);
});
}
async _generateFakeCoinbase (amount = 1) {
// TODO: use Satoshis for all calculations
let num = new BN(amount, 10);
// TODO: remove all fake coinbases
// TODO: remove all short-circuits
// fake coinbase
let cb = new MTX();
let clean = await this.generateCleanKeyPair();
// Coinbase Input
cb.addInput({
prevout: new Outpoint(),
script: new Script(),
sequence: 0xffffffff
});
// Add Output to pay ourselves
cb.addOutput({
address: clean.address,
value: 5000000000
});
// TODO: remove short-circuit
let coin = Coin.fromTX(cb, 0, -1);
let tx = cb.toTX();
// TODO: remove entirely, test short-circuit removal
// await this._addOutputToSpendables(coin);
return {
type: 'BitcoinTransactionOutput',
data: {
tx: cb,
coin: coin
}
};
}
async _getFreeCoinbase (amount = 1) {
let num = new BN(amount, 10);
let max = new BN('5000000000000', 10); // upper limit per coinbase
let hun = new BN('100000000', 10); // one hundred million
let value = num.mul(hun); // amount in Satoshis
if (value.gt(max)) {
console.warn('Value (in satoshis) higher than max:', value.toString(10), `(max was ${max.toString(10)})`);
value = max;
}
let v = value.toString(10);
let w = parseInt(v);
await this._load();
const coins = {};
const coinbase = new MTX();
// INSERT 1 Input
coinbase.addInput({
prevout: new Outpoint(),
script: new Script(),
sequence: 0xffffffff
});
try {
// INSERT 1 Output
coinbase.addOutput({
address: this._getDepositAddress(),
value: w
});
} catch (E) {
console.error('Could not add output:', E);
}
// TODO: wallet._getSpendableOutput()
let coin = Coin.fromTX(coinbase, 0, -1);
this._state.utxos.push(coin);
// console.log('coinbase:', coinbase);
return coinbase;
}
/**
* Signs a transaction with the keyring.
* @param {BcoinTX} tx
*/
async _sign (tx) {
let signature = await tx.sign(this.keyring);
console.log('signing tx:', tx);
console.log('signing sig:', signature);
return Object.assign({}, tx, { signature });
}
/**
* Create a crowdfunding transaction.
* @param {Object} fund
*/
async _createCrowdfund (fund = {}) {
if (!fund.amount) return null;
if (!fund.address) return null;
let index = fund.index || 0;
let hashType = Script.hashType.ANYONECANPAY | Script.hashType.ALL;
mtx.addCoin(this._state.utxos[0]);
mtx.scriptInput(index, this._state.utxos[0], this.keyring);
mtx.signInput(index, this._state.utxos[0], this.keyring, hashType);
await this.commit();
return {
tx: mtx.toTX(),
mtx: mtx
};
}
async _createFromFreshSeed (passphrase = '') {
console.log('creating fresh seed with passphrase:', passphrase);
const seed = await this._createSeed(passphrase);
return seed;
}
async _createSeed (passphrase = '') {
const mnemonic = bip39.generateMnemonic(256);
const interim = bip39.mnemonicToSeedSync(mnemonic, passphrase);
const master = bip32.fromSeed(interim);
return {
phrase: mnemonic.toString(),
master: master.privateKey.toString('hex'),
xprv: master.toBase58()
};
}
async _importSeed (seed) {
let mnemonic = new Mnemonic(seed);
return this._loadSeed(mnemonic.toString());
}
async _createIncentivizedTransaction (config) {
console.log('creating incentivized transaction with config:', config);
let mtx = new MTX();
let data = new Script();
let clean = await this.generateCleanKeyPair();
data.pushSym('OP_IF');
data.pushSym('OP_SHA256');
data.pushData(Buffer.from(config.hash));
data.pushSym('OP_EQUALVERIFY');
data.pushData(Buffer.from(config.payee));
data.pushSym('OP_ELSE');
data.pushInt(config.locktime);
data.pushSym('OP_CHECKSEQUENCEVERIFY');
data.pushSym('OP_DROP');
data.pushData(Buffer.from(clean.public));
data.pushSym('OP_ENDIF');
data.pushSym('OP_CHECKSIG');
data.compile();
console.log('address data:', data);
let segwitAddress = await this.getAddressForScript(data);
mtx.addOutput({
address: segwitAddress,
value: 0
});
// TODO: load available outputs from wallet
let out = await mtx.fund([] /* coins */, {
// TODO: fee estimation
rate: 10000,
changeAddress: this.ring.getAddress()
});
console.log('transaction:', out);
return out;
}
async _getBondAddress () {
await this._load();
let script = new Script();
let clean = await this.generateCleanKeyPair();
if (this.settings.verbosity >= 5) console.log('[AUDIT]', 'getting bond address, clean:', clean);
// write the contract
// script.pushData(clean.public.toString('hex'));
// script.pushSym('OP_CHECKSIG');
// compile the script
// script.compile();
return {
pubkey: clean.public.toString(),
address: clean.address
};
}
async _getSpendableOutput (target, amount = 0) {
let self = this;
let key = null;
let out = null;
let mtx = new MTX();
await this._load();
console.log('funding transaction with coins:', this._state.utxos);
// INSERT 1 Output
mtx.addOutput({
address: target,
value: amount
});
out = await mtx.fund(this._state.utxos, {
// TODO: fee estimation
rate: 10000,
changeAddress: self.ring.getAddress()
});
console.log('out:', out);
console.trace('created mutable transaction:', mtx);
console.trace('created immutable transaction:', mtx.toTX());
return {
tx: mtx.toTX(),
mtx: mtx
};
}
async signInput (mtx, index, redeemScript, value, privateKey, sigHashType, version_or_flags) {
return mtx.signature(
index,
redeemScript,
value,
privateKey,
sigHashType,
version_or_flags
);
}
async getRedeemTX (address, fee, fundingTX, fundingTXoutput, redeemScript, inputScript, locktime, privateKey) {
// Create a mutable transaction object
let redeemTX = new MTX();
// Get the output we want to spend (coins sent to the P2SH address)
let coin = Coin.fromTX(fundingTX, fundingTXoutput, -1);
// Add that coin as an input to our transaction
redeemTX.addCoin(coin);
// Redeem the input coin with either the swap or refund script
redeemTX.inputs[0].script = inputScript;
// Create the output back to our primary wallet
redeemTX.addOutput({
address: address,
value: coin.value - fee
});
// If this was a refund redemption we need to set the sequence
// Sequence is the relative timelock value applied to individual inputs
if (locktime) {
redeemTX.setSequence(0, locktime, this.CSV_seconds);
} else {
redeemTX.inputs[0].sequence = 0xffffffff;
}
// Set SIGHASH and replay protection bits
let version_or_flags = 0;
let type = null;
if (this.libName === 'bcash') {
version_or_flags = this.flags;
type = Script.hashType.SIGHASH_FORKID | Script.hashType.ALL;
}
// Create the signature authorizing the input script to spend the coin
let sig = await this.signInput(
redeemTX,
0,
redeemScript,
coin.value,
privateKey,
type,
version_or_flags
);
// Insert the signature into the input script where we had a `0` placeholder
inputScript.setData(0, sig);
// Finish up and return
inputScript.compile();
return redeemTX;
}
/**
* Generate {@link Script} for claiming a {@link Swap}.
* @param {*} redeemScript
* @param {*} secret
*/
async _getSwapInputScript (redeemScript, secret) {
let inputSwap = new Script();
inputSwap.pushInt(0); // signature placeholder
inputSwap.pushData(secret);
inputSwap.pushInt(1); // <true>
inputSwap.pushData(redeemScript.toRaw()); // P2SH
inputSwap.compile();
return inputSwap;
}
/**
* Generate {@link Script} for reclaiming funds commited to a {@link Swap}.
* @param {*} redeemScript
*/
async _getRefundInputScript (redeemScript) {
let inputRefund = new Script();
inputRefund.pushInt(0); // signature placeholder
inputRefund.pushInt(0); // <false>
inputRefund.pushData(redeemScript.toRaw()); // P2SH
inputRefund.compile();
return inputRefund;
}
async _createOrderForPubkey (pubkey) {
this.emit('log', `creating ORDER transaction with pubkey: ${pubkey}`);
let mtx = new MTX();
let data = new Script();
let clean = await this.generateCleanKeyPair();
let secret = 'fixed secret :)';
let sechash = require('crypto').createHash('sha256').update(secret).digest('hex');
this.emit('log', `SECRET CREATED: ${secret}`);
this.emit('log', `SECHASH: ${sechash}`);
data.pushSym('OP_IF');
data.pushSym('OP_SHA256');
data.pushData(Buffer.from(sechash));
data.pushSym('OP_EQUALVERIFY');
data.pushData(Buffer.from(pubkey));
data.pushSym('OP_ELSE');
data.pushInt(86400);
data.pushSym('OP_CHECKSEQUENCEVERIFY');
data.pushSym('OP_DROP');
data.pushData(Buffer.from(clean.public));
data.pushSym('OP_ENDIF');
data.pushSym('OP_CHECKSIG');
data.compile();
this.emit('log', `[AUDIT] address data: ${data}`);
let segwitAddress = await this.getAddressForScript(data);
let address = await this.getAddressFromRedeemScript(data);
this.emit('log', `[AUDIT] segwit address: ${segwitAddress}`);
this.emit('log', `[AUDIT] normal address: ${address}`);
mtx.addOutput({
address: address,
value: 25000000
});
// ensure a coin exists...
// NOTE: this is tracked in this._state.coins
// and thus does not need to be cast to a variable...
let coinbase = await this._getFreeCoinbase();
// TODO: load available outputs from wallet
let out = await mtx.fund(this._state.utxos, {
// TODO: fee estimation
rate: 10000,
changeAddress: this.ring.getAddress()
});
let tx = mtx.toTX();
let sig = await mtx.sign(this.ring);
this.emit('log', 'transaction:', tx);
this.emit('log', 'sig:', sig);
return {
tx: tx,
mtx: mtx,
sig: sig
};
}
async _scanBlockForTransactions (block) {
console.log('[AUDIT]', 'Scanning block for transactions:', block);
let found = [];
}
async _scanChainForTransactions (chain) {
console.log('[AUDIT]', 'Scanning chain for transactions:', chain);
let transactions = [];
for (let i = 0; i < chain.blocks.length; i++) {
transactions.concat(await this._scanBlockForTransactions(chain.blocks[i]));
}
return transactions;
}
async _createChannel (channel) {
let element = new Channel(channel);
return element;
}
async _allocateSlot () {
for (let i = 0; i < Object.keys(this._state.space).length; i++) {
let slot = this._state.space[Object.keys(this._state.space)[i]];
if (!slot.allocation) {
this._state.space[Object.keys(this._state.space)[i]].allocation = new Secret();
return this._state.space[Object.keys(this._state.space)[i]];
}
}
}
async getFirstAddressSlice (size = 256) {
await this._load();
// aggregate results for return
let slice = [];
if (this.settings.verbosity >= 5) console.log('[AUDIT]', 'generating {@link Space} with settings:', this.settings);
// iterate over length of shard, aggregate addresses
for (let i = 0; i < size; i++) {
// let addr = this.account.deriveReceive(i).getAddress('string', this.settings.network);
const addr = this.key.deriveAccountReceive(i);
let address = await this.addresses.create({
string: addr,
label: `shared address ${i} for wallet ${this.id}`,
allocation: null
});
// TODO: restore address tracking in state
// this._state.space[addr] = address;
slice.push(address);
}
return slice;
}
/**
* Create a public key from a string.
* @param {String} input Hex-encoded string to create key from.
*/
publicKeyFromString (input) {
const buf = Buffer.from(input, 'hex');
const key = new Key({ public: buf });
return key.pubkey;
}
async generateCleanKeyPair () {
if (this.status !== 'loaded') await this._load();
const pair = this.key.deriveKeyPair(++this.index);
const keypair = {
index: this.index,
public: pair.public,
address: payments.p2pkh({
pubkey: Buffer.from(pair.public, 'hex')
})
// keyring: keyring
};
return keypair;
}
async _handleWalletBalance (balance) {
await this._PUT('/balance', balance);
const depositor = new State({ name: this.settings.name || 'default' });
await this._PUT(`/depositors/${depositor.id}/balance`, balance);
this.emit('balance', balance);
}
async _registerAccount (obj) {
if (!obj.name) throw new Error('Account must have "name" property.');
if (!this.database.db.loaded) {
await this.database.open();
}
const account = await this.accounts.create(obj);
if (this.settings.verbosity >= 4) console.log('registering account, created:', account);
if (this.manager) {
this.manager.on('tx', this._handleWalletTransaction.bind(this));
this.manager.on('balance', this._handleWalletBalance.bind(this));
// TODO: check on above events, should be more like...
// this.manager.on('changes', this._handleWalletBalance.bind(this));
}
return account;
}
async _prepareSecret (state) {
const entity = new Actor(state);
return entity;
}
async _loadSeed (seed) {
this.settings.key = { seed };
await this._load();
return this.seed;
}
async _unload () {
return this.database.close();
}
}
module.exports = Wallet;