itns-sidechain/test/wallet-unit-test.js

561 lines
16 KiB
JavaScript

'use strict';
const assert = require('bsert');
const blake2b = require('bcrypto/lib/blake2b');
const base58 = require('bcrypto/lib/encoding/base58');
const random = require('bcrypto/lib/random');
const bio = require('bufio');
const Network = require('../lib/protocol/network');
const MTX = require('../lib/primitives/mtx');
const HDPrivateKey = require('../lib/hd/private');
const Mnemonic = require('../lib/hd/mnemonic');
const WalletDB = require('../lib/wallet/walletdb');
const Wallet = require('../lib/wallet/wallet');
const Account = require('../lib/wallet/account');
const wutils = require('./util/wallet');
const {nextEntry, fakeEntry} = wutils;
const {dummyInput} = require('./util/primitives');
const MemWallet = require('./util/memwallet');
/** @typedef {import('../lib/primitives/tx')} TX */
const mnemonics = require('./data/mnemonic-english.json');
const network = Network.get('main');
describe('Wallet Unit Tests', () => {
describe('constructor', function() {
// abandon, abandon... about
const phrase = mnemonics[0][1];
const passphrase = mnemonics[0][2];
const mnemonic = new Mnemonic(phrase, passphrase);
const hdprivkey = HDPrivateKey.fromMnemonic(mnemonic);
const xprv = hdprivkey.xprivkey();
let wdb;
// m/44'/5355'/0'
const xpub = ''
+ 'xpub6DBMpym6PM3qe7Ug7BwG6zo7dinMMjpk8nmb73czsjkzPTzfQ1d'
+ '5ZvqDea4uNmMVv1Y9DT6v17GuDL1x2km9FQuKqWMdnrDfRiDNrG1nTMr';
// Open and close the WalletDB between tests because
// each time wdb.create is called, it mutates properties
// on the WalletDB instance
beforeEach(async () => {
wdb = new WalletDB({ network: network.type });
await wdb.open();
});
afterEach(async () => {
await wdb.close();
wdb = null;
});
it('should handle options.master', async () => {
// Should fail due to invalid key.
const errorMsg = 'Must create wallet with hd private key.';
const assertMsg = 'Invalid HD.PrivateKey should throw.';
await assert.rejects(async () => {
await wdb.create({ master: Buffer.from('00', 'hex') });
}, errorMsg, assertMsg);
{
// Should instatiate from String.
const wallet = await wdb.create({ master: xprv });
const want = xprv;
const got = wallet.master.key.xprivkey();
assert.deepEqual(got, want, 'Failed to instantiate from String.');
}
{
// Should instatiate from HD.PrivateKey.
const opt = { master: HDPrivateKey.fromMnemonic(mnemonic) };
const wallet = await wdb.create(opt);
const want = xprv;
const got = wallet.master.key.xprivkey();
assert.deepEqual(got, want, 'Failed to instatiate from HD.PrivateKey.');
}
});
it('should handle options.mnemonic', async () => {
// Should instantiate from HD.Mnemonic.
const wallet = await wdb.create({ mnemonic: phrase });
const want = phrase;
const got = wallet.master.mnemonic.phrase;
assert.deepEqual(got, want, 'Phrase mismatch.');
});
it('should handle options.wid', async () => {
{
// Wallet ids increment by one staring at 1 each
// time that a new wallet is created
for (let i = 0; i < 3; i++) {
const wallet = await wdb.create();
const want = i + 1;
const got = wallet.wid;
assert.deepEqual(got, want, 'Wallet ID mismatch.');
}
}
{
// fromOptions should appropriately set value
const w = Wallet.fromOptions(wdb, { wid: 2 });
const want = 2;
const got = w.wid;
assert.equal(got, want, 'Wallet ID mismatch.');
}
{
// Wallet ids can only be uint32s
assert.throws(() => Wallet.fromOptions(wdb, { wid: 2**32 }));
assert.throws(() => Wallet.fromOptions(wdb, { wid: -1 }));
}
});
it('should handle options.id', async () => {
const names = [
// id, expected
['foo', true], // normal name
['1234567', true], // all numbers
['123abc', true], // mix of letters/numbers
['foo-bar', true], // allow dash
['my fav wallet', false], // spaces
[1234567, false], // wrong type
['__proto__', false], // illegal keyword
[' ', false], // whitespace
['.hsd', false], // . prefix
['hsd.', false], // . suffix
['a'.repeat(40), true], // max length
['a'.repeat(41), false] // 1 longer than max length
];
for (const [id, expected] of names) {
if (expected === false) {
const fn = async () => await wdb.create({ id });
await assert.rejects(fn, 'Bad wallet ID.');
} else {
const wallet = await wdb.create({ id });
const got = wallet.id;
const want = id;
assert.equal(got, want);
}
}
{
// Auto generated id matches schema
// BLAKE2b(m/44->public|magic, 20)
// of `0x03be04` to base58
const wallet = await wdb.create({ mnemonic });
// hdprivkey is derived from the mnemonic
const key = hdprivkey.derive(44);
const bw = bio.write(37);
bw.writeBytes(key.publicKey);
bw.writeU32(network.magic);
const hash = blake2b.digest(bw.render(), 20);
const b58 = bio.write(23);
b58.writeU8(0x03);
b58.writeU8(0xbe);
b58.writeU8(0x04);
b58.writeBytes(hash);
const want = base58.encode(b58.render());
const got = wallet.id;
assert.equal(got, want);
}
});
it('should handle auto generation of tokens', async () => {
const wallet = await wdb.create({ mnemonic });
// hdprivkey is derived from the mnemonic
const key = hdprivkey.derive(44, true);
// Always use the same privateKey
function getToken(nonce) {
const bw = bio.write(36);
bw.writeBytes(key.privateKey);
bw.writeU32(nonce);
return blake2b.digest(bw.render());
}
// Assert that the nonce is generated correctly
// for different integers
for (let i = 0; i < 3; i++) {
const nonce = i;
const want = getToken(nonce);
const got = wallet.getToken(nonce);
assert.bufferEqual(want, got);
}
{
// Tokens can only be generated safely for
// up through the MAX_SAFE_INTEGER
const nonce = Number.MAX_SAFE_INTEGER + 1;
const msg = '\'num\' must be a(n) integer';
assert.throws(() => wallet.getToken(nonce), msg);
}
});
it('should handle options.watchOnly', async () => {
{
// Should create a Wallet with a watch only account
// and be able to recover the accountKey
const wallet = await wdb.create({
watchOnly: true,
accountKey: xpub
});
{
const got = wallet.watchOnly;
const want = true;
assert.equal(got, want);
}
{
const key = await wallet.accountKey();
const got = key.xpubkey();
const want = xpub;
assert.equal(got, want);
}
}
{
// Requires an accountKey to instantiate
const fn = async () => await wdb.create({ watchOnly: true });
await assert.rejects(fn, 'Must add HD public keys to watch only wallet.');
}
{
// Wrong type should throw assertion error
const fn = async () => await wdb.create({ watchOnly: 'foo' });
await assert.rejects(fn, 'Assertion failed.');
}
});
it('should handle options.accountDepth', async () => {
{
// fromOptions should appropriately set value
const w = Wallet.fromOptions(wdb, { accountDepth: 2 });
const got = w.accountDepth;
const want = 2;
assert.equal(got, want, 'Account Depth mismatch.');
}
{
// Wallet increments the account depth each time after
// creating an account
for (let i = 0; i < 3; i++) {
const wallet = await wdb.create({ accountDepth: i });
const got = wallet.accountDepth;
const want = i + 1;
assert.equal(got, want);
}
}
{
// Account Depth can only be uint32s
const overflow = { accountDepth: 2**32 };
assert.throws(() => Wallet.fromOptions(wdb, overflow));
const underflow = { accountDepth: -1 };
assert.throws(() => Wallet.fromOptions(wdb, underflow));
}
});
it('should handle options.token', async () => {
{
const token = random.randomBytes(32);
const wallet = await wdb.create({ token });
const got = wallet.token;
const want = token;
assert.bufferEqual(got, want);
}
{
// The token must be 32 bytes
const token = random.randomBytes(16);
const fn = async () => await wdb.create({ token });
await assert.rejects(fn, 'Assertion failed.');
}
});
it('should handle options.tokenDepth', async () => {
{
// Token depth should be set based on the input value
const wallet = await wdb.create({ tokenDepth: 10 });
assert.equal(wallet.tokenDepth, 10);
}
{
// Token depth can only be uint32s
const overflow = { tokenDepth: 2**32 };
assert.throws(() => Wallet.fromOptions(wdb, overflow));
const underflow = { tokenDepth: -1 };
assert.throws(() => Wallet.fromOptions(wdb, underflow));
}
});
it('should handle options.lookahead (account)', async () => {
const wid = 0;
const id = 'primary';
const key = HDPrivateKey.generate();
const accountKey = key.toPublic();
const accountIndex = 0;
const invalid = [
-1000,
-1,
2 ** 32,
2 ** 33
];
const valid = [
0,
1,
1000
];
for (const lookahead of invalid) {
assert.throws(() => {
Account.fromOptions({}, {
id,
wid,
accountKey,
accountIndex,
lookahead
});
});
await assert.rejects(wdb.create({ lookahead }));
}
for (const lookahead of valid) {
const wallet = await wdb.create({ lookahead });
const account = await wallet.getAccount(0);
assert.strictEqual(account.lookahead, lookahead);
}
// Wallet create will take a lot of time generating all lookaheads.
valid.push(2 ** 32 - 1);
for (const lookahead of valid) {
const account = Account.fromOptions({}, {
id,
wid,
accountKey,
accountIndex,
lookahead
});
assert.strictEqual(account.lookahead, lookahead);
}
});
});
describe('addBlock', function() {
const ALT_SEED = 0xdeadbeef;
/** @type {WalletDB} */
let wdb;
/** @type {Wallet} */
let wallet;
/** @type {MemWallet} */
let memwallet;
beforeEach(async () => {
wdb = new WalletDB({
network: network.type,
memory: true
});
await wdb.open();
wallet = wdb.primary;
memwallet = new MemWallet({
network
});
for (let i = 0; i < 10; i++) {
const entry = nextEntry(wdb);
await wdb.addBlock(entry, []);
}
});
afterEach(async () => {
await wdb.close();
wdb = null;
});
// Move forward
it('should progress with 10 block', async () => {
const tip = await wdb.getTip();
for (let i = 0; i < 10; i++) {
const entry = nextEntry(wdb);
const added = await wdb.addBlock(entry, []);
assert.ok(added);
assert.strictEqual(added.txs, 0);
assert.strictEqual(added.filterUpdated, false);
assert.equal(wdb.height, entry.height);
}
assert.strictEqual(wdb.height, tip.height + 10);
});
it('should return number of transactions added (owned)', async () => {
const tip = await wdb.getTip();
const wtx = await fakeWTX(wallet);
const entry = nextEntry(wdb);
const added = await wdb.addBlock(entry, [wtx]);
assert.ok(added);
assert.strictEqual(added.txs, 1);
assert.strictEqual(added.filterUpdated, true);
assert.equal(wdb.height, tip.height + 1);
});
it('should return number of transactions added (none)', async () => {
const tip = await wdb.getTip();
const entry = nextEntry(wdb);
const added = await wdb.addBlock(entry, []);
assert.ok(added);
assert.strictEqual(added.txs, 0);
assert.strictEqual(added.filterUpdated, false);
assert.equal(wdb.height, tip.height + 1);
});
it('should fail to add block on unusual reorg', async () => {
const tip = await wdb.getTip();
const entry = nextEntry(wdb, ALT_SEED, ALT_SEED);
// TODO: Detect sync chain is correct.
const added = await wdb.addBlock(entry, []);
assert.strictEqual(added, null);
assert.strictEqual(wdb.height, tip.height);
});
// Same block
it('should re-add the same block', async () => {
const tip = await wdb.getTip();
const entry = nextEntry(wdb);
const wtx1 = await fakeWTX(wallet);
const wtx2 = await fakeWTX(wallet);
const added1 = await wdb.addBlock(entry, [wtx1]);
assert.ok(added1);
assert.strictEqual(added1.txs, 1);
assert.strictEqual(added1.filterUpdated, true);
assert.equal(wdb.height, tip.height + 1);
// Same TX wont show up second time.
const added2 = await wdb.addBlock(entry, [wtx1]);
assert.ok(added2);
assert.strictEqual(added2.txs, 0);
assert.strictEqual(added2.filterUpdated, false);
assert.equal(wdb.height, tip.height + 1);
const added3 = await wdb.addBlock(entry, [wtx1, wtx2]);
assert.ok(added3);
assert.strictEqual(added3.txs, 1);
// Both txs are using the same address.
assert.strictEqual(added3.filterUpdated, false);
assert.equal(wdb.height, tip.height + 1);
});
it('should ignore txs not owned by wallet', async () => {
const tip = await wdb.getTip();
const addr = memwallet.getReceive().toString(network);
const tx = fakeTX(addr);
const entry = nextEntry(wdb);
const added = await wdb.addBlock(entry, [tx]);
assert.ok(added);
assert.strictEqual(added.txs, 0);
assert.strictEqual(added.filterUpdated, false);
assert.strictEqual(wdb.height, tip.height + 1);
});
// This should not happen, but there should be guards in place.
it('should resync if the block is the same', async () => {
const tip = await wdb.getTip();
const entry = fakeEntry(tip.height, 0, ALT_SEED);
// TODO: Detect sync chain is correct.
const added = await wdb.addBlock(entry, []);
assert.strictEqual(added, null);
});
// LOW BLOCKS
it('should ignore blocks before tip', async () => {
const tip = await wdb.getTip();
const entry = fakeEntry(tip.height - 1);
const wtx = await fakeWTX(wallet);
// ignore low blocks.
const added = await wdb.addBlock(entry, [wtx]);
assert.strictEqual(added, null);
assert.strictEqual(wdb.height, tip.height);
});
it('should sync chain blocks before tip on unusual low block reorg', async () => {
const tip = await wdb.getTip();
const entry = fakeEntry(tip.height - 1, 0, ALT_SEED);
const wtx = await fakeWTX(wallet);
// TODO: Detect sync chain is correct.
// ignore low blocks.
const added = await wdb.addBlock(entry, [wtx]);
assert.strictEqual(added, null);
assert.strictEqual(wdb.height, tip.height);
});
// HIGH BLOCKS
it('should rescan for missed blocks', async () => {
const tip = await wdb.getTip();
// next + 1
const entry = fakeEntry(tip.height + 2);
let rescan = false;
let rescanHash = null;
wdb.client.rescanInteractive = async (hash) => {
rescan = true;
rescanHash = hash;
};
const added = await wdb.addBlock(entry, []);
assert.strictEqual(added, null);
assert.strictEqual(rescan, true);
assert.bufferEqual(rescanHash, tip.hash);
});
});
});
/**
* @param {String} addr
* @returns {TX}
*/
function fakeTX(addr) {
const tx = new MTX();
tx.addInput(dummyInput());
tx.addOutput({
address: addr,
value: 5460
});
return tx.toTX();
}
/**
* @param {Wallet} wallet
* @returns {Promise<TX>}
*/
async function fakeWTX(wallet) {
const addr = await wallet.receiveAddress();
return fakeTX(addr);
}