fcoin/test/wallet-test.js
2018-12-08 02:40:49 -08:00

1626 lines
40 KiB
JavaScript

/* eslint-env mocha */
/* eslint prefer-arrow-callback: "off" */
'use strict';
const assert = require('./util/assert');
const consensus = require('../lib/protocol/consensus');
const util = require('../lib/utils/util');
const hash256 = require('bcrypto/lib/hash256');
const random = require('bcrypto/lib/random');
const WalletDB = require('../lib/wallet/walletdb');
const WorkerPool = require('../lib/workers/workerpool');
const Address = require('../lib/primitives/address');
const MTX = require('../lib/primitives/mtx');
const Coin = require('../lib/primitives/coin');
const KeyRing = require('../lib/primitives/keyring');
const Input = require('../lib/primitives/input');
const Outpoint = require('../lib/primitives/outpoint');
const Script = require('../lib/script/script');
const HD = require('../lib/hd');
const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt'
+ 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ';
const KEY2 = 'xprv9s21ZrQH143K3mqiSThzPtWAabQ22Pjp3uSNnZ53A5bQ4udp'
+ 'faKekc2m4AChLYH1XDzANhrSdxHYWUeTWjYJwFwWFyHkTMnMeAcW4JyRCZa';
const enabled = true;
const workers = new WorkerPool({ enabled });
const wdb = new WalletDB({ workers });
let currentWallet = null;
let importedWallet = null;
let importedKey = null;
let doubleSpendWallet = null;
let doubleSpendCoin = null;
function fromU32(num) {
const data = Buffer.allocUnsafe(4);
data.writeUInt32LE(num, 0, true);
return data;
}
function curBlock(wdb) {
return fakeBlock(wdb.state.height);
};
function nextBlock(wdb) {
return fakeBlock(wdb.state.height + 1);
}
function fakeBlock(height) {
const prev = hash256.digest(fromU32((height - 1) >>> 0));
const hash = hash256.digest(fromU32(height >>> 0));
const root = hash256.digest(fromU32((height | 0x80000000) >>> 0));
return {
hash: hash,
prevBlock: prev,
merkleRoot: root,
time: 500000000 + (height * (10 * 60)),
bits: 0,
nonce: 0,
height: height
};
}
function dummyInput() {
const hash = random.randomBytes(32);
return Input.fromOutpoint(new Outpoint(hash, 0));
}
async function testP2PKH(witness, nesting) {
const flags = Script.flags.STANDARD_VERIFY_FLAGS;
const receiveAddress = nesting ? 'nestedAddress' : 'receiveAddress';
const type = witness ? Address.types.WITNESS : Address.types.PUBKEYHASH;
const wallet = await wdb.create({ witness });
const waddr = await wallet.receiveAddress();
const addr = Address.fromString(waddr.toString(wdb.network), wdb.network);
assert.strictEqual(addr.type, type);
assert.strictEqual(addr.type, waddr.type);
const src = new MTX();
src.addInput(dummyInput());
src.addOutput(await wallet[receiveAddress](), 5460 * 2);
src.addOutput(new Address(), 2 * 5460);
const mtx = new MTX();
mtx.addTX(src, 0);
mtx.addOutput(await wallet.receiveAddress(), 5460);
await wallet.sign(mtx);
const [tx, view] = mtx.commit();
assert(tx.verify(view, flags));
}
async function testP2SH(witness, nesting) {
const flags = Script.flags.STANDARD_VERIFY_FLAGS;
const receiveAddress = nesting ? 'nestedAddress' : 'receiveAddress';
const receiveDepth = nesting ? 'nestedDepth' : 'receiveDepth';
const vector = witness ? 'witness' : 'script';
// Create 3 2-of-3 wallets with our pubkeys as "shared keys"
const options = {
witness,
type: 'multisig',
m: 2,
n: 3
};
const alice = await wdb.create(options);
const bob = await wdb.create(options);
const carol = await wdb.create(options);
const recipient = await wdb.create();
await alice.addSharedKey(0, await bob.accountKey(0));
await alice.addSharedKey(0, await carol.accountKey(0));
await bob.addSharedKey(0, await alice.accountKey(0));
await bob.addSharedKey(0, await carol.accountKey(0));
await carol.addSharedKey(0, await alice.accountKey(0));
await carol.addSharedKey(0, await bob.accountKey(0));
// Our p2sh address
const addr1 = await alice[receiveAddress]();
if (witness) {
const type = nesting ? Address.types.SCRIPTHASH : Address.types.WITNESS;
assert.strictEqual(addr1.type, type);
} else {
assert.strictEqual(addr1.type, Address.types.SCRIPTHASH);
}
assert((await alice[receiveAddress]()).equals(addr1));
assert((await bob[receiveAddress]()).equals(addr1));
assert((await carol[receiveAddress]()).equals(addr1));
const nestedAddr1 = await alice.nestedAddress();
if (witness) {
assert(nestedAddr1);
assert((await alice.nestedAddress()).equals(nestedAddr1));
assert((await bob.nestedAddress()).equals(nestedAddr1));
assert((await carol.nestedAddress()).equals(nestedAddr1));
}
{
// Add a shared unspent transaction to our wallets
const fund = new MTX();
fund.addInput(dummyInput());
fund.addOutput(nesting ? nestedAddr1 : addr1, 5460 * 10);
// Simulate a confirmation
assert.strictEqual(await alice[receiveDepth](), 1);
await wdb.addBlock(nextBlock(wdb), [fund.toTX()]);
assert.strictEqual(await alice[receiveDepth](), 2);
assert.strictEqual(await alice.changeDepth(), 1);
}
const addr2 = await alice[receiveAddress]();
assert(!addr2.equals(addr1));
assert((await alice[receiveAddress]()).equals(addr2));
assert((await bob[receiveAddress]()).equals(addr2));
assert((await carol[receiveAddress]()).equals(addr2));
// Create a tx requiring 2 signatures
const send = new MTX();
send.addOutput(await recipient.receiveAddress(), 5460);
assert(!send.verify(flags));
await alice.fund(send, {
rate: 10000,
round: true
});
await alice.sign(send);
assert(!send.verify(flags));
await bob.sign(send);
const [tx, view] = send.commit();
assert(tx.verify(view, flags));
assert.strictEqual(await alice.changeDepth(), 1);
const change = await alice.changeAddress();
assert((await alice.changeAddress()).equals(change));
assert((await bob.changeAddress()).equals(change));
assert((await carol.changeAddress()).equals(change));
// Simulate a confirmation
{
await wdb.addBlock(nextBlock(wdb), [tx]);
assert.strictEqual(await alice[receiveDepth](), 2);
assert.strictEqual(await alice.changeDepth(), 2);
assert((await alice[receiveAddress]()).equals(addr2));
assert(!(await alice.changeAddress()).equals(change));
}
const change2 = await alice.changeAddress();
assert((await alice.changeAddress()).equals(change2));
assert((await bob.changeAddress()).equals(change2));
assert((await carol.changeAddress()).equals(change2));
const input = tx.inputs[0];
input[vector].setData(2, Buffer.alloc(73, 0x00));
input[vector].compile();
assert(!tx.verify(view, flags));
assert.strictEqual(tx.getFee(view), 10000);
}
describe('Wallet', function() {
this.timeout(process.browser ? 20000 : 5000);
before(async () => {
await wdb.open();
});
after(async () => {
await wdb.close();
});
it('should open walletdb', () => {
consensus.COINBASE_MATURITY = 0;
});
it('should generate new key and address', async () => {
const wallet = await wdb.create();
const addr1 = await wallet.receiveAddress();
assert(addr1);
const str = addr1.toString(wdb.network);
const addr2 = Address.fromString(str, wdb.network);
assert(addr2.equals(addr1));
});
it('should validate existing address', () => {
assert(Address.fromString('1KQ1wMNwXHUYj1nV2xzsRcKUH8gVFpTFUc', 'main'));
});
it('should fail to validate invalid address', () => {
assert.throws(() => {
Address.fromString('1KQ1wMNwXHUYj1nv2xzsRcKUH8gVFpTFUc', 'main');
});
});
it('should create and get wallet', async () => {
const wallet1 = await wdb.create();
const wallet2 = await wdb.get(wallet1.id);
assert(wallet1 === wallet2);
});
it('should sign/verify p2pkh tx', async () => {
await testP2PKH(false, false);
});
it('should sign/verify p2wpkh tx', async () => {
await testP2PKH(true, false);
});
it('should sign/verify p2wpkh tx w/ nested bullshit', async () => {
await testP2PKH(true, true);
});
it('should multisign/verify TX', async () => {
const wallet = await wdb.create({
type: 'multisig',
m: 1,
n: 2
});
const xpriv = HD.PrivateKey.generate();
const key = xpriv.deriveAccount(44, 0, 0).toPublic();
await wallet.addSharedKey(0, key);
const script = Script.fromMultisig(1, 2, [
(await wallet.receiveKey()).publicKey,
key.derivePath('m/0/0').publicKey
]);
// Input transaction (bare 1-of-2 multisig)
const src = new MTX();
src.addInput(dummyInput());
src.addOutput(script, 5460 * 2);
src.addOutput(new Address(), 5460 * 2);
const tx = new MTX();
tx.addTX(src, 0);
tx.addOutput(await wallet.receiveAddress(), 5460);
const maxSize = await tx.estimateSize();
await wallet.sign(tx);
assert(tx.toRaw().length <= maxSize);
assert(tx.verify());
});
it('should handle missed txs', async () => {
const alice = await wdb.create();
const bob = await wdb.create();
// Coinbase
// balance: 51000
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(await alice.receiveAddress(), 50000);
t1.addOutput(await alice.receiveAddress(), 1000);
const t2 = new MTX();
t2.addTX(t1, 0); // 50000
t2.addOutput(await alice.receiveAddress(), 24000);
t2.addOutput(await alice.receiveAddress(), 24000);
// Save for later.
doubleSpendWallet = alice;
doubleSpendCoin = Coin.fromTX(t1, 0, -1);
// balance: 49000
await alice.sign(t2);
const t3 = new MTX();
t3.addTX(t1, 1); // 1000
t3.addTX(t2, 0); // 24000
t3.addOutput(await alice.receiveAddress(), 23000);
// balance: 47000
await alice.sign(t3);
const t4 = new MTX();
t4.addTX(t2, 1); // 24000
t4.addTX(t3, 0); // 23000
t4.addOutput(await alice.receiveAddress(), 11000);
t4.addOutput(await alice.receiveAddress(), 11000);
// balance: 22000
await alice.sign(t4);
const f1 = new MTX();
f1.addTX(t4, 1); // 11000
f1.addOutput(await bob.receiveAddress(), 10000);
// balance: 11000
await alice.sign(f1);
{
await wdb.addTX(t4.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 22000);
}
{
await wdb.addTX(t1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 73000);
}
{
await wdb.addTX(t2.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 71000);
}
{
await wdb.addTX(t3.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 69000);
}
{
await wdb.addTX(f1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 58000);
const txs = await alice.getHistory();
assert(txs.some((wtx) => {
return wtx.hash.equals(f1.hash());
}));
}
{
const balance = await bob.getBalance();
assert.strictEqual(balance.unconfirmed, 10000);
const txs = await bob.getHistory();
assert(txs.some((wtx) => {
return wtx.tx.hash().equals(f1.hash());
}));
}
// Should recover from missed txs on block.
await wdb.addBlock(nextBlock(wdb), [
t1.toTX(),
t2.toTX(),
t3.toTX(),
t4.toTX(),
f1.toTX()
]);
{
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 11000);
assert.strictEqual(balance.confirmed, 11000);
const txs = await alice.getHistory();
assert(txs.some((wtx) => {
return wtx.hash.equals(f1.hash());
}));
}
{
const balance = await bob.getBalance();
assert.strictEqual(balance.unconfirmed, 10000);
assert.strictEqual(balance.confirmed, 10000);
const txs = await bob.getHistory();
assert(txs.some((wtx) => {
return wtx.tx.hash().equals(f1.hash());
}));
}
});
it('should cleanup spenders after double-spend', async () => {
const wallet = doubleSpendWallet;
// Reorg and unconfirm all previous txs.
await wdb.removeBlock(curBlock(wdb));
{
const txs = await wallet.getHistory();
assert.strictEqual(txs.length, 5);
const total = txs.reduce((t, wtx) => {
return t + wtx.tx.getOutputValue();
}, 0);
assert.strictEqual(total, 154000);
}
{
const balance = await wallet.getBalance();
assert.strictEqual(balance.unconfirmed, 11000);
assert.strictEqual(balance.confirmed, 0);
}
{
const tx = new MTX();
tx.addCoin(doubleSpendCoin);
tx.addOutput(await wallet.receiveAddress(), 5000);
await wallet.sign(tx);
await wdb.addTX(tx.toTX());
const balance = await wallet.getBalance();
assert.strictEqual(balance.unconfirmed, 6000);
}
{
const txs = await wallet.getHistory();
assert.strictEqual(txs.length, 2);
const total = txs.reduce((t, wtx) => {
return t + wtx.tx.getOutputValue();
}, 0);
assert.strictEqual(total, 56000);
}
});
it('should handle double-spend (not our input)', async () => {
const wallet = await wdb.create();
const t1 = new MTX();
const input = dummyInput();
t1.addInput(input);
t1.addOutput(await wallet.receiveAddress(), 50000);
await wdb.addTX(t1.toTX());
assert.strictEqual((await wallet.getBalance()).unconfirmed, 50000);
let conflict = false;
wallet.on('conflict', () => {
conflict = true;
});
const t2 = new MTX();
t2.addInput(input);
t2.addOutput(new Address(), 5000);
await wdb.addTX(t2.toTX());
assert(conflict);
assert.strictEqual((await wallet.getBalance()).unconfirmed, 0);
});
it('should handle more missed txs', async () => {
const alice = await wdb.create();
const bob = await wdb.create();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(await alice.receiveAddress(), 50000);
t1.addOutput(await alice.receiveAddress(), 1000);
// balance: 51000
const t2 = new MTX();
t2.addTX(t1, 0); // 50000
t2.addOutput(await alice.receiveAddress(), 24000);
t2.addOutput(await alice.receiveAddress(), 24000);
// balance: 49000
await alice.sign(t2);
const t3 = new MTX();
t3.addTX(t1, 1); // 1000
t3.addTX(t2, 0); // 24000
t3.addOutput(await alice.receiveAddress(), 23000);
// balance: 47000
await alice.sign(t3);
const t4 = new MTX();
t4.addTX(t2, 1); // 24000
t4.addTX(t3, 0); // 23000
t4.addOutput(await alice.receiveAddress(), 11000);
t4.addOutput(await alice.receiveAddress(), 11000);
// balance: 22000
await alice.sign(t4);
const f1 = new MTX();
f1.addTX(t4, 1); // 11000
f1.addOutput(await bob.receiveAddress(), 10000);
// balance: 11000
await alice.sign(f1);
{
await wdb.addTX(t4.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 22000);
}
{
await wdb.addTX(t1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 73000);
}
{
await wdb.addTX(t2.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 71000);
}
{
await wdb.addTX(t3.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 69000);
}
{
await wdb.addTX(f1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 58000);
const txs = await alice.getHistory();
assert(txs.some((wtx) => {
return wtx.tx.hash().equals(f1.hash());
}));
}
{
const balance = await bob.getBalance();
assert.strictEqual(balance.unconfirmed, 10000);
const txs = await bob.getHistory();
assert(txs.some((wtx) => {
return wtx.tx.hash().equals(f1.hash());
}));
}
// Should recover from missed txs on block.
await wdb.addBlock(nextBlock(wdb), [
t1.toTX(),
t2.toTX(),
t3.toTX(),
t4.toTX(),
f1.toTX()
]);
{
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 11000);
}
{
const balance = await bob.getBalance();
assert.strictEqual(balance.unconfirmed, 10000);
}
});
it('should fill tx with inputs', async () => {
const alice = await wdb.create();
const bob = await wdb.create();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
await wdb.addTX(t1.toTX());
// Create new transaction
const m2 = new MTX();
m2.addOutput(await bob.receiveAddress(), 5460);
await alice.fund(m2, {
rate: 10000,
round: true
});
await alice.sign(m2);
const [t2, v2] = m2.commit();
assert(t2.verify(v2));
assert.strictEqual(t2.getInputValue(v2), 16380);
assert.strictEqual(t2.getOutputValue(), 6380);
assert.strictEqual(t2.getFee(v2), 10000);
// Create new transaction
const t3 = new MTX();
t3.addOutput(await bob.receiveAddress(), 15000);
let err;
try {
await alice.fund(t3, {
rate: 10000,
round: true
});
} catch (e) {
err = e;
}
assert(err);
assert.strictEqual(err.requiredFunds, 25000);
});
it('should fill tx with inputs with accurate fee', async () => {
const alice = await wdb.create({
master: KEY1
});
const bob = await wdb.create({
master: KEY2
});
// Coinbase
const t1 = new MTX();
t1.addOutpoint(new Outpoint(consensus.ZERO_HASH, 0));
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
await wdb.addTX(t1.toTX());
// Create new transaction
const m2 = new MTX();
m2.addOutput(await bob.receiveAddress(), 5460);
await alice.fund(m2, {
rate: 10000
});
await alice.sign(m2);
const [t2, v2] = m2.commit();
assert(t2.verify(v2));
assert.strictEqual(t2.getInputValue(v2), 16380);
// Should now have a change output:
assert.strictEqual(t2.getOutputValue(), 11130);
assert.strictEqual(t2.getFee(v2), 5250);
assert.strictEqual(t2.getWeight(), 2084);
assert.strictEqual(t2.getBaseSize(), 521);
assert.strictEqual(t2.getSize(), 521);
assert.strictEqual(t2.getVirtualSize(), 521);
let balance = null;
bob.once('balance', (b) => {
balance = b;
});
await wdb.addTX(t2);
// Create new transaction
const t3 = new MTX();
t3.addOutput(await bob.receiveAddress(), 15000);
let err;
try {
await alice.fund(t3, {
rate: 10000
});
} catch (e) {
err = e;
}
assert(err);
assert(balance);
assert.strictEqual(balance.unconfirmed, 5460);
});
it('should sign multiple inputs using different keys', async () => {
const alice = await wdb.create();
const bob = await wdb.create();
const carol = await wdb.create();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
// Coinbase
const t2 = new MTX();
t2.addInput(dummyInput());
t2.addOutput(await bob.receiveAddress(), 5460);
t2.addOutput(await bob.receiveAddress(), 5460);
t2.addOutput(await bob.receiveAddress(), 5460);
t2.addOutput(await bob.receiveAddress(), 5460);
await wdb.addTX(t1.toTX());
await wdb.addTX(t2.toTX());
// Create our tx with an output
const tx = new MTX();
tx.addOutput(await carol.receiveAddress(), 5460);
const coins1 = await alice.getCoins();
const coins2 = await bob.getCoins();
// Add our unspent inputs to sign
tx.addCoin(coins1[0]);
tx.addCoin(coins1[1]);
tx.addCoin(coins2[0]);
// Sign transaction
assert.strictEqual(await alice.sign(tx), 2);
assert.strictEqual(await bob.sign(tx), 1);
// Verify
assert.strictEqual(tx.verify(), true);
tx.inputs.length = 0;
tx.addCoin(coins1[1]);
tx.addCoin(coins1[2]);
tx.addCoin(coins2[1]);
assert.strictEqual(await alice.sign(tx), 2);
assert.strictEqual(await bob.sign(tx), 1);
// Verify
assert.strictEqual(tx.verify(), true);
});
it('should verify 2-of-3 p2sh tx', async () => {
await testP2SH(false, false);
});
it('should verify 2-of-3 p2wsh tx', async () => {
await testP2SH(true, false);
});
it('should verify 2-of-3 p2wsh tx w/ nested bullshit', async () => {
await testP2SH(true, true);
});
it('should create account', async () => {
const wallet = await wdb.create();
const account = await wallet.createAccount({
name: 'foo'
});
assert(account);
assert(account.initialized);
assert.strictEqual(account.name, 'foo');
assert.strictEqual(account.accountIndex, 1);
assert.strictEqual(account.m, 1);
assert.strictEqual(account.n, 1);
});
it('should fail to create duplicate account', async () => {
const wallet = await wdb.create();
const name = 'foo';
await wallet.createAccount({ name });
let err;
try {
await wallet.createAccount({ name });
} catch (e) {
err = e;
}
assert(err);
assert.strictEqual(err.message, 'Account already exists.');
});
it('should fill tx with account 1', async () => {
const alice = await wdb.create();
const bob = await wdb.create();
{
const account = await alice.createAccount({
name: 'foo'
});
assert.strictEqual(account.name, 'foo');
assert.strictEqual(account.accountIndex, 1);
}
const account = await alice.getAccount('foo');
assert.strictEqual(account.name, 'foo');
assert.strictEqual(account.accountIndex, 1);
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(account.receiveAddress(), 5460);
t1.addOutput(account.receiveAddress(), 5460);
t1.addOutput(account.receiveAddress(), 5460);
t1.addOutput(account.receiveAddress(), 5460);
await wdb.addTX(t1.toTX());
// Create new transaction
const t2 = new MTX();
t2.addOutput(await bob.receiveAddress(), 5460);
await alice.fund(t2, {
rate: 10000,
round: true
});
await alice.sign(t2);
assert(t2.verify());
assert.strictEqual(t2.getInputValue(), 16380);
assert.strictEqual(t2.getOutputValue(), 6380);
assert.strictEqual(t2.getFee(), 10000);
// Create new transaction
const t3 = new MTX();
t3.addOutput(await bob.receiveAddress(), 15000);
let err;
try {
await alice.fund(t3, {
rate: 10000,
round: true
});
} catch (e) {
err = e;
}
assert(err);
assert.strictEqual(err.requiredFunds, 25000);
const accounts = await alice.getAccounts();
assert.deepStrictEqual(accounts, ['default', 'foo']);
});
it('should fail to fill tx with account 1', async () => {
const wallet = await wdb.create();
{
const account = await wallet.createAccount({
name: 'foo'
});
assert.strictEqual(account.name, 'foo');
assert.strictEqual(account.accountIndex, 1);
}
const account = await wallet.getAccount('foo');
assert.strictEqual(account.name, 'foo');
assert.strictEqual(account.accountIndex, 1);
assert(!account.receiveAddress().equals(await wallet.receiveAddress()));
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(await wallet.receiveAddress(), 5460);
t1.addOutput(await wallet.receiveAddress(), 5460);
t1.addOutput(await wallet.receiveAddress(), 5460);
t1.addOutput(account.receiveAddress(), 5460);
await wdb.addTX(t1.toTX());
// Should fill from `foo` and fail
const t2 = new MTX();
t2.addOutput(await wallet.receiveAddress(), 5460);
let err;
try {
await wallet.fund(t2, {
rate: 10000,
round: true,
account: 'foo'
});
} catch (e) {
err = e;
}
assert(err);
// Should fill from whole wallet and succeed
const t3 = new MTX();
t3.addOutput(await wallet.receiveAddress(), 5460);
await wallet.fund(t3, {
rate: 10000,
round: true
});
// Coinbase
const t4 = new MTX();
t4.addInput(dummyInput());
t4.addOutput(await wallet.receiveAddress('foo'), 5460);
t4.addOutput(await wallet.receiveAddress('foo'), 5460);
t4.addOutput(await wallet.receiveAddress('foo'), 5460);
await wdb.addTX(t4.toTX());
// Should fill from `foo` and succeed
const t5 = new MTX();
t5.addOutput(await wallet.receiveAddress(), 5460);
await wallet.fund(t5, {
rate: 10000,
round: true,
account: 'foo'
});
currentWallet = wallet;
});
it('should create two accounts (multiple encryption)', async () => {
{
const wallet = await wdb.create({
id: 'foobar',
passphrase: 'foo'
});
await wallet.destroy();
wdb.unregister(wallet);
}
const wallet = await wdb.get('foobar');
assert(wallet);
const options = {
name: 'foo1'
};
const account = await wallet.createAccount(options, 'foo');
assert(account);
await wallet.lock();
});
it('should fill tx with inputs when encrypted', async () => {
const wallet = await wdb.create({
passphrase: 'foo'
});
wallet.master.stop();
wallet.master.key = null;
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(await wallet.receiveAddress(), 5460);
t1.addOutput(await wallet.receiveAddress(), 5460);
t1.addOutput(await wallet.receiveAddress(), 5460);
t1.addOutput(await wallet.receiveAddress(), 5460);
await wdb.addTX(t1.toTX());
// Create new transaction
const t2 = new MTX();
t2.addOutput(await wallet.receiveAddress(), 5460);
await wallet.fund(t2, {
rate: 10000,
round: true
});
// Should fail
let err;
try {
await wallet.sign(t2, 'bar');
} catch (e) {
err = e;
}
assert(err);
assert(!t2.verify());
// Should succeed
await wallet.sign(t2, 'foo');
assert(t2.verify());
});
it('should fill tx with inputs with subtract fee (1)', async () => {
const alice = await wdb.create();
const bob = await wdb.create();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
await wdb.addTX(t1.toTX());
// Create new transaction
const t2 = new MTX();
t2.addOutput(await bob.receiveAddress(), 21840);
await alice.fund(t2, {
rate: 10000,
round: true,
subtractFee: true
});
await alice.sign(t2);
assert(t2.verify());
assert.strictEqual(t2.getInputValue(), 5460 * 4);
assert.strictEqual(t2.getOutputValue(), 21840 - 10000);
assert.strictEqual(t2.getFee(), 10000);
});
it('should fill tx with inputs with subtract fee (2)', async () => {
const alice = await wdb.create();
const bob = await wdb.create();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
await wdb.addTX(t1.toTX());
const options = {
subtractFee: true,
rate: 10000,
round: true,
outputs: [{ address: await bob.receiveAddress(), value: 21840 }]
};
// Create new transaction
const t2 = await alice.createTX(options);
await alice.sign(t2);
assert(t2.verify());
assert.strictEqual(t2.getInputValue(), 5460 * 4);
assert.strictEqual(t2.getOutputValue(), 21840 - 10000);
assert.strictEqual(t2.getFee(), 10000);
});
it('should fill tx with smart coin selection', async () => {
const alice = await wdb.create();
const bob = await wdb.create();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
t1.addOutput(await alice.receiveAddress(), 5460);
await wdb.addTX(t1.toTX());
// Coinbase
const t2 = new MTX();
t2.addInput(dummyInput());
t2.addOutput(await alice.receiveAddress(), 5460);
t2.addOutput(await alice.receiveAddress(), 5460);
t2.addOutput(await alice.receiveAddress(), 5460);
t2.addOutput(await alice.receiveAddress(), 5460);
await wdb.addBlock(nextBlock(wdb), [t2.toTX()]);
{
const coins = await alice.getSmartCoins();
assert.strictEqual(coins.length, 4);
for (let i = 0; i < coins.length; i++) {
const coin = coins[i];
assert.strictEqual(coin.height, wdb.state.height);
}
}
// Create a change output for ourselves.
await alice.send({
subtractFee: true,
rate: 1000,
depth: 1,
outputs: [{ address: await bob.receiveAddress(), value: 1461 }]
});
const coins = await alice.getSmartCoins();
assert.strictEqual(coins.length, 4);
let total = 0;
{
let found = false;
for (let i = 0; i < coins.length; i++) {
const coin = coins[i];
if (coin.height === -1) {
assert(!found);
assert(coin.value < 5460);
found = true;
} else {
assert.strictEqual(coin.height, wdb.state.height);
}
total += coin.value;
}
assert(found);
}
// Use smart selection
const options = {
subtractFee: true,
smart: true,
rate: 10000,
outputs: [{
address: await bob.receiveAddress(),
value: total
}]
};
const t3 = await alice.createTX(options);
assert.strictEqual(t3.inputs.length, 4);
{
let found = false;
for (let i = 0; i < t3.inputs.length; i++) {
const coin = t3.view.getCoinFor(t3.inputs[i]);
if (coin.height === -1) {
assert(!found);
assert(coin.value < 5460);
found = true;
} else {
assert.strictEqual(coin.height, wdb.state.height);
}
}
assert(found);
}
await alice.sign(t3);
assert(t3.verify());
});
it('should get range of txs', async () => {
const wallet = currentWallet;
const txs = await wallet.getRange(null, {
start: util.now() - 1000
});
assert.strictEqual(txs.length, 2);
});
it('should get range of txs from account', async () => {
const wallet = currentWallet;
const txs = await wallet.getRange('foo', {
start: util.now() - 1000
});
assert.strictEqual(txs.length, 2);
});
it('should not get range of txs from non-existent account', async () => {
const wallet = currentWallet;
let txs, err;
try {
txs = await wallet.getRange('bad', {
start: 0xdeadbeef - 1000
});
} catch (e) {
err = e;
}
assert(!txs);
assert(err);
assert.strictEqual(err.message, 'Account not found.');
});
it('should get account balance', async () => {
const wallet = currentWallet;
const balance = await wallet.getBalance('foo');
assert.strictEqual(balance.unconfirmed, 21840);
});
it('should import privkey', async () => {
const key = KeyRing.generate();
const wallet = await wdb.create({
passphrase: 'test'
});
await wallet.importKey('default', key, 'test');
const wkey = await wallet.getKey(key.getHash());
assert.bufferEqual(wkey.getHash(), key.getHash());
// Coinbase
const t1 = new MTX();
t1.addOutput(key.getAddress(), 5460);
t1.addOutput(key.getAddress(), 5460);
t1.addOutput(key.getAddress(), 5460);
t1.addOutput(key.getAddress(), 5460);
t1.addInput(dummyInput());
await wdb.addTX(t1.toTX());
const wtx = await wallet.getTX(t1.hash());
assert(wtx);
assert.bufferEqual(t1.hash(), wtx.hash);
const options = {
rate: 10000,
round: true,
outputs: [{
address: await wallet.receiveAddress(),
value: 7000
}]
};
// Create new transaction
const t2 = await wallet.createTX(options);
await wallet.sign(t2);
assert(t2.verify());
assert.bufferEqual(t2.inputs[0].prevout.hash, wtx.hash);
importedWallet = wallet;
importedKey = key;
});
it('should import pubkey', async () => {
const key = KeyRing.generate();
const pub = new KeyRing(key.publicKey);
const wallet = await wdb.create({
watchOnly: true
});
await wallet.importKey('default', pub);
const path = await wallet.getPath(pub.getHash());
assert.bufferEqual(path.hash, pub.getHash());
const wkey = await wallet.getKey(pub.getHash());
assert(wkey);
});
it('should import address', async () => {
const key = KeyRing.generate();
const wallet = await wdb.create({
watchOnly: true
});
await wallet.importAddress('default', key.getAddress());
const path = await wallet.getPath(key.getHash());
assert(path);
assert.bufferEqual(path.hash, key.getHash());
const wkey = await wallet.getKey(key.getHash());
assert(!wkey);
});
it('should get details', async () => {
const wallet = currentWallet;
const txs = await wallet.getRange('foo', {
start: util.now() - 1000
});
const details = await wallet.toDetails(txs);
assert(details.some((tx) => {
return tx.toJSON(wdb.network).outputs[0].path.name === 'foo';
}));
});
it('should rename wallet', async () => {
const wallet = currentWallet;
await wallet.rename('test');
const txs = await wallet.getRange('foo', {
start: util.now() - 1000
});
const details = await wallet.toDetails(txs);
assert(details.length > 0);
assert.strictEqual(wallet.id, 'test');
});
it('should change passphrase with encrypted imports', async () => {
const wallet = importedWallet;
const addr = importedKey.getAddress();
assert(wallet.master.encrypted);
let data;
{
const path = await wallet.getPath(addr);
assert(path);
assert(path.data && path.encrypted);
data = path.data;
}
await wallet.decrypt('test');
{
const path = await wallet.getPath(addr);
assert(path);
assert(path.data && !path.encrypted);
assert(await wallet.getKey(addr));
}
await wallet.encrypt('foo');
{
const path = await wallet.getPath(addr);
assert(path);
assert(path.data && path.encrypted);
assert(!data.equals(path.data));
assert(!await wallet.getKey(addr));
}
await wallet.unlock('foo');
const key = await wallet.getKey(addr);
assert(key);
assert.bufferEqual(key.getHash(), addr.getHash());
});
it('should recover from a missed tx', async () => {
const wdb = new WalletDB({ workers });
await wdb.open();
const alice = await wdb.create({
master: KEY1
});
const bob = await wdb.create({
master: KEY1
});
const addr = await alice.receiveAddress();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(addr, 50000);
await wdb.addTX(t1.toTX());
// Bob misses this tx!
const t2 = new MTX();
t2.addTX(t1, 0);
t2.addOutput(addr, 24000);
t2.addOutput(addr, 24000);
await alice.sign(t2);
await alice.add(t2.toTX());
assert.notStrictEqual(
(await alice.getBalance()).unconfirmed,
(await bob.getBalance()).unconfirmed);
// Bob sees this one.
const t3 = new MTX();
t3.addTX(t2, 0);
t3.addTX(t2, 1);
t3.addOutput(addr, 30000);
await alice.sign(t3);
assert.strictEqual((await bob.getBalance()).unconfirmed, 50000);
await wdb.addTX(t3.toTX());
assert.strictEqual((await alice.getBalance()).unconfirmed, 30000);
// t1 gets confirmed.
await wdb.addBlock(nextBlock(wdb), [t1.toTX()]);
// Bob sees t2 on the chain.
await wdb.addBlock(nextBlock(wdb), [t2.toTX()]);
// Bob sees t3 on the chain.
await wdb.addBlock(nextBlock(wdb), [t3.toTX()]);
assert.strictEqual((await bob.getBalance()).unconfirmed, 30000);
});
it('should recover from a missed tx and double spend', async () => {
const wdb = new WalletDB({ workers });
await wdb.open();
const alice = await wdb.create({
master: KEY1
});
const bob = await wdb.create({
master: KEY1
});
const addr = await alice.receiveAddress();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(addr, 50000);
await wdb.addTX(t1.toTX());
// Bob misses this tx!
const t2a = new MTX();
t2a.addTX(t1, 0);
t2a.addOutput(addr, 24000);
t2a.addOutput(addr, 24000);
await alice.sign(t2a);
await alice.add(t2a.toTX());
assert.notStrictEqual(
(await alice.getBalance()).unconfirmed,
(await bob.getBalance()).unconfirmed);
// Bob doublespends.
const t2b = new MTX();
t2b.addTX(t1, 0);
t2b.addOutput(addr, 10000);
t2b.addOutput(addr, 10000);
await bob.sign(t2b);
await bob.add(t2b.toTX());
// Bob sees this one.
const t3 = new MTX();
t3.addTX(t2a, 0);
t3.addTX(t2a, 1);
t3.addOutput(addr, 30000);
await alice.sign(t3);
assert.strictEqual((await bob.getBalance()).unconfirmed, 20000);
await wdb.addTX(t3.toTX());
assert.strictEqual((await alice.getBalance()).unconfirmed, 30000);
// t1 gets confirmed.
await wdb.addBlock(nextBlock(wdb), [t1.toTX()]);
// Bob sees t2a on the chain.
await wdb.addBlock(nextBlock(wdb), [t2a.toTX()]);
// Bob sees t3 on the chain.
await wdb.addBlock(nextBlock(wdb), [t3.toTX()]);
assert.strictEqual((await bob.getBalance()).unconfirmed, 30000);
});
it('should remove a wallet', async () => {
await wdb.create({
id: 'alice100'
});
assert(await wdb.get('alice100'));
await wdb.remove('alice100');
assert(!await wdb.get('alice100'));
});
const keyTypes = [
{
name: 'receive',
method: 'createReceive',
branch: 0
},
{
name: 'change',
method: 'createChange',
branch: 1
},
{
name: 'nested',
method: 'createNested',
branch: 2
}
];
for (const type of keyTypes) {
it(`should create ${type.name} addresses`, async () => {
const account = 0;
const wallet = await wdb.create({
witness: true
});
const addresses = new Set();
for (let i = 0; i < 100; i++) {
const key = await wallet[type.method](account);
addresses.add(key.getAddress('string'));
assert.strictEqual(key.account, account);
assert.strictEqual(key.branch, type.branch);
assert.strictEqual(key.index, i + 1);
}
assert.strictEqual(addresses.size, 100);
});
it(`should create ${type.name} addresses and get their keys`, async () => {
const account = 0;
const wallet = await wdb.create({
witness: true
});
const addresses = new Set();
for (let i = 0; i < 100; i++) {
const key1 = await wallet[type.method](account);
const address = key1.getAddress();
assert(key1, `Could not get ${type.name}`);
addresses.add(address);
assert.strictEqual(key1.account, account);
assert.strictEqual(key1.branch, type.branch);
assert.strictEqual(key1.index, i + 1);
const key2 = await wallet.getKey(address);
assert(key2, `Could not get key for ${address.toString()}` +
`, Key: xpub/${type.branch}/${i+1}`);
assert.strictEqual(key2.name, key1.name);
assert.strictEqual(key2.account, key1.account);
assert.strictEqual(key2.branch, key1.branch);
assert.strictEqual(key2.witness, key1.witness);
assert.strictEqual(key2.nested, key1.nested);
assert.bufferEqual(key2.publicKey, key1.publicKey);
assert.strictEqual(key2.getType(), key1.getType());
}
assert.strictEqual(addresses.size, 100);
});
}
it('should cleanup', async () => {
consensus.COINBASE_MATURITY = 100;
// await wdb.close();
});
});