fcoin/test/wallet-test.js
2017-12-08 16:03:46 +08:00

1489 lines
36 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 encoding = require('../lib/utils/encoding');
const digest = require('../lib/crypto/digest');
const random = require('../lib/crypto/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 workers = new WorkerPool({
enabled: true
});
const wdb = new WalletDB({
db: 'memory',
verify: true,
workers
});
let currentWallet = null;
let importedWallet = null;
let importedKey = null;
let doubleSpendWallet = null;
let doubleSpendCoin = null;
let globalTime = util.now();
let globalHeight = 1;
function nextBlock() {
const height = globalHeight++;
const time = globalTime++;
const prevHead = encoding.U32(height - 1);
const prevHash = digest.hash256(prevHead);
const head = encoding.U32(height);
const hash = digest.hash256(head);
return {
hash: hash.toString('hex'),
height: height,
prevBlock: prevHash.toString('hex'),
time: time,
merkleRoot: encoding.NULL_HASH,
nonce: 0,
bits: 0
};
}
function dummyInput() {
const hash = random.randomBytes(32).toString('hex');
return Input.fromOutpoint(new Outpoint(hash, 0));
}
async function testP2PKH(witness, nesting) {
const flags = Script.flags.STANDARD_VERIFY_FLAGS;
const wallet = await wdb.create({
witness
});
const addr = Address.fromString(wallet.getAddress('string'));
const type = witness ? Address.types.WITNESS : Address.types.PUBKEYHASH;
assert.strictEqual(addr.type, type);
const src = new MTX();
src.addInput(dummyInput());
src.addOutput(nesting ? wallet.getNested() : wallet.getAddress(), 5460 * 2);
src.addOutput(new Address(), 2 * 5460);
const mtx = new MTX();
mtx.addTX(src, 0);
mtx.addOutput(wallet.getAddress(), 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 receive = nesting ? 'nested' : 'receive';
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(bob.account.accountKey);
await alice.addSharedKey(carol.account.accountKey);
await bob.addSharedKey(alice.account.accountKey);
await bob.addSharedKey(carol.account.accountKey);
await carol.addSharedKey(alice.account.accountKey);
await carol.addSharedKey(bob.account.accountKey);
// Our p2sh address
const addr1 = alice.account[receive].getAddress();
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(alice.account[receive].getAddress().equals(addr1));
assert(bob.account[receive].getAddress().equals(addr1));
assert(carol.account[receive].getAddress().equals(addr1));
const nestedAddr1 = alice.getNested();
if (witness) {
assert(nestedAddr1);
assert(alice.getNested().equals(nestedAddr1));
assert(bob.getNested().equals(nestedAddr1));
assert(carol.getNested().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
const block = nextBlock();
assert.strictEqual(alice.account[receiveDepth], 1);
await wdb.addBlock(block, [fund.toTX()]);
assert.strictEqual(alice.account[receiveDepth], 2);
assert.strictEqual(alice.account.changeDepth, 1);
}
const addr2 = alice.account[receive].getAddress();
assert(!addr2.equals(addr1));
assert(alice.account[receive].getAddress().equals(addr2));
assert(bob.account[receive].getAddress().equals(addr2));
assert(carol.account[receive].getAddress().equals(addr2));
// Create a tx requiring 2 signatures
const send = new MTX();
send.addOutput(recipient.getAddress(), 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(alice.account.changeDepth, 1);
const change = alice.account.change.getAddress();
assert(alice.account.change.getAddress().equals(change));
assert(bob.account.change.getAddress().equals(change));
assert(carol.account.change.getAddress().equals(change));
// Simulate a confirmation
{
const block = nextBlock();
await wdb.addBlock(block, [tx]);
assert.strictEqual(alice.account[receiveDepth], 2);
assert.strictEqual(alice.account.changeDepth, 2);
assert(alice.account[receive].getAddress().equals(addr2));
assert(!alice.account.change.getAddress().equals(change));
}
const change2 = alice.account.change.getAddress();
assert(alice.account.change.getAddress().equals(change2));
assert(bob.account.change.getAddress().equals(change2));
assert(carol.account.change.getAddress().equals(change2));
const input = tx.inputs[0];
input[vector].setData(2, encoding.ZERO_SIG);
input[vector].compile();
assert(!tx.verify(view, flags));
assert.strictEqual(tx.getFee(view), 10000);
}
describe('Wallet', function() {
this.timeout(5000);
it('should open walletdb', async () => {
consensus.COINBASE_MATURITY = 0;
await wdb.open();
});
it('should generate new key and address', async () => {
const wallet = await wdb.create();
const addr1 = wallet.getAddress();
assert(addr1);
const str = addr1.toString();
const addr2 = Address.fromString(str);
assert(addr2.equals(addr1));
});
it('should validate existing address', () => {
assert(Address.fromString('LdcyCZgmbwibypUeD6zAhdPEVM3mPj4zxH'));
});
it('should fail to validate invalid address', () => {
assert.throws(() => {
Address.fromString('LdcyCZgmbwibypueD6zAhdPEVM3mPj4zxH');
});
});
it('should create and get wallet', async () => {
const wallet1 = await wdb.create();
await wallet1.destroy();
const wallet2 = await wdb.get(wallet1.id);
assert(wallet1 !== wallet2);
assert(wallet1.master !== wallet2.master);
assert(wallet1.master.key.equals(wallet2.master.key));
assert(wallet1.account.accountKey.equals(wallet2.account.accountKey));
});
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).toPublic();
await wallet.addSharedKey(key);
const script = Script.fromMultisig(1, 2, [
wallet.account.receive.getPublicKey(),
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(wallet.getAddress(), 5460);
const maxSize = await tx.estimateSize();
await wallet.sign(tx);
assert(tx.toRaw().length <= maxSize);
assert(tx.verify());
});
it('should handle missed and invalid txs', async () => {
const alice = await wdb.create();
const bob = await wdb.create();
// Coinbase
// balance: 51000
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(alice.getAddress(), 50000);
t1.addOutput(alice.getAddress(), 1000);
const t2 = new MTX();
t2.addTX(t1, 0); // 50000
t2.addOutput(alice.getAddress(), 24000);
t2.addOutput(alice.getAddress(), 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(alice.getAddress(), 23000);
// balance: 47000
await alice.sign(t3);
const t4 = new MTX();
t4.addTX(t2, 1); // 24000
t4.addTX(t3, 0); // 23000
t4.addOutput(alice.getAddress(), 11000);
t4.addOutput(alice.getAddress(), 11000);
// balance: 22000
await alice.sign(t4);
const f1 = new MTX();
f1.addTX(t4, 1); // 11000
f1.addOutput(bob.getAddress(), 10000);
// balance: 11000
await alice.sign(f1);
const fake = new MTX();
fake.addTX(t1, 1); // 1000 (already redeemed)
fake.addOutput(alice.getAddress(), 500);
// Script inputs but do not sign
await alice.template(fake);
// Fake signature
const input = fake.inputs[0];
input.script.setData(0, encoding.ZERO_SIG);
input.script.compile();
// balance: 11000
// Fake TX should temporarily change output.
{
await wdb.addTX(fake.toTX());
await wdb.addTX(t4.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 22500);
}
{
await wdb.addTX(t1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 72500);
}
{
await wdb.addTX(t2.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 46500);
}
{
await wdb.addTX(t3.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 22000);
}
{
await wdb.addTX(f1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 11000);
const txs = await alice.getHistory();
assert(txs.some((wtx) => {
return wtx.hash === f1.hash('hex');
}));
}
{
const balance = await bob.getBalance();
assert.strictEqual(balance.unconfirmed, 10000);
const txs = await bob.getHistory();
assert(txs.some((wtx) => {
return wtx.tx.hash('hex') === f1.hash('hex');
}));
}
});
it('should cleanup spenders after double-spend', async () => {
const wallet = doubleSpendWallet;
{
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);
}
{
const tx = new MTX();
tx.addCoin(doubleSpendCoin);
tx.addOutput(wallet.getAddress(), 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 missed txs without resolution', async () => {
const wdb = new WalletDB({
name: 'wallet-test',
db: 'memory',
verify: false
});
await wdb.open();
const alice = await wdb.create();
const bob = await wdb.create();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(alice.getAddress(), 50000);
t1.addOutput(alice.getAddress(), 1000);
// balance: 51000
const t2 = new MTX();
t2.addTX(t1, 0); // 50000
t2.addOutput(alice.getAddress(), 24000);
t2.addOutput(alice.getAddress(), 24000);
// balance: 49000
await alice.sign(t2);
const t3 = new MTX();
t3.addTX(t1, 1); // 1000
t3.addTX(t2, 0); // 24000
t3.addOutput(alice.getAddress(), 23000);
// balance: 47000
await alice.sign(t3);
const t4 = new MTX();
t4.addTX(t2, 1); // 24000
t4.addTX(t3, 0); // 23000
t4.addOutput(alice.getAddress(), 11000);
t4.addOutput(alice.getAddress(), 11000);
// balance: 22000
await alice.sign(t4);
const f1 = new MTX();
f1.addTX(t4, 1); // 11000
f1.addOutput(bob.getAddress(), 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, 47000);
}
{
await wdb.addTX(t3.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 22000);
}
{
await wdb.addTX(f1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 11000);
const txs = await alice.getHistory();
assert(txs.some((wtx) => {
return wtx.tx.hash('hex') === f1.hash('hex');
}));
}
{
const balance = await bob.getBalance();
assert.strictEqual(balance.unconfirmed, 10000);
const txs = await bob.getHistory();
assert(txs.some((wtx) => {
return wtx.tx.hash('hex') === f1.hash('hex');
}));
}
await wdb.addTX(t2.toTX());
await wdb.addTX(t3.toTX());
await wdb.addTX(t4.toTX());
await wdb.addTX(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(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
await wdb.addTX(t1.toTX());
// Create new transaction
const m2 = new MTX();
m2.addOutput(bob.getAddress(), 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(bob.getAddress(), 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(encoding.NULL_HASH, 0));
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
await wdb.addTX(t1.toTX());
// Create new transaction
const m2 = new MTX();
m2.addOutput(bob.getAddress(), 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;
bob.once('balance', (b) => {
balance = b;
});
await wdb.addTX(t2);
// Create new transaction
const t3 = new MTX();
t3.addOutput(bob.getAddress(), 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(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
// Coinbase
const t2 = new MTX();
t2.addInput(dummyInput());
t2.addOutput(bob.getAddress(), 5460);
t2.addOutput(bob.getAddress(), 5460);
t2.addOutput(bob.getAddress(), 5460);
t2.addOutput(bob.getAddress(), 5460);
await wdb.addTX(t1.toTX());
await wdb.addTX(t2.toTX());
// Create our tx with an output
const tx = new MTX();
tx.addOutput(carol.getAddress(), 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 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.receive.getAddress(), 5460);
t1.addOutput(account.receive.getAddress(), 5460);
t1.addOutput(account.receive.getAddress(), 5460);
t1.addOutput(account.receive.getAddress(), 5460);
await wdb.addTX(t1.toTX());
// Create new transaction
const t2 = new MTX();
t2.addOutput(bob.getAddress(), 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(bob.getAddress(), 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.strictEqual(wallet.account.accountIndex, 0);
assert(!account.receive.getAddress().equals(
wallet.account.receive.getAddress()));
assert(wallet.getAddress().equals(wallet.account.receive.getAddress()));
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(wallet.getAddress(), 5460);
t1.addOutput(wallet.getAddress(), 5460);
t1.addOutput(wallet.getAddress(), 5460);
t1.addOutput(account.receive.getAddress(), 5460);
await wdb.addTX(t1.toTX());
// Should fill from `foo` and fail
const t2 = new MTX();
t2.addOutput(wallet.getAddress(), 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(wallet.getAddress(), 5460);
await wallet.fund(t3, {
rate: 10000,
round: true
});
// Coinbase
const t4 = new MTX();
t4.addInput(dummyInput());
t4.addOutput(account.receive.getAddress(), 5460);
t4.addOutput(account.receive.getAddress(), 5460);
t4.addOutput(account.receive.getAddress(), 5460);
await wdb.addTX(t4.toTX());
// Should fill from `foo` and succeed
const t5 = new MTX();
t5.addOutput(wallet.getAddress(), 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();
}
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(wallet.getAddress(), 5460);
t1.addOutput(wallet.getAddress(), 5460);
t1.addOutput(wallet.getAddress(), 5460);
t1.addOutput(wallet.getAddress(), 5460);
await wdb.addTX(t1.toTX());
// Create new transaction
const t2 = new MTX();
t2.addOutput(wallet.getAddress(), 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(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
await wdb.addTX(t1.toTX());
// Create new transaction
const t2 = new MTX();
t2.addOutput(bob.getAddress(), 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(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
await wdb.addTX(t1.toTX());
const options = {
subtractFee: true,
rate: 10000,
round: true,
outputs: [{ address: bob.getAddress(), 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(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
t1.addOutput(alice.getAddress(), 5460);
await wdb.addTX(t1.toTX());
// Coinbase
const t2 = new MTX();
t2.addInput(dummyInput());
t2.addOutput(alice.getAddress(), 5460);
t2.addOutput(alice.getAddress(), 5460);
t2.addOutput(alice.getAddress(), 5460);
t2.addOutput(alice.getAddress(), 5460);
const block = nextBlock();
await wdb.addBlock(block, [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, block.height);
}
}
// Create a change output for ourselves.
await alice.send({
subtractFee: true,
rate: 1000,
depth: 1,
outputs: [{ address: bob.getAddress(), 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, block.height);
}
total += coin.value;
}
assert(found);
}
// Use smart selection
const options = {
subtractFee: true,
smart: true,
rate: 10000,
outputs: [{
address: bob.getAddress(),
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, block.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({
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('hex'));
assert.strictEqual(wkey.getHash('hex'), key.getHash('hex'));
// 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('hex'));
assert(wtx);
assert.strictEqual(t1.hash('hex'), wtx.hash);
const options = {
rate: 10000,
round: true,
outputs: [{
address: wallet.getAddress(),
value: 7000
}]
};
// Create new transaction
const t2 = await wallet.createTX(options);
await wallet.sign(t2);
assert(t2.verify());
assert.strictEqual(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('hex'));
assert.strictEqual(path.hash, pub.getHash('hex'));
const wkey = await wallet.getKey(pub.getHash('hex'));
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('hex'));
assert(path);
assert.strictEqual(path.hash, key.getHash('hex'));
const wkey = await wallet.getKey(key.getHash('hex'));
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().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.strictEqual(details[0].toJSON().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.strictEqual(key.getHash('hex'), addr.getHash('hex'));
});
it('should recover from a missed tx', async () => {
const wdb = new WalletDB({
name: 'wallet-test',
db: 'memory',
verify: false
});
await wdb.open();
const alice = await wdb.create({
master: KEY1
});
const bob = await wdb.create({
master: KEY1
});
const addr = alice.getAddress();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(addr, 50000);
await alice.add(t1.toTX());
await bob.add(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 alice.add(t3.toTX());
await bob.add(t3.toTX());
assert.strictEqual((await alice.getBalance()).unconfirmed, 30000);
// Bob sees t2 on the chain.
await bob.add(t2.toTX());
// Bob sees t3 on the chain.
await bob.add(t3.toTX());
assert.strictEqual((await bob.getBalance()).unconfirmed, 30000);
});
it('should recover from a missed tx and double spend', async () => {
const wdb = new WalletDB({
name: 'wallet-test',
db: 'memory',
verify: false
});
await wdb.open();
const alice = await wdb.create({
master: KEY1
});
const bob = await wdb.create({
master: KEY1
});
const addr = alice.getAddress();
// Coinbase
const t1 = new MTX();
t1.addInput(dummyInput());
t1.addOutput(addr, 50000);
await alice.add(t1.toTX());
await bob.add(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 alice.add(t3.toTX());
await bob.add(t3.toTX());
assert.strictEqual((await alice.getBalance()).unconfirmed, 30000);
// Bob sees t2a on the chain.
await bob.add(t2a.toTX());
// Bob sees t3 on the chain.
await bob.add(t3.toTX());
assert.strictEqual((await bob.getBalance()).unconfirmed, 30000);
});
it('should cleanup', () => {
consensus.COINBASE_MATURITY = 100;
});
});