docs: add JS reference source for conversion
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
5a83dd4478
commit
98ce5f2bc9
70 changed files with 152460 additions and 0 deletions
4178
docs/js-blockchain/chain.js
Normal file
4178
docs/js-blockchain/chain.js
Normal file
File diff suppressed because it is too large
Load diff
2542
docs/js-blockchain/chaindb.js
Normal file
2542
docs/js-blockchain/chaindb.js
Normal file
File diff suppressed because it is too large
Load diff
394
docs/js-blockchain/chainentry.js
Normal file
394
docs/js-blockchain/chainentry.js
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
/*!
|
||||
* chainentry.js - chainentry object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const BN = require('bcrypto/lib/bn.js');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const Headers = require('../primitives/headers');
|
||||
const InvItem = require('../primitives/invitem');
|
||||
const util = require('../utils/util');
|
||||
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('../protocol/network')} Network */
|
||||
/** @typedef {import('../primitives/block')} Block */
|
||||
/** @typedef {import('../primitives/merkleblock')} MerkleBlock */
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const ZERO = new BN(0);
|
||||
|
||||
/**
|
||||
* Chain Entry
|
||||
* Represents an entry in the chain.
|
||||
* @alias module:blockchain.ChainEntry
|
||||
* @property {Hash} hash
|
||||
* @property {Number} version
|
||||
* @property {Hash} prevBlock
|
||||
* @property {Hash} merkleRoot
|
||||
* @property {Hash} witnessRoot
|
||||
* @property {Hash} treeRoot
|
||||
* @property {Hash} reservedRoot
|
||||
* @property {Number} time
|
||||
* @property {Number} bits
|
||||
* @property {Buffer} nonce
|
||||
* @property {Number} height
|
||||
* @property {BN} chainwork
|
||||
*/
|
||||
|
||||
class ChainEntry extends bio.Struct {
|
||||
/**
|
||||
* Create a chain entry.
|
||||
* @constructor
|
||||
* @param {Object?} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.hash = consensus.ZERO_HASH;
|
||||
this.version = 0;
|
||||
this.prevBlock = consensus.ZERO_HASH;
|
||||
this.merkleRoot = consensus.ZERO_HASH;
|
||||
this.witnessRoot = consensus.ZERO_HASH;
|
||||
this.treeRoot = consensus.ZERO_HASH;
|
||||
this.reservedRoot = consensus.ZERO_HASH;
|
||||
this.time = 0;
|
||||
this.bits = 0;
|
||||
this.nonce = 0;
|
||||
this.extraNonce = consensus.ZERO_NONCE;
|
||||
this.mask = consensus.ZERO_HASH;
|
||||
this.height = 0;
|
||||
this.chainwork = ZERO;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options.
|
||||
* @param {Object} options
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
assert(options, 'Block data is required.');
|
||||
assert(Buffer.isBuffer(options.hash));
|
||||
assert((options.version >>> 0) === options.version);
|
||||
assert(Buffer.isBuffer(options.prevBlock));
|
||||
assert(Buffer.isBuffer(options.merkleRoot));
|
||||
assert(Buffer.isBuffer(options.witnessRoot));
|
||||
assert(Buffer.isBuffer(options.treeRoot));
|
||||
assert(Buffer.isBuffer(options.reservedRoot));
|
||||
assert(util.isU64(options.time));
|
||||
assert((options.bits >>> 0) === options.bits);
|
||||
assert((options.nonce >>> 0) === options.nonce);
|
||||
assert(Buffer.isBuffer(options.extraNonce));
|
||||
assert(Buffer.isBuffer(options.mask));
|
||||
assert((options.height >>> 0) === options.height);
|
||||
assert(!options.chainwork || BN.isBN(options.chainwork));
|
||||
|
||||
this.hash = options.hash;
|
||||
this.version = options.version;
|
||||
this.prevBlock = options.prevBlock;
|
||||
this.merkleRoot = options.merkleRoot;
|
||||
this.witnessRoot = options.witnessRoot;
|
||||
this.treeRoot = options.treeRoot;
|
||||
this.reservedRoot = options.reservedRoot;
|
||||
this.time = options.time;
|
||||
this.bits = options.bits;
|
||||
this.nonce = options.nonce;
|
||||
this.extraNonce = options.extraNonce;
|
||||
this.mask = options.mask;
|
||||
|
||||
this.height = options.height;
|
||||
this.chainwork = options.chainwork || ZERO;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the proof: (1 << 256) / (target + 1)
|
||||
* @returns {BN} proof
|
||||
*/
|
||||
|
||||
getProof() {
|
||||
const target = consensus.fromCompact(this.bits);
|
||||
|
||||
if (target.isNeg() || target.isZero())
|
||||
return new BN(0);
|
||||
|
||||
return ChainEntry.MAX_CHAINWORK.div(target.iaddn(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the chainwork by
|
||||
* adding proof to previous chainwork.
|
||||
* @param {ChainEntry?} [prev] - Previous entry.
|
||||
* @returns {BN} chainwork
|
||||
*/
|
||||
|
||||
getChainwork(prev) {
|
||||
const proof = this.getProof();
|
||||
|
||||
if (!prev)
|
||||
return proof;
|
||||
|
||||
return proof.iadd(prev.chainwork);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test against the genesis block.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isGenesis() {
|
||||
return this.height === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the entry contains an unknown version bit.
|
||||
* @param {Network} network
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
hasUnknown(network) {
|
||||
return (this.version & network.unknownBits) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the entry contains a version bit.
|
||||
* @param {Number} bit
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
hasBit(bit) {
|
||||
return consensus.hasBit(this.version, bit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from block.
|
||||
* @param {Block|MerkleBlock} block
|
||||
* @param {ChainEntry?} [prev] - Previous entry.
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromBlock(block, prev) {
|
||||
this.hash = block.hash();
|
||||
this.version = block.version;
|
||||
this.prevBlock = block.prevBlock;
|
||||
this.merkleRoot = block.merkleRoot;
|
||||
this.witnessRoot = block.witnessRoot;
|
||||
this.treeRoot = block.treeRoot;
|
||||
this.reservedRoot = block.reservedRoot;
|
||||
this.time = block.time;
|
||||
this.bits = block.bits;
|
||||
this.nonce = block.nonce;
|
||||
this.extraNonce = block.extraNonce;
|
||||
this.mask = block.mask;
|
||||
this.height = prev ? prev.height + 1 : 0;
|
||||
this.chainwork = this.getChainwork(prev);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get serialization size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 36 + consensus.HEADER_SIZE + 32;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the entry to internal database format.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeHash(this.hash);
|
||||
bw.writeU32(this.height);
|
||||
|
||||
// Preheader.
|
||||
bw.writeU32(this.nonce);
|
||||
bw.writeU64(this.time);
|
||||
bw.writeHash(this.prevBlock);
|
||||
bw.writeHash(this.treeRoot);
|
||||
|
||||
// Subheader.
|
||||
bw.writeBytes(this.extraNonce);
|
||||
bw.writeHash(this.reservedRoot);
|
||||
bw.writeHash(this.witnessRoot);
|
||||
bw.writeHash(this.merkleRoot);
|
||||
bw.writeU32(this.version);
|
||||
bw.writeU32(this.bits);
|
||||
|
||||
// Mask.
|
||||
bw.writeBytes(this.mask);
|
||||
|
||||
bw.writeBytes(this.chainwork.toArrayLike(Buffer, 'be', 32));
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized data.
|
||||
* @param {bio.BufferReader} br
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.hash = br.readHash();
|
||||
this.height = br.readU32();
|
||||
|
||||
// Preheader.
|
||||
this.nonce = br.readU32();
|
||||
this.time = br.readU64();
|
||||
this.prevBlock = br.readHash();
|
||||
this.treeRoot = br.readHash();
|
||||
|
||||
// Subheader.
|
||||
this.extraNonce = br.readBytes(consensus.NONCE_SIZE);
|
||||
this.reservedRoot = br.readHash();
|
||||
this.witnessRoot = br.readHash();
|
||||
this.merkleRoot = br.readHash();
|
||||
this.version = br.readU32();
|
||||
this.bits = br.readU32();
|
||||
|
||||
// Mask.
|
||||
this.mask = br.readBytes(32);
|
||||
|
||||
this.chainwork = new BN(br.readBytes(32), 'be');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the entry to an object more
|
||||
* suitable for JSON serialization.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
hash: this.hash.toString('hex'),
|
||||
height: this.height,
|
||||
version: this.version,
|
||||
prevBlock: this.prevBlock.toString('hex'),
|
||||
merkleRoot: this.merkleRoot.toString('hex'),
|
||||
witnessRoot: this.witnessRoot.toString('hex'),
|
||||
treeRoot: this.treeRoot.toString('hex'),
|
||||
reservedRoot: this.reservedRoot.toString('hex'),
|
||||
time: this.time,
|
||||
bits: this.bits,
|
||||
nonce: this.nonce,
|
||||
extraNonce: this.extraNonce.toString('hex'),
|
||||
mask: this.mask.toString('hex'),
|
||||
chainwork: this.chainwork.toString('hex', 64)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from json object.
|
||||
* @param {Object} json
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json, 'Block data is required.');
|
||||
assert((json.height >>> 0) === json.height);
|
||||
assert((json.version >>> 0) === json.version);
|
||||
assert(util.isU64(json.time));
|
||||
assert((json.bits >>> 0) === json.bits);
|
||||
assert((json.nonce >>> 0) === json.nonce);
|
||||
|
||||
const work = util.parseHex(json.chainwork, 32);
|
||||
|
||||
this.hash = json.hash;
|
||||
this.height = json.height;
|
||||
this.version = json.version;
|
||||
this.prevBlock = util.parseHex(json.prevBlock, 32);
|
||||
this.merkleRoot = util.parseHex(json.merkleRoot, 32);
|
||||
this.witnessRoot = util.parseHex(json.witnessRoot, 32);
|
||||
this.treeRoot = util.parseHex(json.treeRoot, 32);
|
||||
this.reservedRoot = util.parseHex(json.reservedRoot, 32);
|
||||
this.time = json.time;
|
||||
this.bits = json.bits;
|
||||
this.nonce = json.nonce;
|
||||
this.extraNonce = util.parseHex(json.extraNonce, consensus.NONCE_SIZE);
|
||||
this.mask = util.parseHex(json.mask, 32);
|
||||
this.chainwork = new BN(work, 'be');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the entry to a headers object.
|
||||
* @returns {Headers}
|
||||
*/
|
||||
|
||||
toHeaders() {
|
||||
return Headers.fromEntry(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the entry to an inv item.
|
||||
* @returns {InvItem}
|
||||
*/
|
||||
|
||||
toInv() {
|
||||
return new InvItem(InvItem.types.BLOCK, this.hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a more user-friendly object.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format() {
|
||||
const json = this.toJSON();
|
||||
json.version = json.version.toString(16);
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate chainentry from block.
|
||||
* @param {Block|MerkleBlock} block
|
||||
* @param {ChainEntry?} [prev] - Previous entry.
|
||||
* @returns {ChainEntry}
|
||||
*/
|
||||
|
||||
static fromBlock(block, prev) {
|
||||
return new this().fromBlock(block, prev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an object is a {@link ChainEntry}.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isChainEntry(obj) {
|
||||
return obj instanceof ChainEntry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The max chainwork (1 << 256).
|
||||
* @const {BN}
|
||||
*/
|
||||
|
||||
ChainEntry.MAX_CHAINWORK = new BN(1).ushln(256);
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = ChainEntry;
|
||||
125
docs/js-blockchain/common.js
Normal file
125
docs/js-blockchain/common.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/*!
|
||||
* common.js - chain constants for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/** @typedef {import('bfilter').BloomFilter} BloomFilter */
|
||||
/** @typedef {import('../types').LockFlags} LockFlags */
|
||||
|
||||
/**
|
||||
* @module blockchain/common
|
||||
*/
|
||||
|
||||
/**
|
||||
* Locktime flags.
|
||||
* @enum {Number}
|
||||
*/
|
||||
|
||||
exports.lockFlags = {};
|
||||
|
||||
/**
|
||||
* Consensus locktime flags (used for block validation).
|
||||
* @const {LockFlags}
|
||||
* @default
|
||||
*/
|
||||
|
||||
exports.MANDATORY_LOCKTIME_FLAGS = 0;
|
||||
|
||||
/**
|
||||
* Standard locktime flags (used for mempool validation).
|
||||
* @const {LockFlags}
|
||||
* @default
|
||||
*/
|
||||
|
||||
exports.STANDARD_LOCKTIME_FLAGS = 0
|
||||
| exports.MANDATORY_LOCKTIME_FLAGS;
|
||||
|
||||
/**
|
||||
* Threshold states for versionbits
|
||||
* @enum {Number}
|
||||
* @default
|
||||
*/
|
||||
|
||||
exports.thresholdStates = {
|
||||
DEFINED: 0,
|
||||
STARTED: 1,
|
||||
LOCKED_IN: 2,
|
||||
ACTIVE: 3,
|
||||
FAILED: 4
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify flags for blocks.
|
||||
* @enum {Number}
|
||||
* @default
|
||||
*/
|
||||
|
||||
exports.flags = {
|
||||
VERIFY_NONE: 0,
|
||||
VERIFY_POW: 1 << 0,
|
||||
VERIFY_BODY: 1 << 1
|
||||
};
|
||||
|
||||
/**
|
||||
* Default block verify flags.
|
||||
* @const {Number}
|
||||
* @default
|
||||
*/
|
||||
|
||||
exports.DEFAULT_FLAGS = 0
|
||||
| exports.flags.VERIFY_POW
|
||||
| exports.flags.VERIFY_BODY;
|
||||
|
||||
/**
|
||||
* Interactive scan actions.
|
||||
* @enum {Number}
|
||||
* @default
|
||||
*/
|
||||
|
||||
exports.scanActions = {
|
||||
NONE: 0,
|
||||
ABORT: 1,
|
||||
NEXT: 2,
|
||||
REPEAT_SET: 3,
|
||||
REPEAT_ADD: 4,
|
||||
REPEAT: 5
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionAbort
|
||||
* @property {exports.scanActions} type - ABORT
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionNext
|
||||
* @property {exports.scanActions} type - NEXT
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionRepeat
|
||||
* @property {exports.ScanAction} type - REPEAT
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionRepeatAdd
|
||||
* @property {exports.scanActions} type - REPEAT_ADD
|
||||
* @property {Buffer[]} chunks
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionRepeatSet
|
||||
* @property {exports.scanActions} type - REPEAT_SET
|
||||
* @property {BloomFilter} filter
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {ActionAbort
|
||||
* | ActionNext
|
||||
* | ActionRepeat
|
||||
* | ActionRepeatAdd
|
||||
* | ActionRepeatSet
|
||||
* } ScanAction
|
||||
*/
|
||||
17
docs/js-blockchain/index.js
Normal file
17
docs/js-blockchain/index.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/*!
|
||||
* blockchain/index.js - blockchain for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @module blockchain
|
||||
*/
|
||||
|
||||
exports.ChainDB = require('./chaindb');
|
||||
exports.ChainEntry = require('./chainentry');
|
||||
exports.Chain = require('./chain');
|
||||
exports.common = require('./common');
|
||||
exports.layout = require('./layout');
|
||||
62
docs/js-blockchain/layout.js
Normal file
62
docs/js-blockchain/layout.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/*!
|
||||
* layout.js - blockchain data layout for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const bdb = require('bdb');
|
||||
|
||||
/*
|
||||
* Database Layout:
|
||||
* V -> db version
|
||||
* O -> chain options
|
||||
* R -> chain state (contains tip)
|
||||
* D -> versionbits deployments
|
||||
* e[hash] -> entry
|
||||
* h[hash] -> height
|
||||
* H[height] -> hash
|
||||
* n[hash] -> next hash
|
||||
* p[hash] -> tip index
|
||||
* b[hash] -> block (deprecated)
|
||||
* t[hash] -> extended tx
|
||||
* c[hash] -> coins
|
||||
* u[hash] -> undo coins (deprecated)
|
||||
* v[bit][hash] -> versionbits state
|
||||
* T[addr-hash][hash] -> dummy (tx by address)
|
||||
* C[addr-hash][hash][index] -> dummy (coin by address)
|
||||
* w[height] -> name undo
|
||||
* s -> tree state
|
||||
* f -> bit field
|
||||
* M -> migration state
|
||||
*/
|
||||
|
||||
const layout = {
|
||||
V: bdb.key('V'),
|
||||
O: bdb.key('O'),
|
||||
R: bdb.key('R'),
|
||||
D: bdb.key('D'),
|
||||
e: bdb.key('e', ['hash256']),
|
||||
h: bdb.key('h', ['hash256']),
|
||||
H: bdb.key('H', ['uint32']),
|
||||
n: bdb.key('n', ['hash256']),
|
||||
p: bdb.key('p', ['hash256']),
|
||||
b: bdb.key('b', ['hash256']),
|
||||
t: bdb.key('t', ['hash256']),
|
||||
c: bdb.key('c', ['hash256', 'uint32']),
|
||||
u: bdb.key('u', ['hash256']),
|
||||
v: bdb.key('v', ['uint8', 'hash256']),
|
||||
T: bdb.key('T', ['hash', 'hash256']),
|
||||
C: bdb.key('C', ['hash', 'hash256', 'uint32']),
|
||||
w: bdb.key('w', ['uint32']),
|
||||
s: bdb.key('s'),
|
||||
f: bdb.key('f'),
|
||||
M: bdb.key('M')
|
||||
};
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = layout;
|
||||
724
docs/js-blockchain/migrations.js
Normal file
724
docs/js-blockchain/migrations.js
Normal file
|
|
@ -0,0 +1,724 @@
|
|||
/*!
|
||||
* blockchain/migrations.js - blockchain data migrations for hsd
|
||||
* Copyright (c) 2021, Nodari Chkuaselidze (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const Logger = require('blgr');
|
||||
const bio = require('bufio');
|
||||
const {encoding} = bio;
|
||||
const bdb = require('bdb');
|
||||
const Network = require('../protocol/network');
|
||||
const rules = require('../covenants/rules');
|
||||
const Block = require('../primitives/block');
|
||||
const CoinView = require('../coins/coinview');
|
||||
const UndoCoins = require('../coins/undocoins');
|
||||
const layout = require('./layout');
|
||||
const AbstractMigration = require('../migrations/migration');
|
||||
const migrator = require('../migrations/migrator');
|
||||
const {
|
||||
Migrator,
|
||||
oldLayout,
|
||||
types
|
||||
} = migrator;
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {ReturnType<bdb.DB['batch']>} Batch */
|
||||
/** @typedef {migrator.types} MigrationType */
|
||||
/** @typedef {migrator.MigrationContext} MigrationContext */
|
||||
|
||||
/**
|
||||
* Switch to new migrations layout.
|
||||
*/
|
||||
|
||||
class MigrateMigrations extends AbstractMigration {
|
||||
/**
|
||||
* Create migrations migration.
|
||||
* @param {ChainMigratorOptions} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.options = options;
|
||||
this.logger = options.logger.context('chain-migration-migrate');
|
||||
this.db = options.db;
|
||||
this.ldb = options.ldb;
|
||||
this.layout = MigrateMigrations.layout();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<MigrationType>}
|
||||
*/
|
||||
|
||||
async check() {
|
||||
return types.MIGRATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actual migration
|
||||
* @param {Batch} b
|
||||
* @param {MigrationContext} ctx
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async migrate(b, ctx) {
|
||||
this.logger.info('Migrating migrations..');
|
||||
|
||||
const oldLayout = this.layout.oldLayout;
|
||||
let nextMigration = 1;
|
||||
const skipped = [];
|
||||
|
||||
const oldMigrations = await this.ldb.keys({
|
||||
gte: oldLayout.M.min(),
|
||||
lte: oldLayout.M.max(),
|
||||
parse: key => oldLayout.M.decode(key)[0]
|
||||
});
|
||||
|
||||
for (const id of oldMigrations) {
|
||||
b.del(oldLayout.M.encode(id));
|
||||
|
||||
if (id === 1) {
|
||||
if (this.options.prune) {
|
||||
skipped.push(1);
|
||||
}
|
||||
|
||||
nextMigration = 2;
|
||||
}
|
||||
}
|
||||
|
||||
this.db.writeVersion(b, 2);
|
||||
|
||||
ctx.state.version = 0;
|
||||
ctx.state.skipped = skipped;
|
||||
ctx.state.nextMigration = nextMigration;
|
||||
}
|
||||
|
||||
static info() {
|
||||
return {
|
||||
name: 'Migrate ChainDB migrations',
|
||||
description: 'ChainDB migration layout has changed.'
|
||||
};
|
||||
}
|
||||
|
||||
static layout() {
|
||||
return {
|
||||
oldLayout: {
|
||||
M: bdb.key('M', ['uint32'])
|
||||
},
|
||||
newLayout: {
|
||||
M: bdb.key('M')
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate chain state and correct total supply.
|
||||
* Applies to ChainDB v1
|
||||
*/
|
||||
|
||||
class MigrateChainState extends AbstractMigration {
|
||||
/**
|
||||
* Create migration chain state
|
||||
* @constructor
|
||||
* @param {ChainMigratorOptions} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.options = options;
|
||||
this.logger = options.logger.context('chain-migration-chainstate');
|
||||
this.db = options.db;
|
||||
this.ldb = options.ldb;
|
||||
this.layout = MigrateChainState.layout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the migration applies to the database
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async check() {
|
||||
if (this.options.spv)
|
||||
return types.FAKE_MIGRATE;
|
||||
|
||||
if (this.options.prune)
|
||||
return types.SKIP;
|
||||
|
||||
return types.MIGRATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warnings when skipped.
|
||||
*/
|
||||
|
||||
warning() {
|
||||
if (!this.options.prune)
|
||||
throw new Error('No warnings to show!');
|
||||
|
||||
this.logger.warning('Pruned nodes cannot migrate the chain state.');
|
||||
this.logger.warning('Your total chain value may be inaccurate!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Actual migration
|
||||
* @param {Batch} b
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async migrate(b) {
|
||||
this.logger.info('Migrating chain state.');
|
||||
this.logger.info('This may take a few minutes...');
|
||||
|
||||
const rawState = await this.ldb.get(this.layout.R.encode());
|
||||
const tipHash = rawState.slice(0, 32);
|
||||
const rawTipHeight = await this.ldb.get(this.layout.h.encode(tipHash));
|
||||
const tipHeight = rawTipHeight.readUInt32LE(0);
|
||||
const pending = {
|
||||
coin: 0,
|
||||
value: 0,
|
||||
burned: 0
|
||||
};
|
||||
|
||||
for (let height = 0; height <= tipHeight; height++) {
|
||||
const hash = await this.ldb.get(this.layout.H.encode(height));
|
||||
const block = await this.getBlock(hash);
|
||||
assert(block);
|
||||
|
||||
const view = await this.getBlockView(block);
|
||||
|
||||
for (let i = 0; i < block.txs.length; i++) {
|
||||
const tx = block.txs[i];
|
||||
|
||||
if (i > 0) {
|
||||
for (const {prevout} of tx.inputs) {
|
||||
const output = view.getOutput(prevout);
|
||||
assert(output);
|
||||
|
||||
if (output.covenant.type >= rules.types.REGISTER
|
||||
&& output.covenant.type <= rules.types.REVOKE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pending.coin -= 1;
|
||||
pending.value -= output.value;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < tx.outputs.length; i++) {
|
||||
const output = tx.outputs[i];
|
||||
|
||||
if (output.isUnspendable())
|
||||
continue;
|
||||
|
||||
if (output.covenant.isRegister()) {
|
||||
pending.coin += 1;
|
||||
pending.burned += output.value;
|
||||
}
|
||||
|
||||
if (output.covenant.type >= rules.types.REGISTER
|
||||
&& output.covenant.type <= rules.types.REVOKE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (output.covenant.isClaim()) {
|
||||
if (output.covenant.getU32(5) !== 1)
|
||||
continue;
|
||||
}
|
||||
|
||||
pending.coin += 1;
|
||||
pending.value += output.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prefix hash + tx (8)
|
||||
// we write coin (8) + value (8) + burned (8)
|
||||
encoding.writeU64(rawState, pending.coin, 40);
|
||||
encoding.writeU64(rawState, pending.value, 40 + 8);
|
||||
encoding.writeU64(rawState, pending.burned, 40 + 16);
|
||||
b.put(this.layout.R.encode(), rawState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Block (old layout)
|
||||
* @param {Hash} hash
|
||||
* @returns {Promise<Block>}
|
||||
*/
|
||||
|
||||
async getBlock(hash) {
|
||||
assert(Buffer.isBuffer(hash));
|
||||
const raw = await this.ldb.get(this.layout.b.encode(hash));
|
||||
|
||||
if (!raw)
|
||||
return null;
|
||||
|
||||
return Block.decode(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block view (old layout)
|
||||
* @param {Block} block
|
||||
* @returns {Promise} - UndoCoins
|
||||
*/
|
||||
|
||||
async getBlockView(block) {
|
||||
const hash = block.hash();
|
||||
const view = new CoinView();
|
||||
const raw = await this.ldb.get(this.layout.u.encode(hash));
|
||||
|
||||
if (!raw)
|
||||
return view;
|
||||
|
||||
// getBlockView logic.
|
||||
const undo = UndoCoins.decode(raw);
|
||||
|
||||
if (undo.isEmpty())
|
||||
return view;
|
||||
|
||||
for (let i = block.txs.length - 1; i > 0; i--) {
|
||||
const tx = block.txs[i];
|
||||
|
||||
for (let j = tx.inputs.length - 1; j >= 0; j--) {
|
||||
const input = tx.inputs[j];
|
||||
undo.apply(view, input.prevout);
|
||||
}
|
||||
}
|
||||
|
||||
// Undo coins should be empty.
|
||||
assert(undo.isEmpty(), 'Undo coins data inconsistency.');
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
static info() {
|
||||
return {
|
||||
name: 'Chain State migration',
|
||||
description: 'Chain state is corrupted.'
|
||||
};
|
||||
}
|
||||
|
||||
static layout() {
|
||||
return {
|
||||
// R -> tip hash
|
||||
R: bdb.key('R'),
|
||||
// h[hash] -> height
|
||||
h: bdb.key('h', ['hash256']),
|
||||
// H[height] -> hash
|
||||
H: bdb.key('H', ['uint32']),
|
||||
// b[hash] -> block
|
||||
b: bdb.key('b', ['hash256']),
|
||||
// u[hash] -> undo coins
|
||||
u: bdb.key('u', ['hash256'])
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate block and undo data to BlockStore from chainDB.
|
||||
*/
|
||||
|
||||
class MigrateBlockStore extends AbstractMigration {
|
||||
/**
|
||||
* Create MigrateBlockStore object.
|
||||
* @param {ChainMigratorOptions} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.options = options;
|
||||
this.logger = options.logger.context('chain-migration-blockstore');
|
||||
this.db = options.db;
|
||||
this.ldb = options.ldb;
|
||||
this.blocks = options.db.blocks;
|
||||
this.layout = MigrateBlockStore.layout();
|
||||
|
||||
this.batchWriteSize = 10000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the ChainDB has the blocks.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async check() {
|
||||
if (this.options.spv)
|
||||
return types.FAKE_MIGRATE;
|
||||
|
||||
return types.MIGRATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate blocks and undo blocks
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async migrate() {
|
||||
assert(this.blocks, 'Could not find blockstore.');
|
||||
|
||||
this.logger.info('Migrating blocks and undo blocks.');
|
||||
this.logger.info('This may take a few minutes...');
|
||||
|
||||
await this.migrateBlocks();
|
||||
await this.migrateUndoBlocks();
|
||||
|
||||
this.logger.info('Compacting database...');
|
||||
this.logger.info('This may take a few minutes...');
|
||||
await this.ldb.compactRange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the block data.
|
||||
*/
|
||||
|
||||
async migrateBlocks() {
|
||||
this.logger.info('Migrating blocks...');
|
||||
let parent = this.ldb.batch();
|
||||
|
||||
const iter = this.ldb.iterator({
|
||||
gte: this.layout.b.min(),
|
||||
lte: this.layout.b.max(),
|
||||
keys: true,
|
||||
values: true
|
||||
});
|
||||
|
||||
let total = 0;
|
||||
|
||||
await iter.each(async (key, value) => {
|
||||
const hash = key.slice(1);
|
||||
await this.blocks.writeBlock(hash, value);
|
||||
parent.del(key);
|
||||
|
||||
if (++total % this.batchWriteSize === 0) {
|
||||
await parent.write();
|
||||
this.logger.debug('Migrated up %d blocks.', total);
|
||||
parent = this.ldb.batch();
|
||||
}
|
||||
});
|
||||
|
||||
await parent.write();
|
||||
this.logger.info('Migrated all %d blocks.', total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the undo data.
|
||||
*/
|
||||
|
||||
async migrateUndoBlocks() {
|
||||
this.logger.info('Migrating undo blocks...');
|
||||
|
||||
let parent = this.ldb.batch();
|
||||
|
||||
const iter = this.ldb.iterator({
|
||||
gte: this.layout.u.min(),
|
||||
lte: this.layout.u.max(),
|
||||
keys: true,
|
||||
values: true
|
||||
});
|
||||
|
||||
let total = 0;
|
||||
|
||||
await iter.each(async (key, value) => {
|
||||
const hash = key.slice(1);
|
||||
await this.blocks.writeUndo(hash, value);
|
||||
parent.del(key);
|
||||
|
||||
if (++total % this.batchWriteSize === 0) {
|
||||
await parent.write();
|
||||
this.logger.debug('Migrated up %d undo blocks.', total);
|
||||
parent = this.ldb.batch();
|
||||
}
|
||||
});
|
||||
|
||||
await parent.write();
|
||||
this.logger.info('Migrated all %d undo blocks.', total);
|
||||
}
|
||||
|
||||
static info() {
|
||||
return {
|
||||
name: 'BlockStore migration',
|
||||
description: 'Move block and undo data to the'
|
||||
+ ' blockstore from the chainDB.'
|
||||
};
|
||||
}
|
||||
|
||||
static layout() {
|
||||
return {
|
||||
// b[hash] -> block
|
||||
b: bdb.key('b', ['hash256']),
|
||||
// u[hash] -> undo coins
|
||||
u: bdb.key('u', ['hash256'])
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrate Tree State
|
||||
*/
|
||||
|
||||
class MigrateTreeState extends AbstractMigration {
|
||||
/**
|
||||
* Create tree state migrator
|
||||
* @constructor
|
||||
* @param {ChainMigratorOptions} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.options = options;
|
||||
this.logger = options.logger.context('chain-migration-tree-state');
|
||||
this.db = options.db;
|
||||
this.ldb = options.ldb;
|
||||
this.network = options.network;
|
||||
this.layout = MigrateTreeState.layout();
|
||||
}
|
||||
|
||||
async check() {
|
||||
return types.MIGRATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Batch} b
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async migrate(b) {
|
||||
if (this.options.spv) {
|
||||
this.db.writeVersion(b, 3);
|
||||
return;
|
||||
}
|
||||
|
||||
const {treeInterval} = this.network.names;
|
||||
const rawState = await this.ldb.get(this.layout.R.encode());
|
||||
const tipHash = rawState.slice(0, 32);
|
||||
const rawTipHeight = await this.ldb.get(this.layout.h.encode(tipHash));
|
||||
const tipHeight = rawTipHeight.readUInt32LE(0);
|
||||
const lastCommitHeight = tipHeight - (tipHeight % treeInterval);
|
||||
const hash = await this.ldb.get(this.layout.s.encode());
|
||||
assert(hash && hash.length === 32);
|
||||
|
||||
// new tree root
|
||||
// see chaindb.js TreeState
|
||||
const buff = Buffer.alloc(72);
|
||||
encoding.writeBytes(buff, hash, 0);
|
||||
encoding.writeU32(buff, lastCommitHeight, 32);
|
||||
|
||||
this.db.writeVersion(b, 3);
|
||||
b.put(this.layout.s.encode(), buff);
|
||||
}
|
||||
|
||||
static info() {
|
||||
return {
|
||||
name: 'Migrate Tree State',
|
||||
description: 'Add compaction information to the tree state.'
|
||||
};
|
||||
}
|
||||
|
||||
static layout() {
|
||||
return {
|
||||
// R -> tip hash
|
||||
R: bdb.key('R'),
|
||||
// h[hash] -> height
|
||||
h: bdb.key('h', ['hash256']),
|
||||
// s -> tree state
|
||||
s: bdb.key('s')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MigrateMigrationStateV1 extends AbstractMigration {
|
||||
/**
|
||||
* Create migration migration state
|
||||
* @constructor
|
||||
* @param {ChainMigratorOptions} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.options = options;
|
||||
this.logger = options.logger.context('chain-migration-migration-state-v1');
|
||||
this.db = options.db;
|
||||
this.ldb = options.ldb;
|
||||
this.network = options.network;
|
||||
}
|
||||
|
||||
async check() {
|
||||
return types.MIGRATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Batch} b
|
||||
* @param {MigrationContext} ctx
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async migrate(b, ctx) {
|
||||
ctx.state.version = 1;
|
||||
}
|
||||
|
||||
static info() {
|
||||
return {
|
||||
name: 'Migrate Migration State',
|
||||
description: 'Migrate migration state to v1'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chain Migrator
|
||||
* @alias module:blockchain.ChainMigrator
|
||||
*/
|
||||
class ChainMigrator extends Migrator {
|
||||
/**
|
||||
* Create ChainMigrator object.
|
||||
* @constructor
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super(new ChainMigratorOptions(options));
|
||||
|
||||
this.logger = this.options.logger.context('chain-migrations');
|
||||
this.flagError = 'Restart with `hsd --chain-migrate='
|
||||
+ this.lastMigration + '`';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check chaindb flags
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async verifyDB() {
|
||||
await this.db.verifyFlags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of migrations to run
|
||||
* @returns {Promise<Set>}
|
||||
*/
|
||||
|
||||
async getMigrationsToRun() {
|
||||
const state = await this.getState();
|
||||
const lastID = this.getLastMigrationID();
|
||||
|
||||
if (state.nextMigration > lastID)
|
||||
return new Set();
|
||||
|
||||
const ids = new Set();
|
||||
|
||||
for (let i = state.nextMigration; i <= lastID; i++)
|
||||
ids.add(i);
|
||||
|
||||
if (state.nextMigration === 0 && await this.ldb.get(oldLayout.M.encode(1)))
|
||||
ids.delete(1);
|
||||
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ChainMigratorOptions
|
||||
* @alias module:blockchain.ChainMigratorOptions
|
||||
*/
|
||||
|
||||
class ChainMigratorOptions {
|
||||
/**
|
||||
* Create Chain Migrator Options.
|
||||
* @constructor
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
this.network = Network.primary;
|
||||
this.logger = Logger.global;
|
||||
|
||||
this.migrations = ChainMigrator.migrations;
|
||||
this.migrateFlag = -1;
|
||||
|
||||
this.dbVersion = 0;
|
||||
this.db = null;
|
||||
this.ldb = null;
|
||||
this.layout = layout;
|
||||
|
||||
this.spv = false;
|
||||
this.prune = false;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from object.
|
||||
* @param {Object} options
|
||||
* @returns {ChainMigratorOptions}
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
if (options.network != null)
|
||||
this.network = Network.get(options.network);
|
||||
|
||||
if (options.logger != null) {
|
||||
assert(typeof options.logger === 'object');
|
||||
this.logger = options.logger;
|
||||
}
|
||||
|
||||
if (options.chainDB != null) {
|
||||
assert(typeof options.chainDB === 'object');
|
||||
this.db = options.chainDB;
|
||||
this.ldb = this.db.db;
|
||||
}
|
||||
|
||||
if (options.chainMigrate != null) {
|
||||
assert(typeof options.chainMigrate === 'number');
|
||||
this.migrateFlag = options.chainMigrate;
|
||||
}
|
||||
|
||||
if (options.dbVersion != null) {
|
||||
assert(typeof options.dbVersion === 'number');
|
||||
this.dbVersion = options.dbVersion;
|
||||
}
|
||||
|
||||
if (options.migrations != null) {
|
||||
assert(typeof options.migrations === 'object');
|
||||
this.migrations = options.migrations;
|
||||
}
|
||||
|
||||
if (options.spv != null) {
|
||||
assert(typeof options.spv === 'boolean');
|
||||
this.spv = options.spv;
|
||||
}
|
||||
|
||||
if (options.prune != null) {
|
||||
assert(typeof options.prune === 'boolean');
|
||||
this.prune = options.prune;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// List of the migrations with ids
|
||||
ChainMigrator.migrations = {
|
||||
0: MigrateMigrations,
|
||||
1: MigrateChainState,
|
||||
2: MigrateBlockStore,
|
||||
3: MigrateTreeState,
|
||||
4: MigrateMigrationStateV1
|
||||
};
|
||||
|
||||
// Expose migrations
|
||||
ChainMigrator.MigrateChainState = MigrateChainState;
|
||||
ChainMigrator.MigrateMigrations = MigrateMigrations;
|
||||
ChainMigrator.MigrateBlockStore = MigrateBlockStore;
|
||||
ChainMigrator.MigrateTreeState = MigrateTreeState;
|
||||
ChainMigrator.MigrateMigrationStateV1 = MigrateMigrationStateV1;
|
||||
|
||||
module.exports = ChainMigrator;
|
||||
369
docs/js-blockchain/records.js
Normal file
369
docs/js-blockchain/records.js
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
/*!
|
||||
* records.js - chaindb records
|
||||
* Copyright (c) 2024 The Handshake Developers (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const {BufferMap} = require('buffer-map');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const Network = require('../protocol/network');
|
||||
|
||||
/**
|
||||
* ChainFlags
|
||||
*/
|
||||
|
||||
class ChainFlags extends bio.Struct {
|
||||
/**
|
||||
* Create chain flags.
|
||||
* @alias module:blockchain.ChainFlags
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.network = Network.primary;
|
||||
this.spv = false;
|
||||
this.prune = false;
|
||||
this.indexTX = false;
|
||||
this.indexAddress = false;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
fromOptions(options) {
|
||||
this.network = Network.get(options.network);
|
||||
|
||||
if (options.spv != null) {
|
||||
assert(typeof options.spv === 'boolean');
|
||||
this.spv = options.spv;
|
||||
}
|
||||
|
||||
if (options.prune != null) {
|
||||
assert(typeof options.prune === 'boolean');
|
||||
this.prune = options.prune;
|
||||
}
|
||||
|
||||
if (options.indexTX != null) {
|
||||
assert(typeof options.indexTX === 'boolean');
|
||||
this.indexTX = options.indexTX;
|
||||
}
|
||||
|
||||
if (options.indexAddress != null) {
|
||||
assert(typeof options.indexAddress === 'boolean');
|
||||
this.indexAddress = options.indexAddress;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return 12;
|
||||
}
|
||||
|
||||
write(bw) {
|
||||
let flags = 0;
|
||||
|
||||
if (this.spv)
|
||||
flags |= 1 << 0;
|
||||
|
||||
if (this.prune)
|
||||
flags |= 1 << 1;
|
||||
|
||||
if (this.indexTX)
|
||||
flags |= 1 << 2;
|
||||
|
||||
if (this.indexAddress)
|
||||
flags |= 1 << 3;
|
||||
|
||||
bw.writeU32(this.network.magic);
|
||||
bw.writeU32(flags);
|
||||
bw.writeU32(0);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
this.network = Network.fromMagic(br.readU32());
|
||||
|
||||
const flags = br.readU32();
|
||||
|
||||
this.spv = (flags & 1) !== 0;
|
||||
this.prune = (flags & 2) !== 0;
|
||||
this.indexTX = (flags & 4) !== 0;
|
||||
this.indexAddress = (flags & 8) !== 0;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chain State
|
||||
*/
|
||||
|
||||
class ChainState extends bio.Struct {
|
||||
/**
|
||||
* Create chain state.
|
||||
* @alias module:blockchain.ChainState
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.tip = consensus.ZERO_HASH;
|
||||
this.tx = 0;
|
||||
this.coin = 0;
|
||||
this.value = 0;
|
||||
this.burned = 0;
|
||||
this.committed = false;
|
||||
}
|
||||
|
||||
inject(state) {
|
||||
this.tip = state.tip;
|
||||
this.tx = state.tx;
|
||||
this.coin = state.coin;
|
||||
this.value = state.value;
|
||||
this.burned = state.burned;
|
||||
return this;
|
||||
}
|
||||
|
||||
connect(block) {
|
||||
this.tx += block.txs.length;
|
||||
}
|
||||
|
||||
disconnect(block) {
|
||||
this.tx -= block.txs.length;
|
||||
}
|
||||
|
||||
add(coin) {
|
||||
this.coin += 1;
|
||||
this.value += coin.value;
|
||||
}
|
||||
|
||||
spend(coin) {
|
||||
this.coin -= 1;
|
||||
this.value -= coin.value;
|
||||
}
|
||||
|
||||
burn(coin) {
|
||||
this.coin += 1;
|
||||
this.burned += coin.value;
|
||||
}
|
||||
|
||||
unburn(coin) {
|
||||
this.coin -= 1;
|
||||
this.burned -= coin.value;
|
||||
}
|
||||
|
||||
commit(hash) {
|
||||
assert(Buffer.isBuffer(hash));
|
||||
this.tip = hash;
|
||||
this.committed = true;
|
||||
return this.encode();
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return 64;
|
||||
}
|
||||
|
||||
write(bw) {
|
||||
bw.writeHash(this.tip);
|
||||
bw.writeU64(this.tx);
|
||||
bw.writeU64(this.coin);
|
||||
bw.writeU64(this.value);
|
||||
bw.writeU64(this.burned);
|
||||
return bw;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
this.tip = br.readHash();
|
||||
this.tx = br.readU64();
|
||||
this.coin = br.readU64();
|
||||
this.value = br.readU64();
|
||||
this.burned = br.readU64();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State Cache
|
||||
*/
|
||||
|
||||
class StateCache {
|
||||
/**
|
||||
* Create state cache.
|
||||
* @alias module:blockchain.StateCache
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
constructor(network) {
|
||||
this.network = network;
|
||||
this.bits = [];
|
||||
this.updates = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
for (let i = 0; i < 32; i++)
|
||||
this.bits.push(null);
|
||||
|
||||
for (const {bit} of this.network.deploys) {
|
||||
assert(!this.bits[bit]);
|
||||
this.bits[bit] = new BufferMap();
|
||||
}
|
||||
}
|
||||
|
||||
set(bit, entry, state) {
|
||||
const cache = this.bits[bit];
|
||||
|
||||
assert(cache);
|
||||
|
||||
if (cache.get(entry.hash) !== state) {
|
||||
cache.set(entry.hash, state);
|
||||
this.updates.push(new CacheUpdate(bit, entry.hash, state));
|
||||
}
|
||||
}
|
||||
|
||||
get(bit, entry) {
|
||||
const cache = this.bits[bit];
|
||||
|
||||
assert(cache);
|
||||
|
||||
const state = cache.get(entry.hash);
|
||||
|
||||
if (state == null)
|
||||
return -1;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
commit() {
|
||||
this.updates.length = 0;
|
||||
}
|
||||
|
||||
drop() {
|
||||
for (const {bit, hash} of this.updates) {
|
||||
const cache = this.bits[bit];
|
||||
assert(cache);
|
||||
cache.delete(hash);
|
||||
}
|
||||
|
||||
this.updates.length = 0;
|
||||
}
|
||||
|
||||
insert(bit, hash, state) {
|
||||
const cache = this.bits[bit];
|
||||
assert(cache);
|
||||
cache.set(hash, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache Update
|
||||
*/
|
||||
|
||||
class CacheUpdate {
|
||||
/**
|
||||
* Create cache update.
|
||||
* @constructor
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
constructor(bit, hash, state) {
|
||||
this.bit = bit;
|
||||
this.hash = hash;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
encode() {
|
||||
const data = Buffer.allocUnsafe(1);
|
||||
data[0] = this.state;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tree related state.
|
||||
*/
|
||||
|
||||
class TreeState extends bio.Struct {
|
||||
/**
|
||||
* Create tree state.
|
||||
* @constructor
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.treeRoot = consensus.ZERO_HASH;
|
||||
this.commitHeight = 0;
|
||||
this.compactionRoot = consensus.ZERO_HASH;
|
||||
this.compactionHeight = 0;
|
||||
|
||||
this.committed = false;
|
||||
}
|
||||
|
||||
inject(state) {
|
||||
this.treeRoot = state.treeRoot;
|
||||
this.commitHeight = state.treeHeight;
|
||||
this.compactionHeight = state.compactionHeight;
|
||||
this.compactionRoot = state.compactionRoot;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
compact(hash, height) {
|
||||
assert(Buffer.isBuffer(hash));
|
||||
assert((height >>> 0) === height);
|
||||
|
||||
this.compactionRoot = hash;
|
||||
this.compactionHeight = height;
|
||||
};
|
||||
|
||||
commit(hash, height) {
|
||||
assert(Buffer.isBuffer(hash));
|
||||
assert((height >>> 0) === height);
|
||||
|
||||
this.treeRoot = hash;
|
||||
this.commitHeight = height;
|
||||
this.committed = true;
|
||||
return this.encode();
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return 72;
|
||||
}
|
||||
|
||||
write(bw) {
|
||||
bw.writeHash(this.treeRoot);
|
||||
bw.writeU32(this.commitHeight);
|
||||
bw.writeHash(this.compactionRoot);
|
||||
bw.writeU32(this.compactionHeight);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
this.treeRoot = br.readHash();
|
||||
this.commitHeight = br.readU32();
|
||||
this.compactionRoot = br.readHash();
|
||||
this.compactionHeight = br.readU32();
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
exports.ChainFlags = ChainFlags;
|
||||
exports.ChainState = ChainState;
|
||||
exports.StateCache = StateCache;
|
||||
exports.TreeState = TreeState;
|
||||
exports.CacheUpdate = CacheUpdate;
|
||||
168
docs/js-covenants/bitfield.js
Normal file
168
docs/js-covenants/bitfield.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const AirdropProof = require('../primitives/airdropproof');
|
||||
const {TREE_LEAVES} = AirdropProof;
|
||||
|
||||
/**
|
||||
* Field
|
||||
*/
|
||||
|
||||
class Field {
|
||||
constructor(size = 0) {
|
||||
assert((size >>> 0) === size);
|
||||
|
||||
this.size = size;
|
||||
this.field = Buffer.alloc((size + 7) >>> 3, 0x00);
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
set(i, val) {
|
||||
assert((i >>> 0) === i);
|
||||
assert(i < this.size);
|
||||
assert((val >>> 0) === val);
|
||||
assert(val === 0 || val === 1);
|
||||
|
||||
if (val)
|
||||
this.field[i >>> 3] |= 1 << (7 - (i & 7));
|
||||
else
|
||||
this.field[i >>> 3] &= ~(1 << (7 - (i & 7)));
|
||||
|
||||
this.dirty = true;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get(i) {
|
||||
assert((i >>> 0) === i);
|
||||
|
||||
if (i >= this.size)
|
||||
return 1;
|
||||
|
||||
return (this.field[i >>> 3] >> (7 - (i & 7))) & 1;
|
||||
}
|
||||
|
||||
isSpent(i) {
|
||||
return Boolean(this.get(i));
|
||||
}
|
||||
|
||||
spend(i) {
|
||||
return this.set(i, 1);
|
||||
}
|
||||
|
||||
unspend(i) {
|
||||
return this.set(i, 0);
|
||||
}
|
||||
|
||||
encode() {
|
||||
this.dirty = false;
|
||||
return this.field;
|
||||
}
|
||||
|
||||
decode(data) {
|
||||
assert(Buffer.isBuffer(data));
|
||||
this.field = data;
|
||||
this.dirty = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
static decode(size, data) {
|
||||
return new this(size).decode(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BitField
|
||||
*/
|
||||
|
||||
class BitField extends Field {
|
||||
constructor() {
|
||||
super(TREE_LEAVES);
|
||||
}
|
||||
|
||||
static decode(data) {
|
||||
return new this().decode(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BitView
|
||||
*/
|
||||
|
||||
class BitView {
|
||||
constructor() {
|
||||
this.bits = new Map();
|
||||
}
|
||||
|
||||
spend(field, tx) {
|
||||
assert(field instanceof Field);
|
||||
assert(tx && tx.isCoinbase());
|
||||
|
||||
for (let i = 1; i < tx.inputs.length; i++) {
|
||||
const input = tx.inputs[i];
|
||||
const output = tx.output(i);
|
||||
const {witness} = input;
|
||||
|
||||
assert(output && witness.items.length === 1);
|
||||
|
||||
const {covenant} = output;
|
||||
|
||||
if (!covenant.isNone())
|
||||
continue;
|
||||
|
||||
const proof = AirdropProof.decode(witness.items[0]);
|
||||
const index = proof.position();
|
||||
|
||||
if (!this.bits.has(index))
|
||||
this.bits.set(index, field.get(index));
|
||||
|
||||
if (this.bits.get(index) !== 0)
|
||||
return false;
|
||||
|
||||
this.bits.set(index, 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
undo(tx) {
|
||||
assert(tx && tx.isCoinbase());
|
||||
|
||||
for (let i = 1; i < tx.inputs.length; i++) {
|
||||
const input = tx.inputs[i];
|
||||
const output = tx.output(i);
|
||||
const {witness} = input;
|
||||
|
||||
assert(output && witness.items.length === 1);
|
||||
|
||||
const {covenant} = output;
|
||||
|
||||
if (!covenant.isNone())
|
||||
continue;
|
||||
|
||||
const proof = AirdropProof.decode(witness.items[0]);
|
||||
const index = proof.position();
|
||||
|
||||
this.bits.set(index, 0);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
commit(field) {
|
||||
assert(field instanceof Field);
|
||||
|
||||
for (const [bit, spent] of this.bits)
|
||||
field.set(bit, spent);
|
||||
|
||||
return field.dirty;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
exports.Field = Field;
|
||||
exports.BitField = BitField;
|
||||
exports.BitView = BitView;
|
||||
15
docs/js-covenants/index.js
Normal file
15
docs/js-covenants/index.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/*!
|
||||
* covenants/index.js - covenants for hsd
|
||||
* Copyright (c) 2019, handshake-org developers (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @module covenants
|
||||
*/
|
||||
|
||||
exports.Namestate = require('./namestate');
|
||||
exports.Ownership = require('./ownership');
|
||||
exports.Rules = require('./rules');
|
||||
116
docs/js-covenants/locked-browser.js
Normal file
116
docs/js-covenants/locked-browser.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const sha3 = require('bcrypto/lib/sha3');
|
||||
const data = require('./lockup.json');
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const ZERO_HASH = sha3.zero.toString('hex');
|
||||
|
||||
/**
|
||||
* Locked up
|
||||
*/
|
||||
|
||||
class LockedUp {
|
||||
constructor(data) {
|
||||
const meta = data[ZERO_HASH];
|
||||
|
||||
this.data = data;
|
||||
this.size = meta[0];
|
||||
}
|
||||
|
||||
has(hash) {
|
||||
assert(Buffer.isBuffer(hash) && hash.length === 32);
|
||||
|
||||
const hex = hash.toString('hex');
|
||||
const item = this.data[hex];
|
||||
|
||||
if (!item)
|
||||
return false;
|
||||
|
||||
return Array.isArray(item);
|
||||
}
|
||||
|
||||
get(hash) {
|
||||
assert(Buffer.isBuffer(hash) && hash.length === 32);
|
||||
|
||||
const hex = hash.toString('hex');
|
||||
const item = this.data[hex];
|
||||
|
||||
if (!item || !Array.isArray(item))
|
||||
return null;
|
||||
|
||||
const target = item[0];
|
||||
const flags = item[1];
|
||||
const index = target.indexOf('.');
|
||||
|
||||
assert(index !== -1);
|
||||
|
||||
const root = (flags & 1) !== 0;
|
||||
const custom = (flags & 2) !== 0;
|
||||
const name = target.substring(0, index);
|
||||
|
||||
return {
|
||||
name,
|
||||
hash,
|
||||
target,
|
||||
root,
|
||||
custom
|
||||
};
|
||||
}
|
||||
|
||||
hasByName(name) {
|
||||
assert(typeof name === 'string');
|
||||
|
||||
if (name.length === 0 || name.length > 63)
|
||||
return false;
|
||||
|
||||
return this.has(hashName(name));
|
||||
}
|
||||
|
||||
getByName(name) {
|
||||
assert(typeof name === 'string');
|
||||
|
||||
if (name.length === 0 || name.length > 63)
|
||||
return null;
|
||||
|
||||
return this.get(hashName(name));
|
||||
}
|
||||
|
||||
*entries() {
|
||||
const keys = Object.keys(this.data);
|
||||
|
||||
for (const key of keys) {
|
||||
const hash = Buffer.from(key, 'hex');
|
||||
|
||||
yield [hash, this.get(hash)];
|
||||
}
|
||||
}
|
||||
|
||||
*keys() {
|
||||
const keys = Object.keys(this.data);
|
||||
|
||||
for (const key of keys)
|
||||
yield Buffer.from(key, 'hex');
|
||||
}
|
||||
|
||||
*values() {
|
||||
for (const [, item] of this.entries())
|
||||
yield item;
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.entries();
|
||||
}
|
||||
}
|
||||
|
||||
function hashName(name) {
|
||||
const raw = Buffer.from(name.toLowerCase(), 'ascii');
|
||||
return sha3.digest(raw);
|
||||
}
|
||||
|
||||
exports.LockedUp = LockedUp;
|
||||
exports.locked = new LockedUp(data);
|
||||
173
docs/js-covenants/locked.js
Normal file
173
docs/js-covenants/locked.js
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const Path = require('path');
|
||||
const fs = require('bfile');
|
||||
const sha3 = require('bcrypto/lib/sha3');
|
||||
|
||||
const FILE = Path.resolve(__dirname, 'lockup.db');
|
||||
const DATA = fs.readFileSync(FILE);
|
||||
|
||||
/**
|
||||
* Locked up
|
||||
*/
|
||||
|
||||
class LockedUp {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
this.size = readU32(data, 0);
|
||||
}
|
||||
|
||||
get prefixSize() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
_compare(b, off) {
|
||||
const a = this.data;
|
||||
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const x = a[off + i];
|
||||
const y = b[i];
|
||||
|
||||
if (x < y)
|
||||
return -1;
|
||||
|
||||
if (x > y)
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
_find(key) {
|
||||
let start = 0;
|
||||
let end = this.size - 1;
|
||||
|
||||
while (start <= end) {
|
||||
const index = (start + end) >>> 1;
|
||||
const pos = this.prefixSize + index * 36;
|
||||
const cmp = this._compare(key, pos);
|
||||
|
||||
if (cmp === 0)
|
||||
return readU32(this.data, pos + 32);
|
||||
|
||||
if (cmp < 0)
|
||||
start = index + 1;
|
||||
else
|
||||
end = index - 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
_target(pos) {
|
||||
const len = this.data[pos];
|
||||
return this.data.toString('ascii', pos + 1, pos + 1 + len);
|
||||
}
|
||||
|
||||
_flags(pos) {
|
||||
const len = this.data[pos];
|
||||
return this.data[pos + 1 + len];
|
||||
}
|
||||
|
||||
_index(pos) {
|
||||
const len = this.data[pos];
|
||||
return this.data[pos + 1 + len + 1];
|
||||
}
|
||||
|
||||
_get(hash, pos) {
|
||||
const target = this._target(pos);
|
||||
const flags = this._flags(pos);
|
||||
const index = this._index(pos);
|
||||
const root = (flags & 1) !== 0;
|
||||
const custom = (flags & 2) !== 0;
|
||||
const name = target.substring(0, index);
|
||||
|
||||
return {
|
||||
name,
|
||||
hash,
|
||||
target,
|
||||
root,
|
||||
custom
|
||||
};
|
||||
}
|
||||
|
||||
has(hash) {
|
||||
assert(Buffer.isBuffer(hash) && hash.length === 32);
|
||||
|
||||
return this._find(hash) !== -1;
|
||||
}
|
||||
|
||||
get(hash) {
|
||||
assert(Buffer.isBuffer(hash) && hash.length === 32);
|
||||
|
||||
const pos = this._find(hash);
|
||||
|
||||
if (pos === -1)
|
||||
return null;
|
||||
|
||||
return this._get(hash, pos);
|
||||
}
|
||||
|
||||
hasByName(name) {
|
||||
assert(typeof name === 'string');
|
||||
|
||||
if (name.length === 0 || name.length > 63)
|
||||
return false;
|
||||
|
||||
return this.has(hashName(name));
|
||||
}
|
||||
|
||||
getByName(name) {
|
||||
assert(typeof name === 'string');
|
||||
|
||||
if (name.length === 0 || name.length > 63)
|
||||
return null;
|
||||
|
||||
return this.get(hashName(name));
|
||||
}
|
||||
|
||||
*entries() {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
const pos = this.prefixSize + i * 36;
|
||||
const hash = this.data.slice(pos, pos + 32);
|
||||
const ptr = readU32(this.data, pos + 32);
|
||||
const item = this._get(hash, ptr);
|
||||
|
||||
yield [hash, item];
|
||||
}
|
||||
}
|
||||
|
||||
*keys() {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
const pos = this.prefixSize + i * 36;
|
||||
|
||||
yield this.data.slice(pos, pos + 32);
|
||||
}
|
||||
}
|
||||
|
||||
*values() {
|
||||
for (const [, item] of this.entries())
|
||||
yield item;
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.entries();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
function readU32(data, off) {
|
||||
return data.readUInt32LE(off);
|
||||
}
|
||||
|
||||
function hashName(name) {
|
||||
const raw = Buffer.from(name.toLowerCase(), 'ascii');
|
||||
return sha3.digest(raw);
|
||||
}
|
||||
|
||||
exports.LockedUp = LockedUp;
|
||||
exports.locked = new LockedUp(DATA);
|
||||
BIN
docs/js-covenants/lockup.db
Normal file
BIN
docs/js-covenants/lockup.db
Normal file
Binary file not shown.
11557
docs/js-covenants/lockup.json
Normal file
11557
docs/js-covenants/lockup.json
Normal file
File diff suppressed because it is too large
Load diff
330
docs/js-covenants/namedelta.js
Normal file
330
docs/js-covenants/namedelta.js
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
/*!
|
||||
* namedelta.js - name deltas for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const bio = require('bufio');
|
||||
const Outpoint = require('../primitives/outpoint');
|
||||
const {encoding} = bio;
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const EMPTY = Buffer.alloc(0);
|
||||
|
||||
/**
|
||||
* NameDelta
|
||||
* @extends {bio.Struct}
|
||||
*/
|
||||
|
||||
class NameDelta extends bio.Struct {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.height = null;
|
||||
this.renewal = null;
|
||||
this.owner = null;
|
||||
this.value = null;
|
||||
this.highest = null;
|
||||
this.data = null;
|
||||
this.transfer = null;
|
||||
this.revoked = null;
|
||||
this.claimed = null;
|
||||
this.renewals = null;
|
||||
this.registered = null;
|
||||
this.expired = null;
|
||||
this.weak = null;
|
||||
}
|
||||
|
||||
isNull() {
|
||||
return this.height === null
|
||||
&& this.renewal === null
|
||||
&& this.owner === null
|
||||
&& this.value === null
|
||||
&& this.highest === null
|
||||
&& this.data === null
|
||||
&& this.transfer === null
|
||||
&& this.revoked === null
|
||||
&& this.claimed === null
|
||||
&& this.renewals === null
|
||||
&& this.registered === null
|
||||
&& this.expired === null
|
||||
&& this.weak === null;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
|
||||
size += 4;
|
||||
|
||||
if (this.height !== null)
|
||||
size += 4;
|
||||
|
||||
if (this.renewal !== null)
|
||||
size += 4;
|
||||
|
||||
if (this.owner !== null) {
|
||||
if (!this.owner.isNull())
|
||||
size += 32 + encoding.sizeVarint(this.owner.index);
|
||||
}
|
||||
|
||||
if (this.value !== null) {
|
||||
if (this.value !== 0)
|
||||
size += encoding.sizeVarint(this.value);
|
||||
}
|
||||
|
||||
if (this.highest !== null) {
|
||||
if (this.highest !== 0)
|
||||
size += encoding.sizeVarint(this.highest);
|
||||
}
|
||||
|
||||
if (this.data !== null) {
|
||||
if (this.data)
|
||||
size += encoding.sizeVarlen(this.data.length);
|
||||
}
|
||||
|
||||
if (this.transfer !== null) {
|
||||
if (this.transfer !== 0)
|
||||
size += 4;
|
||||
}
|
||||
|
||||
if (this.revoked !== null) {
|
||||
if (this.revoked !== 0)
|
||||
size += 4;
|
||||
}
|
||||
|
||||
if (this.claimed !== null) {
|
||||
if (this.claimed !== 0)
|
||||
size += 4;
|
||||
}
|
||||
|
||||
if (this.renewals !== null) {
|
||||
if (this.renewals !== 0)
|
||||
size += encoding.sizeVarint(this.renewals);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
getField() {
|
||||
let field = 0;
|
||||
|
||||
if (this.height !== null)
|
||||
field |= 1 << 0;
|
||||
|
||||
if (this.renewal !== null)
|
||||
field |= 1 << 1;
|
||||
|
||||
if (this.owner !== null) {
|
||||
field |= 1 << 2;
|
||||
if (!this.owner.isNull())
|
||||
field |= 1 << 3;
|
||||
}
|
||||
|
||||
if (this.value !== null) {
|
||||
field |= 1 << 4;
|
||||
if (this.value !== 0)
|
||||
field |= 1 << 5;
|
||||
}
|
||||
|
||||
if (this.highest !== null) {
|
||||
field |= 1 << 6;
|
||||
if (this.highest !== 0)
|
||||
field |= 1 << 7;
|
||||
}
|
||||
|
||||
if (this.data !== null) {
|
||||
field |= 1 << 8;
|
||||
if (this.data)
|
||||
field |= 1 << 9;
|
||||
}
|
||||
|
||||
if (this.transfer !== null) {
|
||||
field |= 1 << 10;
|
||||
if (this.transfer !== 0)
|
||||
field |= 1 << 11;
|
||||
}
|
||||
|
||||
if (this.revoked !== null) {
|
||||
field |= 1 << 12;
|
||||
if (this.revoked !== 0)
|
||||
field |= 1 << 13;
|
||||
}
|
||||
|
||||
if (this.claimed !== null) {
|
||||
field |= 1 << 14;
|
||||
if (this.claimed !== 0)
|
||||
field |= 1 << 15;
|
||||
}
|
||||
|
||||
if (this.renewals !== null) {
|
||||
field |= 1 << 16;
|
||||
if (this.renewals !== 0)
|
||||
field |= 1 << 17;
|
||||
}
|
||||
|
||||
if (this.registered !== null) {
|
||||
field |= 1 << 18;
|
||||
if (this.registered)
|
||||
field |= 1 << 19;
|
||||
}
|
||||
|
||||
if (this.expired !== null) {
|
||||
field |= 1 << 20;
|
||||
if (this.expired)
|
||||
field |= 1 << 21;
|
||||
}
|
||||
|
||||
if (this.weak !== null) {
|
||||
field |= 1 << 22;
|
||||
if (this.weak)
|
||||
field |= 1 << 23;
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
write(bw) {
|
||||
bw.writeU32(this.getField());
|
||||
|
||||
if (this.height !== null)
|
||||
bw.writeU32(this.height);
|
||||
|
||||
if (this.renewal !== null)
|
||||
bw.writeU32(this.renewal);
|
||||
|
||||
if (this.owner !== null) {
|
||||
if (!this.owner.isNull()) {
|
||||
bw.writeHash(this.owner.hash);
|
||||
bw.writeVarint(this.owner.index);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.value !== null) {
|
||||
if (this.value !== 0)
|
||||
bw.writeVarint(this.value);
|
||||
}
|
||||
|
||||
if (this.highest !== null) {
|
||||
if (this.highest !== 0)
|
||||
bw.writeVarint(this.highest);
|
||||
}
|
||||
|
||||
if (this.data !== null) {
|
||||
if (this.data)
|
||||
bw.writeVarBytes(this.data);
|
||||
}
|
||||
|
||||
if (this.transfer !== null) {
|
||||
if (this.transfer !== 0)
|
||||
bw.writeU32(this.transfer);
|
||||
}
|
||||
|
||||
if (this.revoked !== null) {
|
||||
if (this.revoked !== 0)
|
||||
bw.writeU32(this.revoked);
|
||||
}
|
||||
|
||||
if (this.claimed !== null) {
|
||||
if (this.claimed !== 0)
|
||||
bw.writeU32(this.claimed);
|
||||
}
|
||||
|
||||
if (this.renewals !== null) {
|
||||
if (this.renewals !== 0)
|
||||
bw.writeVarint(this.renewals);
|
||||
}
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
const field = br.readU32();
|
||||
|
||||
if (field & (1 << 0))
|
||||
this.height = br.readU32();
|
||||
|
||||
if (field & (1 << 1))
|
||||
this.renewal = br.readU32();
|
||||
|
||||
if (field & (1 << 2)) {
|
||||
this.owner = new Outpoint();
|
||||
if (field & (1 << 3)) {
|
||||
this.owner.hash = br.readHash();
|
||||
this.owner.index = br.readVarint();
|
||||
}
|
||||
}
|
||||
|
||||
if (field & (1 << 4)) {
|
||||
this.value = 0;
|
||||
if (field & (1 << 5))
|
||||
this.value = br.readVarint();
|
||||
}
|
||||
|
||||
if (field & (1 << 6)) {
|
||||
this.highest = 0;
|
||||
if (field & (1 << 7))
|
||||
this.highest = br.readVarint();
|
||||
}
|
||||
|
||||
if (field & (1 << 8)) {
|
||||
this.data = EMPTY;
|
||||
if (field & (1 << 9))
|
||||
this.data = br.readVarBytes();
|
||||
}
|
||||
|
||||
if (field & (1 << 10)) {
|
||||
this.transfer = 0;
|
||||
if (field & (1 << 11))
|
||||
this.transfer = br.readU32();
|
||||
}
|
||||
|
||||
if (field & (1 << 12)) {
|
||||
this.revoked = 0;
|
||||
if (field & (1 << 13))
|
||||
this.revoked = br.readU32();
|
||||
}
|
||||
|
||||
if (field & (1 << 14)) {
|
||||
this.claimed = 0;
|
||||
if (field & (1 << 15))
|
||||
this.claimed = br.readU32();
|
||||
}
|
||||
|
||||
if (field & (1 << 16)) {
|
||||
this.renewals = 0;
|
||||
if (field & (1 << 17))
|
||||
this.renewals = br.readVarint();
|
||||
}
|
||||
|
||||
if (field & (1 << 18)) {
|
||||
this.registered = false;
|
||||
if (field & (1 << 19))
|
||||
this.registered = true;
|
||||
}
|
||||
|
||||
if (field & (1 << 20)) {
|
||||
this.expired = false;
|
||||
if (field & (1 << 21))
|
||||
this.expired = true;
|
||||
}
|
||||
|
||||
if (field & (1 << 22)) {
|
||||
this.weak = false;
|
||||
if (field & (1 << 23))
|
||||
this.weak = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = NameDelta;
|
||||
BIN
docs/js-covenants/names.db
Normal file
BIN
docs/js-covenants/names.db
Normal file
Binary file not shown.
90046
docs/js-covenants/names.json
Normal file
90046
docs/js-covenants/names.json
Normal file
File diff suppressed because it is too large
Load diff
899
docs/js-covenants/namestate.js
Normal file
899
docs/js-covenants/namestate.js
Normal file
|
|
@ -0,0 +1,899 @@
|
|||
/*!
|
||||
* namestate.js - name states for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const Network = require('../protocol/network');
|
||||
const Outpoint = require('../primitives/outpoint');
|
||||
const {ZERO_HASH} = require('../protocol/consensus');
|
||||
const NameDelta = require('./namedelta');
|
||||
const {encoding} = bio;
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const states = {
|
||||
OPENING: 0,
|
||||
LOCKED: 1,
|
||||
BIDDING: 2,
|
||||
REVEAL: 3,
|
||||
CLOSED: 4,
|
||||
REVOKED: 5
|
||||
};
|
||||
|
||||
const statesByVal = {
|
||||
[states.OPENING]: 'OPENING',
|
||||
[states.LOCKED]: 'LOCKED',
|
||||
[states.BIDDING]: 'BIDDING',
|
||||
[states.REVEAL]: 'REVEAL',
|
||||
[states.CLOSED]: 'CLOSED',
|
||||
[states.REVOKED]: 'REVOKED'
|
||||
};
|
||||
|
||||
const EMPTY = Buffer.alloc(0);
|
||||
|
||||
/**
|
||||
* NameState
|
||||
* @extends {bio.Struct}
|
||||
*/
|
||||
|
||||
class NameState extends bio.Struct {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = EMPTY;
|
||||
this.nameHash = ZERO_HASH;
|
||||
this.height = 0;
|
||||
this.renewal = 0;
|
||||
this.owner = new Outpoint();
|
||||
this.value = 0;
|
||||
this.highest = 0;
|
||||
this.data = EMPTY;
|
||||
this.transfer = 0;
|
||||
this.revoked = 0;
|
||||
this.claimed = 0;
|
||||
this.renewals = 0;
|
||||
this.registered = false;
|
||||
this.expired = false;
|
||||
this.weak = false;
|
||||
|
||||
// Not serialized.
|
||||
this._delta = null;
|
||||
}
|
||||
|
||||
get delta() {
|
||||
if (!this._delta)
|
||||
this._delta = new NameDelta();
|
||||
return this._delta;
|
||||
}
|
||||
|
||||
set delta(delta) {
|
||||
this._delta = delta;
|
||||
}
|
||||
|
||||
inject(ns) {
|
||||
assert(ns instanceof this.constructor);
|
||||
|
||||
this.name = ns.name;
|
||||
this.nameHash = ns.nameHash;
|
||||
this.height = ns.height;
|
||||
this.renewal = ns.renewal;
|
||||
this.owner = ns.owner;
|
||||
this.value = ns.value;
|
||||
this.highest = ns.highest;
|
||||
this.data = ns.data;
|
||||
this.transfer = ns.transfer;
|
||||
this.revoked = ns.revoked;
|
||||
this.claimed = ns.claimed;
|
||||
this.renewals = ns.renewals;
|
||||
this.registered = ns.registered;
|
||||
this.expired = ns.expired;
|
||||
this.weak = ns.weak;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._delta = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
isNull() {
|
||||
return this.height === 0
|
||||
&& this.renewal === 0
|
||||
&& this.owner.isNull()
|
||||
&& this.value === 0
|
||||
&& this.highest === 0
|
||||
&& this.data.length === 0
|
||||
&& this.transfer === 0
|
||||
&& this.revoked === 0
|
||||
&& this.claimed === 0
|
||||
&& this.renewals === 0
|
||||
&& this.registered === false
|
||||
&& this.expired === false
|
||||
&& this.weak === false;
|
||||
}
|
||||
|
||||
hasDelta() {
|
||||
return this._delta && !this._delta.isNull();
|
||||
}
|
||||
|
||||
state(height, network) {
|
||||
assert((height >>> 0) === height);
|
||||
assert(network && network.names);
|
||||
|
||||
const {
|
||||
treeInterval,
|
||||
lockupPeriod,
|
||||
biddingPeriod,
|
||||
revealPeriod
|
||||
} = network.names;
|
||||
|
||||
const openPeriod = treeInterval + 1;
|
||||
|
||||
if (this.revoked !== 0)
|
||||
return states.REVOKED;
|
||||
|
||||
if (this.claimed !== 0) {
|
||||
if (height < this.height + lockupPeriod)
|
||||
return states.LOCKED;
|
||||
return states.CLOSED;
|
||||
}
|
||||
|
||||
if (height < this.height + openPeriod)
|
||||
return states.OPENING;
|
||||
|
||||
if (height < this.height + openPeriod + biddingPeriod)
|
||||
return states.BIDDING;
|
||||
|
||||
if (height < this.height + openPeriod + biddingPeriod + revealPeriod)
|
||||
return states.REVEAL;
|
||||
|
||||
return states.CLOSED;
|
||||
}
|
||||
|
||||
isOpening(height, network) {
|
||||
return this.state(height, network) === states.OPENING;
|
||||
}
|
||||
|
||||
isLocked(height, network) {
|
||||
return this.state(height, network) === states.LOCKED;
|
||||
}
|
||||
|
||||
isBidding(height, network) {
|
||||
return this.state(height, network) === states.BIDDING;
|
||||
}
|
||||
|
||||
isReveal(height, network) {
|
||||
return this.state(height, network) === states.REVEAL;
|
||||
}
|
||||
|
||||
isClosed(height, network) {
|
||||
return this.state(height, network) === states.CLOSED;
|
||||
}
|
||||
|
||||
isRevoked(height, network) {
|
||||
return this.state(height, network) === states.REVOKED;
|
||||
}
|
||||
|
||||
isRedeemable(height, network) {
|
||||
return this.state(height, network) >= states.CLOSED;
|
||||
}
|
||||
|
||||
isClaimable(height, network) {
|
||||
assert((height >>> 0) === height);
|
||||
assert(network && network.names);
|
||||
|
||||
return this.claimed !== 0
|
||||
&& !network.names.noReserved
|
||||
&& height < network.names.claimPeriod;
|
||||
}
|
||||
|
||||
isExpired(height, network) {
|
||||
assert((height >>> 0) === height);
|
||||
assert(network && network.names);
|
||||
|
||||
if (this.revoked !== 0) {
|
||||
if (height < this.revoked + network.names.auctionMaturity)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Can only renew once we reach the closed state.
|
||||
if (!this.isClosed(height, network))
|
||||
return false;
|
||||
|
||||
// Claimed names can only expire once the claim period is over.
|
||||
if (this.isClaimable(height, network))
|
||||
return false;
|
||||
|
||||
// If we haven't been renewed in two years, start over.
|
||||
if (height >= this.renewal + network.names.renewalWindow)
|
||||
return true;
|
||||
|
||||
// If nobody revealed their bids, start over.
|
||||
if (this.owner.isNull())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
maybeExpire(height, network) {
|
||||
if (this.isExpired(height, network)) {
|
||||
const {data} = this;
|
||||
|
||||
// Note: we keep the name data even
|
||||
// after expiration (but not revocation).
|
||||
this.reset(height);
|
||||
this.setExpired(true);
|
||||
this.setData(data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
reset(height) {
|
||||
assert((height >>> 0) === height);
|
||||
|
||||
this.setHeight(height);
|
||||
this.setRenewal(height);
|
||||
this.setOwner(new Outpoint());
|
||||
this.setValue(0);
|
||||
this.setHighest(0);
|
||||
this.setData(null);
|
||||
this.setTransfer(0);
|
||||
this.setRevoked(0);
|
||||
this.setClaimed(0);
|
||||
this.setRenewals(0);
|
||||
this.setRegistered(false);
|
||||
this.setExpired(false);
|
||||
this.setWeak(false);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
set(name, height) {
|
||||
assert(Buffer.isBuffer(name));
|
||||
|
||||
this.name = name;
|
||||
this.reset(height);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setHeight(height) {
|
||||
assert((height >>> 0) === height);
|
||||
|
||||
if (height === this.height)
|
||||
return this;
|
||||
|
||||
if (this.delta.height === null)
|
||||
this.delta.height = this.height;
|
||||
|
||||
this.height = height;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setRenewal(renewal) {
|
||||
assert((renewal >>> 0) === renewal);
|
||||
|
||||
if (renewal === this.renewal)
|
||||
return this;
|
||||
|
||||
if (this.delta.renewal === null)
|
||||
this.delta.renewal = this.renewal;
|
||||
|
||||
this.renewal = renewal;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setOwner(owner) {
|
||||
assert(owner instanceof Outpoint);
|
||||
|
||||
if (owner.equals(this.owner))
|
||||
return this;
|
||||
|
||||
if (this.delta.owner === null)
|
||||
this.delta.owner = this.owner;
|
||||
|
||||
this.owner = owner;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
assert(Number.isSafeInteger(value) && value >= 0);
|
||||
|
||||
if (value === this.value)
|
||||
return this;
|
||||
|
||||
if (this.delta.value === null)
|
||||
this.delta.value = this.value;
|
||||
|
||||
this.value = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setHighest(highest) {
|
||||
assert(Number.isSafeInteger(highest) && highest >= 0);
|
||||
|
||||
if (highest === this.highest)
|
||||
return this;
|
||||
|
||||
if (this.delta.highest === null)
|
||||
this.delta.highest = this.highest;
|
||||
|
||||
this.highest = highest;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
if (data === null)
|
||||
data = EMPTY;
|
||||
|
||||
assert(Buffer.isBuffer(data));
|
||||
|
||||
if (this.data.equals(data))
|
||||
return this;
|
||||
|
||||
if (this.delta.data === null)
|
||||
this.delta.data = this.data;
|
||||
|
||||
this.data = data;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setTransfer(transfer) {
|
||||
assert((transfer >>> 0) === transfer);
|
||||
|
||||
if (transfer === this.transfer)
|
||||
return this;
|
||||
|
||||
if (this.delta.transfer === null)
|
||||
this.delta.transfer = this.transfer;
|
||||
|
||||
this.transfer = transfer;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setRevoked(revoked) {
|
||||
assert((revoked >>> 0) === revoked);
|
||||
|
||||
if (revoked === this.revoked)
|
||||
return this;
|
||||
|
||||
if (this.delta.revoked === null)
|
||||
this.delta.revoked = this.revoked;
|
||||
|
||||
this.revoked = revoked;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setClaimed(claimed) {
|
||||
assert((claimed >>> 0) === claimed);
|
||||
|
||||
if (claimed === this.claimed)
|
||||
return this;
|
||||
|
||||
if (this.delta.claimed === null)
|
||||
this.delta.claimed = this.claimed;
|
||||
|
||||
this.claimed = claimed;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setRenewals(renewals) {
|
||||
assert((renewals >>> 0) === renewals);
|
||||
|
||||
if (renewals === this.renewals)
|
||||
return this;
|
||||
|
||||
if (this.delta.renewals === null)
|
||||
this.delta.renewals = this.renewals;
|
||||
|
||||
this.renewals = renewals;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setRegistered(registered) {
|
||||
assert(typeof registered === 'boolean');
|
||||
|
||||
if (registered === this.registered)
|
||||
return this;
|
||||
|
||||
if (this.delta.registered === null)
|
||||
this.delta.registered = this.registered;
|
||||
|
||||
this.registered = registered;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setExpired(expired) {
|
||||
assert(typeof expired === 'boolean');
|
||||
|
||||
if (expired === this.expired)
|
||||
return this;
|
||||
|
||||
if (this.delta.expired === null)
|
||||
this.delta.expired = this.expired;
|
||||
|
||||
this.expired = expired;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setWeak(weak) {
|
||||
assert(typeof weak === 'boolean');
|
||||
|
||||
if (weak === this.weak)
|
||||
return this;
|
||||
|
||||
if (this.delta.weak === null)
|
||||
this.delta.weak = this.weak;
|
||||
|
||||
this.weak = weak;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
applyState(delta) {
|
||||
assert(delta instanceof NameDelta);
|
||||
|
||||
if (delta.height !== null)
|
||||
this.height = delta.height;
|
||||
|
||||
if (delta.renewal !== null)
|
||||
this.renewal = delta.renewal;
|
||||
|
||||
if (delta.owner !== null)
|
||||
this.owner = delta.owner;
|
||||
|
||||
if (delta.value !== null)
|
||||
this.value = delta.value;
|
||||
|
||||
if (delta.highest !== null)
|
||||
this.highest = delta.highest;
|
||||
|
||||
if (delta.data !== null)
|
||||
this.data = delta.data;
|
||||
|
||||
if (delta.transfer !== null)
|
||||
this.transfer = delta.transfer;
|
||||
|
||||
if (delta.revoked !== null)
|
||||
this.revoked = delta.revoked;
|
||||
|
||||
if (delta.claimed !== null)
|
||||
this.claimed = delta.claimed;
|
||||
|
||||
if (delta.renewals !== null)
|
||||
this.renewals = delta.renewals;
|
||||
|
||||
if (delta.registered !== null)
|
||||
this.registered = delta.registered;
|
||||
|
||||
if (delta.expired !== null)
|
||||
this.expired = delta.expired;
|
||||
|
||||
if (delta.weak !== null)
|
||||
this.weak = delta.weak;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
|
||||
size += 1;
|
||||
size += this.name.length;
|
||||
size += 2;
|
||||
size += this.data.length;
|
||||
|
||||
size += 4;
|
||||
size += 4;
|
||||
|
||||
size += 2;
|
||||
|
||||
if (!this.owner.isNull())
|
||||
size += 32 + encoding.sizeVarint(this.owner.index);
|
||||
|
||||
if (this.value !== 0)
|
||||
size += encoding.sizeVarint(this.value);
|
||||
|
||||
if (this.highest !== 0)
|
||||
size += encoding.sizeVarint(this.highest);
|
||||
|
||||
if (this.transfer !== 0)
|
||||
size += 4;
|
||||
|
||||
if (this.revoked !== 0)
|
||||
size += 4;
|
||||
|
||||
if (this.claimed !== 0)
|
||||
size += 4;
|
||||
|
||||
if (this.renewals !== 0)
|
||||
size += encoding.sizeVarint(this.renewals);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
getField() {
|
||||
let field = 0;
|
||||
|
||||
if (!this.owner.isNull())
|
||||
field |= 1 << 0;
|
||||
|
||||
if (this.value !== 0)
|
||||
field |= 1 << 1;
|
||||
|
||||
if (this.highest !== 0)
|
||||
field |= 1 << 2;
|
||||
|
||||
if (this.transfer !== 0)
|
||||
field |= 1 << 3;
|
||||
|
||||
if (this.revoked !== 0)
|
||||
field |= 1 << 4;
|
||||
|
||||
if (this.claimed !== 0)
|
||||
field |= 1 << 5;
|
||||
|
||||
if (this.renewals !== 0)
|
||||
field |= 1 << 6;
|
||||
|
||||
if (this.registered)
|
||||
field |= 1 << 7;
|
||||
|
||||
if (this.expired)
|
||||
field |= 1 << 8;
|
||||
|
||||
if (this.weak)
|
||||
field |= 1 << 9;
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
write(bw) {
|
||||
bw.writeU8(this.name.length);
|
||||
bw.writeBytes(this.name);
|
||||
bw.writeU16(this.data.length);
|
||||
bw.writeBytes(this.data);
|
||||
|
||||
bw.writeU32(this.height);
|
||||
bw.writeU32(this.renewal);
|
||||
|
||||
bw.writeU16(this.getField());
|
||||
|
||||
if (!this.owner.isNull()) {
|
||||
bw.writeHash(this.owner.hash);
|
||||
bw.writeVarint(this.owner.index);
|
||||
}
|
||||
|
||||
if (this.value !== 0)
|
||||
bw.writeVarint(this.value);
|
||||
|
||||
if (this.highest !== 0)
|
||||
bw.writeVarint(this.highest);
|
||||
|
||||
if (this.transfer !== 0)
|
||||
bw.writeU32(this.transfer);
|
||||
|
||||
if (this.revoked !== 0)
|
||||
bw.writeU32(this.revoked);
|
||||
|
||||
if (this.claimed !== 0)
|
||||
bw.writeU32(this.claimed);
|
||||
|
||||
if (this.renewals !== 0)
|
||||
bw.writeVarint(this.renewals);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
this.name = br.readBytes(br.readU8());
|
||||
this.data = br.readBytes(br.readU16());
|
||||
this.height = br.readU32();
|
||||
this.renewal = br.readU32();
|
||||
|
||||
const field = br.readU16();
|
||||
|
||||
if (field & (1 << 0)) {
|
||||
this.owner.hash = br.readHash();
|
||||
this.owner.index = br.readVarint();
|
||||
}
|
||||
|
||||
if (field & (1 << 1))
|
||||
this.value = br.readVarint();
|
||||
|
||||
if (field & (1 << 2))
|
||||
this.highest = br.readVarint();
|
||||
|
||||
if (field & (1 << 3))
|
||||
this.transfer = br.readU32();
|
||||
|
||||
if (field & (1 << 4))
|
||||
this.revoked = br.readU32();
|
||||
|
||||
if (field & (1 << 5))
|
||||
this.claimed = br.readU32();
|
||||
|
||||
if (field & (1 << 6))
|
||||
this.renewals = br.readVarint();
|
||||
|
||||
if (field & (1 << 7))
|
||||
this.registered = true;
|
||||
|
||||
if (field & (1 << 8))
|
||||
this.expired = true;
|
||||
|
||||
if (field & (1 << 9))
|
||||
this.weak = true;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getJSON(height, network) {
|
||||
let state = undefined;
|
||||
let stats = undefined;
|
||||
|
||||
if (height != null) {
|
||||
network = Network.get(network);
|
||||
state = this.state(height, network);
|
||||
state = statesByVal[state];
|
||||
stats = this.toStats(height, network);
|
||||
}
|
||||
|
||||
return {
|
||||
name: this.name.toString('binary'),
|
||||
nameHash: this.nameHash.toString('hex'),
|
||||
state: state,
|
||||
height: this.height,
|
||||
renewal: this.renewal,
|
||||
owner: this.owner.toJSON(),
|
||||
value: this.value,
|
||||
highest: this.highest,
|
||||
data: this.data.toString('hex'),
|
||||
transfer: this.transfer,
|
||||
revoked: this.revoked,
|
||||
claimed: this.claimed,
|
||||
renewals: this.renewals,
|
||||
registered: this.registered,
|
||||
expired: this.expired,
|
||||
weak: this.weak,
|
||||
stats: stats
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object');
|
||||
assert(typeof json.name === 'string');
|
||||
assert(json.name.length >= 0 && json.name.length <= 63);
|
||||
assert(typeof json.nameHash === 'string');
|
||||
assert(json.nameHash.length === 64);
|
||||
assert((json.height >>> 0) === json.height);
|
||||
assert((json.renewal >>> 0) === json.renewal);
|
||||
assert(json.owner && typeof json.owner === 'object');
|
||||
assert(Number.isSafeInteger(json.value) && json.value >= 0);
|
||||
assert(Number.isSafeInteger(json.highest) && json.highest >= 0);
|
||||
assert(typeof json.data === 'string');
|
||||
assert((json.data.length & 1) === 0);
|
||||
assert((json.transfer >>> 0) === json.transfer);
|
||||
assert((json.revoked >>> 0) === json.revoked);
|
||||
assert((json.claimed >>> 0) === json.claimed);
|
||||
assert((json.renewals >>> 0) === json.renewals);
|
||||
assert(typeof json.registered === 'boolean');
|
||||
assert(typeof json.expired === 'boolean');
|
||||
assert(typeof json.weak === 'boolean');
|
||||
|
||||
this.name = Buffer.from(json.name, 'binary');
|
||||
this.nameHash = Buffer.from(json.nameHash, 'hex');
|
||||
this.height = json.height;
|
||||
this.renewal = json.renewal;
|
||||
this.owner = Outpoint.fromJSON(json.owner);
|
||||
this.value = json.value;
|
||||
this.highest = json.highest;
|
||||
this.data = Buffer.from(json.data, 'hex');
|
||||
this.transfer = json.transfer;
|
||||
this.revoked = json.revoked;
|
||||
this.claimed = json.claimed;
|
||||
this.renewals = json.renewals;
|
||||
this.registered = json.registered;
|
||||
this.expired = json.expired;
|
||||
this.weak = json.weak;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toStats(height, network) {
|
||||
assert((height >>> 0) === height);
|
||||
assert(network && network.names);
|
||||
|
||||
const {blocksPerDay} = network.pow;
|
||||
const blocksPerHour = blocksPerDay / 24;
|
||||
|
||||
const {
|
||||
treeInterval,
|
||||
lockupPeriod,
|
||||
biddingPeriod,
|
||||
revealPeriod,
|
||||
renewalWindow,
|
||||
auctionMaturity,
|
||||
transferLockup,
|
||||
claimPeriod
|
||||
} = network.names;
|
||||
|
||||
const openPeriod = treeInterval + 1;
|
||||
|
||||
const stats = {};
|
||||
|
||||
let state = this.state(height, network);
|
||||
|
||||
// Special case for a state that is not a state:
|
||||
// EXPIRED but not revoked.
|
||||
const EXPIRED = -1;
|
||||
if (this.isExpired(height, network)) {
|
||||
if (this.owner.isNull())
|
||||
return null;
|
||||
|
||||
if (state !== states.REVOKED)
|
||||
state = EXPIRED;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case states.OPENING: {
|
||||
const start = this.height;
|
||||
const end = this.height + openPeriod;
|
||||
const blocks = end - height;
|
||||
const hours = blocks / blocksPerHour;
|
||||
|
||||
stats.openPeriodStart = start;
|
||||
stats.openPeriodEnd = end;
|
||||
|
||||
stats.blocksUntilBidding = blocks;
|
||||
stats.hoursUntilBidding = Number(hours.toFixed(2));
|
||||
break;
|
||||
}
|
||||
case states.LOCKED: {
|
||||
const start = this.height;
|
||||
const end = this.height + lockupPeriod;
|
||||
const blocks = end - height;
|
||||
const hours = blocks / blocksPerHour;
|
||||
|
||||
stats.lockupPeriodStart = start;
|
||||
stats.lockupPeriodEnd = end;
|
||||
|
||||
stats.blocksUntilClosed = blocks;
|
||||
stats.hoursUntilClosed = Number(hours.toFixed(2));
|
||||
break;
|
||||
}
|
||||
case states.BIDDING: {
|
||||
const start = this.height + openPeriod;
|
||||
const end = start + biddingPeriod;
|
||||
const blocks = end - height;
|
||||
const hours = blocks / blocksPerHour;
|
||||
|
||||
stats.bidPeriodStart = start;
|
||||
stats.bidPeriodEnd = end;
|
||||
|
||||
stats.blocksUntilReveal = blocks;
|
||||
stats.hoursUntilReveal = Number(hours.toFixed(2));
|
||||
break;
|
||||
}
|
||||
case states.REVEAL: {
|
||||
const start = this.height + openPeriod + biddingPeriod;
|
||||
const end = start + revealPeriod;
|
||||
const blocks = end - height;
|
||||
const hours = blocks / blocksPerHour;
|
||||
|
||||
stats.revealPeriodStart = start;
|
||||
stats.revealPeriodEnd = end;
|
||||
|
||||
stats.blocksUntilClose = blocks;
|
||||
stats.hoursUntilClose = Number(hours.toFixed(2));
|
||||
break;
|
||||
}
|
||||
case states.CLOSED: {
|
||||
const start = this.renewal;
|
||||
const normalEnd = start + renewalWindow;
|
||||
const end = this.claimed ? Math.max(claimPeriod, normalEnd) : normalEnd;
|
||||
const blocks = end - height;
|
||||
const days = blocks / blocksPerDay;
|
||||
|
||||
stats.renewalPeriodStart = start;
|
||||
stats.renewalPeriodEnd = end;
|
||||
|
||||
stats.blocksUntilExpire = blocks;
|
||||
assert(stats.blocksUntilExpire >= 0);
|
||||
stats.daysUntilExpire = Number(days.toFixed(2));
|
||||
|
||||
// Add these details if name is in mid-transfer
|
||||
if (this.transfer !== 0) {
|
||||
const start = this.transfer;
|
||||
const end = start + transferLockup;
|
||||
const blocks = end - height;
|
||||
const hours = blocks / blocksPerHour;
|
||||
|
||||
stats.transferLockupStart = start;
|
||||
stats.transferLockupEnd = end;
|
||||
|
||||
stats.blocksUntilValidFinalize = blocks;
|
||||
stats.hoursUntilValidFinalize = Number(hours.toFixed(2));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case states.REVOKED: {
|
||||
const start = this.revoked;
|
||||
const end = start + auctionMaturity;
|
||||
const blocks = end - height;
|
||||
const hours = blocks / blocksPerHour;
|
||||
|
||||
stats.revokePeriodStart = start;
|
||||
stats.revokePeriodEnd = end;
|
||||
|
||||
stats.blocksUntilReopen = blocks;
|
||||
stats.hoursUntilReopen = Number(hours.toFixed(2));
|
||||
break;
|
||||
}
|
||||
case EXPIRED: {
|
||||
const expired = this.renewal + renewalWindow;
|
||||
stats.blocksSinceExpired = height - expired;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
format(height, network) {
|
||||
return this.getJSON(height, network);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Static
|
||||
*/
|
||||
|
||||
NameState.states = states;
|
||||
NameState.statesByVal = statesByVal;
|
||||
|
||||
// Max size: 668
|
||||
NameState.MAX_SIZE = (0
|
||||
+ 1 + 63
|
||||
+ 2 + 512
|
||||
+ 4
|
||||
+ 4
|
||||
+ 2
|
||||
+ 32
|
||||
+ 9
|
||||
+ 9
|
||||
+ 9
|
||||
+ 4
|
||||
+ 4
|
||||
+ 4
|
||||
+ 9);
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = NameState;
|
||||
312
docs/js-covenants/ownership.js
Normal file
312
docs/js-covenants/ownership.js
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
/*!
|
||||
* ownership.js - DNSSEC ownership proofs for hsd
|
||||
* Copyright (c) 2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const base32 = require('bcrypto/lib/encoding/base32');
|
||||
const util = require('bns/lib/util');
|
||||
const blake2b = require('bcrypto/lib/blake2b');
|
||||
const StubResolver = require('bns/lib/resolver/stub');
|
||||
const BNSOwnership = require('bns/lib/ownership');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const Network = require('../protocol/network');
|
||||
const reserved = require('./reserved');
|
||||
const {Proof: BNSProof} = BNSOwnership;
|
||||
|
||||
/** @typedef {import('../types').NetworkType} NetworkType */
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const EMPTY = Buffer.alloc(0);
|
||||
|
||||
/** @type {Ownership} */
|
||||
let ownership = null;
|
||||
|
||||
/**
|
||||
* Proof
|
||||
*/
|
||||
|
||||
class Proof extends BNSProof {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} data
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
decode(data) {
|
||||
const br = bio.read(data);
|
||||
|
||||
if (data.length > 10000)
|
||||
throw new Error('Proof too large.');
|
||||
|
||||
this.read(br);
|
||||
|
||||
if (br.left() !== 0)
|
||||
throw new Error('Trailing data.');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {String[]}
|
||||
*/
|
||||
|
||||
getNames() {
|
||||
const target = this.getTarget();
|
||||
|
||||
if (target === '.')
|
||||
return ['', target];
|
||||
|
||||
return [util.label(target, 0), target];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
getName() {
|
||||
return this.getNames()[0];
|
||||
}
|
||||
|
||||
addData(items) {
|
||||
return ownership.addData(this, items);
|
||||
}
|
||||
|
||||
getData(network) {
|
||||
return ownership.getData(this, network);
|
||||
}
|
||||
|
||||
isWeak() {
|
||||
return ownership.isWeak(this);
|
||||
}
|
||||
|
||||
getWindow() {
|
||||
return ownership.getWindow(this);
|
||||
}
|
||||
|
||||
isSane() {
|
||||
return ownership.isSane(this);
|
||||
}
|
||||
|
||||
verifyTimes(time) {
|
||||
return ownership.verifyTimes(this, time);
|
||||
}
|
||||
|
||||
verifySignatures() {
|
||||
return ownership.verifySignatures(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ownership
|
||||
*/
|
||||
|
||||
class Ownership extends BNSOwnership {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.Resolver = StubResolver;
|
||||
this.secure = true;
|
||||
|
||||
this.Proof = Proof;
|
||||
this.OwnershipProof = Proof;
|
||||
}
|
||||
|
||||
hasPrefix(proof, target, [txt]) {
|
||||
// Used only in testing.
|
||||
return /^hns-[0-9a-z]+:/.test(txt);
|
||||
}
|
||||
|
||||
isData(proof, target, [txt], network) {
|
||||
assert(network && typeof network.claimPrefix === 'string');
|
||||
|
||||
const prefix = network.claimPrefix;
|
||||
|
||||
return util.startsWith(txt, prefix);
|
||||
}
|
||||
|
||||
decodeData(txt, network) {
|
||||
if (typeof network === 'string')
|
||||
network = Network.get(network);
|
||||
|
||||
assert(network && typeof network.claimPrefix === 'string');
|
||||
|
||||
const prefix = network.claimPrefix;
|
||||
const b32 = txt.substring(prefix.length);
|
||||
const raw = base32.decode(b32);
|
||||
|
||||
const br = bio.read(raw);
|
||||
const version = br.readU8();
|
||||
const size = br.readU8();
|
||||
const hash = br.readBytes(size);
|
||||
const fee = br.readVarint();
|
||||
const commitHash = br.readHash();
|
||||
const commitHeight = br.readU32();
|
||||
|
||||
return {
|
||||
address: {
|
||||
version,
|
||||
hash: hash.toString('hex')
|
||||
},
|
||||
fee,
|
||||
commitHash: commitHash.toString('hex'),
|
||||
commitHeight
|
||||
};
|
||||
}
|
||||
|
||||
parseData(proof, target, [txt], network) {
|
||||
assert(target !== '.');
|
||||
assert(network && typeof network.claimPrefix === 'string');
|
||||
|
||||
const prefix = network.claimPrefix;
|
||||
const b32 = txt.substring(prefix.length);
|
||||
const raw = base32.decode(b32);
|
||||
|
||||
const br = bio.read(raw);
|
||||
const version = br.readU8();
|
||||
|
||||
if (version > 31)
|
||||
return null;
|
||||
|
||||
const size = br.readU8();
|
||||
|
||||
if (size < 2 || size > 40)
|
||||
return null;
|
||||
|
||||
const hash = br.readBytes(size);
|
||||
const fee = br.readVarint();
|
||||
|
||||
if (fee > consensus.MAX_MONEY)
|
||||
return null;
|
||||
|
||||
const commitHash = br.readHash();
|
||||
const commitHeight = br.readU32();
|
||||
|
||||
br.verifyChecksum(blake2b.digest);
|
||||
|
||||
if (br.left() !== 0)
|
||||
return null;
|
||||
|
||||
const name = util.label(target, 0);
|
||||
const item = reserved.getByName(name);
|
||||
|
||||
if (!item)
|
||||
return null;
|
||||
|
||||
if (target !== item.target)
|
||||
return null;
|
||||
|
||||
const value = item.value;
|
||||
|
||||
if (fee > value)
|
||||
return null;
|
||||
|
||||
const [inception, expiration] = proof.getWindow();
|
||||
|
||||
if (inception === 0 && expiration === 0)
|
||||
return null;
|
||||
|
||||
const weak = proof.isWeak();
|
||||
const data = new ProofData();
|
||||
|
||||
data.name = name;
|
||||
data.target = target;
|
||||
data.weak = weak;
|
||||
data.commitHash = commitHash;
|
||||
data.commitHeight = commitHeight;
|
||||
data.inception = inception;
|
||||
data.expiration = expiration;
|
||||
data.fee = fee;
|
||||
data.value = value;
|
||||
data.version = version;
|
||||
data.hash = hash;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
createData(address, fee, commitHash, commitHeight, network) {
|
||||
assert(address && address.hash);
|
||||
assert(Number.isSafeInteger(fee) && fee >= 0);
|
||||
assert(Buffer.isBuffer(commitHash) && commitHash.length === 32);
|
||||
assert((commitHeight >>> 0) === commitHeight);
|
||||
assert(commitHeight !== 0);
|
||||
assert(network && network.claimPrefix);
|
||||
|
||||
const prefix = network.claimPrefix;
|
||||
const {version, hash} = address;
|
||||
|
||||
assert(typeof prefix === 'string');
|
||||
assert((version & 0xff) === version);
|
||||
assert(Buffer.isBuffer(hash));
|
||||
assert(version <= 31);
|
||||
assert(hash.length >= 2 && hash.length <= 40);
|
||||
|
||||
const size = 1
|
||||
+ 1 + hash.length
|
||||
+ bio.sizeVarint(fee)
|
||||
+ 32
|
||||
+ 4
|
||||
+ 4;
|
||||
|
||||
const bw = bio.write(size);
|
||||
|
||||
bw.writeU8(version);
|
||||
bw.writeU8(hash.length);
|
||||
bw.writeBytes(hash);
|
||||
bw.writeVarint(fee);
|
||||
bw.writeHash(commitHash);
|
||||
bw.writeU32(commitHeight);
|
||||
bw.writeChecksum(blake2b.digest);
|
||||
|
||||
const raw = bw.render();
|
||||
|
||||
return prefix + base32.encode(raw);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ProofData
|
||||
*/
|
||||
|
||||
class ProofData {
|
||||
constructor() {
|
||||
this.name = '';
|
||||
this.target = '.';
|
||||
this.weak = false;
|
||||
this.commitHash = blake2b.zero;
|
||||
this.commitHeight = 0;
|
||||
this.inception = 0;
|
||||
this.expiration = 0;
|
||||
this.fee = 0;
|
||||
this.value = 0;
|
||||
this.version = 0;
|
||||
this.hash = EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Ownership
|
||||
*/
|
||||
|
||||
ownership = new Ownership();
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
Ownership.Proof = Proof;
|
||||
Ownership.OwnershipProof = Proof;
|
||||
Ownership.ProofData = ProofData;
|
||||
Ownership.ownership = ownership;
|
||||
|
||||
module.exports = Ownership;
|
||||
145
docs/js-covenants/reserved-browser.js
Normal file
145
docs/js-covenants/reserved-browser.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const sha3 = require('bcrypto/lib/sha3');
|
||||
const data = require('./names.json');
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const ZERO_HASH = sha3.zero.toString('hex');
|
||||
|
||||
/**
|
||||
* Reserved
|
||||
*/
|
||||
|
||||
class Reserved {
|
||||
constructor(data) {
|
||||
const meta = data[ZERO_HASH];
|
||||
|
||||
this.data = data;
|
||||
this.size = meta[0];
|
||||
this.nameValue = meta[1];
|
||||
this.rootValue = meta[2];
|
||||
this.topValue = meta[3];
|
||||
}
|
||||
|
||||
has(hash) {
|
||||
assert(Buffer.isBuffer(hash) && hash.length === 32);
|
||||
|
||||
const hex = hash.toString('hex');
|
||||
const item = this.data[hex];
|
||||
|
||||
if (!item)
|
||||
return false;
|
||||
|
||||
return Array.isArray(item);
|
||||
}
|
||||
|
||||
get(hash) {
|
||||
assert(Buffer.isBuffer(hash) && hash.length === 32);
|
||||
|
||||
const hex = hash.toString('hex');
|
||||
const item = this.data[hex];
|
||||
|
||||
if (!item || !Array.isArray(item))
|
||||
return null;
|
||||
|
||||
const target = item[0];
|
||||
const flags = item[1];
|
||||
const index = target.indexOf('.');
|
||||
|
||||
assert(index !== -1);
|
||||
|
||||
const root = (flags & 1) !== 0;
|
||||
const top100 = (flags & 2) !== 0;
|
||||
const custom = (flags & 4) !== 0;
|
||||
const zero = (flags & 8) !== 0;
|
||||
const name = target.substring(0, index);
|
||||
|
||||
let value = this.nameValue;
|
||||
|
||||
if (root)
|
||||
value += this.rootValue;
|
||||
|
||||
if (top100)
|
||||
value += this.topValue;
|
||||
|
||||
if (custom)
|
||||
value += item[2];
|
||||
|
||||
if (zero)
|
||||
value = 0;
|
||||
|
||||
return {
|
||||
name,
|
||||
hash,
|
||||
target,
|
||||
value,
|
||||
root,
|
||||
top100,
|
||||
custom,
|
||||
zero
|
||||
};
|
||||
}
|
||||
|
||||
hasByName(name) {
|
||||
assert(typeof name === 'string');
|
||||
|
||||
if (name.length === 0 || name.length > 63)
|
||||
return false;
|
||||
|
||||
return this.has(hashName(name));
|
||||
}
|
||||
|
||||
getByName(name) {
|
||||
assert(typeof name === 'string');
|
||||
|
||||
if (name.length === 0 || name.length > 63)
|
||||
return null;
|
||||
|
||||
return this.get(hashName(name));
|
||||
}
|
||||
|
||||
*entries() {
|
||||
const keys = Object.keys(this.data);
|
||||
|
||||
for (const key of keys) {
|
||||
const hash = Buffer.from(key, 'hex');
|
||||
|
||||
yield [hash, this.get(hash)];
|
||||
}
|
||||
}
|
||||
|
||||
*keys() {
|
||||
const keys = Object.keys(this.data);
|
||||
|
||||
for (const key of keys)
|
||||
yield Buffer.from(key, 'hex');
|
||||
}
|
||||
|
||||
*values() {
|
||||
for (const [, item] of this.entries())
|
||||
yield item;
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.entries();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
function hashName(name) {
|
||||
const raw = Buffer.from(name.toLowerCase(), 'ascii');
|
||||
return sha3.digest(raw);
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = new Reserved(data);
|
||||
210
docs/js-covenants/reserved.js
Normal file
210
docs/js-covenants/reserved.js
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const Path = require('path');
|
||||
const fs = require('bfile');
|
||||
const sha3 = require('bcrypto/lib/sha3');
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const FILE = Path.resolve(__dirname, 'names.db');
|
||||
const DATA = fs.readFileSync(FILE);
|
||||
|
||||
/**
|
||||
* Reserved
|
||||
*/
|
||||
|
||||
class Reserved {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
this.size = readU32(data, 0);
|
||||
this.nameValue = readU64(data, 4);
|
||||
this.rootValue = readU64(data, 12);
|
||||
this.topValue = readU64(data, 20);
|
||||
}
|
||||
|
||||
_compare(b, off) {
|
||||
const a = this.data;
|
||||
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const x = a[off + i];
|
||||
const y = b[i];
|
||||
|
||||
if (x < y)
|
||||
return -1;
|
||||
|
||||
if (x > y)
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
_find(key) {
|
||||
let start = 0;
|
||||
let end = this.size - 1;
|
||||
|
||||
while (start <= end) {
|
||||
const index = (start + end) >>> 1;
|
||||
const pos = 28 + index * 36;
|
||||
const cmp = this._compare(key, pos);
|
||||
|
||||
if (cmp === 0)
|
||||
return readU32(this.data, pos + 32);
|
||||
|
||||
if (cmp < 0)
|
||||
start = index + 1;
|
||||
else
|
||||
end = index - 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
_target(pos) {
|
||||
const len = this.data[pos];
|
||||
return this.data.toString('ascii', pos + 1, pos + 1 + len);
|
||||
}
|
||||
|
||||
_flags(pos) {
|
||||
const len = this.data[pos];
|
||||
return this.data[pos + 1 + len];
|
||||
}
|
||||
|
||||
_index(pos) {
|
||||
const len = this.data[pos];
|
||||
return this.data[pos + 1 + len + 1];
|
||||
}
|
||||
|
||||
_value(pos) {
|
||||
const len = this.data[pos];
|
||||
const off = pos + 1 + len + 1 + 1;
|
||||
return readU64(this.data, off);
|
||||
}
|
||||
|
||||
_get(hash, pos) {
|
||||
const target = this._target(pos);
|
||||
const flags = this._flags(pos);
|
||||
const index = this._index(pos);
|
||||
const root = (flags & 1) !== 0;
|
||||
const top100 = (flags & 2) !== 0;
|
||||
const custom = (flags & 4) !== 0;
|
||||
const zero = (flags & 8) !== 0;
|
||||
const name = target.substring(0, index);
|
||||
|
||||
let value = this.nameValue;
|
||||
|
||||
if (root)
|
||||
value += this.rootValue;
|
||||
|
||||
if (top100)
|
||||
value += this.topValue;
|
||||
|
||||
if (custom)
|
||||
value += this._value(pos);
|
||||
|
||||
if (zero)
|
||||
value = 0;
|
||||
|
||||
return {
|
||||
name,
|
||||
hash,
|
||||
target,
|
||||
value,
|
||||
root,
|
||||
top100,
|
||||
custom,
|
||||
zero
|
||||
};
|
||||
}
|
||||
|
||||
has(hash) {
|
||||
assert(Buffer.isBuffer(hash) && hash.length === 32);
|
||||
|
||||
return this._find(hash) !== -1;
|
||||
}
|
||||
|
||||
get(hash) {
|
||||
assert(Buffer.isBuffer(hash) && hash.length === 32);
|
||||
|
||||
const pos = this._find(hash);
|
||||
|
||||
if (pos === -1)
|
||||
return null;
|
||||
|
||||
return this._get(hash, pos);
|
||||
}
|
||||
|
||||
hasByName(name) {
|
||||
assert(typeof name === 'string');
|
||||
|
||||
if (name.length === 0 || name.length > 63)
|
||||
return false;
|
||||
|
||||
return this.has(hashName(name));
|
||||
}
|
||||
|
||||
getByName(name) {
|
||||
assert(typeof name === 'string');
|
||||
|
||||
if (name.length === 0 || name.length > 63)
|
||||
return null;
|
||||
|
||||
return this.get(hashName(name));
|
||||
}
|
||||
|
||||
*entries() {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
const pos = 28 + i * 36;
|
||||
const hash = this.data.slice(pos, pos + 32);
|
||||
const ptr = readU32(this.data, pos + 32);
|
||||
const item = this._get(hash, ptr);
|
||||
|
||||
yield [hash, item];
|
||||
}
|
||||
}
|
||||
|
||||
*keys() {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
const pos = 28 + i * 36;
|
||||
|
||||
yield this.data.slice(pos, pos + 32);
|
||||
}
|
||||
}
|
||||
|
||||
*values() {
|
||||
for (const [, item] of this.entries())
|
||||
yield item;
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.entries();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
function readU32(data, off) {
|
||||
return data.readUInt32LE(off);
|
||||
}
|
||||
|
||||
function readU64(data, off) {
|
||||
const lo = data.readUInt32LE(off);
|
||||
const hi = data.readUInt32LE(off + 4);
|
||||
return hi * 0x100000000 + lo;
|
||||
}
|
||||
|
||||
function hashName(name) {
|
||||
const raw = Buffer.from(name.toLowerCase(), 'ascii');
|
||||
return sha3.digest(raw);
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = new Reserved(DATA);
|
||||
1496
docs/js-covenants/rules.js
Normal file
1496
docs/js-covenants/rules.js
Normal file
File diff suppressed because it is too large
Load diff
68
docs/js-covenants/undo.js
Normal file
68
docs/js-covenants/undo.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const NameDelta = require('./namedelta');
|
||||
|
||||
class NameUndo extends bio.Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.names = [];
|
||||
}
|
||||
|
||||
fromView(view) {
|
||||
assert(view && view.names);
|
||||
|
||||
for (const ns of view.names.values()) {
|
||||
if (!ns.hasDelta())
|
||||
continue;
|
||||
|
||||
this.names.push([ns.nameHash, ns.delta]);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
|
||||
size += 4;
|
||||
|
||||
for (const [, delta] of this.names) {
|
||||
size += 32;
|
||||
size += delta.getSize();
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
write(bw) {
|
||||
bw.writeU32(this.names.length);
|
||||
|
||||
for (const [nameHash, delta] of this.names) {
|
||||
bw.writeBytes(nameHash);
|
||||
delta.write(bw);
|
||||
}
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
const count = br.readU32();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const nameHash = br.readBytes(32);
|
||||
const delta = NameDelta.read(br);
|
||||
|
||||
this.names.push([nameHash, delta]);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
static fromView(view) {
|
||||
return new this().fromView(view);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NameUndo;
|
||||
81
docs/js-covenants/view.js
Normal file
81
docs/js-covenants/view.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const {BufferMap} = require('buffer-map');
|
||||
const NameState = require('./namestate');
|
||||
const NameUndo = require('./undo');
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
|
||||
class View {
|
||||
constructor() {
|
||||
/** @type {BufferMap<NameState>} */
|
||||
this.names = new BufferMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} db
|
||||
* @param {Hash} nameHash
|
||||
* @returns {NameState}
|
||||
*/
|
||||
|
||||
getNameStateSync(db, nameHash) {
|
||||
assert(db && typeof db.getNameState === 'function');
|
||||
assert(Buffer.isBuffer(nameHash));
|
||||
|
||||
const cache = this.names.get(nameHash);
|
||||
|
||||
if (cache)
|
||||
return cache;
|
||||
|
||||
/** @type {NameState?} */
|
||||
const ns = db.getNameState(nameHash);
|
||||
|
||||
if (!ns) {
|
||||
const ns = new NameState();
|
||||
ns.nameHash = nameHash;
|
||||
this.names.set(nameHash, ns);
|
||||
return ns;
|
||||
}
|
||||
|
||||
this.names.set(nameHash, ns);
|
||||
|
||||
return ns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} db
|
||||
* @param {Hash} nameHash
|
||||
* @returns {Promise<NameState>}
|
||||
*/
|
||||
|
||||
async getNameState(db, nameHash) {
|
||||
assert(db && typeof db.getNameState === 'function');
|
||||
assert(Buffer.isBuffer(nameHash));
|
||||
|
||||
const cache = this.names.get(nameHash);
|
||||
|
||||
if (cache)
|
||||
return cache;
|
||||
|
||||
/** @type {NameState?} */
|
||||
const ns = await db.getNameState(nameHash);
|
||||
|
||||
if (!ns) {
|
||||
const ns = new NameState();
|
||||
ns.nameHash = nameHash;
|
||||
this.names.set(nameHash, ns);
|
||||
return ns;
|
||||
}
|
||||
|
||||
this.names.set(nameHash, ns);
|
||||
|
||||
return ns;
|
||||
}
|
||||
|
||||
toNameUndo() {
|
||||
return NameUndo.fromView(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = View;
|
||||
56
docs/js-dns/common.js
Normal file
56
docs/js-dns/common.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/*!
|
||||
* common.js - dns constants for hsd
|
||||
* Copyright (c) 2021, The Handshake Developers (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @module dns/common
|
||||
*/
|
||||
|
||||
exports.DUMMY = Buffer.alloc(0);
|
||||
|
||||
// About one mainnet Urkel Tree interval.
|
||||
// (60 seconds * 10 minutes * 36)
|
||||
exports.DEFAULT_TTL = 21600;
|
||||
|
||||
// NS SOA RRSIG NSEC DNSKEY
|
||||
// Types available for the root "."
|
||||
exports.TYPE_MAP_ROOT = Buffer.from('000722000000000380', 'hex');
|
||||
|
||||
// RRSIG NSEC
|
||||
exports.TYPE_MAP_EMPTY = Buffer.from('0006000000000003', 'hex');
|
||||
|
||||
// NS RRSIG NSEC
|
||||
exports.TYPE_MAP_NS = Buffer.from('0006200000000003', 'hex');
|
||||
|
||||
// TXT RRSIG NSEC
|
||||
exports.TYPE_MAP_TXT = Buffer.from('0006000080000003', 'hex');
|
||||
|
||||
// A RRSIG NSEC
|
||||
exports.TYPE_MAP_A = Buffer.from('0006400000000003', 'hex');
|
||||
|
||||
// AAAA RRSIG NSEC
|
||||
exports.TYPE_MAP_AAAA = Buffer.from('0006000000080003', 'hex');
|
||||
|
||||
exports.hsTypes = {
|
||||
DS: 0,
|
||||
NS: 1,
|
||||
GLUE4: 2,
|
||||
GLUE6: 3,
|
||||
SYNTH4: 4,
|
||||
SYNTH6: 5,
|
||||
TXT: 6
|
||||
};
|
||||
|
||||
exports.hsTypesByVal = {
|
||||
[exports.hsTypes.DS]: 'DS',
|
||||
[exports.hsTypes.NS]: 'NS',
|
||||
[exports.hsTypes.GLUE4]: 'GLUE4',
|
||||
[exports.hsTypes.GLUE6]: 'GLUE6',
|
||||
[exports.hsTypes.SYNTH4]: 'SYNTH4',
|
||||
[exports.hsTypes.SYNTH6]: 'SYNTH6',
|
||||
[exports.hsTypes.TXT]: 'TXT'
|
||||
};
|
||||
62
docs/js-dns/key.js
Normal file
62
docs/js-dns/key.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/*!
|
||||
* key.js - dnssec key for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const {dnssec, wire} = require('bns');
|
||||
const {Record} = wire;
|
||||
|
||||
// pub: 034fd714449d8cfcccfdaba52c64d63e3aca72be3f94bfeb60aeb5a42ed3d0c205
|
||||
exports.kskPriv = Buffer.from(
|
||||
'1c74c825c5b0f08cf6be846bfc93c423f03e3e1f6202fb2d96474b1520bbafad',
|
||||
'hex');
|
||||
|
||||
// pub: 032399cfb3a72515ad609f09fd22954319d24b7c438dce00f535c7ee13010856e2
|
||||
exports.zskPriv = Buffer.from(
|
||||
'54276ff8604a3494c5c76d6651f14b289c7253ba636be4bfd7969308f48da47d',
|
||||
'hex');
|
||||
|
||||
exports.ksk = Record.fromJSON({
|
||||
name: '.',
|
||||
ttl: 10800,
|
||||
class: 'IN',
|
||||
type: 'DNSKEY',
|
||||
data: {
|
||||
flags: 257,
|
||||
protocol: 3,
|
||||
algorithm: 13,
|
||||
publicKey: ''
|
||||
+ 'T9cURJ2M/Mz9q6UsZNY+Ospyvj+Uv+tgrrWkLtPQwgU/Xu5Yk0l02Sn5ua2x'
|
||||
+ 'AQfEYIzRO6v5iA+BejMeEwNP4Q=='
|
||||
}
|
||||
});
|
||||
|
||||
exports.zsk = Record.fromJSON({
|
||||
name: '.',
|
||||
ttl: 10800,
|
||||
class: 'IN',
|
||||
type: 'DNSKEY',
|
||||
data: {
|
||||
flags: 256,
|
||||
protocol: 3,
|
||||
algorithm: 13,
|
||||
publicKey: ''
|
||||
+ 'I5nPs6clFa1gnwn9IpVDGdJLfEONzgD1NcfuEwEIVuIoHdZGgvVblsLNbRO+'
|
||||
+ 'spW3nQYHg92svhy1HOjTiFBIsQ=='
|
||||
}
|
||||
});
|
||||
|
||||
// . DS 35215 13 2
|
||||
// 7C50EA94A63AEECB65B510D1EAC1846C973A89D4AB292287D5A4D715136B57A3
|
||||
exports.ds = dnssec.createDS(exports.ksk, dnssec.hashes.SHA256);
|
||||
|
||||
exports.signKSK = function signKSK(section, type) {
|
||||
return dnssec.signType(section, type, exports.ksk, exports.kskPriv);
|
||||
};
|
||||
|
||||
exports.signZSK = function signZSK(section, type) {
|
||||
return dnssec.signType(section, type, exports.zsk, exports.zskPriv);
|
||||
};
|
||||
66
docs/js-dns/nsec.js
Normal file
66
docs/js-dns/nsec.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const {wire, util} = require('bns');
|
||||
const {Record, NSECRecord, types} = wire;
|
||||
const {DEFAULT_TTL} = require('./common');
|
||||
|
||||
function create(name, nextDomain, typeBitmap) {
|
||||
const rr = new Record();
|
||||
const rd = new NSECRecord();
|
||||
rr.name = util.fqdn(name);
|
||||
rr.type = types.NSEC;
|
||||
rr.ttl = DEFAULT_TTL;
|
||||
|
||||
rd.nextDomain = util.fqdn(nextDomain);
|
||||
rd.typeBitmap = typeBitmap;
|
||||
rr.data = rd;
|
||||
|
||||
return rr;
|
||||
}
|
||||
|
||||
// Find the successor of a top level name
|
||||
function nextName(tld) {
|
||||
tld = util.trimFQDN(tld.toLowerCase());
|
||||
|
||||
// If the label is already 63 octets
|
||||
// increment last character by one
|
||||
if (tld.length === 63) {
|
||||
// Assuming no escaped octets are present
|
||||
let last = tld.charCodeAt(62);
|
||||
last = String.fromCharCode(last + 1);
|
||||
return tld.slice(0, -1) + last + '.';
|
||||
}
|
||||
|
||||
return tld + '\\000.';
|
||||
}
|
||||
|
||||
// Find the predecessor of a top level name
|
||||
function prevName(tld) {
|
||||
tld = util.trimFQDN(tld.toLowerCase());
|
||||
assert(tld.length !== 0);
|
||||
|
||||
// Decrement the last character by 1
|
||||
// assuming no escaped octets are present
|
||||
let last = tld.charCodeAt(tld.length - 1);
|
||||
last = String.fromCharCode(last - 1);
|
||||
tld = tld.slice(0, -1) + last;
|
||||
|
||||
// See RFC4034 6.1 Canonical DNS Name Order
|
||||
// https://tools.ietf.org/html/rfc4034#section-6.1
|
||||
// Appending \255 prevents names that begin
|
||||
// with the decremented name from falling
|
||||
// in range i.e. if the name is `hello` a lexically
|
||||
// smaller name is `helln` append `\255`
|
||||
// to ensure that helln\255 > hellna
|
||||
// while keeping helln\255 < hello
|
||||
if (tld.length < 63) {
|
||||
tld += '\\255';
|
||||
}
|
||||
|
||||
return util.fqdn(tld);
|
||||
}
|
||||
|
||||
exports.create = create;
|
||||
exports.prevName = prevName;
|
||||
exports.nextName = nextName;
|
||||
948
docs/js-dns/resource.js
Normal file
948
docs/js-dns/resource.js
Normal file
|
|
@ -0,0 +1,948 @@
|
|||
/*!
|
||||
* resource.js - hns records for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const {encoding, wire, util} = require('bns');
|
||||
const base32 = require('bcrypto/lib/encoding/base32');
|
||||
const {IP} = require('binet');
|
||||
const bio = require('bufio');
|
||||
const key = require('./key');
|
||||
const nsec = require('./nsec');
|
||||
const {Struct} = bio;
|
||||
const {
|
||||
DUMMY,
|
||||
DEFAULT_TTL,
|
||||
TYPE_MAP_EMPTY,
|
||||
TYPE_MAP_NS,
|
||||
TYPE_MAP_TXT,
|
||||
hsTypes
|
||||
} = require('./common');
|
||||
|
||||
const {
|
||||
sizeName,
|
||||
writeNameBW,
|
||||
readNameBR,
|
||||
sizeString,
|
||||
writeStringBW,
|
||||
readStringBR,
|
||||
isName,
|
||||
readIP,
|
||||
writeIP
|
||||
} = encoding;
|
||||
|
||||
const {
|
||||
Message,
|
||||
Record,
|
||||
ARecord,
|
||||
AAAARecord,
|
||||
NSRecord,
|
||||
TXTRecord,
|
||||
DSRecord,
|
||||
types
|
||||
} = wire;
|
||||
|
||||
/**
|
||||
* Resource
|
||||
* @extends {Struct}
|
||||
*/
|
||||
|
||||
class Resource extends Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.ttl = DEFAULT_TTL;
|
||||
this.records = [];
|
||||
}
|
||||
|
||||
hasType(type) {
|
||||
assert((type & 0xff) === type);
|
||||
|
||||
for (const record of this.records) {
|
||||
if (record.type === type)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
hasNS() {
|
||||
for (const {type} of this.records) {
|
||||
if (type < hsTypes.NS || type > hsTypes.SYNTH6)
|
||||
continue;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
hasDS() {
|
||||
return this.hasType(hsTypes.DS);
|
||||
}
|
||||
|
||||
encode() {
|
||||
const bw = bio.write(512);
|
||||
this.write(bw, new Map());
|
||||
return bw.slice();
|
||||
}
|
||||
|
||||
getSize(map) {
|
||||
let size = 1;
|
||||
|
||||
for (const rr of this.records)
|
||||
size += 1 + rr.getSize(map);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
write(bw, map) {
|
||||
bw.writeU8(0);
|
||||
|
||||
for (const rr of this.records) {
|
||||
bw.writeU8(rr.type);
|
||||
rr.write(bw, map);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
const version = br.readU8();
|
||||
|
||||
if (version !== 0)
|
||||
throw new Error(`Unknown serialization version: ${version}.`);
|
||||
|
||||
while (br.left()) {
|
||||
const RD = typeToClass(br.readU8());
|
||||
|
||||
// Break at unknown records.
|
||||
if (!RD)
|
||||
break;
|
||||
|
||||
this.records.push(RD.read(br));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toNS(name) {
|
||||
const authority = [];
|
||||
const set = new Set();
|
||||
|
||||
for (const record of this.records) {
|
||||
switch (record.type) {
|
||||
case hsTypes.NS:
|
||||
case hsTypes.GLUE4:
|
||||
case hsTypes.GLUE6:
|
||||
case hsTypes.SYNTH4:
|
||||
case hsTypes.SYNTH6:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
const rr = record.toDNS(name, this.ttl);
|
||||
|
||||
if (set.has(rr.data.ns))
|
||||
continue;
|
||||
|
||||
set.add(rr.data.ns);
|
||||
authority.push(rr);
|
||||
}
|
||||
|
||||
return authority;
|
||||
}
|
||||
|
||||
toGlue(name) {
|
||||
const additional = [];
|
||||
|
||||
for (const record of this.records) {
|
||||
switch (record.type) {
|
||||
case hsTypes.GLUE4:
|
||||
case hsTypes.GLUE6:
|
||||
if (!util.isSubdomain(name, record.ns))
|
||||
continue;
|
||||
break;
|
||||
case hsTypes.SYNTH4:
|
||||
case hsTypes.SYNTH6:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
additional.push(record.toGlue(record.ns, this.ttl));
|
||||
}
|
||||
|
||||
return additional;
|
||||
}
|
||||
|
||||
toDS(name) {
|
||||
const answer = [];
|
||||
|
||||
for (const record of this.records) {
|
||||
if (record.type !== hsTypes.DS)
|
||||
continue;
|
||||
|
||||
answer.push(record.toDNS(name, this.ttl));
|
||||
}
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
toTXT(name) {
|
||||
const answer = [];
|
||||
|
||||
for (const record of this.records) {
|
||||
if (record.type !== hsTypes.TXT)
|
||||
continue;
|
||||
|
||||
answer.push(record.toDNS(name, this.ttl));
|
||||
}
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
toZone(name, sign = false) {
|
||||
const zone = [];
|
||||
const set = new Set();
|
||||
|
||||
for (const record of this.records) {
|
||||
const rr = record.toDNS(name, this.ttl);
|
||||
|
||||
if (rr.type === types.NS) {
|
||||
if (set.has(rr.data.ns))
|
||||
continue;
|
||||
|
||||
set.add(rr.data.ns);
|
||||
}
|
||||
|
||||
zone.push(rr);
|
||||
}
|
||||
|
||||
if (sign) {
|
||||
const set = new Set();
|
||||
|
||||
for (const rr of zone)
|
||||
set.add(rr.type);
|
||||
|
||||
const types = [...set].sort();
|
||||
|
||||
for (const type of types)
|
||||
key.signZSK(zone, type);
|
||||
}
|
||||
|
||||
// Add the glue last.
|
||||
for (const record of this.records) {
|
||||
switch (record.type) {
|
||||
case hsTypes.GLUE4:
|
||||
case hsTypes.GLUE6:
|
||||
case hsTypes.SYNTH4:
|
||||
case hsTypes.SYNTH6: {
|
||||
if (!util.isSubdomain(name, record.ns))
|
||||
continue;
|
||||
|
||||
zone.push(record.toGlue(record.ns, this.ttl));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return zone;
|
||||
}
|
||||
|
||||
toReferral(name, type, isTLD) {
|
||||
const res = new Message();
|
||||
|
||||
// no DS referrals for TLDs
|
||||
const badReferral = isTLD && type === types.DS;
|
||||
|
||||
if (this.hasNS() && !badReferral) {
|
||||
res.authority = this.toNS(name).concat(this.toDS(name));
|
||||
|
||||
res.additional = this.toGlue(name);
|
||||
|
||||
if (this.hasDS()) {
|
||||
key.signZSK(res.authority, types.DS);
|
||||
} else {
|
||||
// unsigned zone proof
|
||||
res.authority.push(this.toNSEC(name));
|
||||
key.signZSK(res.authority, types.NSEC);
|
||||
}
|
||||
} else {
|
||||
// Needs SOA.
|
||||
res.aa = true;
|
||||
// negative answer proof
|
||||
res.authority.push(this.toNSEC(name));
|
||||
key.signZSK(res.authority, types.NSEC);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
toNSEC(name) {
|
||||
let typeMap = TYPE_MAP_EMPTY;
|
||||
|
||||
if (this.hasNS())
|
||||
typeMap = TYPE_MAP_NS;
|
||||
else if (this.hasType(hsTypes.TXT))
|
||||
typeMap = TYPE_MAP_TXT;
|
||||
|
||||
return nsec.create(name, nsec.nextName(name), typeMap);
|
||||
}
|
||||
|
||||
toDNS(name, type) {
|
||||
assert(util.isFQDN(name));
|
||||
assert((type >>> 0) === type);
|
||||
|
||||
const labels = util.split(name);
|
||||
|
||||
// Referral.
|
||||
if (labels.length > 1) {
|
||||
const tld = util.from(name, labels, -1);
|
||||
return this.toReferral(tld, type, false);
|
||||
}
|
||||
|
||||
// Potentially an answer.
|
||||
const res = new Message();
|
||||
|
||||
// TLDs are authoritative over their own NS & TXT records.
|
||||
// The NS records in the root zone are just "hints"
|
||||
// and therefore are not signed by the root ZSK.
|
||||
// The only records root is authoritative over is DS.
|
||||
switch (type) {
|
||||
case types.TXT:
|
||||
if (!this.hasNS()) {
|
||||
res.aa = true;
|
||||
res.answer = this.toTXT(name);
|
||||
key.signZSK(res.answer, types.TXT);
|
||||
}
|
||||
break;
|
||||
case types.DS:
|
||||
res.aa = true;
|
||||
res.answer = this.toDS(name);
|
||||
key.signZSK(res.answer, types.DS);
|
||||
break;
|
||||
}
|
||||
|
||||
// Nope, we may need a referral
|
||||
if (res.answer.length === 0 && res.authority.length === 0) {
|
||||
return this.toReferral(name, type, true);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
getJSON(name) {
|
||||
const json = { records: [] };
|
||||
|
||||
for (const record of this.records)
|
||||
json.records.push(record.getJSON());
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object', 'Invalid json.');
|
||||
assert(Array.isArray(json.records), 'Invalid records.');
|
||||
|
||||
for (const item of json.records) {
|
||||
assert(item && typeof item === 'object', 'Invalid record.');
|
||||
|
||||
const RD = stringToClass(item.type);
|
||||
|
||||
if (!RD)
|
||||
throw new Error(`Unknown type: ${item.type}.`);
|
||||
|
||||
this.records.push(RD.fromJSON(item));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DS
|
||||
* @extends {Struct}
|
||||
*/
|
||||
|
||||
class DS extends Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.keyTag = 0;
|
||||
this.algorithm = 0;
|
||||
this.digestType = 0;
|
||||
this.digest = DUMMY;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return hsTypes.DS;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return 5 + this.digest.length;
|
||||
}
|
||||
|
||||
write(bw) {
|
||||
bw.writeU16BE(this.keyTag);
|
||||
bw.writeU8(this.algorithm);
|
||||
bw.writeU8(this.digestType);
|
||||
bw.writeU8(this.digest.length);
|
||||
bw.writeBytes(this.digest);
|
||||
return this;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
this.keyTag = br.readU16BE();
|
||||
this.algorithm = br.readU8();
|
||||
this.digestType = br.readU8();
|
||||
this.digest = br.readBytes(br.readU8());
|
||||
return this;
|
||||
}
|
||||
|
||||
toDNS(name = '.', ttl = DEFAULT_TTL) {
|
||||
assert(util.isFQDN(name));
|
||||
assert((ttl >>> 0) === ttl);
|
||||
|
||||
const rr = new Record();
|
||||
const rd = new DSRecord();
|
||||
|
||||
rr.name = name;
|
||||
rr.type = types.DS;
|
||||
rr.ttl = ttl;
|
||||
rr.data = rd;
|
||||
|
||||
rd.keyTag = this.keyTag;
|
||||
rd.algorithm = this.algorithm;
|
||||
rd.digestType = this.digestType;
|
||||
rd.digest = this.digest;
|
||||
|
||||
return rr;
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
type: 'DS',
|
||||
keyTag: this.keyTag,
|
||||
algorithm: this.algorithm,
|
||||
digestType: this.digestType,
|
||||
digest: this.digest.toString('hex')
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object', 'Invalid DS record.');
|
||||
assert(json.type === 'DS',
|
||||
'Invalid DS record. Type must be "DS".');
|
||||
assert((json.keyTag & 0xffff) === json.keyTag,
|
||||
'Invalid DS record. KeyTag must be a uint16.');
|
||||
assert((json.algorithm & 0xff) === json.algorithm,
|
||||
'Invalid DS record. Algorithm must be a uint8.');
|
||||
assert((json.digestType & 0xff) === json.digestType,
|
||||
'Invalid DS record. DigestType must be a uint8.');
|
||||
assert(typeof json.digest === 'string',
|
||||
'Invalid DS record. Digest must be a String.');
|
||||
assert((json.digest.length >>> 1) <= 255,
|
||||
'Invalid DS record. Digest is too large.');
|
||||
|
||||
this.keyTag = json.keyTag;
|
||||
this.algorithm = json.algorithm;
|
||||
this.digestType = json.digestType;
|
||||
this.digest = util.parseHex(json.digest);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NS
|
||||
* @extends {Struct}
|
||||
*/
|
||||
|
||||
class NS extends Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.ns = '.';
|
||||
}
|
||||
|
||||
get type() {
|
||||
return hsTypes.NS;
|
||||
}
|
||||
|
||||
getSize(map) {
|
||||
return sizeName(this.ns, map);
|
||||
}
|
||||
|
||||
write(bw, map) {
|
||||
writeNameBW(bw, this.ns, map);
|
||||
return this;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
this.ns = readNameBR(br);
|
||||
return this;
|
||||
}
|
||||
|
||||
toDNS(name = '.', ttl = DEFAULT_TTL) {
|
||||
return createNS(name, ttl, this.ns);
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
type: 'NS',
|
||||
ns: this.ns
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object',
|
||||
'Invalid NS record.');
|
||||
assert(json.type === 'NS',
|
||||
'Invalid NS record. Type must be "NS".');
|
||||
assert(isName(json.ns),
|
||||
'Invalid NS record. ns must be a valid name.');
|
||||
|
||||
this.ns = json.ns;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GLUE4
|
||||
* @extends {Struct}
|
||||
*/
|
||||
|
||||
class GLUE4 extends Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.ns = '.';
|
||||
this.address = '0.0.0.0';
|
||||
}
|
||||
|
||||
get type() {
|
||||
return hsTypes.GLUE4;
|
||||
}
|
||||
|
||||
getSize(map) {
|
||||
return sizeName(this.ns, map) + 4;
|
||||
}
|
||||
|
||||
write(bw, map) {
|
||||
writeNameBW(bw, this.ns, map);
|
||||
writeIP(bw, this.address, 4);
|
||||
return this;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
this.ns = readNameBR(br);
|
||||
this.address = readIP(br, 4);
|
||||
return this;
|
||||
}
|
||||
|
||||
toDNS(name = '.', ttl = DEFAULT_TTL) {
|
||||
return createNS(name, ttl, this.ns);
|
||||
}
|
||||
|
||||
toGlue(name = '.', ttl = DEFAULT_TTL) {
|
||||
return createA(name, ttl, this.address);
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
type: 'GLUE4',
|
||||
ns: this.ns,
|
||||
address: this.address
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object', 'Invalid GLUE4 record.');
|
||||
assert(json.type === 'GLUE4',
|
||||
'Invalid GLUE4 record. Type must be "GLUE4".');
|
||||
assert(isName(json.ns),
|
||||
'Invalid GLUE4 record. ns must be a valid name.');
|
||||
assert(IP.isIPv4String(json.address),
|
||||
'Invalid GLUE4 record. Address must be a valid IPv4 address.');
|
||||
|
||||
this.ns = json.ns;
|
||||
this.address = IP.normalize(json.address);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GLUE6
|
||||
* @extends {Struct}
|
||||
*/
|
||||
|
||||
class GLUE6 extends Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.ns = '.';
|
||||
this.address = '::';
|
||||
}
|
||||
|
||||
get type() {
|
||||
return hsTypes.GLUE6;
|
||||
}
|
||||
|
||||
getSize(map) {
|
||||
return sizeName(this.ns, map) + 16;
|
||||
}
|
||||
|
||||
write(bw, map) {
|
||||
writeNameBW(bw, this.ns, map);
|
||||
writeIP(bw, this.address, 16);
|
||||
return this;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
this.ns = readNameBR(br);
|
||||
this.address = readIP(br, 16);
|
||||
return this;
|
||||
}
|
||||
|
||||
toDNS(name = '.', ttl = DEFAULT_TTL) {
|
||||
return createNS(name, ttl, this.ns);
|
||||
}
|
||||
|
||||
toGlue(name = '.', ttl = DEFAULT_TTL) {
|
||||
return createAAAA(name, ttl, this.address);
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
type: 'GLUE6',
|
||||
ns: this.ns,
|
||||
address: this.address
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object', 'Invalid GLUE6 record.');
|
||||
assert(json.type === 'GLUE6',
|
||||
'Invalid GLUE6 record. Type must be "GLUE6".');
|
||||
assert(isName(json.ns),
|
||||
'Invalid GLUE6 record. ns must be a valid name.');
|
||||
assert(IP.isIPv6String(json.address),
|
||||
'Invalid GLUE6 record. Address must be a valid IPv6 address.');
|
||||
|
||||
this.ns = json.ns;
|
||||
this.address = IP.normalize(json.address);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SYNTH4
|
||||
* @extends {Struct}
|
||||
*/
|
||||
|
||||
class SYNTH4 extends Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.address = '0.0.0.0';
|
||||
}
|
||||
|
||||
get type() {
|
||||
return hsTypes.SYNTH4;
|
||||
}
|
||||
|
||||
get ns() {
|
||||
const ip = IP.toBuffer(this.address).slice(12);
|
||||
return `_${base32.encodeHex(ip)}._synth.`;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
write(bw) {
|
||||
writeIP(bw, this.address, 4);
|
||||
return this;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
this.address = readIP(br, 4);
|
||||
return this;
|
||||
}
|
||||
|
||||
toDNS(name = '.', ttl = DEFAULT_TTL) {
|
||||
return createNS(name, ttl, this.ns);
|
||||
}
|
||||
|
||||
toGlue(name = '.', ttl = DEFAULT_TTL) {
|
||||
return createA(name, ttl, this.address);
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
type: 'SYNTH4',
|
||||
address: this.address
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object', 'Invalid SYNTH4 record.');
|
||||
assert(json.type === 'SYNTH4',
|
||||
'Invalid SYNTH4 record. Type must be "SYNTH4".');
|
||||
assert(IP.isIPv4String(json.address),
|
||||
'Invalid SYNTH4 record. Address must be a valid IPv4 address.');
|
||||
|
||||
this.address = IP.normalize(json.address);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SYNTH6
|
||||
* @extends {Struct}
|
||||
*/
|
||||
|
||||
class SYNTH6 extends Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.address = '::';
|
||||
}
|
||||
|
||||
get type() {
|
||||
return hsTypes.SYNTH6;
|
||||
}
|
||||
|
||||
get ns() {
|
||||
const ip = IP.toBuffer(this.address);
|
||||
return `_${base32.encodeHex(ip)}._synth.`;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
write(bw) {
|
||||
writeIP(bw, this.address, 16);
|
||||
return this;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
this.address = readIP(br, 16);
|
||||
return this;
|
||||
}
|
||||
|
||||
toDNS(name = '.', ttl = DEFAULT_TTL) {
|
||||
return createNS(name, ttl, this.ns);
|
||||
}
|
||||
|
||||
toGlue(name = '.', ttl = DEFAULT_TTL) {
|
||||
return createAAAA(name, ttl, this.address);
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
type: 'SYNTH6',
|
||||
address: this.address
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object', 'Invalid SYNTH6 record.');
|
||||
assert(json.type === 'SYNTH6',
|
||||
'Invalid SYNTH6 record. Type must be "SYNTH6".');
|
||||
assert(IP.isIPv6String(json.address),
|
||||
'Invalid SYNTH6 record. Address must be a valid IPv6 address.');
|
||||
|
||||
this.address = IP.normalize(json.address);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TXT
|
||||
* @extends {Struct}
|
||||
*/
|
||||
|
||||
class TXT extends Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.txt = [];
|
||||
}
|
||||
|
||||
get type() {
|
||||
return hsTypes.TXT;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
let size = 1;
|
||||
for (const txt of this.txt)
|
||||
size += sizeString(txt);
|
||||
return size;
|
||||
}
|
||||
|
||||
write(bw) {
|
||||
bw.writeU8(this.txt.length);
|
||||
|
||||
for (const txt of this.txt)
|
||||
writeStringBW(bw, txt);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
read(br) {
|
||||
const count = br.readU8();
|
||||
|
||||
for (let i = 0; i < count; i++)
|
||||
this.txt.push(readStringBR(br));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toDNS(name = '.', ttl = DEFAULT_TTL) {
|
||||
assert(util.isFQDN(name));
|
||||
assert((ttl >>> 0) === ttl);
|
||||
|
||||
const rr = new Record();
|
||||
const rd = new TXTRecord();
|
||||
|
||||
rr.name = name;
|
||||
rr.type = types.TXT;
|
||||
rr.ttl = ttl;
|
||||
rr.data = rd;
|
||||
|
||||
rd.txt.push(...this.txt);
|
||||
|
||||
return rr;
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
type: 'TXT',
|
||||
txt: this.txt
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object',
|
||||
'Invalid TXT record.');
|
||||
assert(json.type === 'TXT',
|
||||
'Invalid TXT record. Type must be "TXT".');
|
||||
assert(Array.isArray(json.txt),
|
||||
'Invalid TXT record. txt must be an Array.');
|
||||
|
||||
for (const txt of json.txt) {
|
||||
assert(typeof txt === 'string',
|
||||
'Invalid TXT record. Entries in txt Array must be type String.');
|
||||
assert(txt.length <= 255,
|
||||
'Invalid TXT record. Entries in txt Array must be <= 255 in length.');
|
||||
|
||||
this.txt.push(txt);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
function typeToClass(type) {
|
||||
assert((type & 0xff) === type);
|
||||
switch (type) {
|
||||
case hsTypes.DS:
|
||||
return DS;
|
||||
case hsTypes.NS:
|
||||
return NS;
|
||||
case hsTypes.GLUE4:
|
||||
return GLUE4;
|
||||
case hsTypes.GLUE6:
|
||||
return GLUE6;
|
||||
case hsTypes.SYNTH4:
|
||||
return SYNTH4;
|
||||
case hsTypes.SYNTH6:
|
||||
return SYNTH6;
|
||||
case hsTypes.TXT:
|
||||
return TXT;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stringToClass(type) {
|
||||
assert(typeof type === 'string');
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(hsTypes, type))
|
||||
return null;
|
||||
|
||||
return typeToClass(hsTypes[type]);
|
||||
}
|
||||
|
||||
function createNS(name, ttl, ns) {
|
||||
assert(util.isFQDN(name));
|
||||
assert((ttl >>> 0) === ttl);
|
||||
assert(util.isFQDN(ns));
|
||||
|
||||
const rr = new Record();
|
||||
const rd = new NSRecord();
|
||||
|
||||
rr.name = name;
|
||||
rr.ttl = ttl;
|
||||
rr.type = types.NS;
|
||||
rr.data = rd;
|
||||
rd.ns = ns;
|
||||
|
||||
return rr;
|
||||
}
|
||||
|
||||
function createA(name, ttl, address) {
|
||||
assert(util.isFQDN(name));
|
||||
assert((ttl >>> 0) === ttl);
|
||||
assert(IP.isIPv4String(address));
|
||||
|
||||
const rr = new Record();
|
||||
const rd = new ARecord();
|
||||
|
||||
rr.name = name;
|
||||
rr.ttl = ttl;
|
||||
rr.type = types.A;
|
||||
rr.data = rd;
|
||||
rd.address = address;
|
||||
|
||||
return rr;
|
||||
}
|
||||
|
||||
function createAAAA(name, ttl, address) {
|
||||
assert(util.isFQDN(name));
|
||||
assert((ttl >>> 0) === ttl);
|
||||
assert(IP.isIPv6String(address));
|
||||
|
||||
const rr = new Record();
|
||||
const rd = new AAAARecord();
|
||||
|
||||
rr.name = name;
|
||||
rr.ttl = ttl;
|
||||
rr.type = types.AAAA;
|
||||
rr.data = rd;
|
||||
rd.address = address;
|
||||
|
||||
return rr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
exports.Resource = Resource;
|
||||
exports.DS = DS;
|
||||
exports.NS = NS;
|
||||
exports.GLUE4 = GLUE4;
|
||||
exports.GLUE6 = GLUE6;
|
||||
exports.SYNTH4 = SYNTH4;
|
||||
exports.SYNTH6 = SYNTH6;
|
||||
exports.TXT = TXT;
|
||||
836
docs/js-dns/server.js
Normal file
836
docs/js-dns/server.js
Normal file
|
|
@ -0,0 +1,836 @@
|
|||
/*!
|
||||
* dns.js - dns server for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const IP = require('binet');
|
||||
const Logger = require('blgr');
|
||||
const bns = require('bns');
|
||||
const UnboundResolver = require('bns/lib/resolver/unbound');
|
||||
const RecursiveResolver = require('bns/lib/resolver/recursive');
|
||||
const RootResolver = require('bns/lib/resolver/root');
|
||||
const secp256k1 = require('bcrypto/lib/secp256k1');
|
||||
const LRU = require('blru');
|
||||
const base32 = require('bcrypto/lib/encoding/base32');
|
||||
const NameState = require('../covenants/namestate');
|
||||
const rules = require('../covenants/rules');
|
||||
const reserved = require('../covenants/reserved');
|
||||
const {Resource} = require('./resource');
|
||||
const key = require('./key');
|
||||
const nsec = require('./nsec');
|
||||
const {
|
||||
DEFAULT_TTL,
|
||||
TYPE_MAP_ROOT,
|
||||
TYPE_MAP_EMPTY,
|
||||
TYPE_MAP_NS,
|
||||
TYPE_MAP_A,
|
||||
TYPE_MAP_AAAA
|
||||
} = require('./common');
|
||||
|
||||
const {
|
||||
DNSServer,
|
||||
hsig,
|
||||
wire,
|
||||
util
|
||||
} = bns;
|
||||
|
||||
const {
|
||||
Message,
|
||||
Record,
|
||||
ARecord,
|
||||
AAAARecord,
|
||||
NSRecord,
|
||||
SOARecord,
|
||||
types,
|
||||
codes
|
||||
} = wire;
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const RES_OPT = { inet6: false, tcp: true };
|
||||
const CACHE_TTL = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* RootCache
|
||||
*/
|
||||
|
||||
class RootCache {
|
||||
constructor(size) {
|
||||
this.cache = new LRU(size);
|
||||
}
|
||||
|
||||
set(name, type, msg) {
|
||||
const key = toKey(name, type);
|
||||
const raw = msg.compress();
|
||||
|
||||
this.cache.set(key, {
|
||||
time: Date.now(),
|
||||
raw
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get(name, type) {
|
||||
const key = toKey(name, type);
|
||||
const item = this.cache.get(key);
|
||||
|
||||
if (!item)
|
||||
return null;
|
||||
|
||||
if (Date.now() > item.time + CACHE_TTL)
|
||||
return null;
|
||||
|
||||
return Message.decode(item.raw);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.cache.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RootServer
|
||||
* @extends {DNSServer}
|
||||
*/
|
||||
|
||||
class RootServer extends DNSServer {
|
||||
constructor(options) {
|
||||
super(RES_OPT);
|
||||
|
||||
this.ra = false;
|
||||
this.edns = true;
|
||||
this.dnssec = true;
|
||||
this.noSig0 = false;
|
||||
this.icann = new RootResolver(RES_OPT);
|
||||
|
||||
this.logger = Logger.global;
|
||||
this.key = secp256k1.privateKeyGenerate();
|
||||
this.host = '127.0.0.1';
|
||||
this.port = 5300;
|
||||
this.lookup = null;
|
||||
this.middle = null;
|
||||
this.publicHost = '127.0.0.1';
|
||||
|
||||
// Plugins can add or remove items from
|
||||
// this set before the server is opened.
|
||||
this.blacklist = new Set([
|
||||
'bit', // Namecoin
|
||||
'eth', // ENS
|
||||
'exit', // Tor
|
||||
'gnu', // GNUnet (GNS)
|
||||
'i2p', // Invisible Internet Project
|
||||
'onion', // Tor
|
||||
'tor', // OnioNS
|
||||
'zkey' // GNS
|
||||
]);
|
||||
|
||||
this.cache = new RootCache(3000);
|
||||
|
||||
if (options)
|
||||
this.initOptions(options);
|
||||
|
||||
// Create SYNTH record to use for root zone NS
|
||||
let ip = IP.toBuffer(this.publicHost);
|
||||
if (IP.family(this.publicHost) === 4)
|
||||
ip = ip.slice(12);
|
||||
this.synth = `_${base32.encodeHex(ip)}._synth.`;
|
||||
|
||||
this.initNode();
|
||||
}
|
||||
|
||||
initOptions(options) {
|
||||
assert(options);
|
||||
|
||||
this.parseOptions(options);
|
||||
|
||||
if (options.logger != null) {
|
||||
assert(typeof options.logger === 'object');
|
||||
this.logger = options.logger.context('ns');
|
||||
}
|
||||
|
||||
if (options.key != null) {
|
||||
assert(Buffer.isBuffer(options.key));
|
||||
assert(options.key.length === 32);
|
||||
this.key = options.key;
|
||||
}
|
||||
|
||||
if (options.host != null) {
|
||||
assert(typeof options.host === 'string');
|
||||
this.host = IP.normalize(options.host);
|
||||
this.publicHost = this.host;
|
||||
}
|
||||
|
||||
if (options.port != null) {
|
||||
assert((options.port & 0xffff) === options.port);
|
||||
assert(options.port !== 0);
|
||||
this.port = options.port;
|
||||
}
|
||||
|
||||
if (options.lookup != null) {
|
||||
assert(typeof options.lookup === 'function');
|
||||
this.lookup = options.lookup;
|
||||
}
|
||||
|
||||
if (options.noSig0 != null) {
|
||||
assert(typeof options.noSig0 === 'boolean');
|
||||
this.noSig0 = options.noSig0;
|
||||
}
|
||||
|
||||
if (options.publicHost != null) {
|
||||
assert(typeof options.publicHost === 'string');
|
||||
this.publicHost = IP.normalize(options.publicHost);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
initNode() {
|
||||
this.on('error', (err) => {
|
||||
this.logger.error(err);
|
||||
});
|
||||
|
||||
this.on('query', (req, res) => {
|
||||
this.logMessage('\n\nDNS Request:', req);
|
||||
this.logMessage('\n\nDNS Response:', res);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
logMessage(prefix, msg) {
|
||||
if (this.logger.level < 5)
|
||||
return;
|
||||
|
||||
const logs = msg.toString().trim().split('\n');
|
||||
|
||||
this.logger.spam(prefix);
|
||||
|
||||
for (const log of logs)
|
||||
this.logger.spam(log);
|
||||
}
|
||||
|
||||
signSize() {
|
||||
if (!this.sig0)
|
||||
return 94;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
sign(msg, host, port) {
|
||||
if (!this.noSig0)
|
||||
return hsig.sign(msg, this.key);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
async lookupName(name) {
|
||||
if (!this.lookup)
|
||||
throw new Error('Tree not available.');
|
||||
|
||||
const hash = rules.hashName(name);
|
||||
const data = await this.lookup(hash);
|
||||
|
||||
if (!data)
|
||||
return null;
|
||||
|
||||
const ns = NameState.decode(data);
|
||||
|
||||
if (ns.data.length === 0)
|
||||
return null;
|
||||
|
||||
return ns.data;
|
||||
}
|
||||
|
||||
async response(req, rinfo) {
|
||||
const [qs] = req.question;
|
||||
const name = qs.name.toLowerCase();
|
||||
const type = qs.type;
|
||||
|
||||
// Our root zone.
|
||||
if (name === '.') {
|
||||
const res = new Message();
|
||||
|
||||
res.aa = true;
|
||||
|
||||
switch (type) {
|
||||
case types.ANY:
|
||||
case types.NS:
|
||||
res.answer.push(this.toNS());
|
||||
key.signZSK(res.answer, types.NS);
|
||||
|
||||
if (IP.family(this.publicHost) === 4) {
|
||||
res.additional.push(this.toA());
|
||||
key.signZSK(res.additional, types.A);
|
||||
} else {
|
||||
res.additional.push(this.toAAAA());
|
||||
key.signZSK(res.additional, types.AAAA);
|
||||
}
|
||||
|
||||
break;
|
||||
case types.SOA:
|
||||
res.answer.push(this.toSOA());
|
||||
key.signZSK(res.answer, types.SOA);
|
||||
|
||||
res.authority.push(this.toNS());
|
||||
key.signZSK(res.authority, types.NS);
|
||||
|
||||
if (IP.family(this.publicHost) === 4) {
|
||||
res.additional.push(this.toA());
|
||||
key.signZSK(res.additional, types.A);
|
||||
} else {
|
||||
res.additional.push(this.toAAAA());
|
||||
key.signZSK(res.additional, types.AAAA);
|
||||
}
|
||||
|
||||
break;
|
||||
case types.DNSKEY:
|
||||
res.answer.push(key.ksk.deepClone());
|
||||
res.answer.push(key.zsk.deepClone());
|
||||
key.signKSK(res.answer, types.DNSKEY);
|
||||
break;
|
||||
default:
|
||||
// Minimally covering NSEC proof:
|
||||
res.authority.push(this.toNSEC());
|
||||
key.signZSK(res.authority, types.NSEC);
|
||||
res.authority.push(this.toSOA());
|
||||
key.signZSK(res.authority, types.SOA);
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// Process the name.
|
||||
const labels = util.split(name);
|
||||
const tld = util.label(name, labels, -1);
|
||||
|
||||
// Handle reverse pointers.
|
||||
if (tld === '_synth' && labels.length <= 2 && name[0] === '_') {
|
||||
const res = new Message();
|
||||
const rr = new Record();
|
||||
|
||||
res.aa = true;
|
||||
rr.name = name;
|
||||
rr.ttl = 21600;
|
||||
|
||||
// TLD '._synth' is being queried on its own, send SOA
|
||||
// so recursive asks again with complete synth record.
|
||||
if (labels.length === 1) {
|
||||
// Empty non-terminal proof:
|
||||
res.authority.push(
|
||||
nsec.create(
|
||||
'_synth.',
|
||||
'\\000._synth.',
|
||||
TYPE_MAP_EMPTY
|
||||
)
|
||||
);
|
||||
key.signZSK(res.authority, types.NSEC);
|
||||
|
||||
res.authority.push(this.toSOA());
|
||||
key.signZSK(res.authority, types.SOA);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
const hash = util.label(name, labels, -2);
|
||||
const ip = IP.map(base32.decodeHex(hash.substring(1)));
|
||||
const synthType = IP.isIPv4(ip) ? types.A : types.AAAA;
|
||||
|
||||
// Query must be for the correct synth version
|
||||
if (type !== synthType) {
|
||||
// SYNTH4/6 proof:
|
||||
const typeMap = synthType === types.A ? TYPE_MAP_A : TYPE_MAP_AAAA;
|
||||
res.authority.push(nsec.create(name, '\\000.' + name, typeMap));
|
||||
key.signZSK(res.authority, types.NSEC);
|
||||
|
||||
res.authority.push(this.toSOA());
|
||||
key.signZSK(res.authority, types.SOA);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
if (synthType === types.A) {
|
||||
rr.type = types.A;
|
||||
rr.data = new ARecord();
|
||||
} else {
|
||||
rr.type = types.AAAA;
|
||||
rr.data = new AAAARecord();
|
||||
}
|
||||
|
||||
rr.data.address = IP.toString(ip);
|
||||
|
||||
res.answer.push(rr);
|
||||
key.signZSK(res.answer, rr.type);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// REFUSED for invalid names
|
||||
// this simplifies NSEC proofs
|
||||
// by avoiding octets like \000
|
||||
// Also, this decreases load on
|
||||
// the server since it avoids signing
|
||||
// useless proofs for invalid TLDs
|
||||
// (These requests are most
|
||||
// likely bad anyways)
|
||||
if (!rules.verifyName(tld)) {
|
||||
const res = new Message();
|
||||
res.code = codes.REFUSED;
|
||||
return res;
|
||||
}
|
||||
|
||||
// Ask the urkel tree for the name data.
|
||||
const data = !this.blacklist.has(tld)
|
||||
? (await this.lookupName(tld))
|
||||
: null;
|
||||
|
||||
// Non-existent domain.
|
||||
if (!data) {
|
||||
const item = this.getReserved(tld);
|
||||
|
||||
// This name is in the existing root zone.
|
||||
// Fall back to ICANN's servers if not yet
|
||||
// registered on the handshake blockchain.
|
||||
// This is an example of "Dynamic Fallback"
|
||||
// as mentioned in the whitepaper.
|
||||
if (item && item.root) {
|
||||
const res = await this.icann.lookup(tld);
|
||||
|
||||
if (res.ad && res.code !== codes.NXDOMAIN) {
|
||||
// answer must be a referral since lookup
|
||||
// function always asks for NS
|
||||
assert(res.code === codes.NOERROR);
|
||||
assert(res.answer.length === 0);
|
||||
assert(hasValidOwner(res.authority, tld));
|
||||
|
||||
res.ad = false;
|
||||
res.question = [qs];
|
||||
const secure = util.hasType(res.authority, types.DS);
|
||||
|
||||
// no DS referrals for TLDs
|
||||
if (type === types.DS && labels.length === 1) {
|
||||
const dsSet = util.extractSet(res.authority,
|
||||
util.fqdn(tld), types.DS);
|
||||
|
||||
res.aa = true;
|
||||
res.answer = dsSet;
|
||||
key.signZSK(res.answer, types.DS);
|
||||
res.authority = [];
|
||||
res.additional = [];
|
||||
|
||||
if (res.answer.length === 0) {
|
||||
res.authority.push(this.toSOA());
|
||||
key.signZSK(res.authority, types.SOA);
|
||||
}
|
||||
}
|
||||
|
||||
// No DS we must add a minimally covering proof
|
||||
if (!secure) {
|
||||
// Replace any NSEC/NSEC3 records
|
||||
const filterTypes = [types.NSEC, types.NSEC3];
|
||||
res.authority = util.filterSet(res.authority, ...filterTypes);
|
||||
const next = nsec.nextName(tld);
|
||||
const rr = nsec.create(tld, next, TYPE_MAP_NS);
|
||||
res.authority.push(rr);
|
||||
key.signZSK(res.authority, types.NSEC);
|
||||
} else {
|
||||
key.signZSK(res.authority, types.DS);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const res = new Message();
|
||||
|
||||
res.code = codes.NXDOMAIN;
|
||||
res.aa = true;
|
||||
|
||||
// Doesn't exist.
|
||||
//
|
||||
// We should be giving a real NSEC proof
|
||||
// here, but I don't think it's possible
|
||||
// with the current construction.
|
||||
//
|
||||
// I imagine this would only be possible
|
||||
// if NSEC3 begins to support BLAKE2b for
|
||||
// name hashing. Even then, it's still
|
||||
// not possible for SPV nodes since they
|
||||
// can't arbitrarily iterate over the tree.
|
||||
//
|
||||
// Instead, we give a minimally covering
|
||||
// NSEC record based on rfc4470
|
||||
// https://tools.ietf.org/html/rfc4470
|
||||
|
||||
// Proving the name doesn't exist
|
||||
const prev = nsec.prevName(tld);
|
||||
const next = nsec.nextName(tld);
|
||||
const nameSet = [nsec.create(prev, next, TYPE_MAP_EMPTY)];
|
||||
key.signZSK(nameSet, types.NSEC);
|
||||
|
||||
// Proving a wildcard doesn't exist
|
||||
const wildcardSet = [nsec.create('!.', '+.', TYPE_MAP_EMPTY)];
|
||||
key.signZSK(wildcardSet, types.NSEC);
|
||||
|
||||
res.authority = res.authority.concat(nameSet, wildcardSet);
|
||||
res.authority.push(this.toSOA());
|
||||
key.signZSK(res.authority, types.SOA);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// Our resolution.
|
||||
const resource = Resource.decode(data);
|
||||
const res = resource.toDNS(name, type);
|
||||
|
||||
if (res.answer.length === 0 && res.aa) {
|
||||
res.authority.push(this.toSOA());
|
||||
key.signZSK(res.authority, types.SOA);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async resolve(req, rinfo) {
|
||||
const [qs] = req.question;
|
||||
const {name, type} = qs;
|
||||
const tld = util.from(name, -1);
|
||||
|
||||
// Plugins can insert middleware here and hijack the
|
||||
// lookup for special TLDs before checking Urkel tree.
|
||||
// We also pass the entire question in case a plugin
|
||||
// is able to return an authoritative (non-referral) answer.
|
||||
if (typeof this.middle === 'function') {
|
||||
let res;
|
||||
try {
|
||||
res = await this.middle(tld, req, rinfo);
|
||||
} catch (e) {
|
||||
this.logger.warning(
|
||||
'Root server middleware resolution failed for name: %s',
|
||||
name
|
||||
);
|
||||
this.logger.debug(e.stack);
|
||||
}
|
||||
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// Hit the cache first.
|
||||
const cache = this.cache.get(name, type);
|
||||
|
||||
if (cache)
|
||||
return cache;
|
||||
|
||||
const res = await this.response(req, rinfo);
|
||||
|
||||
if (!util.equal(tld, '_synth.'))
|
||||
this.cache.set(name, type, res);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async open() {
|
||||
await super.open(this.port, this.host);
|
||||
|
||||
this.logger.info('Root nameserver listening on port %d.', this.port);
|
||||
}
|
||||
|
||||
getReserved(tld) {
|
||||
return reserved.getByName(tld);
|
||||
}
|
||||
|
||||
// Intended to be called by plugin.
|
||||
signRRSet(rrset, type) {
|
||||
key.signZSK(rrset, type);
|
||||
}
|
||||
|
||||
resetCache() {
|
||||
this.cache.reset();
|
||||
}
|
||||
|
||||
serial() {
|
||||
const date = new Date();
|
||||
const y = date.getUTCFullYear() * 1e6;
|
||||
const m = (date.getUTCMonth() + 1) * 1e4;
|
||||
const d = date.getUTCDate() * 1e2;
|
||||
const h = date.getUTCHours();
|
||||
return y + m + d + h;
|
||||
}
|
||||
|
||||
toSOA() {
|
||||
const rr = new Record();
|
||||
const rd = new SOARecord();
|
||||
|
||||
rr.name = '.';
|
||||
rr.type = types.SOA;
|
||||
rr.ttl = 86400;
|
||||
rr.data = rd;
|
||||
rd.ns = '.';
|
||||
rd.mbox = '.';
|
||||
rd.serial = this.serial();
|
||||
rd.refresh = 1800;
|
||||
rd.retry = 900;
|
||||
rd.expire = 604800;
|
||||
rd.minttl = DEFAULT_TTL;
|
||||
|
||||
return rr;
|
||||
}
|
||||
|
||||
toNS() {
|
||||
const rr = new Record();
|
||||
const rd = new NSRecord();
|
||||
rr.name = '.';
|
||||
rr.type = types.NS;
|
||||
rr.ttl = 518400;
|
||||
rr.data = rd;
|
||||
rd.ns = this.synth;
|
||||
return rr;
|
||||
}
|
||||
|
||||
// Glue only
|
||||
toA() {
|
||||
const rr = new Record();
|
||||
const rd = new ARecord();
|
||||
rr.name = this.synth;
|
||||
rr.type = types.A;
|
||||
rr.ttl = 518400;
|
||||
rr.data = rd;
|
||||
rd.address = this.publicHost;
|
||||
return rr;
|
||||
}
|
||||
|
||||
// Glue only
|
||||
toAAAA() {
|
||||
const rr = new Record();
|
||||
const rd = new AAAARecord();
|
||||
rr.name = this.synth;
|
||||
rr.type = types.AAAA;
|
||||
rr.ttl = 518400;
|
||||
rr.data = rd;
|
||||
rd.address = this.publicHost;
|
||||
return rr;
|
||||
}
|
||||
|
||||
toNSEC() {
|
||||
const next = nsec.nextName('.');
|
||||
return nsec.create('.', next, TYPE_MAP_ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RecursiveServer
|
||||
* @extends {DNSServer}
|
||||
*/
|
||||
|
||||
class RecursiveServer extends DNSServer {
|
||||
constructor(options) {
|
||||
super(RES_OPT);
|
||||
|
||||
this.ra = true;
|
||||
this.edns = true;
|
||||
this.dnssec = true;
|
||||
this.noSig0 = false;
|
||||
this.noAny = true;
|
||||
|
||||
this.logger = Logger.global;
|
||||
this.key = secp256k1.privateKeyGenerate();
|
||||
|
||||
this.host = '127.0.0.1';
|
||||
this.port = 5301;
|
||||
this.stubHost = '127.0.0.1';
|
||||
this.stubPort = 5300;
|
||||
|
||||
this.hns = new UnboundResolver({
|
||||
inet6: false,
|
||||
tcp: true,
|
||||
edns: true,
|
||||
dnssec: true,
|
||||
minimize: true
|
||||
});
|
||||
|
||||
if (options)
|
||||
this.initOptions(options);
|
||||
|
||||
this.initNode();
|
||||
|
||||
this.hns.setStub(this.stubHost, this.stubPort, key.ds);
|
||||
}
|
||||
|
||||
initOptions(options) {
|
||||
assert(options);
|
||||
|
||||
this.parseOptions(options);
|
||||
|
||||
if (options.logger != null) {
|
||||
assert(typeof options.logger === 'object');
|
||||
this.logger = options.logger.context('rs');
|
||||
}
|
||||
|
||||
if (options.key != null) {
|
||||
assert(Buffer.isBuffer(options.key));
|
||||
assert(options.key.length === 32);
|
||||
this.key = options.key;
|
||||
}
|
||||
|
||||
if (options.host != null) {
|
||||
assert(typeof options.host === 'string');
|
||||
this.host = IP.normalize(options.host);
|
||||
}
|
||||
|
||||
if (options.port != null) {
|
||||
assert((options.port & 0xffff) === options.port);
|
||||
assert(options.port !== 0);
|
||||
this.port = options.port;
|
||||
}
|
||||
|
||||
if (options.stubHost != null) {
|
||||
assert(typeof options.stubHost === 'string');
|
||||
|
||||
this.stubHost = IP.normalize(options.stubHost);
|
||||
|
||||
if (this.stubHost === '0.0.0.0' || this.stubHost === '::')
|
||||
this.stubHost = '127.0.0.1';
|
||||
}
|
||||
|
||||
if (options.stubPort != null) {
|
||||
assert((options.stubPort & 0xffff) === options.stubPort);
|
||||
assert(options.stubPort !== 0);
|
||||
this.stubPort = options.stubPort;
|
||||
}
|
||||
|
||||
if (options.noSig0 != null) {
|
||||
assert(typeof options.noSig0 === 'boolean');
|
||||
this.noSig0 = options.noSig0;
|
||||
}
|
||||
|
||||
if (options.noUnbound != null) {
|
||||
assert(typeof options.noUnbound === 'boolean');
|
||||
if (options.noUnbound) {
|
||||
this.hns = new RecursiveResolver({
|
||||
inet6: false,
|
||||
tcp: true,
|
||||
edns: true,
|
||||
dnssec: true,
|
||||
minimize: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
initNode() {
|
||||
this.hns.on('log', (...args) => {
|
||||
this.logger.debug(...args);
|
||||
});
|
||||
|
||||
this.on('error', (err) => {
|
||||
this.logger.error(err);
|
||||
});
|
||||
|
||||
this.on('query', (req, res) => {
|
||||
this.logMessage('\n\nDNS Request:', req);
|
||||
this.logMessage('\n\nDNS Response:', res);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
logMessage(prefix, msg) {
|
||||
if (this.logger.level < 5)
|
||||
return;
|
||||
|
||||
const logs = msg.toString().trim().split('\n');
|
||||
|
||||
this.logger.spam(prefix);
|
||||
|
||||
for (const log of logs)
|
||||
this.logger.spam(log);
|
||||
}
|
||||
|
||||
signSize() {
|
||||
if (!this.noSig0)
|
||||
return 94;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
sign(msg, host, port) {
|
||||
if (!this.noSig0)
|
||||
return hsig.sign(msg, this.key);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
async open(...args) {
|
||||
await this.hns.open();
|
||||
|
||||
await super.open(this.port, this.host);
|
||||
|
||||
this.logger.info('Recursive server listening on port %d.', this.port);
|
||||
}
|
||||
|
||||
async close() {
|
||||
await super.close();
|
||||
await this.hns.close();
|
||||
}
|
||||
|
||||
async resolve(req, rinfo) {
|
||||
const [qs] = req.question;
|
||||
return this.hns.resolve(qs);
|
||||
}
|
||||
|
||||
async lookup(name, type) {
|
||||
return this.hns.lookup(name, type);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
function toKey(name, type) {
|
||||
const labels = util.split(name);
|
||||
const label = util.from(name, labels, -1);
|
||||
|
||||
// Ignore type if we're a referral.
|
||||
if (labels.length > 1)
|
||||
return label.toLowerCase();
|
||||
|
||||
let key = '';
|
||||
key += label.toLowerCase();
|
||||
key += ';';
|
||||
key += type.toString(10);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
function hasValidOwner(section, owner) {
|
||||
owner = util.fqdn(owner);
|
||||
|
||||
for (const rr of section) {
|
||||
if (rr.type === types.NS)
|
||||
continue;
|
||||
|
||||
if (!util.equal(rr.name, owner))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
exports.RootServer = RootServer;
|
||||
exports.RecursiveServer = RecursiveServer;
|
||||
592
docs/js-primitives/abstractblock.js
Normal file
592
docs/js-primitives/abstractblock.js
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
/*!
|
||||
* abstractblock.js - abstract block object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const BLAKE2b = require('bcrypto/lib/blake2b');
|
||||
const SHA3 = require('bcrypto/lib/sha3');
|
||||
const bio = require('bufio');
|
||||
const InvItem = require('./invitem');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const util = require('../utils/util');
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
|
||||
/**
|
||||
* Abstract Block
|
||||
* The class which all block-like objects inherit from.
|
||||
* @alias module:primitives.AbstractBlock
|
||||
* @abstract
|
||||
* @property {Number} version
|
||||
* @property {Hash} prevBlock
|
||||
* @property {Hash} merkleRoot
|
||||
* @property {Number} time
|
||||
* @property {Number} bits
|
||||
* @property {Number} nonce
|
||||
*/
|
||||
|
||||
class AbstractBlock extends bio.Struct {
|
||||
/**
|
||||
* Create an abstract block.
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.version = 0;
|
||||
this.prevBlock = consensus.ZERO_HASH;
|
||||
this.merkleRoot = consensus.ZERO_HASH;
|
||||
this.witnessRoot = consensus.ZERO_HASH;
|
||||
this.treeRoot = consensus.ZERO_HASH;
|
||||
this.reservedRoot = consensus.ZERO_HASH;
|
||||
this.time = 0;
|
||||
this.bits = 0;
|
||||
this.nonce = 0;
|
||||
this.extraNonce = consensus.ZERO_NONCE;
|
||||
this.mask = consensus.ZERO_HASH;
|
||||
|
||||
this.mutable = false;
|
||||
|
||||
/** @type {Buffer?} */
|
||||
this._hash = null;
|
||||
/** @type {Buffer?} */
|
||||
this._maskHash = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
parseOptions(options) {
|
||||
assert(options, 'Block data is required.');
|
||||
assert((options.version >>> 0) === options.version);
|
||||
assert(Buffer.isBuffer(options.prevBlock));
|
||||
assert(Buffer.isBuffer(options.merkleRoot));
|
||||
assert(Buffer.isBuffer(options.witnessRoot));
|
||||
assert(Buffer.isBuffer(options.treeRoot));
|
||||
assert(Buffer.isBuffer(options.reservedRoot));
|
||||
assert(util.isU64(options.time));
|
||||
assert((options.bits >>> 0) === options.bits);
|
||||
assert((options.nonce >>> 0) === options.nonce);
|
||||
assert(Buffer.isBuffer(options.extraNonce)
|
||||
&& options.extraNonce.length === consensus.NONCE_SIZE);
|
||||
assert(Buffer.isBuffer(options.mask));
|
||||
|
||||
this.version = options.version;
|
||||
this.prevBlock = options.prevBlock;
|
||||
this.merkleRoot = options.merkleRoot;
|
||||
this.witnessRoot = options.witnessRoot;
|
||||
this.treeRoot = options.treeRoot;
|
||||
this.reservedRoot = options.reservedRoot;
|
||||
this.time = options.time;
|
||||
this.bits = options.bits;
|
||||
this.nonce = options.nonce;
|
||||
this.extraNonce = options.extraNonce;
|
||||
this.mask = options.mask;
|
||||
|
||||
if (options.mutable != null) {
|
||||
assert(typeof options.mutable === 'boolean');
|
||||
this.mutable = options.mutable;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from json object.
|
||||
* @param {Object} json
|
||||
*/
|
||||
|
||||
parseJSON(json) {
|
||||
assert(json, 'Block data is required.');
|
||||
assert((json.version >>> 0) === json.version);
|
||||
assert((json.time >>> 0) === json.time);
|
||||
assert((json.bits >>> 0) === json.bits);
|
||||
assert((json.nonce >>> 0) === json.nonce);
|
||||
|
||||
this.version = json.version;
|
||||
this.prevBlock = util.parseHex(json.prevBlock, 32);
|
||||
this.merkleRoot = util.parseHex(json.merkleRoot, 32);
|
||||
this.witnessRoot = util.parseHex(json.witnessRoot, 32);
|
||||
this.treeRoot = util.parseHex(json.treeRoot, 32);
|
||||
this.reservedRoot = util.parseHex(json.reservedRoot, 32);
|
||||
this.time = json.time;
|
||||
this.bits = json.bits;
|
||||
this.nonce = json.nonce;
|
||||
this.extraNonce = util.parseHex(json.extraNonce, consensus.NONCE_SIZE);
|
||||
this.mask = util.parseHex(json.mask, 32);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the block is a memblock.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isMemory() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any cached values (abstract).
|
||||
*/
|
||||
|
||||
_refresh() {
|
||||
this._hash = null;
|
||||
this._maskHash = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any cached values.
|
||||
*/
|
||||
|
||||
refresh() {
|
||||
return this._refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash the block header.
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
hash() {
|
||||
if (this.mutable)
|
||||
return this.powHash();
|
||||
|
||||
if (!this._hash)
|
||||
this._hash = this.powHash();
|
||||
|
||||
return this._hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash the block header.
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
hashHex() {
|
||||
return this.hash().toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get header size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
sizeHead() {
|
||||
return consensus.HEADER_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the block headers.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
toHead() {
|
||||
const size = this.sizeHead();
|
||||
return this.writeHead(bio.write(size)).render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized data.
|
||||
* @param {Buffer} data
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromHead(data) {
|
||||
return this.readHead(bio.read(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve deterministically random padding.
|
||||
* @param {Number} size
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
padding(size) {
|
||||
assert((size >>> 0) === size);
|
||||
|
||||
const pad = Buffer.alloc(size);
|
||||
|
||||
for (let i = 0; i < size; i++)
|
||||
pad[i] = this.prevBlock[i % 32] ^ this.treeRoot[i % 32];
|
||||
|
||||
return pad;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize subheader for proof.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
toSubhead() {
|
||||
const bw = bio.write(128);
|
||||
|
||||
// The subheader contains miner-mutable
|
||||
// and less essential data (that is,
|
||||
// less essential for SPV resolvers).
|
||||
bw.writeBytes(this.extraNonce);
|
||||
bw.writeHash(this.reservedRoot);
|
||||
bw.writeHash(this.witnessRoot);
|
||||
bw.writeHash(this.merkleRoot);
|
||||
bw.writeU32(this.version);
|
||||
bw.writeU32(this.bits);
|
||||
|
||||
// Exactly one blake2b block (128 bytes).
|
||||
assert(bw.offset === BLAKE2b.blockSize);
|
||||
|
||||
return bw.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute subheader hash.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
subHash() {
|
||||
return BLAKE2b.digest(this.toSubhead());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute xor bytes hash.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
maskHash() {
|
||||
if (this._maskHash != null)
|
||||
return this._maskHash;
|
||||
|
||||
// Hash with previous block in case a pool wants
|
||||
// to re-use the same mask for the next block!
|
||||
return BLAKE2b.multi(this.prevBlock, this.mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute commitment hash.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
commitHash() {
|
||||
// Note for mining pools: do not send
|
||||
// the mask itself to individual miners.
|
||||
return BLAKE2b.multi(this.subHash(), this.maskHash());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize preheader.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
toPrehead() {
|
||||
const bw = bio.write(128);
|
||||
|
||||
// The preheader contains only the truly
|
||||
// essential data. This optimizes for
|
||||
// SPV resolvers, who may only need
|
||||
// access to the tree root as well as
|
||||
// the ability to validate the PoW.
|
||||
//
|
||||
// Note that we don't consider the
|
||||
// target commitment "essential" as
|
||||
// the pow can still be validated
|
||||
// contextually without it.
|
||||
//
|
||||
// Furthermore, the preheader does not
|
||||
// contain any miner malleable data
|
||||
// aside from the timestamp and nonce.
|
||||
//
|
||||
// Any malleable data is contained
|
||||
// within the commitment hash. Miners
|
||||
// are penalized for updating this
|
||||
// data, as it will cost them two
|
||||
// rounds of hashing.
|
||||
//
|
||||
// We could drop the padding here and
|
||||
// just use a 20 byte blake2 hash for
|
||||
// the xor bytes (which seems much
|
||||
// cleaner), but this is insecure due
|
||||
// to the following attack:
|
||||
// todo - explain attack.
|
||||
//
|
||||
// The position of the nonce and
|
||||
// timestamp intentionally provide
|
||||
// incentives to keep the timestamp
|
||||
// up-to-date.
|
||||
//
|
||||
// The first 8 bytes of this block
|
||||
// of data can be treated as a uint64
|
||||
// and incremented as such. If more
|
||||
// than a second has passed since
|
||||
// the last timestamp update, a miner
|
||||
// can simply let the nonce overflow
|
||||
// into the timestamp.
|
||||
bw.writeU32(this.nonce);
|
||||
bw.writeU64(this.time);
|
||||
bw.writeBytes(this.padding(20));
|
||||
bw.writeHash(this.prevBlock);
|
||||
bw.writeHash(this.treeRoot);
|
||||
bw.writeHash(this.commitHash());
|
||||
|
||||
// Exactly one blake2b block (128 bytes).
|
||||
assert(bw.offset === BLAKE2b.blockSize);
|
||||
|
||||
return bw.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate share hash.
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
shareHash() {
|
||||
const data = this.toPrehead();
|
||||
|
||||
// 128 bytes (output as BLAKE2b-512).
|
||||
const left = BLAKE2b.digest(data, 64);
|
||||
|
||||
// 128 + 8 = 136 bytes.
|
||||
const right = SHA3.multi(data, this.padding(8));
|
||||
|
||||
// 64 + 32 + 32 = 128 bytes.
|
||||
return BLAKE2b.multi(left, this.padding(32), right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate PoW hash.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
powHash() {
|
||||
const hash = this.shareHash();
|
||||
|
||||
// XOR the PoW hash with arbitrary bytes.
|
||||
// This can optionally be used by mining
|
||||
// pools to mitigate block withholding
|
||||
// attacks. Idea from Kevin Pan:
|
||||
//
|
||||
// https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-October/015163.html
|
||||
//
|
||||
// The goal here is to allow a pool to
|
||||
// deny individual miners the ability
|
||||
// to recognize whether they have found
|
||||
// a block, but still allow them to
|
||||
// recognize a share.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// Network target:
|
||||
// 00000000 00000000 10000000 ...
|
||||
//
|
||||
// Share target:
|
||||
// 00000000 10000000 00000000 ...
|
||||
//
|
||||
// Mask:
|
||||
// 00000000 01010101 10000000 ...
|
||||
//
|
||||
// The mask bytes are hidden from the
|
||||
// individual miner, but known to the
|
||||
// pool, and precommitted to in the
|
||||
// block header (i.e. hashed).
|
||||
//
|
||||
// Following our example further:
|
||||
//
|
||||
// Miner share:
|
||||
// 00000000 01010101 00000000 ...
|
||||
//
|
||||
// PoW hash (after XOR):
|
||||
// 00000000 00000000 10000000 ...
|
||||
//
|
||||
// At this point, the miner has found
|
||||
// a block, but this is unknown to
|
||||
// him or her as they do not have
|
||||
// access to the mask bytes directly.
|
||||
for (let i = 0; i < 32; i++)
|
||||
hash[i] ^= this.mask[i];
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the block headers.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
writeHead(bw) {
|
||||
// Preheader.
|
||||
bw.writeU32(this.nonce);
|
||||
bw.writeU64(this.time);
|
||||
bw.writeHash(this.prevBlock);
|
||||
bw.writeHash(this.treeRoot);
|
||||
|
||||
// Subheader.
|
||||
bw.writeBytes(this.extraNonce);
|
||||
bw.writeHash(this.reservedRoot);
|
||||
bw.writeHash(this.witnessRoot);
|
||||
bw.writeHash(this.merkleRoot);
|
||||
bw.writeU32(this.version);
|
||||
bw.writeU32(this.bits);
|
||||
|
||||
// Mask.
|
||||
bw.writeBytes(this.mask);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the block headers.
|
||||
* @param {bio.BufferReader} br
|
||||
*/
|
||||
|
||||
readHead(br) {
|
||||
// Preheader.
|
||||
this.nonce = br.readU32();
|
||||
this.time = br.readU64();
|
||||
this.prevBlock = br.readHash();
|
||||
this.treeRoot = br.readHash();
|
||||
|
||||
// Subheader.
|
||||
this.extraNonce = br.readBytes(consensus.NONCE_SIZE);
|
||||
this.reservedRoot = br.readHash();
|
||||
this.witnessRoot = br.readHash();
|
||||
this.merkleRoot = br.readHash();
|
||||
this.version = br.readU32();
|
||||
this.bits = br.readU32();
|
||||
|
||||
// Mask.
|
||||
this.mask = br.readBytes(32);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode to miner serialization.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
toMiner() {
|
||||
const bw = bio.write(128 + 128);
|
||||
|
||||
// Preheader.
|
||||
bw.writeU32(this.nonce);
|
||||
bw.writeU64(this.time);
|
||||
bw.writeBytes(this.padding(20));
|
||||
bw.writeHash(this.prevBlock);
|
||||
bw.writeHash(this.treeRoot);
|
||||
|
||||
// Replace commitment hash with mask hash.
|
||||
bw.writeHash(this.maskHash());
|
||||
|
||||
// Subheader.
|
||||
bw.writeBytes(this.extraNonce);
|
||||
bw.writeHash(this.reservedRoot);
|
||||
bw.writeHash(this.witnessRoot);
|
||||
bw.writeHash(this.merkleRoot);
|
||||
bw.writeU32(this.version);
|
||||
bw.writeU32(this.bits);
|
||||
|
||||
return bw.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode from miner serialization.
|
||||
* @param {Buffer} data
|
||||
*/
|
||||
|
||||
fromMiner(data) {
|
||||
const br = bio.read(data);
|
||||
|
||||
// Preheader.
|
||||
this.nonce = br.readU32();
|
||||
this.time = br.readU64();
|
||||
|
||||
const padding = br.readBytes(20);
|
||||
|
||||
this.prevBlock = br.readHash();
|
||||
this.treeRoot = br.readHash();
|
||||
|
||||
assert(padding.equals(this.padding(20)));
|
||||
|
||||
// Note: mask _hash_.
|
||||
this._maskHash = br.readHash();
|
||||
|
||||
// Subheader.
|
||||
this.extraNonce = br.readBytes(consensus.NONCE_SIZE);
|
||||
this.reservedRoot = br.readHash();
|
||||
this.witnessRoot = br.readHash();
|
||||
this.merkleRoot = br.readHash();
|
||||
this.version = br.readU32();
|
||||
this.bits = br.readU32();
|
||||
|
||||
// Mask (unknown).
|
||||
this.mask = Buffer.alloc(32, 0x00);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the block.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verify() {
|
||||
if (!this.verifyPOW())
|
||||
return false;
|
||||
|
||||
if (!this.verifyBody())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify proof-of-work.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verifyPOW() {
|
||||
return consensus.verifyPOW(this.hash(), this.bits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the block.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verifyBody() {
|
||||
throw new Error('Abstract method.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the block to an inv item.
|
||||
* @returns {InvItem}
|
||||
*/
|
||||
|
||||
toInv() {
|
||||
return new InvItem(InvItem.types.BLOCK, this.hash());
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode from miner serialization.
|
||||
* @param {Buffer} data
|
||||
*/
|
||||
|
||||
static fromMiner(data) {
|
||||
return new this().fromMiner(data);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = AbstractBlock;
|
||||
566
docs/js-primitives/address.js
Normal file
566
docs/js-primitives/address.js
Normal file
|
|
@ -0,0 +1,566 @@
|
|||
/*!
|
||||
* address.js - address object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const bech32 = require('bcrypto/lib/encoding/bech32');
|
||||
const blake2b = require('bcrypto/lib/blake2b');
|
||||
const sha3 = require('bcrypto/lib/sha3');
|
||||
const Network = require('../protocol/network');
|
||||
const consensus = require('../protocol/consensus');
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').NetworkType} NetworkType */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('../script/script')} Script */
|
||||
/** @typedef {import('../script/witness')} Witness */
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const ZERO_HASH160 = Buffer.alloc(20, 0x00);
|
||||
|
||||
/**
|
||||
* @typedef {Object} AddressOptions
|
||||
* @property {Hash} hash
|
||||
* @property {Number} version
|
||||
*/
|
||||
|
||||
/**
|
||||
* Address
|
||||
* Represents an address.
|
||||
* @alias module:primitives.Address
|
||||
* @property {Number} version
|
||||
* @property {Buffer} hash
|
||||
*/
|
||||
|
||||
class Address extends bio.Struct {
|
||||
/**
|
||||
* Create an address.
|
||||
* @constructor
|
||||
* @param {AddressOptions|String} [options]
|
||||
* @param {(NetworkType|Network)?} [network]
|
||||
*/
|
||||
|
||||
constructor(options, network) {
|
||||
super();
|
||||
|
||||
this.version = 0;
|
||||
this.hash = ZERO_HASH160;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options, network);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {AddressOptions|String} options
|
||||
* @param {(NetworkType|Network)?} [network]
|
||||
*/
|
||||
|
||||
fromOptions(options, network) {
|
||||
if (typeof options === 'string')
|
||||
return this.fromString(options, network);
|
||||
|
||||
assert(options);
|
||||
|
||||
const {hash, version} = options;
|
||||
|
||||
return this.fromHash(hash, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the sigops in a script, taking into account witness programs.
|
||||
* @param {Witness} witness
|
||||
* @returns {Number} sigop count
|
||||
*/
|
||||
|
||||
getSigops(witness) {
|
||||
if (this.version === 0) {
|
||||
if (this.hash.length === 20)
|
||||
return 1;
|
||||
|
||||
if (this.hash.length === 32 && witness.items.length > 0) {
|
||||
const redeem = witness.getRedeem();
|
||||
return redeem.getSigops();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address hash.
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
getHash() {
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the address is null.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isNull() {
|
||||
if (this.hash.length === 20)
|
||||
return this.hash.equals(ZERO_HASH160);
|
||||
|
||||
if (this.hash.length === 32)
|
||||
return this.hash.equals(consensus.ZERO_HASH);
|
||||
|
||||
for (let i = 0; i < this.hash.length; i++) {
|
||||
if (this.hash[i] !== 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the address is unspendable.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isUnspendable() {
|
||||
return this.isNulldata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test equality against another address.
|
||||
* @param {Address} addr
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
equals(addr) {
|
||||
assert(addr instanceof Address);
|
||||
|
||||
return this.version === addr.version
|
||||
&& this.hash.equals(addr.hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare against another address.
|
||||
* @param {Address} addr
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
compare(addr) {
|
||||
assert(addr instanceof Address);
|
||||
|
||||
const cmp = this.version - addr.version;
|
||||
|
||||
if (cmp !== 0)
|
||||
return cmp;
|
||||
|
||||
return this.hash.compare(addr.hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from another address.
|
||||
* @param {Address} addr
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
inject(addr) {
|
||||
this.version = addr.version;
|
||||
this.hash = addr.hash;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone address.
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
clone() {
|
||||
// @ts-ignore
|
||||
return new this.constructor().inject(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the address object to a bech32 address.
|
||||
* @param {(NetworkType|Network)?} [network]
|
||||
* @returns {String}
|
||||
* @throws Error on bad hash/prefix.
|
||||
*/
|
||||
|
||||
toString(network) {
|
||||
const version = this.version;
|
||||
const hash = this.hash;
|
||||
|
||||
assert(version <= 31);
|
||||
assert(hash.length >= 2 && hash.length <= 40);
|
||||
|
||||
network = Network.get(network);
|
||||
|
||||
const hrp = network.addressPrefix;
|
||||
|
||||
return bech32.encode(hrp, version, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate address from pubkey.
|
||||
* @param {Buffer} key
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
fromPubkey(key) {
|
||||
assert(Buffer.isBuffer(key) && key.length === 33);
|
||||
return this.fromHash(blake2b.digest(key, 20), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate address from script.
|
||||
* @param {Script} script
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
fromScript(script) {
|
||||
assert(script && typeof script.encode === 'function');
|
||||
return this.fromHash(sha3.digest(script.encode()), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from bech32 address.
|
||||
* @param {String} data
|
||||
* @param {(NetworkType|Network)?} [network]
|
||||
* @throws Parse error
|
||||
*/
|
||||
|
||||
fromString(data, network) {
|
||||
assert(typeof data === 'string');
|
||||
|
||||
const [hrp, version, hash] = bech32.decode(data);
|
||||
|
||||
Network.fromAddress(hrp, network);
|
||||
|
||||
return this.fromHash(hash, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from witness.
|
||||
* @param {Witness} witness
|
||||
* @returns {Address|null}
|
||||
*/
|
||||
|
||||
fromWitness(witness) {
|
||||
const [, pk] = witness.getPubkeyhashInput();
|
||||
|
||||
if (pk) {
|
||||
this.hash = blake2b.digest(pk, 20);
|
||||
this.version = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
const redeem = witness.getScripthashInput();
|
||||
|
||||
if (redeem) {
|
||||
this.hash = sha3.digest(redeem);
|
||||
this.version = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from a hash.
|
||||
* @param {Hash} hash
|
||||
* @param {Number} [version=0]
|
||||
* @throws on bad hash size
|
||||
*/
|
||||
|
||||
fromHash(hash, version) {
|
||||
if (version == null)
|
||||
version = 0;
|
||||
|
||||
assert(Buffer.isBuffer(hash));
|
||||
assert((version & 0xff) === version);
|
||||
|
||||
assert(version >= 0 && version <= 31, 'Bad program version.');
|
||||
assert(hash.length >= 2 && hash.length <= 40, 'Hash is the wrong size.');
|
||||
|
||||
if (version === 0) {
|
||||
assert(hash.length === 20 || hash.length === 32,
|
||||
'Witness program hash is the wrong size.');
|
||||
}
|
||||
|
||||
this.hash = hash;
|
||||
this.version = version;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from witness pubkeyhash.
|
||||
* @param {Buffer} hash
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
fromPubkeyhash(hash) {
|
||||
assert(hash && hash.length === 20, 'P2WPKH must be 20 bytes.');
|
||||
return this.fromHash(hash, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from witness scripthash.
|
||||
* @param {Buffer} hash
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
fromScripthash(hash) {
|
||||
assert(hash && hash.length === 32, 'P2WSH must be 32 bytes.');
|
||||
return this.fromHash(hash, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from witness program.
|
||||
* @param {Number} version
|
||||
* @param {Buffer} hash
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
fromProgram(version, hash) {
|
||||
assert(version >= 0, 'Bad version for witness program.');
|
||||
return this.fromHash(hash, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate address from nulldata.
|
||||
* @param {Buffer} data
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
fromNulldata(data) {
|
||||
return this.fromHash(data, 31);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the address is witness pubkeyhash.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isPubkeyhash() {
|
||||
return this.version === 0 && this.hash.length === 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the address is witness scripthash.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isScripthash() {
|
||||
return this.version === 0 && this.hash.length === 32;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the address is unspendable.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isNulldata() {
|
||||
return this.version === 31;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the address is an unknown witness program.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isUnknown() {
|
||||
switch (this.version) {
|
||||
case 0:
|
||||
return this.hash.length !== 20 && this.hash.length !== 32;
|
||||
case 31:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test address validity.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isValid() {
|
||||
assert(this.version >= 0);
|
||||
|
||||
if (this.version > 31)
|
||||
return false;
|
||||
|
||||
if (this.hash.length < 2 || this.hash.length > 40)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate address size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 1 + 1 + this.hash.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write address to buffer writer.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeU8(this.version);
|
||||
bw.writeU8(this.hash.length);
|
||||
bw.writeBytes(this.hash);
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read address from buffer reader.
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
const version = br.readU8();
|
||||
assert(version <= 31);
|
||||
|
||||
const size = br.readU8();
|
||||
assert(size >= 2 && size <= 40);
|
||||
|
||||
const hash = br.readBytes(size);
|
||||
|
||||
return this.fromHash(hash, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect the Address.
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
format() {
|
||||
return '<Address:'
|
||||
+ ` version=${this.version}`
|
||||
+ ` str=${this.toString()}`
|
||||
+ '>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate address from pubkey.
|
||||
* @param {Buffer} key
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
static fromPubkey(key) {
|
||||
return new this().fromPubkey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate address from script.
|
||||
* @param {Script} script
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
static fromScript(script) {
|
||||
return new this().fromScript(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Address from a witness.
|
||||
* Attempt to extract address
|
||||
* properties from a witness.
|
||||
* @param {Witness} witness
|
||||
* @returns {Address|null}
|
||||
*/
|
||||
|
||||
static fromWitness(witness) {
|
||||
return new this().fromWitness(witness);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a naked address from hash/version.
|
||||
* @param {Hash} hash
|
||||
* @param {Number} [version=0]
|
||||
* @returns {Address}
|
||||
* @throws on bad hash size
|
||||
*/
|
||||
|
||||
static fromHash(hash, version) {
|
||||
return new this().fromHash(hash, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate address from witness pubkeyhash.
|
||||
* @param {Buffer} hash
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
static fromPubkeyhash(hash) {
|
||||
return new this().fromPubkeyhash(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate address from witness scripthash.
|
||||
* @param {Buffer} hash
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
static fromScripthash(hash) {
|
||||
return new this().fromScripthash(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate address from witness program.
|
||||
* @param {Number} version
|
||||
* @param {Buffer} hash
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
static fromProgram(version, hash) {
|
||||
return new this().fromProgram(version, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate address from nulldata.
|
||||
* @param {Buffer} data
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
static fromNulldata(data) {
|
||||
return new this().fromNulldata(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash of a base58 address or address-related object.
|
||||
* @param {Address|Hash} data
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
static getHash(data) {
|
||||
if (!data)
|
||||
throw new Error('Object is not an address.');
|
||||
|
||||
if (Buffer.isBuffer(data))
|
||||
return data;
|
||||
|
||||
if (data instanceof Address)
|
||||
return data.hash;
|
||||
|
||||
throw new Error('Object is not an address.');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Address;
|
||||
483
docs/js-primitives/airdropkey.js
Normal file
483
docs/js-primitives/airdropkey.js
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
/* eslint camelcase: 'off' */
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const base16 = require('bcrypto/lib/encoding/base16');
|
||||
const bech32 = require('bcrypto/lib/encoding/bech32');
|
||||
const BLAKE2b = require('bcrypto/lib/blake2b');
|
||||
const SHA256 = require('bcrypto/lib/sha256');
|
||||
const rsa = require('bcrypto/lib/rsa');
|
||||
const p256 = require('bcrypto/lib/p256');
|
||||
const ed25519 = require('bcrypto/lib/ed25519');
|
||||
const {countLeft} = require('bcrypto/lib/encoding/util');
|
||||
const Goo = require('goosig');
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').Amount} AmountValue */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
|
||||
/*
|
||||
* Goo
|
||||
*/
|
||||
|
||||
const goo = new Goo(Goo.RSA2048, 2, 3);
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const keyTypes = {
|
||||
RSA: 0,
|
||||
GOO: 1,
|
||||
P256: 2,
|
||||
ED25519: 3,
|
||||
ADDRESS: 4
|
||||
};
|
||||
|
||||
const keyTypesByVal = {
|
||||
[keyTypes.RSA]: 'RSA',
|
||||
[keyTypes.GOO]: 'GOO',
|
||||
[keyTypes.P256]: 'P256',
|
||||
[keyTypes.ED25519]: 'ED25519',
|
||||
[keyTypes.ADDRESS]: 'ADDRESS'
|
||||
};
|
||||
|
||||
const EMPTY = Buffer.alloc(0);
|
||||
|
||||
/**
|
||||
* AirdropKey
|
||||
*/
|
||||
|
||||
class AirdropKey extends bio.Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.type = keyTypes.RSA;
|
||||
this.n = EMPTY;
|
||||
this.e = EMPTY;
|
||||
this.C1 = EMPTY;
|
||||
this.point = EMPTY;
|
||||
this.version = 0;
|
||||
this.address = EMPTY;
|
||||
this.value = 0;
|
||||
this.sponsor = false;
|
||||
this.nonce = SHA256.zero;
|
||||
this.tweak = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AirdropKey} key
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
inject(key) {
|
||||
assert(key instanceof AirdropKey);
|
||||
|
||||
this.type = key.type;
|
||||
this.n = key.n;
|
||||
this.e = key.e;
|
||||
this.C1 = key.C1;
|
||||
this.point = key.point;
|
||||
this.version = key.version;
|
||||
this.address = key.address;
|
||||
this.value = key.value;
|
||||
this.sponsor = key.sponsor;
|
||||
this.nonce = key.nonce;
|
||||
this.tweak = key.tweak;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
isRSA() {
|
||||
return this.type === keyTypes.RSA;
|
||||
}
|
||||
|
||||
isGoo() {
|
||||
return this.type === keyTypes.GOO;
|
||||
}
|
||||
|
||||
isP256() {
|
||||
return this.type === keyTypes.P256;
|
||||
}
|
||||
|
||||
isED25519() {
|
||||
return this.type === keyTypes.ED25519;
|
||||
}
|
||||
|
||||
isAddress() {
|
||||
return this.type === keyTypes.ADDRESS;
|
||||
}
|
||||
|
||||
isWeak() {
|
||||
if (!this.isRSA())
|
||||
return false;
|
||||
|
||||
return countLeft(this.n) < 2048 - 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
validate() {
|
||||
switch (this.type) {
|
||||
case keyTypes.RSA: {
|
||||
let key;
|
||||
|
||||
try {
|
||||
key = rsa.publicKeyImport({ n: this.n, e: this.e });
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bits = rsa.publicKeyBits(key);
|
||||
|
||||
// Allow 1024 bit RSA for now.
|
||||
// We can softfork out later.
|
||||
return bits >= 1024 && bits <= 4096;
|
||||
}
|
||||
|
||||
case keyTypes.GOO: {
|
||||
return this.C1.length === goo.size;
|
||||
}
|
||||
|
||||
case keyTypes.P256: {
|
||||
return p256.publicKeyVerify(this.point);
|
||||
}
|
||||
|
||||
case keyTypes.ED25519: {
|
||||
return ed25519.publicKeyVerify(this.point);
|
||||
}
|
||||
|
||||
case keyTypes.ADDRESS: {
|
||||
return true;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new assert.AssertionError('Invalid key type.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} msg
|
||||
* @param {Buffer} sig
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verify(msg, sig) {
|
||||
assert(Buffer.isBuffer(msg));
|
||||
assert(Buffer.isBuffer(sig));
|
||||
|
||||
switch (this.type) {
|
||||
case keyTypes.RSA: {
|
||||
let key;
|
||||
|
||||
try {
|
||||
key = rsa.publicKeyImport({ n: this.n, e: this.e });
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return rsa.verify(SHA256, msg, sig, key);
|
||||
}
|
||||
|
||||
case keyTypes.GOO: {
|
||||
return goo.verify(msg, sig, this.C1);
|
||||
}
|
||||
|
||||
case keyTypes.P256: {
|
||||
return p256.verify(msg, sig, this.point);
|
||||
}
|
||||
|
||||
case keyTypes.ED25519: {
|
||||
return ed25519.verify(msg, sig, this.point);
|
||||
}
|
||||
|
||||
case keyTypes.ADDRESS: {
|
||||
return true;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new assert.AssertionError('Invalid key type.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
hash() {
|
||||
const bw = bio.pool(this.getSize());
|
||||
this.write(bw);
|
||||
return BLAKE2b.digest(bw.render());
|
||||
}
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
|
||||
size += 1;
|
||||
|
||||
switch (this.type) {
|
||||
case keyTypes.RSA:
|
||||
assert(this.n.length <= 0xffff);
|
||||
assert(this.e.length <= 0xff);
|
||||
size += 2;
|
||||
size += this.n.length;
|
||||
size += 1;
|
||||
size += this.e.length;
|
||||
size += 32;
|
||||
break;
|
||||
case keyTypes.GOO:
|
||||
size += goo.size;
|
||||
break;
|
||||
case keyTypes.P256:
|
||||
size += 33;
|
||||
size += 32;
|
||||
break;
|
||||
case keyTypes.ED25519:
|
||||
size += 32;
|
||||
size += 32;
|
||||
break;
|
||||
case keyTypes.ADDRESS:
|
||||
size += 1;
|
||||
size += 1;
|
||||
size += this.address.length;
|
||||
size += 8;
|
||||
size += 1;
|
||||
break;
|
||||
default:
|
||||
throw new assert.AssertionError('Invalid key type.');
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeU8(this.type);
|
||||
|
||||
switch (this.type) {
|
||||
case keyTypes.RSA:
|
||||
bw.writeU16(this.n.length);
|
||||
bw.writeBytes(this.n);
|
||||
bw.writeU8(this.e.length);
|
||||
bw.writeBytes(this.e);
|
||||
bw.writeBytes(this.nonce);
|
||||
break;
|
||||
case keyTypes.GOO:
|
||||
bw.writeBytes(this.C1);
|
||||
break;
|
||||
case keyTypes.P256:
|
||||
case keyTypes.ED25519:
|
||||
bw.writeBytes(this.point);
|
||||
bw.writeBytes(this.nonce);
|
||||
break;
|
||||
case keyTypes.ADDRESS:
|
||||
bw.writeU8(this.version);
|
||||
bw.writeU8(this.address.length);
|
||||
bw.writeBytes(this.address);
|
||||
bw.writeU64(this.value);
|
||||
bw.writeU8(this.sponsor ? 1 : 0);
|
||||
break;
|
||||
default:
|
||||
throw new assert.AssertionError('Invalid key type.');
|
||||
}
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.type = br.readU8();
|
||||
|
||||
switch (this.type) {
|
||||
case keyTypes.RSA: {
|
||||
this.n = br.readBytes(br.readU16());
|
||||
this.e = br.readBytes(br.readU8());
|
||||
this.nonce = br.readBytes(32);
|
||||
break;
|
||||
}
|
||||
|
||||
case keyTypes.GOO: {
|
||||
this.C1 = br.readBytes(goo.size);
|
||||
break;
|
||||
}
|
||||
|
||||
case keyTypes.P256: {
|
||||
this.point = br.readBytes(33);
|
||||
this.nonce = br.readBytes(32);
|
||||
break;
|
||||
}
|
||||
|
||||
case keyTypes.ED25519: {
|
||||
this.point = br.readBytes(32);
|
||||
this.nonce = br.readBytes(32);
|
||||
break;
|
||||
}
|
||||
|
||||
case keyTypes.ADDRESS: {
|
||||
this.version = br.readU8();
|
||||
this.address = br.readBytes(br.readU8());
|
||||
this.value = br.readU64();
|
||||
this.sponsor = br.readU8() === 1;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error('Unknown key type.');
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} addr
|
||||
* @param {AmountValue} value
|
||||
* @param {Boolean} sponsor
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromAddress(addr, value, sponsor = false) {
|
||||
assert(typeof addr === 'string');
|
||||
assert(Number.isSafeInteger(value) && value >= 0);
|
||||
assert(typeof sponsor === 'boolean');
|
||||
|
||||
const [hrp, version, hash] = bech32.decode(addr);
|
||||
|
||||
assert(hrp === 'hs' || hrp === 'ts' || hrp === 'rs');
|
||||
assert(version === 0);
|
||||
assert(hash.length === 20 || hash.length === 32);
|
||||
|
||||
this.type = keyTypes.ADDRESS;
|
||||
this.version = version;
|
||||
this.address = hash;
|
||||
this.value = value;
|
||||
this.sponsor = sponsor;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
type: keyTypesByVal[this.type] || 'UNKNOWN',
|
||||
n: this.n.length > 0
|
||||
? this.n.toString('hex')
|
||||
: undefined,
|
||||
e: this.e.length > 0
|
||||
? this.e.toString('hex')
|
||||
: undefined,
|
||||
C1: this.C1.length > 0
|
||||
? this.C1.toString('hex')
|
||||
: undefined,
|
||||
point: this.point.length > 0
|
||||
? this.point.toString('hex')
|
||||
: undefined,
|
||||
version: this.address.length > 0
|
||||
? this.version
|
||||
: undefined,
|
||||
address: this.address.length > 0
|
||||
? this.address.toString('hex')
|
||||
: undefined,
|
||||
value: this.value || undefined,
|
||||
sponsor: this.value
|
||||
? this.sponsor
|
||||
: undefined,
|
||||
nonce: !this.isGoo() && !this.isAddress()
|
||||
? this.nonce.toString('hex')
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} json
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object');
|
||||
assert(typeof json.type === 'string');
|
||||
assert(Object.prototype.hasOwnProperty.call(keyTypes, json.type));
|
||||
|
||||
this.type = keyTypes[json.type];
|
||||
|
||||
console.log(base16.decode.toString());
|
||||
switch (this.type) {
|
||||
case keyTypes.RSA: {
|
||||
this.n = base16.decode(json.n);
|
||||
this.e = base16.decode(json.e);
|
||||
this.nonce = base16.decode(json.nonce);
|
||||
break;
|
||||
}
|
||||
|
||||
case keyTypes.GOO: {
|
||||
this.C1 = base16.decode(json.C1);
|
||||
break;
|
||||
}
|
||||
|
||||
case keyTypes.P256: {
|
||||
this.point = base16.decode(json.point);
|
||||
this.nonce = base16.decode(json.nonce);
|
||||
break;
|
||||
}
|
||||
|
||||
case keyTypes.ED25519: {
|
||||
this.point = base16.decode(json.point);
|
||||
this.nonce = base16.decode(json.nonce);
|
||||
break;
|
||||
}
|
||||
|
||||
case keyTypes.ADDRESS: {
|
||||
assert((json.version & 0xff) === json.version);
|
||||
assert(Number.isSafeInteger(json.value) && json.value >= 0);
|
||||
assert(typeof json.sponsor === 'boolean');
|
||||
this.version = json.version;
|
||||
this.address = base16.decode(json.address);
|
||||
this.value = json.value;
|
||||
this.sponsor = json.sponsor;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error('Unknown key type.');
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} addr
|
||||
* @param {AmountValue} value
|
||||
* @param {Boolean} sponsor
|
||||
* @returns {AirdropKey}
|
||||
*/
|
||||
|
||||
static fromAddress(addr, value, sponsor) {
|
||||
return new this().fromAddress(addr, value, sponsor);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Static
|
||||
*/
|
||||
|
||||
AirdropKey.keyTypes = keyTypes;
|
||||
AirdropKey.keyTypesByVal = keyTypesByVal;
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = AirdropKey;
|
||||
520
docs/js-primitives/airdropproof.js
Normal file
520
docs/js-primitives/airdropproof.js
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const base16 = require('bcrypto/lib/encoding/base16');
|
||||
const blake2b = require('bcrypto/lib/blake2b');
|
||||
const sha256 = require('bcrypto/lib/sha256');
|
||||
const merkle = require('bcrypto/lib/mrkl');
|
||||
const AirdropKey = require('./airdropkey');
|
||||
const InvItem = require('./invitem');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const {keyTypes} = AirdropKey;
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const EMPTY = Buffer.alloc(0);
|
||||
const SPONSOR_FEE = 500e6;
|
||||
const RECIPIENT_FEE = 100e6;
|
||||
|
||||
// SHA256("HNS Signature")
|
||||
const CONTEXT = Buffer.from(
|
||||
'5b21ff4a0fcf78123915eaa0003d2a3e1855a9b15e3441da2ef5a4c01eaf4ff3',
|
||||
'hex');
|
||||
|
||||
const AIRDROP_ROOT = Buffer.from(
|
||||
'10d748eda1b9c67b94d3244e0211677618a9b4b329e896ad90431f9f48034bad',
|
||||
'hex');
|
||||
|
||||
const AIRDROP_REWARD = 4246994314;
|
||||
const AIRDROP_DEPTH = 18;
|
||||
const AIRDROP_SUBDEPTH = 3;
|
||||
const AIRDROP_LEAVES = 216199;
|
||||
const AIRDROP_SUBLEAVES = 8;
|
||||
|
||||
const FAUCET_ROOT = Buffer.from(
|
||||
'e2c0299a1e466773516655f09a64b1e16b2579530de6c4a59ce5654dea45180f',
|
||||
'hex');
|
||||
|
||||
const FAUCET_DEPTH = 11;
|
||||
const FAUCET_LEAVES = 1358;
|
||||
|
||||
const TREE_LEAVES = AIRDROP_LEAVES + FAUCET_LEAVES;
|
||||
|
||||
const MAX_PROOF_SIZE = 3400; // 3253
|
||||
|
||||
/** @typedef {ReturnType<AirdropProof['getJSON']>} AirdropProofJSON */
|
||||
|
||||
/**
|
||||
* AirdropProof
|
||||
*/
|
||||
|
||||
class AirdropProof extends bio.Struct {
|
||||
constructor() {
|
||||
super();
|
||||
this.index = 0;
|
||||
/** @type {Hash[]} */
|
||||
this.proof = [];
|
||||
this.subindex = 0;
|
||||
/** @type {Hash[]} */
|
||||
this.subproof = [];
|
||||
this.key = EMPTY;
|
||||
this.version = 0;
|
||||
this.address = EMPTY;
|
||||
this.fee = 0;
|
||||
this.signature = EMPTY;
|
||||
}
|
||||
|
||||
getSize(sighash = false) {
|
||||
let size = 0;
|
||||
|
||||
if (sighash)
|
||||
size += 32;
|
||||
|
||||
size += 4;
|
||||
size += 1;
|
||||
size += this.proof.length * 32;
|
||||
size += 1;
|
||||
size += 1;
|
||||
size += this.subproof.length * 32;
|
||||
size += bio.sizeVarBytes(this.key);
|
||||
size += 1;
|
||||
size += 1;
|
||||
size += this.address.length;
|
||||
size += bio.sizeVarint(this.fee);
|
||||
|
||||
if (!sighash)
|
||||
size += bio.sizeVarBytes(this.signature);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BufioWriter} bw
|
||||
* @param {Boolean} [sighash=false]
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw, sighash = false) {
|
||||
if (sighash)
|
||||
bw.writeBytes(CONTEXT);
|
||||
|
||||
bw.writeU32(this.index);
|
||||
bw.writeU8(this.proof.length);
|
||||
|
||||
for (const hash of this.proof)
|
||||
bw.writeBytes(hash);
|
||||
|
||||
bw.writeU8(this.subindex);
|
||||
bw.writeU8(this.subproof.length);
|
||||
|
||||
for (const hash of this.subproof)
|
||||
bw.writeBytes(hash);
|
||||
|
||||
bw.writeVarBytes(this.key);
|
||||
bw.writeU8(this.version);
|
||||
bw.writeU8(this.address.length);
|
||||
bw.writeBytes(this.address);
|
||||
bw.writeVarint(this.fee);
|
||||
|
||||
if (!sighash)
|
||||
bw.writeVarBytes(this.signature);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} data
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
decode(data) {
|
||||
const br = bio.read(data);
|
||||
|
||||
if (data.length > MAX_PROOF_SIZE)
|
||||
throw new Error('Proof too large.');
|
||||
|
||||
this.read(br);
|
||||
|
||||
if (br.left() !== 0)
|
||||
throw new Error('Trailing data.');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.index = br.readU32();
|
||||
assert(this.index < AIRDROP_LEAVES);
|
||||
|
||||
const count = br.readU8();
|
||||
assert(count <= AIRDROP_DEPTH);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const hash = br.readBytes(32);
|
||||
this.proof.push(hash);
|
||||
}
|
||||
|
||||
this.subindex = br.readU8();
|
||||
assert(this.subindex < AIRDROP_SUBLEAVES);
|
||||
|
||||
const total = br.readU8();
|
||||
assert(total <= AIRDROP_SUBDEPTH);
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
const hash = br.readBytes(32);
|
||||
this.subproof.push(hash);
|
||||
}
|
||||
|
||||
this.key = br.readVarBytes();
|
||||
assert(this.key.length > 0);
|
||||
|
||||
this.version = br.readU8();
|
||||
|
||||
assert(this.version <= 31);
|
||||
|
||||
const size = br.readU8();
|
||||
assert(size >= 2 && size <= 40);
|
||||
|
||||
this.address = br.readBytes(size);
|
||||
this.fee = br.readVarint();
|
||||
this.signature = br.readVarBytes();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
hash() {
|
||||
const bw = bio.pool(this.getSize());
|
||||
this.write(bw);
|
||||
return blake2b.digest(bw.render());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Hash} [expect]
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verifyMerkle(expect) {
|
||||
if (expect == null) {
|
||||
expect = this.isAddress()
|
||||
? FAUCET_ROOT
|
||||
: AIRDROP_ROOT;
|
||||
}
|
||||
|
||||
assert(Buffer.isBuffer(expect));
|
||||
assert(expect.length === 32);
|
||||
|
||||
const {subproof, subindex} = this;
|
||||
const {proof, index} = this;
|
||||
const leaf = blake2b.digest(this.key);
|
||||
|
||||
if (this.isAddress()) {
|
||||
const root = merkle.deriveRoot(blake2b, leaf, proof, index);
|
||||
|
||||
return root.equals(expect);
|
||||
}
|
||||
|
||||
const subroot = merkle.deriveRoot(blake2b, leaf, subproof, subindex);
|
||||
const root = merkle.deriveRoot(blake2b, subroot, proof, index);
|
||||
|
||||
return root.equals(expect);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
signatureData() {
|
||||
const size = this.getSize(true);
|
||||
const bw = bio.pool(size);
|
||||
|
||||
this.write(bw, true);
|
||||
|
||||
return bw.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
signatureHash() {
|
||||
return sha256.digest(this.signatureData());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {AirdropKey|null}
|
||||
*/
|
||||
|
||||
getKey() {
|
||||
try {
|
||||
return AirdropKey.decode(this.key);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verifySignature() {
|
||||
const key = this.getKey();
|
||||
|
||||
if (!key)
|
||||
return false;
|
||||
|
||||
if (key.isAddress()) {
|
||||
const fee = key.sponsor
|
||||
? SPONSOR_FEE
|
||||
: RECIPIENT_FEE;
|
||||
|
||||
return this.version === key.version
|
||||
&& this.address.equals(key.address)
|
||||
&& this.fee === fee
|
||||
&& this.signature.length === 0;
|
||||
}
|
||||
|
||||
const msg = this.signatureHash();
|
||||
|
||||
return key.verify(msg, this.signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
position() {
|
||||
let index = this.index;
|
||||
|
||||
// Position in the bitfield.
|
||||
// Bitfield is organized as:
|
||||
// [airdrop-bits] || [faucet-bits]
|
||||
if (this.isAddress()) {
|
||||
assert(index < FAUCET_LEAVES);
|
||||
index += AIRDROP_LEAVES;
|
||||
} else {
|
||||
assert(index < AIRDROP_LEAVES);
|
||||
}
|
||||
|
||||
assert(index < TREE_LEAVES);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
toTX(TX, Input, Output) {
|
||||
const tx = new TX();
|
||||
|
||||
tx.inputs.push(new Input());
|
||||
tx.outputs.push(new Output());
|
||||
|
||||
const input = new Input();
|
||||
const output = new Output();
|
||||
|
||||
input.witness.items.push(this.encode());
|
||||
|
||||
output.value = this.getValue() - this.fee;
|
||||
output.address.version = this.version;
|
||||
output.address.hash = this.address;
|
||||
|
||||
tx.inputs.push(input);
|
||||
tx.outputs.push(output);
|
||||
|
||||
tx.refresh();
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
toInv() {
|
||||
return new InvItem(InvItem.types.AIRDROP, this.hash());
|
||||
}
|
||||
|
||||
getWeight() {
|
||||
return this.getSize();
|
||||
}
|
||||
|
||||
getVirtualSize() {
|
||||
const scale = consensus.WITNESS_SCALE_FACTOR;
|
||||
return (this.getWeight() + scale - 1) / scale | 0;
|
||||
}
|
||||
|
||||
isWeak() {
|
||||
const key = this.getKey();
|
||||
|
||||
if (!key)
|
||||
return false;
|
||||
|
||||
return key.isWeak();
|
||||
}
|
||||
|
||||
isAddress() {
|
||||
if (this.key.length === 0)
|
||||
return false;
|
||||
|
||||
return this.key[0] === keyTypes.ADDRESS;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
if (!this.isAddress())
|
||||
return AIRDROP_REWARD;
|
||||
|
||||
const key = this.getKey();
|
||||
|
||||
if (!key)
|
||||
return 0;
|
||||
|
||||
return key.value;
|
||||
}
|
||||
|
||||
isSane() {
|
||||
if (this.key.length === 0)
|
||||
return false;
|
||||
|
||||
if (this.version > 31)
|
||||
return false;
|
||||
|
||||
if (this.address.length < 2 || this.address.length > 40)
|
||||
return false;
|
||||
|
||||
const value = this.getValue();
|
||||
|
||||
if (value < 0 || value > consensus.MAX_MONEY)
|
||||
return false;
|
||||
|
||||
if (this.fee < 0 || this.fee > value)
|
||||
return false;
|
||||
|
||||
if (this.isAddress()) {
|
||||
if (this.subproof.length !== 0)
|
||||
return false;
|
||||
|
||||
if (this.subindex !== 0)
|
||||
return false;
|
||||
|
||||
if (this.proof.length > FAUCET_DEPTH)
|
||||
return false;
|
||||
|
||||
if (this.index >= FAUCET_LEAVES)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.subproof.length > AIRDROP_SUBDEPTH)
|
||||
return false;
|
||||
|
||||
if (this.subindex >= AIRDROP_SUBLEAVES)
|
||||
return false;
|
||||
|
||||
if (this.proof.length > AIRDROP_DEPTH)
|
||||
return false;
|
||||
|
||||
if (this.index >= AIRDROP_LEAVES)
|
||||
return false;
|
||||
|
||||
if (this.getSize() > MAX_PROOF_SIZE)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Hash} [expect]
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verify(expect) {
|
||||
if (!this.isSane())
|
||||
return false;
|
||||
|
||||
if (!this.verifyMerkle(expect))
|
||||
return false;
|
||||
|
||||
if (!this.verifySignature())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
const key = this.getKey();
|
||||
|
||||
return {
|
||||
index: this.index,
|
||||
proof: this.proof.map(h => h.toString('hex')),
|
||||
subindex: this.subindex,
|
||||
subproof: this.subproof.map(h => h.toString('hex')),
|
||||
key: key ? key.toJSON() : null,
|
||||
version: this.version,
|
||||
address: this.address.toString('hex'),
|
||||
fee: this.fee,
|
||||
signature: this.signature.toString('hex')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AirdropProofJSON} json
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object');
|
||||
assert((json.index >>> 0) === json.index);
|
||||
assert(Array.isArray(json.proof));
|
||||
assert((json.subindex >>> 0) === json.subindex);
|
||||
assert(Array.isArray(json.subproof));
|
||||
assert(json.key == null || (json.key && typeof json.key === 'object'));
|
||||
assert((json.version & 0xff) === json.version);
|
||||
assert(typeof json.address === 'string');
|
||||
assert(Number.isSafeInteger(json.fee) && json.fee >= 0);
|
||||
assert(typeof json.signature === 'string');
|
||||
|
||||
this.index = json.index;
|
||||
|
||||
for (const hash of json.proof)
|
||||
this.proof.push(base16.decode(hash));
|
||||
|
||||
this.subindex = json.subindex;
|
||||
|
||||
for (const hash of json.subproof)
|
||||
this.subproof.push(base16.decode(hash));
|
||||
|
||||
if (json.key)
|
||||
this.key = AirdropKey.fromJSON(json.key).encode();
|
||||
|
||||
this.version = json.version;
|
||||
this.address = base16.decode(json.address);
|
||||
this.fee = json.fee;
|
||||
this.signature = base16.decode(json.signature);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Static
|
||||
*/
|
||||
|
||||
AirdropProof.AIRDROP_ROOT = AIRDROP_ROOT;
|
||||
AirdropProof.FAUCET_ROOT = FAUCET_ROOT;
|
||||
AirdropProof.TREE_LEAVES = TREE_LEAVES;
|
||||
AirdropProof.AIRDROP_LEAVES = AIRDROP_LEAVES;
|
||||
AirdropProof.FAUCET_LEAVES = FAUCET_LEAVES;
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = AirdropProof;
|
||||
550
docs/js-primitives/block.js
Normal file
550
docs/js-primitives/block.js
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
/*!
|
||||
* block.js - block object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const {BufferSet} = require('buffer-map');
|
||||
const blake2b = require('bcrypto/lib/blake2b');
|
||||
const merkle = require('bcrypto/lib/mrkl');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const AbstractBlock = require('./abstractblock');
|
||||
const TX = require('./tx');
|
||||
const MerkleBlock = require('./merkleblock');
|
||||
const Headers = require('./headers');
|
||||
const Network = require('../protocol/network');
|
||||
const util = require('../utils/util');
|
||||
const {encoding} = bio;
|
||||
|
||||
/** @typedef {import('bfilter').BloomFilter} BloomFilter */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').Amount} AmountValue */
|
||||
/** @typedef {import('../types').RawBlock} RawBlock */
|
||||
/** @typedef {import('../coins/coinview')} CoinView */
|
||||
|
||||
/**
|
||||
* Block
|
||||
* Represents a full block.
|
||||
* @alias module:primitives.Block
|
||||
* @extends AbstractBlock
|
||||
*/
|
||||
|
||||
class Block extends AbstractBlock {
|
||||
/**
|
||||
* Create a block.
|
||||
* @constructor
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
/** @type {TX[]} */
|
||||
this.txs = [];
|
||||
|
||||
/** @type {Buffer?} */
|
||||
this._raw = null;
|
||||
/** @type {Sizes?} */
|
||||
this._sizes = null;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
this.parseOptions(options);
|
||||
|
||||
if (options.txs) {
|
||||
assert(Array.isArray(options.txs));
|
||||
for (const tx of options.txs) {
|
||||
assert(tx instanceof TX);
|
||||
this.txs.push(tx);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any cached values.
|
||||
* @param {Boolean?} [all] - Clear transactions.
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
refresh(all) {
|
||||
this._refresh();
|
||||
|
||||
this._raw = null;
|
||||
this._sizes = null;
|
||||
|
||||
if (!all)
|
||||
return this;
|
||||
|
||||
for (const tx of this.txs)
|
||||
tx.refresh();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate virtual block size.
|
||||
* @returns {Number} Virtual size.
|
||||
*/
|
||||
|
||||
getVirtualSize() {
|
||||
const scale = consensus.WITNESS_SCALE_FACTOR;
|
||||
return (this.getWeight() + scale - 1) / scale | 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate block weight.
|
||||
* @returns {Number} weight
|
||||
*/
|
||||
|
||||
getWeight() {
|
||||
const {base, witness} = this.getSizes();
|
||||
const total = base + witness;
|
||||
return base * (consensus.WITNESS_SCALE_FACTOR - 1) + total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get real block size.
|
||||
* @returns {Number} size
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
const {base, witness} = this.getSizes();
|
||||
return base + witness;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base block size (without witness).
|
||||
* @returns {Number} size
|
||||
*/
|
||||
|
||||
getBaseSize() {
|
||||
const {base} = this.getSizes();
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the block contains a
|
||||
* transaction with a non-empty witness.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
hasWitness() {
|
||||
for (const tx of this.txs) {
|
||||
if (tx.hasWitness())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the block's transaction vector against a hash.
|
||||
* @param {Hash} hash
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
hasTX(hash) {
|
||||
return this.indexOf(hash) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the index of a transaction in the block.
|
||||
* @param {Hash} hash
|
||||
* @returns {Number} index (-1 if not present).
|
||||
*/
|
||||
|
||||
indexOf(hash) {
|
||||
for (let i = 0; i < this.txs.length; i++) {
|
||||
const tx = this.txs[i];
|
||||
if (tx.hash().equals(hash))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate merkle root.
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
createMerkleRoot() {
|
||||
const leaves = [];
|
||||
|
||||
for (const tx of this.txs)
|
||||
leaves.push(tx.hash());
|
||||
|
||||
return merkle.createRoot(blake2b, leaves);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate witness root.
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
createWitnessRoot() {
|
||||
const leaves = [];
|
||||
|
||||
for (const tx of this.txs)
|
||||
leaves.push(tx.witnessHash());
|
||||
|
||||
return merkle.createRoot(blake2b, leaves);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the merkle root from the block header.
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
getMerkleRoot() {
|
||||
return this.merkleRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do non-contextual verification on the block. Including checking the block
|
||||
* size, the coinbase and the merkle root. This is consensus-critical.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verifyBody() {
|
||||
const [valid] = this.checkBody();
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do non-contextual verification on the block. Including checking the block
|
||||
* size, the coinbase and the merkle root. This is consensus-critical.
|
||||
* @returns {Array} [valid, reason, score]
|
||||
*/
|
||||
|
||||
checkBody() {
|
||||
// Check base size.
|
||||
if (this.txs.length === 0
|
||||
|| this.txs.length > consensus.MAX_BLOCK_SIZE
|
||||
|| this.getBaseSize() > consensus.MAX_BLOCK_SIZE) {
|
||||
return [false, 'bad-blk-length', 100];
|
||||
}
|
||||
|
||||
// Check block weight.
|
||||
if (this.getWeight() > consensus.MAX_BLOCK_WEIGHT)
|
||||
return [false, 'bad-blk-weight', 100];
|
||||
|
||||
// Check merkle root.
|
||||
const merkleRoot = this.createMerkleRoot();
|
||||
|
||||
if (merkleRoot.equals(consensus.ZERO_HASH))
|
||||
return [false, 'bad-txnmrklroot', 100];
|
||||
|
||||
if (!merkleRoot.equals(this.merkleRoot))
|
||||
return [false, 'bad-txnmrklroot', 100];
|
||||
|
||||
// Check witness root.
|
||||
const witnessRoot = this.createWitnessRoot();
|
||||
|
||||
if (!witnessRoot.equals(this.witnessRoot))
|
||||
return [false, 'bad-witnessroot', 100];
|
||||
|
||||
// First TX must be a coinbase.
|
||||
if (this.txs.length === 0 || !this.txs[0].isCoinbase())
|
||||
return [false, 'bad-cb-missing', 100];
|
||||
|
||||
// Test all transactions.
|
||||
for (let i = 0; i < this.txs.length; i++) {
|
||||
const tx = this.txs[i];
|
||||
|
||||
// The rest of the txs must not be coinbases.
|
||||
if (i > 0 && tx.isCoinbase())
|
||||
return [false, 'bad-cb-multiple', 100];
|
||||
|
||||
// Sanity checks.
|
||||
const [valid, reason, score] = tx.checkSanity();
|
||||
|
||||
if (!valid)
|
||||
return [valid, reason, score];
|
||||
}
|
||||
|
||||
return [true, 'valid', 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the coinbase height from the coinbase input script.
|
||||
* @returns {Number} height (-1 if not present).
|
||||
*/
|
||||
|
||||
getCoinbaseHeight() {
|
||||
if (this.txs.length === 0)
|
||||
return -1;
|
||||
|
||||
const cb = this.txs[0];
|
||||
return cb.locktime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "claimed" reward by the coinbase.
|
||||
* @returns {AmountValue} claimed
|
||||
*/
|
||||
|
||||
getClaimed() {
|
||||
assert(this.txs.length > 0);
|
||||
assert(this.txs[0].isCoinbase());
|
||||
return this.txs[0].getOutputValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique outpoint hashes in the
|
||||
* block. Coinbases are ignored.
|
||||
* @returns {Hash[]} Outpoint hashes.
|
||||
*/
|
||||
|
||||
getPrevout() {
|
||||
const prevout = new BufferSet();
|
||||
|
||||
for (let i = 1; i < this.txs.length; i++) {
|
||||
const tx = this.txs[i];
|
||||
|
||||
for (const input of tx.inputs)
|
||||
prevout.add(input.prevout.hash);
|
||||
}
|
||||
|
||||
return prevout.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect the block and return a more
|
||||
* user-friendly representation of the data.
|
||||
* @param {CoinView} [view]
|
||||
* @param {Number} [height]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format(view, height) {
|
||||
return {
|
||||
hash: this.hash().toString('hex'),
|
||||
height: height != null ? height : -1,
|
||||
size: this.getSize(),
|
||||
virtualSize: this.getVirtualSize(),
|
||||
date: util.date(this.time),
|
||||
version: this.version.toString(16),
|
||||
prevBlock: this.prevBlock.toString('hex'),
|
||||
merkleRoot: this.merkleRoot.toString('hex'),
|
||||
witnessRoot: this.witnessRoot.toString('hex'),
|
||||
treeRoot: this.treeRoot.toString('hex'),
|
||||
reservedRoot: this.reservedRoot.toString('hex'),
|
||||
time: this.time,
|
||||
bits: this.bits,
|
||||
nonce: this.nonce,
|
||||
extraNonce: this.extraNonce.toString('hex'),
|
||||
mask: this.mask.toString('hex'),
|
||||
txs: this.txs.map((tx, i) => {
|
||||
return tx.format(view, null, i);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the block to an object suitable
|
||||
* for JSON serialization.
|
||||
* @param {Network} [network]
|
||||
* @param {CoinView} [view]
|
||||
* @param {Number} [height]
|
||||
* @param {Number} [depth]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON(network, view, height, depth) {
|
||||
network = Network.get(network);
|
||||
return {
|
||||
hash: this.hash().toString('hex'),
|
||||
height: height,
|
||||
depth: depth,
|
||||
version: this.version,
|
||||
prevBlock: this.prevBlock.toString('hex'),
|
||||
merkleRoot: this.merkleRoot.toString('hex'),
|
||||
witnessRoot: this.witnessRoot.toString('hex'),
|
||||
treeRoot: this.treeRoot.toString('hex'),
|
||||
reservedRoot: this.reservedRoot.toString('hex'),
|
||||
time: this.time,
|
||||
bits: this.bits,
|
||||
nonce: this.nonce,
|
||||
extraNonce: this.extraNonce.toString('hex'),
|
||||
mask: this.mask.toString('hex'),
|
||||
txs: this.txs.map((tx, i) => {
|
||||
return tx.getJSON(network, view, null, i);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from json object.
|
||||
* @param {Object} json
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json, 'Block data is required.');
|
||||
assert(Array.isArray(json.txs));
|
||||
|
||||
this.parseJSON(json);
|
||||
|
||||
for (const tx of json.txs)
|
||||
this.txs.push(TX.fromJSON(tx));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized data.
|
||||
* @param {bio.BufferReader} br
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
br.start();
|
||||
|
||||
this.readHead(br);
|
||||
|
||||
const count = br.readVarint();
|
||||
|
||||
let witness = 0;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const tx = TX.read(br);
|
||||
witness += tx._sizes.witness;
|
||||
this.txs.push(tx);
|
||||
}
|
||||
|
||||
if (!this.mutable) {
|
||||
const raw = br.endData();
|
||||
const base = raw.length - witness;
|
||||
this._raw = raw;
|
||||
this._sizes = new Sizes(base, witness);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the Block to a MerkleBlock.
|
||||
* @param {BloomFilter} filter - Bloom filter for transactions
|
||||
* to match. The merkle block will contain only the
|
||||
* matched transactions.
|
||||
* @returns {MerkleBlock}
|
||||
*/
|
||||
|
||||
toMerkle(filter) {
|
||||
return MerkleBlock.fromBlock(this, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
if (this._raw) {
|
||||
bw.writeBytes(this._raw);
|
||||
return bw;
|
||||
}
|
||||
|
||||
this.writeHead(bw);
|
||||
|
||||
bw.writeVarint(this.txs.length);
|
||||
|
||||
for (const tx of this.txs)
|
||||
tx.write(bw);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
encode() {
|
||||
if (this.mutable)
|
||||
return super.encode();
|
||||
|
||||
if (!this._raw)
|
||||
this._raw = super.encode();
|
||||
|
||||
return this._raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the block to a headers object.
|
||||
* @returns {Headers}
|
||||
*/
|
||||
|
||||
toHeaders() {
|
||||
return Headers.fromBlock(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get real block size with witness.
|
||||
* @returns {Sizes}
|
||||
*/
|
||||
|
||||
getSizes() {
|
||||
if (this._sizes)
|
||||
return this._sizes;
|
||||
|
||||
let base = 0;
|
||||
let witness = 0;
|
||||
|
||||
base += this.sizeHead();
|
||||
base += encoding.sizeVarint(this.txs.length);
|
||||
|
||||
for (const tx of this.txs) {
|
||||
const sizes = tx.getSizes();
|
||||
base += sizes.base;
|
||||
witness += sizes.witness;
|
||||
}
|
||||
|
||||
const sizes = new Sizes(base, witness);
|
||||
|
||||
if (!this.mutable)
|
||||
this._sizes = sizes;
|
||||
|
||||
return sizes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an object is a Block.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isBlock(obj) {
|
||||
return obj instanceof Block;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
class Sizes {
|
||||
constructor(base, witness) {
|
||||
this.base = base;
|
||||
this.witness = witness;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Block;
|
||||
352
docs/js-primitives/claim.js
Normal file
352
docs/js-primitives/claim.js
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
/*!
|
||||
* claim.js - DNSSEC ownership proofs for hsd
|
||||
* Copyright (c) 2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const blake2b = require('bcrypto/lib/blake2b');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const policy = require('../protocol/policy');
|
||||
const rules = require('../covenants/rules');
|
||||
const Ownership = require('../covenants/ownership');
|
||||
const InvItem = require('./invitem');
|
||||
const TX = require('./tx');
|
||||
const Input = require('./input');
|
||||
const Output = require('./output');
|
||||
const {OwnershipProof} = Ownership;
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').Amount} AmountValue */
|
||||
/** @typedef {import('../types').Rate} Rate */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('../protocol/network')} Network */
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const EMPTY = Buffer.alloc(0);
|
||||
|
||||
/**
|
||||
* Claim
|
||||
* @extends {bio.Struct}
|
||||
*/
|
||||
|
||||
class Claim extends bio.Struct {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.blob = EMPTY;
|
||||
|
||||
/** @type {Hash?} */
|
||||
this._hash = null;
|
||||
this._data = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
refresh() {
|
||||
this._hash = null;
|
||||
this._data = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
hash() {
|
||||
if (!this._hash)
|
||||
this._hash = blake2b.digest(this.blob);
|
||||
|
||||
return this._hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
hashHex() {
|
||||
return this.hash().toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Network} network
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getData(network) {
|
||||
if (!this._data) {
|
||||
const proof = this.getProof();
|
||||
|
||||
if (!proof)
|
||||
return null;
|
||||
|
||||
const data = proof.getData(network);
|
||||
|
||||
if (!data)
|
||||
return null;
|
||||
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
return this._data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 2 + this.blob.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeU16(this.blob.length);
|
||||
bw.writeBytes(this.blob);
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} data
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
decode(data) {
|
||||
const br = bio.read(data);
|
||||
|
||||
if (data.length > 2 + 10000)
|
||||
throw new Error('Proof too large.');
|
||||
|
||||
this.read(br);
|
||||
|
||||
if (br.left() !== 0)
|
||||
throw new Error('Trailing data.');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
const size = br.readU16();
|
||||
|
||||
if (size > 10000)
|
||||
throw new Error('Invalid claim size.');
|
||||
|
||||
this.blob = br.readBytes(size);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {InvItem}
|
||||
*/
|
||||
|
||||
toInv() {
|
||||
return new InvItem(InvItem.types.CLAIM, this.hash());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getWeight() {
|
||||
return this.getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getVirtualSize() {
|
||||
const scale = consensus.WITNESS_SCALE_FACTOR;
|
||||
return (this.getWeight() + scale - 1) / scale | 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} [size]
|
||||
* @param {Number} [rate]
|
||||
* @returns {AmountValue}
|
||||
*/
|
||||
|
||||
getMinFee(size, rate) {
|
||||
if (size == null)
|
||||
size = this.getVirtualSize();
|
||||
|
||||
return policy.getMinFee(size, rate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Network} [network]
|
||||
* @returns {AmountValue}
|
||||
*/
|
||||
|
||||
getFee(network) {
|
||||
const data = this.getData(network);
|
||||
assert(data);
|
||||
return data.fee;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} [size]
|
||||
* @param {Network} [network]
|
||||
* @returns {Rate}
|
||||
*/
|
||||
|
||||
getRate(size, network) {
|
||||
const fee = this.getFee(network);
|
||||
|
||||
if (size == null)
|
||||
size = this.getVirtualSize();
|
||||
|
||||
return policy.getRate(size, fee);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Network} network
|
||||
* @param {Number} height
|
||||
* @returns {TX}
|
||||
*/
|
||||
|
||||
toTX(network, height) {
|
||||
const data = this.getData(network);
|
||||
assert(data);
|
||||
|
||||
const tx = new TX();
|
||||
|
||||
tx.inputs.push(new Input());
|
||||
tx.outputs.push(new Output());
|
||||
|
||||
const input = new Input();
|
||||
input.witness.items.push(this.blob);
|
||||
|
||||
const output = new Output();
|
||||
|
||||
output.value = data.value - data.fee;
|
||||
|
||||
output.address.version = data.version;
|
||||
output.address.hash = data.hash;
|
||||
|
||||
let flags = 0;
|
||||
|
||||
if (data.weak)
|
||||
flags |= 1;
|
||||
|
||||
output.covenant.setClaim(
|
||||
rules.hashName(data.name),
|
||||
height,
|
||||
Buffer.from(data.name, 'binary'),
|
||||
flags,
|
||||
data.commitHash,
|
||||
data.commitHeight
|
||||
);
|
||||
|
||||
tx.inputs.push(input);
|
||||
tx.outputs.push(output);
|
||||
|
||||
tx.refresh();
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {OwnershipProof}
|
||||
*/
|
||||
|
||||
getProof() {
|
||||
try {
|
||||
return this.toProof();
|
||||
} catch (e) {
|
||||
return new OwnershipProof();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {OwnershipProof}
|
||||
*/
|
||||
|
||||
toProof() {
|
||||
return OwnershipProof.decode(this.blob);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
toBlob() {
|
||||
return this.blob;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON() {
|
||||
const proof = this.getProof();
|
||||
return proof.toJSON();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from blob.
|
||||
* @param {Buffer} blob
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromBlob(blob) {
|
||||
assert(Buffer.isBuffer(blob));
|
||||
this.blob = blob;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OwnershipProof} proof
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromProof(proof) {
|
||||
assert(proof instanceof OwnershipProof);
|
||||
this.blob = proof.encode();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate claim from raw proof.
|
||||
* @param {Buffer} blob
|
||||
* @returns {Claim}
|
||||
*/
|
||||
|
||||
static fromBlob(blob) {
|
||||
return new this().fromBlob(blob);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate claim from proof.
|
||||
* @param {OwnershipProof} proof
|
||||
* @returns {Claim}
|
||||
*/
|
||||
|
||||
static fromProof(proof) {
|
||||
return new this().fromProof(proof);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Claim;
|
||||
362
docs/js-primitives/coin.js
Normal file
362
docs/js-primitives/coin.js
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
/*!
|
||||
* coin.js - coin object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const Amount = require('../ui/amount');
|
||||
const Output = require('./output');
|
||||
const Network = require('../protocol/network');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const Outpoint = require('./outpoint');
|
||||
const util = require('../utils/util');
|
||||
|
||||
/** @typedef {import('bufio').BufferReader} BufferReader */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('../types').NetworkType} NetworkType */
|
||||
/** @typedef {import('../types').HexHash} HexHash */
|
||||
/** @typedef {import('./tx')} TX */
|
||||
|
||||
/**
|
||||
* Coin
|
||||
* Represents an unspent output.
|
||||
* @alias module:primitives.Coin
|
||||
* @extends Output
|
||||
* @property {Number} version
|
||||
* @property {Number} height
|
||||
* @property {Amount} value
|
||||
* @property {Script} script
|
||||
* @property {Boolean} coinbase
|
||||
* @property {Hash} hash
|
||||
* @property {Number} index
|
||||
*/
|
||||
|
||||
class Coin extends Output {
|
||||
/**
|
||||
* Create a coin.
|
||||
* @constructor
|
||||
* @param {Object?} [options]
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.version = 1;
|
||||
this.height = -1;
|
||||
this.coinbase = false;
|
||||
this.hash = consensus.ZERO_HASH;
|
||||
this.index = 0;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject options into coin.
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
assert(options, 'Coin data is required.');
|
||||
|
||||
if (options.version != null) {
|
||||
assert((options.version >>> 0) === options.version,
|
||||
'Version must be a uint32.');
|
||||
this.version = options.version;
|
||||
}
|
||||
|
||||
if (options.height != null) {
|
||||
if (options.height !== -1) {
|
||||
assert((options.height >>> 0) === options.height,
|
||||
'Height must be a uint32.');
|
||||
this.height = options.height;
|
||||
} else {
|
||||
this.height = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.value != null) {
|
||||
assert(Number.isSafeInteger(options.value) && options.value >= 0,
|
||||
'Value must be a uint64.');
|
||||
this.value = options.value;
|
||||
}
|
||||
|
||||
if (options.address)
|
||||
this.address.fromOptions(options.address);
|
||||
|
||||
if (options.covenant)
|
||||
this.covenant.fromOptions(options.covenant);
|
||||
|
||||
if (options.coinbase != null) {
|
||||
assert(typeof options.coinbase === 'boolean',
|
||||
'Coinbase must be a boolean.');
|
||||
this.coinbase = options.coinbase;
|
||||
}
|
||||
|
||||
if (options.hash != null) {
|
||||
assert(Buffer.isBuffer(options.hash));
|
||||
this.hash = options.hash;
|
||||
}
|
||||
|
||||
if (options.index != null) {
|
||||
assert((options.index >>> 0) === options.index,
|
||||
'Index must be a uint32.');
|
||||
this.index = options.index;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the coin.
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
clone() {
|
||||
assert(false, 'Coins are not cloneable.');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate number of confirmations since coin was created.
|
||||
* @param {Number} height - Current chain height. Network
|
||||
* height is used if not passed in.
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
getDepth(height) {
|
||||
assert(typeof height === 'number', 'Must pass a height.');
|
||||
|
||||
if (this.height === -1)
|
||||
return 0;
|
||||
|
||||
if (height === -1)
|
||||
return 0;
|
||||
|
||||
if (height < this.height)
|
||||
return 0;
|
||||
|
||||
return height - this.height + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize coin to a key
|
||||
* suitable for a hash table.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
toKey() {
|
||||
return Outpoint.toKey(this.hash, this.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from hash table key.
|
||||
* @param {Buffer} key
|
||||
* @returns {Coin}
|
||||
*/
|
||||
|
||||
fromKey(key) {
|
||||
const {hash, index} = Outpoint.fromKey(key);
|
||||
this.hash = hash;
|
||||
this.index = index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate coin from hash table key.
|
||||
* @param {Buffer} key
|
||||
* @returns {Coin}
|
||||
*/
|
||||
|
||||
static fromKey(key) {
|
||||
return new this().fromKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get little-endian hash.
|
||||
* @returns {HexHash?}
|
||||
*/
|
||||
|
||||
txid() {
|
||||
if (!this.hash)
|
||||
return null;
|
||||
return this.hash.toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the coin to a more user-friendly object.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format() {
|
||||
return {
|
||||
version: this.version,
|
||||
height: this.height,
|
||||
value: Amount.coin(this.value),
|
||||
address: this.address,
|
||||
covenant: this.covenant.toJSON(),
|
||||
coinbase: this.coinbase,
|
||||
hash: this.txid(),
|
||||
index: this.index
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the coin to an object suitable
|
||||
* for JSON serialization.
|
||||
* @param {Network} [network]
|
||||
* @param {Boolean} [minimal]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON(network, minimal) {
|
||||
network = Network.get(network);
|
||||
|
||||
return {
|
||||
version: this.version,
|
||||
height: this.height,
|
||||
value: this.value,
|
||||
address: this.address.toString(network),
|
||||
covenant: this.covenant.toJSON(),
|
||||
coinbase: this.coinbase,
|
||||
hash: !minimal ? this.txid() : undefined,
|
||||
index: !minimal ? this.index : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject JSON properties into coin.
|
||||
* @param {Object} json
|
||||
* @param {(NetworkType|Network)?} [network]
|
||||
*/
|
||||
|
||||
fromJSON(json, network) {
|
||||
assert(json, 'Coin data required.');
|
||||
assert((json.version >>> 0) === json.version, 'Version must be a uint32.');
|
||||
assert(json.height === -1 || (json.height >>> 0) === json.height,
|
||||
'Height must be a uint32.');
|
||||
assert(util.isU64(json.value), 'Value must be a uint64.');
|
||||
assert(typeof json.coinbase === 'boolean', 'Coinbase must be a boolean.');
|
||||
|
||||
this.version = json.version;
|
||||
this.height = json.height;
|
||||
this.value = json.value;
|
||||
this.address.fromString(json.address, network);
|
||||
this.coinbase = json.coinbase;
|
||||
|
||||
if (json.covenant != null)
|
||||
this.covenant.fromJSON(json.covenant);
|
||||
|
||||
if (json.hash != null) {
|
||||
assert((json.index >>> 0) === json.index, 'Index must be a uint32.');
|
||||
this.hash = util.parseHex(json.hash, 32);
|
||||
this.index = json.index;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate size of coin.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 17 + this.address.getSize() + this.covenant.getVarSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the coin to a buffer writer.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
let height = this.height;
|
||||
|
||||
if (height === -1)
|
||||
height = 0xffffffff;
|
||||
|
||||
bw.writeU32(this.version);
|
||||
bw.writeU32(height);
|
||||
bw.writeU64(this.value);
|
||||
this.address.write(bw);
|
||||
this.covenant.write(bw);
|
||||
bw.writeU8(this.coinbase ? 1 : 0);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized buffer writer.
|
||||
* @param {BufferReader} br
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.version = br.readU32();
|
||||
this.height = br.readU32();
|
||||
this.value = br.readU64();
|
||||
this.address.read(br);
|
||||
this.covenant.read(br);
|
||||
this.coinbase = br.readU8() === 1;
|
||||
|
||||
if (this.height === 0xffffffff)
|
||||
this.height = -1;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from TX.
|
||||
* @param {TX} tx
|
||||
* @param {Number} index
|
||||
* @param {Number} height
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromTX(tx, index, height) {
|
||||
assert(typeof index === 'number');
|
||||
assert(typeof height === 'number');
|
||||
assert(index >= 0 && index < tx.outputs.length);
|
||||
this.version = tx.version;
|
||||
this.height = height;
|
||||
this.value = tx.outputs[index].value;
|
||||
this.address = tx.outputs[index].address;
|
||||
this.covenant = tx.outputs[index].covenant;
|
||||
this.coinbase = tx.isCoinbase();
|
||||
this.hash = tx.hash();
|
||||
this.index = index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a coin from a TX
|
||||
* @param {TX} tx
|
||||
* @param {Number} index - Output index.
|
||||
* @param {Number} height - Chain height.
|
||||
* @returns {Coin}
|
||||
*/
|
||||
|
||||
static fromTX(tx, index, height) {
|
||||
return new this().fromTX(tx, index, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test an object to see if it is a Coin.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isCoin(obj) {
|
||||
return obj instanceof Coin;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Coin;
|
||||
891
docs/js-primitives/covenant.js
Normal file
891
docs/js-primitives/covenant.js
Normal file
|
|
@ -0,0 +1,891 @@
|
|||
/*!
|
||||
* covenant.js - covenant object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const util = require('../utils/util');
|
||||
const rules = require('../covenants/rules');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const {encoding} = bio;
|
||||
const {types, typesByVal} = rules;
|
||||
|
||||
/** @typedef {import('bfilter').BloomFilter} BloomFilter */
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('./address')} Address */
|
||||
|
||||
/** @typedef {ReturnType<Covenant['getJSON']>} CovenantJSON */
|
||||
|
||||
/**
|
||||
* Covenant
|
||||
* @alias module:primitives.Covenant
|
||||
* @property {Number} type
|
||||
* @property {Buffer[]} items
|
||||
* @property {Number} length
|
||||
*/
|
||||
|
||||
class Covenant extends bio.Struct {
|
||||
/**
|
||||
* Create a covenant.
|
||||
* @constructor
|
||||
* @param {rules.types|Object} [type]
|
||||
* @param {Buffer[]} [items]
|
||||
*/
|
||||
|
||||
constructor(type, items) {
|
||||
super();
|
||||
|
||||
this.type = types.NONE;
|
||||
this.items = [];
|
||||
|
||||
if (type != null)
|
||||
this.fromOptions(type, items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {rules.types|Object} [type]
|
||||
* @param {Buffer[]} [items]
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromOptions(type, items) {
|
||||
if (type && typeof type === 'object') {
|
||||
items = type.items;
|
||||
type = type.type;
|
||||
}
|
||||
|
||||
if (Array.isArray(type))
|
||||
return this.fromArray(type);
|
||||
|
||||
if (type != null) {
|
||||
assert((type & 0xff) === type);
|
||||
this.type = type;
|
||||
if (items)
|
||||
return this.fromArray(items);
|
||||
return this;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item.
|
||||
* @param {Number} index
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
get(index) {
|
||||
if (index < 0)
|
||||
index += this.items.length;
|
||||
|
||||
assert((index >>> 0) === index);
|
||||
assert(index < this.items.length);
|
||||
|
||||
return this.items[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an item.
|
||||
* @param {Number} index
|
||||
* @param {Buffer} item
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
set(index, item) {
|
||||
if (index < 0)
|
||||
index += this.items.length;
|
||||
|
||||
assert((index >>> 0) === index);
|
||||
assert(index <= this.items.length);
|
||||
assert(Buffer.isBuffer(item));
|
||||
|
||||
this.items[index] = item;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push an item.
|
||||
* @param {Buffer} item
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
push(item) {
|
||||
assert(Buffer.isBuffer(item));
|
||||
this.items.push(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a uint8.
|
||||
* @param {Number} index
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getU8(index) {
|
||||
const item = this.get(index);
|
||||
assert(item.length === 1);
|
||||
return item[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a uint8.
|
||||
* @param {Number} num
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
pushU8(num) {
|
||||
assert((num & 0xff) === num);
|
||||
const item = Buffer.allocUnsafe(1);
|
||||
item[0] = num;
|
||||
this.push(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a uint32.
|
||||
* @param {Number} index
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getU32(index) {
|
||||
const item = this.get(index);
|
||||
assert(item.length === 4);
|
||||
return bio.readU32(item, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a uint32.
|
||||
* @param {Number} num
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
pushU32(num) {
|
||||
assert((num >>> 0) === num);
|
||||
const item = Buffer.allocUnsafe(4);
|
||||
bio.writeU32(item, num, 0);
|
||||
this.push(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a hash.
|
||||
* @param {Number} index
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
getHash(index) {
|
||||
const item = this.get(index);
|
||||
assert(item.length === 32);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a hash.
|
||||
* @param {Hash} hash
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
pushHash(hash) {
|
||||
assert(Buffer.isBuffer(hash));
|
||||
assert(hash.length === 32);
|
||||
this.push(hash);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string.
|
||||
* @param {Number} index
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
getString(index) {
|
||||
const item = this.get(index);
|
||||
assert(item.length >= 1 && item.length <= 63);
|
||||
return item.toString('binary');
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a string.
|
||||
* @param {String} str
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
pushString(str) {
|
||||
assert(typeof str === 'string');
|
||||
assert(str.length >= 1 && str.length <= 63);
|
||||
this.push(Buffer.from(str, 'binary'));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is known.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isKnown() {
|
||||
return this.type <= types.REVOKE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is unknown.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isUnknown() {
|
||||
return this.type > types.REVOKE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is a payment.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isNone() {
|
||||
return this.type === types.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is a claim.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isClaim() {
|
||||
return this.type === types.CLAIM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is an open.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isOpen() {
|
||||
return this.type === types.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is a bid.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isBid() {
|
||||
return this.type === types.BID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is a reveal.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isReveal() {
|
||||
return this.type === types.REVEAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is a redeem.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isRedeem() {
|
||||
return this.type === types.REDEEM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is a register.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isRegister() {
|
||||
return this.type === types.REGISTER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is an update.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isUpdate() {
|
||||
return this.type === types.UPDATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is a renewal.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isRenew() {
|
||||
return this.type === types.RENEW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is a transfer.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isTransfer() {
|
||||
return this.type === types.TRANSFER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is a finalize.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isFinalize() {
|
||||
return this.type === types.FINALIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is a revocation.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isRevoke() {
|
||||
return this.type === types.REVOKE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build helpers
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set covenant to NONE.
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setNone() {
|
||||
this.type = types.NONE;
|
||||
this.items = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to OPEN.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Buffer} rawName
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setOpen(nameHash, rawName) {
|
||||
this.type = types.OPEN;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(0);
|
||||
this.push(rawName);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to BID.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Number} height
|
||||
* @param {Buffer} rawName
|
||||
* @param {Hash} blind
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setBid(nameHash, height, rawName, blind) {
|
||||
this.type = types.BID;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(height);
|
||||
this.push(rawName);
|
||||
this.pushHash(blind);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to REVEAL.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Number} height
|
||||
* @param {Hash} nonce
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setReveal(nameHash, height, nonce) {
|
||||
this.type = types.REVEAL;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(height);
|
||||
this.pushHash(nonce);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to REDEEM.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Number} height
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setRedeem(nameHash, height) {
|
||||
this.type = types.REDEEM;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(height);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to REGISTER.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Number} height
|
||||
* @param {Buffer} record
|
||||
* @param {Hash} blockHash
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setRegister(nameHash, height, record, blockHash) {
|
||||
this.type = types.REGISTER;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(height);
|
||||
this.push(record);
|
||||
this.pushHash(blockHash);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to UPDATE.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Number} height
|
||||
* @param {Buffer} resource
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setUpdate(nameHash, height, resource) {
|
||||
this.type = types.UPDATE;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(height);
|
||||
this.push(resource);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to RENEW.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Number} height
|
||||
* @param {Hash} blockHash
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setRenew(nameHash, height, blockHash) {
|
||||
this.type = types.RENEW;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(height);
|
||||
this.pushHash(blockHash);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to TRANSFER.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Number} height
|
||||
* @param {Address} address
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setTransfer(nameHash, height, address) {
|
||||
this.type = types.TRANSFER;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(height);
|
||||
this.pushU8(address.version);
|
||||
this.push(address.hash);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to REVOKE.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Number} height
|
||||
* @param {Buffer} rawName
|
||||
* @param {Number} flags
|
||||
* @param {Number} claimed
|
||||
* @param {Number} renewals
|
||||
* @param {Hash} blockHash
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setFinalize(nameHash, height, rawName, flags, claimed, renewals, blockHash) {
|
||||
this.type = types.FINALIZE;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(height);
|
||||
this.push(rawName);
|
||||
this.pushU8(flags);
|
||||
this.pushU32(claimed);
|
||||
this.pushU32(renewals);
|
||||
this.pushHash(blockHash);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to REVOKE.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Number} height
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setRevoke(nameHash, height) {
|
||||
this.type = types.REVOKE;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(height);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set covenant to CLAIM.
|
||||
* @param {Hash} nameHash
|
||||
* @param {Number} height
|
||||
* @param {Buffer} rawName
|
||||
* @param {Number} flags
|
||||
* @param {Hash} commitHash
|
||||
* @param {Number} commitHeight
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
setClaim(nameHash, height, rawName, flags, commitHash, commitHeight) {
|
||||
this.type = types.CLAIM;
|
||||
this.items = [];
|
||||
this.pushHash(nameHash);
|
||||
this.pushU32(height);
|
||||
this.push(rawName);
|
||||
this.pushU8(flags);
|
||||
this.pushHash(commitHash);
|
||||
this.pushU32(commitHeight);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is name-related.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isName() {
|
||||
if (this.type < types.CLAIM)
|
||||
return false;
|
||||
|
||||
if (this.type > types.REVOKE)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a covenant type should be
|
||||
* considered subject to the dust policy rule.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isDustworthy() {
|
||||
switch (this.type) {
|
||||
case types.NONE:
|
||||
case types.BID:
|
||||
return true;
|
||||
default:
|
||||
return this.type > types.REVOKE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a coin should be considered
|
||||
* unspendable in the coin selector.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isNonspendable() {
|
||||
switch (this.type) {
|
||||
case types.NONE:
|
||||
case types.OPEN:
|
||||
case types.REDEEM:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a covenant should be considered "linked".
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isLinked() {
|
||||
return this.type >= types.REVEAL && this.type <= types.REVOKE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert covenant to an array of buffers.
|
||||
* @returns {Buffer[]}
|
||||
*/
|
||||
|
||||
toArray() {
|
||||
return this.items.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from an array of buffers.
|
||||
* @private
|
||||
* @param {Buffer[]} items
|
||||
*/
|
||||
|
||||
fromArray(items) {
|
||||
assert(Array.isArray(items));
|
||||
this.items = items;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the covenant is unspendable.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isUnspendable() {
|
||||
return this.type === types.REVOKE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the covenant to a string.
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
toString() {
|
||||
return this.encode().toString('hex', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from covenant.
|
||||
* Used for cloning.
|
||||
* @param {this} covenant
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
inject(covenant) {
|
||||
assert(covenant instanceof this.constructor);
|
||||
this.type = covenant.type;
|
||||
this.items = covenant.items.slice();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the covenant against a bloom filter.
|
||||
* @param {BloomFilter} filter
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
test(filter) {
|
||||
for (const item of this.items) {
|
||||
if (item.length === 0)
|
||||
continue;
|
||||
|
||||
if (filter.test(item))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a data element in a covenant.
|
||||
* @param {Buffer} data - Data element to match against.
|
||||
* @returns {Number} Index (`-1` if not present).
|
||||
*/
|
||||
|
||||
indexOf(data) {
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
const item = this.items[i];
|
||||
if (item.equals(data))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate size of the covenant
|
||||
* excluding the varint size bytes.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
|
||||
for (const item of this.items)
|
||||
size += encoding.sizeVarBytes(item);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate size of the covenant
|
||||
* including the varint size bytes.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getVarSize() {
|
||||
return 1 + encoding.sizeVarint(this.items.length) + this.getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write covenant to a buffer writer.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeU8(this.type);
|
||||
bw.writeVarint(this.items.length);
|
||||
|
||||
for (const item of this.items)
|
||||
bw.writeVarBytes(item);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode covenant.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
encode() {
|
||||
const bw = bio.write(this.getVarSize());
|
||||
this.write(bw);
|
||||
return bw.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert covenant to a hex string.
|
||||
*/
|
||||
|
||||
getJSON() {
|
||||
const items = [];
|
||||
|
||||
for (const item of this.items)
|
||||
items.push(item.toString('hex'));
|
||||
|
||||
return {
|
||||
type: this.type,
|
||||
action: typesByVal[this.type],
|
||||
items
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from json object.
|
||||
* @param {CovenantJSON} json
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object', 'Covenant must be an object.');
|
||||
assert((json.type & 0xff) === json.type);
|
||||
assert(Array.isArray(json.items));
|
||||
|
||||
this.type = json.type;
|
||||
|
||||
for (const str of json.items) {
|
||||
const item = util.parseHex(str, -1);
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from buffer reader.
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.type = br.readU8();
|
||||
|
||||
const count = br.readVarint();
|
||||
|
||||
if (count > consensus.MAX_SCRIPT_STACK)
|
||||
throw new Error('Too many covenant items.');
|
||||
|
||||
for (let i = 0; i < count; i++)
|
||||
this.items.push(br.readVarBytes());
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject items from string.
|
||||
* @param {String|String[]} items
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromString(items) {
|
||||
if (!Array.isArray(items)) {
|
||||
assert(typeof items === 'string');
|
||||
|
||||
items = items.trim();
|
||||
|
||||
if (items.length === 0)
|
||||
return this;
|
||||
|
||||
items = items.split(/\s+/);
|
||||
}
|
||||
|
||||
for (const item of items)
|
||||
this.items.push(util.parseHex(item, -1));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect a covenant object.
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
format() {
|
||||
return `<Covenant: ${typesByVal[this.type]}:${this.toString()}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insantiate covenant from an array of buffers.
|
||||
* @param {Buffer[]} items
|
||||
* @returns {Covenant}
|
||||
*/
|
||||
|
||||
static fromArray(items) {
|
||||
return new this().fromArray(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test an object to see if it is a covenant.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isCovenant(obj) {
|
||||
return obj instanceof Covenant;
|
||||
}
|
||||
}
|
||||
|
||||
Covenant.types = types;
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Covenant;
|
||||
216
docs/js-primitives/headers.js
Normal file
216
docs/js-primitives/headers.js
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
/*!
|
||||
* headers.js - headers object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const util = require('../utils/util');
|
||||
const AbstractBlock = require('./abstractblock');
|
||||
|
||||
/** @typedef {import('bufio').BufferReader} BufferReader */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('../types').NetworkType} NetworkType */
|
||||
/** @typedef {import('../protocol/network')} Network */
|
||||
/** @typedef {import('../blockchain/chainentry')} ChainEntry */
|
||||
/** @typedef {import('../coins/coinview')} CoinView */
|
||||
/** @typedef {import('./block')} Block */
|
||||
/** @typedef {import('./merkleblock')} MerkleBlock */
|
||||
|
||||
/**
|
||||
* Headers
|
||||
* Represents block headers obtained
|
||||
* from the network via `headers`.
|
||||
* @alias module:primitives.Headers
|
||||
* @extends AbstractBlock
|
||||
*/
|
||||
|
||||
class Headers extends AbstractBlock {
|
||||
/**
|
||||
* Create headers.
|
||||
* @constructor
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
if (options)
|
||||
this.parseOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform non-contextual
|
||||
* verification on the headers.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verifyBody() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get size of the headers.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return this.sizeHead();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the headers to a buffer writer.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
this.writeHead(bw);
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from buffer reader.
|
||||
* @param {BufferReader} br
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.readHead(br);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate headers from serialized data.
|
||||
* @param {Buffer} data
|
||||
* @returns {Headers}
|
||||
*/
|
||||
|
||||
static fromHead(data) {
|
||||
return new this().fromHead(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate headers from a chain entry.
|
||||
* @param {ChainEntry} entry
|
||||
* @returns {Headers}
|
||||
*/
|
||||
|
||||
static fromEntry(entry) {
|
||||
const headers = new this();
|
||||
headers.version = entry.version;
|
||||
headers.prevBlock = entry.prevBlock;
|
||||
headers.merkleRoot = entry.merkleRoot;
|
||||
headers.witnessRoot = entry.witnessRoot;
|
||||
headers.treeRoot = entry.treeRoot;
|
||||
headers.reservedRoot = entry.reservedRoot;
|
||||
headers.time = entry.time;
|
||||
headers.bits = entry.bits;
|
||||
headers.nonce = entry.nonce;
|
||||
headers.extraNonce = entry.extraNonce;
|
||||
headers.mask = entry.mask;
|
||||
headers._hash = entry.hash;
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the block to a headers object.
|
||||
* @returns {Headers}
|
||||
*/
|
||||
|
||||
toHeaders() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the block to a headers object.
|
||||
* @param {Block|MerkleBlock} block
|
||||
* @returns {Headers}
|
||||
*/
|
||||
|
||||
static fromBlock(block) {
|
||||
const headers = new this(block);
|
||||
headers._hash = block._hash;
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the block to an object suitable
|
||||
* for JSON serialization.
|
||||
* @param {(NetworkType|Network)?} [network]
|
||||
* @param {CoinView} [view]
|
||||
* @param {Number} [height]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON(network, view, height) {
|
||||
return {
|
||||
hash: this.hash().toString('hex'),
|
||||
height: height,
|
||||
version: this.version,
|
||||
prevBlock: this.prevBlock.toString('hex'),
|
||||
merkleRoot: this.merkleRoot.toString('hex'),
|
||||
witnessRoot: this.witnessRoot.toString('hex'),
|
||||
treeRoot: this.treeRoot.toString('hex'),
|
||||
reservedRoot: this.reservedRoot.toString('hex'),
|
||||
time: this.time,
|
||||
bits: this.bits,
|
||||
nonce: this.nonce,
|
||||
extraNonce: this.extraNonce.toString('hex'),
|
||||
mask: this.mask.toString('hex')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from json object.
|
||||
* @param {Object} json
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
this.parseJSON(json);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect the headers and return a more
|
||||
* user-friendly representation of the data.
|
||||
* @param {CoinView} [view]
|
||||
* @param {Number} [height]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format(view, height) {
|
||||
return {
|
||||
hash: this.hash().toString('hex'),
|
||||
height: height != null ? height : -1,
|
||||
date: util.date(this.time),
|
||||
version: this.version.toString(16),
|
||||
prevBlock: this.prevBlock.toString('hex'),
|
||||
merkleRoot: this.merkleRoot.toString('hex'),
|
||||
witnessRoot: this.witnessRoot.toString('hex'),
|
||||
treeRoot: this.treeRoot.toString('hex'),
|
||||
reservedRoot: this.reservedRoot.toString('hex'),
|
||||
time: this.time,
|
||||
bits: this.bits,
|
||||
nonce: this.nonce,
|
||||
extraNonce: this.extraNonce.toString('hex'),
|
||||
mask: this.mask.toString('hex')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test an object to see if it is a Headers object.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isHeaders(obj) {
|
||||
return obj instanceof Headers;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Headers;
|
||||
29
docs/js-primitives/index.js
Normal file
29
docs/js-primitives/index.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*!
|
||||
* primitives/index.js - handshake primitives for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @module primitives
|
||||
*/
|
||||
|
||||
exports.AbstractBlock = require('./abstractblock');
|
||||
exports.Address = require('./address');
|
||||
exports.Block = require('./block');
|
||||
exports.Claim = require('./claim');
|
||||
exports.Coin = require('./coin');
|
||||
exports.Covenant = require('./covenant');
|
||||
exports.Headers = require('./headers');
|
||||
exports.Input = require('./input');
|
||||
exports.InvItem = require('./invitem');
|
||||
exports.KeyRing = require('./keyring');
|
||||
exports.MemBlock = require('./memblock');
|
||||
exports.MerkleBlock = require('./merkleblock');
|
||||
exports.MTX = require('./mtx');
|
||||
exports.Outpoint = require('./outpoint');
|
||||
exports.Output = require('./output');
|
||||
exports.TX = require('./tx');
|
||||
exports.TXMeta = require('./txmeta');
|
||||
343
docs/js-primitives/input.js
Normal file
343
docs/js-primitives/input.js
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
/*!
|
||||
* input.js - input object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const Network = require('../protocol/network');
|
||||
const Witness = require('../script/witness');
|
||||
const Outpoint = require('./outpoint');
|
||||
|
||||
/** @typedef {import('../types').NetworkType} NetworkType */
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('./tx')} TX */
|
||||
/** @typedef {import('./coin')} Coin */
|
||||
/** @typedef {import('./address')} Address */
|
||||
/** @typedef {import('../wallet/path')} Path */
|
||||
|
||||
/** @typedef {ReturnType<Input['getJSON']>} InputJSON */
|
||||
|
||||
/**
|
||||
* Input
|
||||
* Represents a transaction input.
|
||||
* @alias module:primitives.Input
|
||||
* @property {Outpoint} prevout - Outpoint.
|
||||
* @property {Script} script - Input script / scriptSig.
|
||||
* @property {Number} sequence - nSequence.
|
||||
* @property {Witness} witness - Witness (empty if not present).
|
||||
*/
|
||||
|
||||
class Input extends bio.Struct {
|
||||
/**
|
||||
* Create transaction input.
|
||||
* @constructor
|
||||
* @param {Object?} [options]
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.prevout = new Outpoint();
|
||||
this.witness = new Witness();
|
||||
this.sequence = 0xffffffff;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
assert(options, 'Input data is required.');
|
||||
|
||||
this.prevout.fromOptions(options.prevout);
|
||||
|
||||
if (options.witness)
|
||||
this.witness.fromOptions(options.witness);
|
||||
|
||||
if (options.sequence != null) {
|
||||
assert((options.sequence >>> 0) === options.sequence,
|
||||
'Sequence must be a uint32.');
|
||||
this.sequence = options.sequence;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the input.
|
||||
* @param {this} input
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
inject(input) {
|
||||
this.prevout = input.prevout;
|
||||
this.witness.inject(input.witness);
|
||||
this.sequence = input.sequence;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test equality against another input.
|
||||
* @param {Input} input
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
equals(input) {
|
||||
assert(Input.isInput(input));
|
||||
return this.prevout.equals(input.prevout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare against another input (BIP69).
|
||||
* @param {Input} input
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
compare(input) {
|
||||
assert(Input.isInput(input));
|
||||
return this.prevout.compare(input.prevout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous output script's address. Will "guess"
|
||||
* based on the input script and/or witness if coin
|
||||
* is not available.
|
||||
* @param {Coin?} [coin]
|
||||
* @returns {Address?} addr
|
||||
*/
|
||||
|
||||
getAddress(coin) {
|
||||
if (this.isCoinbase())
|
||||
return null;
|
||||
|
||||
if (coin)
|
||||
return coin.getAddress();
|
||||
|
||||
return this.witness.getInputAddress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address hash.
|
||||
* @param {Coin?} [coin]
|
||||
* @returns {Hash?} hash
|
||||
*/
|
||||
|
||||
getHash(coin) {
|
||||
const addr = this.getAddress(coin);
|
||||
|
||||
if (!addr)
|
||||
return null;
|
||||
|
||||
return addr.getHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if nSequence is equal to uint32max.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isFinal() {
|
||||
return this.sequence === 0xffffffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if outpoint is null.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isCoinbase() {
|
||||
return this.prevout.isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the input to a more user-friendly object.
|
||||
* @param {Coin?} coin
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format(coin) {
|
||||
return {
|
||||
address: this.getAddress(coin),
|
||||
prevout: this.prevout,
|
||||
witness: this.witness,
|
||||
sequence: this.sequence,
|
||||
coin: coin || null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the input to an object suitable
|
||||
* for JSON serialization.
|
||||
* @param {NetworkType|Network} [network]
|
||||
* @param {Coin} [coin]
|
||||
* @param {Path} [path]
|
||||
*/
|
||||
|
||||
getJSON(network, coin, path) {
|
||||
network = Network.get(network);
|
||||
|
||||
let addr;
|
||||
if (!coin) {
|
||||
addr = this.getAddress();
|
||||
if (addr)
|
||||
addr = addr.toString(network);
|
||||
}
|
||||
|
||||
return {
|
||||
prevout: this.prevout.toJSON(),
|
||||
witness: this.witness.toJSON(),
|
||||
sequence: this.sequence,
|
||||
address: addr,
|
||||
coin: coin ? coin.getJSON(network, true) : undefined,
|
||||
path: path ? path.getJSON(network) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from a JSON object.
|
||||
* @param {InputJSON} json
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json, 'Input data is required.');
|
||||
assert((json.sequence >>> 0) === json.sequence,
|
||||
'Sequence must be a uint32.');
|
||||
this.prevout.fromJSON(json.prevout);
|
||||
this.witness.fromJSON(json.witness);
|
||||
this.sequence = json.sequence;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate size of serialized input.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 40;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the input to a buffer writer.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
this.prevout.write(bw);
|
||||
bw.writeU32(this.sequence);
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from buffer reader.
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.prevout.read(br);
|
||||
this.sequence = br.readU32();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from outpoint.
|
||||
* @param {Outpoint} outpoint
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromOutpoint(outpoint) {
|
||||
assert(Buffer.isBuffer(outpoint.hash));
|
||||
assert(typeof outpoint.index === 'number');
|
||||
this.prevout.hash = outpoint.hash;
|
||||
this.prevout.index = outpoint.index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate input from outpoint.
|
||||
* @param {Outpoint} outpoint
|
||||
* @returns {Input}
|
||||
*/
|
||||
|
||||
static fromOutpoint(outpoint) {
|
||||
return new this().fromOutpoint(outpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from coin.
|
||||
* @private
|
||||
* @param {Coin} coin
|
||||
*/
|
||||
|
||||
fromCoin(coin) {
|
||||
assert(Buffer.isBuffer(coin.hash));
|
||||
assert(typeof coin.index === 'number');
|
||||
this.prevout.hash = coin.hash;
|
||||
this.prevout.index = coin.index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate input from coin.
|
||||
* @param {Coin} coin
|
||||
* @returns {Input}
|
||||
*/
|
||||
|
||||
static fromCoin(coin) {
|
||||
return new this().fromCoin(coin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from transaction.
|
||||
* @param {TX} tx
|
||||
* @param {Number} index
|
||||
*/
|
||||
|
||||
fromTX(tx, index) {
|
||||
assert(tx);
|
||||
assert(typeof index === 'number');
|
||||
assert(index >= 0 && index < tx.outputs.length);
|
||||
this.prevout.hash = tx.hash();
|
||||
this.prevout.index = index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate input from tx.
|
||||
* @param {TX} tx
|
||||
* @param {Number} index
|
||||
* @returns {Input}
|
||||
*/
|
||||
|
||||
static fromTX(tx, index) {
|
||||
return new this().fromTX(tx, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test an object to see if it is an Input.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isInput(obj) {
|
||||
return obj instanceof Input;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Input;
|
||||
161
docs/js-primitives/invitem.js
Normal file
161
docs/js-primitives/invitem.js
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/*!
|
||||
* invitem.js - inv item object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const bio = require('bufio');
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
|
||||
/**
|
||||
* Inv Item
|
||||
* @alias module:primitives.InvItem
|
||||
* @constructor
|
||||
* @property {InvType} type
|
||||
* @property {Hash} hash
|
||||
*/
|
||||
|
||||
class InvItem extends bio.Struct {
|
||||
/**
|
||||
* Create an inv item.
|
||||
* @constructor
|
||||
* @param {InvItem.types} type
|
||||
* @param {Hash} hash
|
||||
*/
|
||||
|
||||
constructor(type, hash) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write inv item to buffer writer.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 36;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write inv item to buffer writer.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeU32(this.type);
|
||||
bw.writeHash(this.hash);
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from buffer reader.
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.type = br.readU32();
|
||||
this.hash = br.readHash();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the inv item is a block.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isBlock() {
|
||||
switch (this.type) {
|
||||
case InvItem.types.BLOCK:
|
||||
case InvItem.types.FILTERED_BLOCK:
|
||||
case InvItem.types.CMPCT_BLOCK:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the inv item is a tx.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isTX() {
|
||||
switch (this.type) {
|
||||
case InvItem.types.TX:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the inv item is a claim.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isClaim() {
|
||||
switch (this.type) {
|
||||
case InvItem.types.CLAIM:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the inv item is an airdrop proof.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isAirdrop() {
|
||||
switch (this.type) {
|
||||
case InvItem.types.AIRDROP:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inv types.
|
||||
* @enum {Number}
|
||||
* @default
|
||||
*/
|
||||
|
||||
InvItem.types = {
|
||||
TX: 1,
|
||||
BLOCK: 2,
|
||||
FILTERED_BLOCK: 3,
|
||||
CMPCT_BLOCK: 4,
|
||||
CLAIM: 5,
|
||||
AIRDROP: 6
|
||||
};
|
||||
|
||||
/**
|
||||
* Inv types by value.
|
||||
* @const {Object}
|
||||
*/
|
||||
|
||||
InvItem.typesByVal = {
|
||||
1: 'TX',
|
||||
2: 'BLOCK',
|
||||
3: 'FILTERED_BLOCK',
|
||||
4: 'CMPCT_BLOCK',
|
||||
5: 'CLAIM',
|
||||
6: 'AIRDROP'
|
||||
};
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = InvItem;
|
||||
649
docs/js-primitives/keyring.js
Normal file
649
docs/js-primitives/keyring.js
Normal file
|
|
@ -0,0 +1,649 @@
|
|||
/*!
|
||||
* keyring.js - keyring object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const base58 = require('bcrypto/lib/encoding/base58');
|
||||
const bio = require('bufio');
|
||||
const blake2b = require('bcrypto/lib/blake2b');
|
||||
const hash256 = require('bcrypto/lib/hash256');
|
||||
const Network = require('../protocol/network');
|
||||
const Script = require('../script/script');
|
||||
const Address = require('./address');
|
||||
const Output = require('./output');
|
||||
const secp256k1 = require('bcrypto/lib/secp256k1');
|
||||
|
||||
/** @typedef {import('../types').NetworkType} NetworkType */
|
||||
/** @typedef {import('../types').Base58String} Base58String */
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('./tx')} TX */
|
||||
|
||||
/*
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const ZERO_KEY = Buffer.alloc(33, 0x00);
|
||||
|
||||
/**
|
||||
* Key Ring
|
||||
* Represents a key ring which amounts to an address.
|
||||
* @alias module:primitives.KeyRing
|
||||
*/
|
||||
|
||||
class KeyRing extends bio.Struct {
|
||||
/**
|
||||
* Create a key ring.
|
||||
* @constructor
|
||||
* @param {Object?} [options]
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.publicKey = ZERO_KEY;
|
||||
/** @type {Buffer?} */
|
||||
this.privateKey = null;
|
||||
/** @type {Script?} */
|
||||
this.script = null;
|
||||
|
||||
/** @type {Hash?} */
|
||||
this._keyHash = null;
|
||||
/** @type {Address?} */
|
||||
this._keyAddress = null;
|
||||
/** @type {Hash?} */
|
||||
this._scriptHash = null;
|
||||
/** @type {Address?} */
|
||||
this._scriptAddress = null;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
let key = toKey(options);
|
||||
|
||||
if (Buffer.isBuffer(key))
|
||||
return this.fromKey(key);
|
||||
|
||||
key = toKey(options.key);
|
||||
|
||||
if (options.publicKey)
|
||||
key = toKey(options.publicKey);
|
||||
|
||||
if (options.privateKey)
|
||||
key = toKey(options.privateKey);
|
||||
|
||||
const script = options.script;
|
||||
|
||||
if (script)
|
||||
return this.fromScript(key, script);
|
||||
|
||||
return this.fromKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached key/script hashes.
|
||||
*/
|
||||
|
||||
refresh() {
|
||||
this._keyHash = null;
|
||||
this._keyAddress = null;
|
||||
this._scriptHash = null;
|
||||
this._scriptAddress = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject data from private key.
|
||||
* @param {Buffer} key
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromPrivate(key) {
|
||||
assert(Buffer.isBuffer(key), 'Private key must be a buffer.');
|
||||
assert(secp256k1.privateKeyVerify(key), 'Not a valid private key.');
|
||||
|
||||
this.privateKey = key;
|
||||
this.publicKey = secp256k1.publicKeyCreate(key, true);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate keyring from a private key.
|
||||
* @param {Buffer} key
|
||||
* @returns {KeyRing}
|
||||
*/
|
||||
|
||||
static fromPrivate(key) {
|
||||
return new this().fromPrivate(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject data from public key.
|
||||
* @param {Buffer} key
|
||||
*/
|
||||
|
||||
fromPublic(key) {
|
||||
assert(Buffer.isBuffer(key), 'Public key must be a buffer.');
|
||||
assert(secp256k1.publicKeyVerify(key) && key.length === 33,
|
||||
'Not a valid public key.');
|
||||
this.publicKey = key;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a keyring.
|
||||
* @returns {KeyRing}
|
||||
*/
|
||||
|
||||
generate() {
|
||||
const key = secp256k1.privateKeyGenerate();
|
||||
return this.fromKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a keyring.
|
||||
* @returns {KeyRing}
|
||||
*/
|
||||
|
||||
static generate() {
|
||||
return new this().generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate keyring from a public key.
|
||||
* @param {Buffer} publicKey
|
||||
* @returns {KeyRing}
|
||||
*/
|
||||
|
||||
static fromPublic(publicKey) {
|
||||
return new this().fromPublic(publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject data from public key.
|
||||
* @param {Buffer} key
|
||||
*/
|
||||
|
||||
fromKey(key) {
|
||||
assert(Buffer.isBuffer(key), 'Key must be a buffer.');
|
||||
|
||||
if (key.length === 32)
|
||||
return this.fromPrivate(key);
|
||||
|
||||
return this.fromPublic(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate keyring from a public key.
|
||||
* @param {Buffer} key
|
||||
* @returns {KeyRing}
|
||||
*/
|
||||
|
||||
static fromKey(key) {
|
||||
return new this().fromKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject data from script.
|
||||
* @private
|
||||
* @param {Buffer} key
|
||||
* @param {Script} script
|
||||
*/
|
||||
|
||||
fromScript(key, script) {
|
||||
assert(script instanceof Script, 'Non-script passed into KeyRing.');
|
||||
|
||||
this.fromKey(key);
|
||||
this.script = script;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate keyring from script.
|
||||
* @param {Buffer} key
|
||||
* @param {Script} script
|
||||
* @returns {KeyRing}
|
||||
*/
|
||||
|
||||
static fromScript(key, script) {
|
||||
return new this().fromScript(key, script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate WIF serialization size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSecretSize() {
|
||||
let size = 0;
|
||||
|
||||
size += 1;
|
||||
size += this.privateKey.length;
|
||||
size += 1;
|
||||
size += 4;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert key to a secret.
|
||||
* @param {(Network|NetworkType)?} network
|
||||
* @returns {Base58String}
|
||||
*/
|
||||
|
||||
toSecret(network) {
|
||||
const size = this.getSecretSize();
|
||||
const bw = bio.write(size);
|
||||
|
||||
assert(this.privateKey, 'Cannot serialize without private key.');
|
||||
|
||||
network = Network.get(network);
|
||||
|
||||
bw.writeU8(network.keyPrefix.privkey);
|
||||
bw.writeBytes(this.privateKey);
|
||||
bw.writeU8(1);
|
||||
|
||||
bw.writeChecksum(hash256.digest);
|
||||
|
||||
return base58.encode(bw.render());
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized secret.
|
||||
* @param {Base58String} data
|
||||
* @param {(Network|NetworkType)?} [network]
|
||||
*/
|
||||
|
||||
fromSecret(data, network) {
|
||||
const br = bio.read(base58.decode(data), true);
|
||||
|
||||
const version = br.readU8();
|
||||
|
||||
Network.fromWIF(version, network);
|
||||
|
||||
const key = br.readBytes(32);
|
||||
|
||||
assert(br.readU8() === 1, 'Bad compression flag.');
|
||||
br.verifyChecksum(hash256.digest);
|
||||
|
||||
return this.fromPrivate(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a keyring from a serialized secret.
|
||||
* @param {Base58String} data
|
||||
* @param {(Network|NetworkType)?} network
|
||||
* @returns {KeyRing}
|
||||
*/
|
||||
|
||||
static fromSecret(data, network) {
|
||||
return new this().fromSecret(data, network);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get private key.
|
||||
* @returns {Buffer} Private key.
|
||||
*/
|
||||
|
||||
getPrivateKey() {
|
||||
if (!this.privateKey)
|
||||
return null;
|
||||
|
||||
return this.privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public key.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get redeem script.
|
||||
* @returns {Script}
|
||||
*/
|
||||
|
||||
getScript() {
|
||||
return this.script;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scripthash.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
getScriptHash() {
|
||||
if (!this.script)
|
||||
return null;
|
||||
|
||||
if (!this._scriptHash)
|
||||
this._scriptHash = this.script.sha3();
|
||||
|
||||
return this._scriptHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scripthash address.
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
getScriptAddress() {
|
||||
if (!this.script)
|
||||
return null;
|
||||
|
||||
if (!this._scriptAddress) {
|
||||
const hash = this.getScriptHash();
|
||||
const addr = Address.fromScripthash(hash);
|
||||
this._scriptAddress = addr;
|
||||
}
|
||||
|
||||
return this._scriptAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public key hash.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
getKeyHash() {
|
||||
if (!this._keyHash)
|
||||
this._keyHash = blake2b.digest(this.publicKey, 20);
|
||||
|
||||
return this._keyHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pubkeyhash address.
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
getKeyAddress() {
|
||||
if (!this._keyAddress) {
|
||||
const hash = this.getKeyHash();
|
||||
const addr = Address.fromPubkeyhash(hash);
|
||||
this._keyAddress = addr;
|
||||
}
|
||||
|
||||
return this._keyAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hash.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
getHash() {
|
||||
if (this.script)
|
||||
return this.getScriptHash();
|
||||
|
||||
return this.getKeyHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base58 address.
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
getAddress() {
|
||||
if (this.script)
|
||||
return this.getScriptAddress();
|
||||
|
||||
return this.getKeyAddress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test an address hash against hash and program hash.
|
||||
* @param {Buffer} hash
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
ownHash(hash) {
|
||||
if (!hash)
|
||||
return false;
|
||||
|
||||
if (hash.equals(this.getKeyHash()))
|
||||
return true;
|
||||
|
||||
if (this.script) {
|
||||
if (hash.equals(this.getScriptHash()))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether transaction output belongs to this address.
|
||||
* @param {TX|Output} tx - Transaction or Output.
|
||||
* @param {Number?} [index] - Output index.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
ownOutput(tx, index) {
|
||||
let output;
|
||||
|
||||
if (tx instanceof Output) {
|
||||
output = tx;
|
||||
} else {
|
||||
output = tx.outputs[index];
|
||||
assert(output, 'Output does not exist.');
|
||||
}
|
||||
|
||||
return this.ownHash(output.getHash());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a hash against script hashes to
|
||||
* find the correct redeem script, if any.
|
||||
* @param {Buffer} hash
|
||||
* @returns {Script|null}
|
||||
*/
|
||||
|
||||
getRedeem(hash) {
|
||||
if (this.script) {
|
||||
if (hash.equals(this.getScriptHash()))
|
||||
return this.script;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a message.
|
||||
* @param {Buffer} msg
|
||||
* @returns {Buffer} Signature in DER format.
|
||||
*/
|
||||
|
||||
sign(msg) {
|
||||
assert(this.privateKey, 'Cannot sign without private key.');
|
||||
return secp256k1.sign(msg, this.privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a message.
|
||||
* @param {Buffer} msg
|
||||
* @param {Buffer} sig - Signature in DER format.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verify(msg, sig) {
|
||||
return secp256k1.verify(msg, sig, this.publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get witness program version.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect keyring.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format() {
|
||||
return this.toJSON();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an KeyRing to a more json-friendly object.
|
||||
* @param {(NetworkType|Network)?} [network]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON(network) {
|
||||
return {
|
||||
publicKey: this.publicKey.toString('hex'),
|
||||
script: this.script ? this.script.toHex() : null,
|
||||
address: this.getAddress().toString(network)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from json object.
|
||||
* @param {Object} json
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json);
|
||||
assert(typeof json.publicKey === 'string');
|
||||
assert(!json.script || typeof json.script === 'string');
|
||||
|
||||
this.publicKey = Buffer.from(json.publicKey, 'hex');
|
||||
|
||||
if (json.script)
|
||||
this.script = Script.fromHex(json.script);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate serialization size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
size += 1;
|
||||
|
||||
if (this.privateKey)
|
||||
size += 32;
|
||||
else
|
||||
size += 33;
|
||||
|
||||
size += this.script
|
||||
? this.script.getVarSize()
|
||||
: 1;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the keyring to a buffer writer.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
if (this.privateKey) {
|
||||
bw.writeU8(0);
|
||||
bw.writeBytes(this.privateKey);
|
||||
} else {
|
||||
bw.writeU8(1);
|
||||
bw.writeBytes(this.publicKey);
|
||||
}
|
||||
|
||||
if (this.script)
|
||||
bw.writeVarBytes(this.script.encode());
|
||||
else
|
||||
bw.writeVarint(0);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from buffer reader.
|
||||
* @param {bio.BufferReader} br
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
const type = br.readU8();
|
||||
|
||||
switch (type) {
|
||||
case 0: {
|
||||
const key = br.readBytes(32);
|
||||
this.privateKey = key;
|
||||
this.publicKey = secp256k1.publicKeyCreate(key, true);
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
const key = br.readBytes(33);
|
||||
assert(secp256k1.publicKeyVerify(key), 'Invalid public key.');
|
||||
this.publicKey = key;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error('Invalid key.');
|
||||
}
|
||||
}
|
||||
|
||||
const script = br.readVarBytes();
|
||||
|
||||
if (script.length > 0)
|
||||
this.script = Script.decode(script);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an object is a KeyRing.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isKeyRing(obj) {
|
||||
return obj instanceof KeyRing;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
function toKey(opt) {
|
||||
if (!opt)
|
||||
return opt;
|
||||
|
||||
if (opt.privateKey)
|
||||
return opt.privateKey;
|
||||
|
||||
if (opt.publicKey)
|
||||
return opt.publicKey;
|
||||
|
||||
return opt;
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = KeyRing;
|
||||
264
docs/js-primitives/memblock.js
Normal file
264
docs/js-primitives/memblock.js
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
/*!
|
||||
* memblock.js - memblock block object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const AbstractBlock = require('./abstractblock');
|
||||
const Block = require('./block');
|
||||
const Headers = require('./headers');
|
||||
const Input = require('./input');
|
||||
const Output = require('./output');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const DUMMY = Buffer.alloc(0);
|
||||
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
|
||||
/**
|
||||
* Mem Block
|
||||
* A block object which is essentially a "placeholder"
|
||||
* for a full {@link Block} object. The v8 garbage
|
||||
* collector's head will explode if there is too much
|
||||
* data on the javascript heap. Blocks can currently
|
||||
* be up to 1mb in size. In the future, they may be
|
||||
* 2mb, 8mb, or maybe 20mb, who knows? A MemBlock
|
||||
* is an optimization which defers parsing of
|
||||
* the serialized transactions (the block Buffer) until
|
||||
* the block has passed through the chain queue and
|
||||
* is about to enter the chain. This keeps a lot data
|
||||
* off of the javascript heap for most of the time a
|
||||
* block even exists in memory, and manages to keep a
|
||||
* lot of strain off of the garbage collector. Having
|
||||
* 500mb of blocks on the js heap would not be a good
|
||||
* thing.
|
||||
* @alias module:primitives.MemBlock
|
||||
* @extends AbstractBlock
|
||||
*/
|
||||
|
||||
class MemBlock extends AbstractBlock {
|
||||
/**
|
||||
* Create a mem block.
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._raw = DUMMY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the block is a memblock.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isMemory() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve deterministically random padding.
|
||||
* @param {Number} size
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
padding(size) {
|
||||
assert((size >>> 0) === size);
|
||||
|
||||
const pad = Buffer.alloc(size);
|
||||
const prevBlock = this._raw.slice(12, 12 + 32);
|
||||
const treeRoot = this._raw.slice(12 + 32, 12 + 32 + 32);
|
||||
|
||||
for (let i = 0; i < size; i++)
|
||||
pad[i] = prevBlock[i % 32] ^ treeRoot[i % 32];
|
||||
|
||||
return pad;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the block headers.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
toPrehead() {
|
||||
return Headers.decode(this._raw).toPrehead();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate PoW hash.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
powHash() {
|
||||
const hash = this.shareHash();
|
||||
const mask = this._raw.slice(consensus.HEADER_SIZE - 32,
|
||||
consensus.HEADER_SIZE);
|
||||
|
||||
for (let i = 0; i < 32; i++)
|
||||
hash[i] ^= mask[i];
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the block headers.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
toHead() {
|
||||
return this._raw.slice(0, consensus.HEADER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full block size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return this._raw.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the block.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verifyBody() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the coinbase height
|
||||
* from the coinbase input script.
|
||||
* @returns {Number} height (-1 if not present).
|
||||
*/
|
||||
|
||||
getCoinbaseHeight() {
|
||||
try {
|
||||
return this.parseCoinbaseHeight();
|
||||
} catch (e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the coinbase height
|
||||
* from the coinbase input script.
|
||||
* @private
|
||||
* @returns {Number} height (-1 if not present).
|
||||
*/
|
||||
|
||||
parseCoinbaseHeight() {
|
||||
const br = bio.read(this._raw, true);
|
||||
|
||||
br.seek(consensus.HEADER_SIZE);
|
||||
|
||||
const txCount = br.readVarint();
|
||||
|
||||
if (txCount === 0)
|
||||
return -1;
|
||||
|
||||
br.seek(4);
|
||||
|
||||
const inCount = br.readVarint();
|
||||
|
||||
for (let i = 0; i < inCount; i++)
|
||||
Input.read(br);
|
||||
|
||||
const outCount = br.readVarint();
|
||||
|
||||
for (let i = 0; i < outCount; i++)
|
||||
Output.read(br);
|
||||
|
||||
return br.readU32();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from buffer reader.
|
||||
* @param {bio.BufferReader} br
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
assert(br.offset === 0);
|
||||
|
||||
this.readHead(br);
|
||||
|
||||
this._raw = br.data;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized data.
|
||||
* @param {Buffer} data
|
||||
*/
|
||||
|
||||
decode(data) {
|
||||
const br = bio.read(data);
|
||||
return this.read(br);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return serialized block data.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeBytes(this._raw);
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return serialized block data.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
encode() {
|
||||
return this._raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the serialized block data
|
||||
* and create an actual {@link Block}.
|
||||
* @returns {Block}
|
||||
* @throws Parse error
|
||||
*/
|
||||
|
||||
toBlock() {
|
||||
const block = Block.decode(this._raw);
|
||||
|
||||
block._hash = this._hash;
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the block to a headers object.
|
||||
* @returns {Headers}
|
||||
*/
|
||||
|
||||
toHeaders() {
|
||||
return Headers.fromBlock(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an object is a MemBlock.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isMemBlock(obj) {
|
||||
return obj instanceof MemBlock;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = MemBlock;
|
||||
611
docs/js-primitives/merkleblock.js
Normal file
611
docs/js-primitives/merkleblock.js
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
/*!
|
||||
* merkleblock.js - merkleblock object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const blake2b = require('bcrypto/lib/blake2b');
|
||||
const merkle = require('bcrypto/lib/mrkl');
|
||||
const {BufferMap, BufferSet} = require('buffer-map');
|
||||
const util = require('../utils/util');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const AbstractBlock = require('./abstractblock');
|
||||
const Headers = require('./headers');
|
||||
const DUMMY = Buffer.from([0]);
|
||||
const {encoding} = bio;
|
||||
|
||||
/** @typedef {import('bfilter').BloomFilter} BloomFilter */
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('../coins/coinview')} CoinView */
|
||||
/** @typedef {import('../protocol/network')} Network */
|
||||
/** @typedef {import('./block')} Block */
|
||||
/** @typedef {import('./tx')} TX */
|
||||
|
||||
/**
|
||||
* Merkle Block
|
||||
* Represents a merkle (filtered) block.
|
||||
* @alias module:primitives.MerkleBlock
|
||||
* @extends AbstractBlock
|
||||
*/
|
||||
|
||||
class MerkleBlock extends AbstractBlock {
|
||||
/**
|
||||
* Create a merkle block.
|
||||
* @constructor
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
/** @type {TX[]} */
|
||||
this.txs = [];
|
||||
/** @type {Hash[]} */
|
||||
this.hashes = [];
|
||||
this.flags = DUMMY;
|
||||
|
||||
this.totalTX = 0;
|
||||
this._tree = null;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
this.parseOptions(options);
|
||||
|
||||
assert(options, 'MerkleBlock data is required.');
|
||||
assert(Array.isArray(options.hashes));
|
||||
assert(Buffer.isBuffer(options.flags));
|
||||
assert((options.totalTX >>> 0) === options.totalTX);
|
||||
|
||||
if (options.hashes) {
|
||||
for (const hash of options.hashes) {
|
||||
assert(Buffer.isBuffer(hash));
|
||||
this.hashes.push(hash);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.flags) {
|
||||
assert(Buffer.isBuffer(options.flags));
|
||||
this.flags = options.flags;
|
||||
}
|
||||
|
||||
if (options.totalTX != null) {
|
||||
assert((options.totalTX >>> 0) === options.totalTX);
|
||||
this.totalTX = options.totalTX;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any cached values.
|
||||
* @param {Boolean?} [all] - Clear transactions.
|
||||
*/
|
||||
|
||||
refresh(all) {
|
||||
this._refresh();
|
||||
this._tree = null;
|
||||
|
||||
if (!all)
|
||||
return this;
|
||||
|
||||
for (const tx of this.txs)
|
||||
tx.refresh();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the block's _matched_ transaction vector against a hash.
|
||||
* @param {Hash} hash
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
hasTX(hash) {
|
||||
return this.indexOf(hash) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the block's _matched_ transaction vector against a hash.
|
||||
* @param {Hash} hash
|
||||
* @returns {Number} Index.
|
||||
*/
|
||||
|
||||
indexOf(hash) {
|
||||
const tree = this.getTree();
|
||||
const index = tree.map.get(hash);
|
||||
|
||||
if (index == null)
|
||||
return -1;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the partial merkletree.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
verifyBody() {
|
||||
const [valid] = this.checkBody();
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the partial merkletree.
|
||||
* @private
|
||||
* @returns {Array} [valid, reason, score]
|
||||
*/
|
||||
|
||||
checkBody() {
|
||||
const tree = this.getTree();
|
||||
|
||||
if (!tree.root.equals(this.merkleRoot))
|
||||
return [false, 'bad-txnmrklroot', 100];
|
||||
|
||||
return [true, 'valid', 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the matches from partial merkle
|
||||
* tree and calculate merkle root.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getTree() {
|
||||
if (!this._tree) {
|
||||
try {
|
||||
this._tree = this.extractTree();
|
||||
} catch (e) {
|
||||
this._tree = new PartialTree();
|
||||
}
|
||||
}
|
||||
return this._tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the matches from partial merkle
|
||||
* tree and calculate merkle root.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
extractTree() {
|
||||
const matches = [];
|
||||
const indexes = [];
|
||||
const map = new BufferMap();
|
||||
const hashes = this.hashes;
|
||||
const flags = this.flags;
|
||||
const totalTX = this.totalTX;
|
||||
const sentinel = merkle.hashEmpty(blake2b);
|
||||
|
||||
let bitsUsed = 0;
|
||||
let hashUsed = 0;
|
||||
let height = 0;
|
||||
let failed = false;
|
||||
|
||||
const width = (height) => {
|
||||
return (totalTX + (1 << height) - 1) >>> height;
|
||||
};
|
||||
|
||||
const traverse = (height, pos) => {
|
||||
if (bitsUsed >= flags.length * 8) {
|
||||
failed = true;
|
||||
return consensus.ZERO_HASH;
|
||||
}
|
||||
|
||||
const parent = (flags[bitsUsed / 8 | 0] >>> (bitsUsed % 8)) & 1;
|
||||
|
||||
bitsUsed += 1;
|
||||
|
||||
if (height === 0 || !parent) {
|
||||
if (hashUsed >= hashes.length) {
|
||||
failed = true;
|
||||
return consensus.ZERO_HASH;
|
||||
}
|
||||
|
||||
const hash = hashes[hashUsed];
|
||||
|
||||
hashUsed += 1;
|
||||
|
||||
if (height === 0 && parent) {
|
||||
matches.push(hash);
|
||||
indexes.push(pos);
|
||||
map.set(hash, pos);
|
||||
return merkle.hashLeaf(blake2b, hash);
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
const left = traverse(height - 1, pos * 2);
|
||||
|
||||
let right;
|
||||
|
||||
if (pos * 2 + 1 < width(height - 1))
|
||||
right = traverse(height - 1, pos * 2 + 1);
|
||||
else
|
||||
right = sentinel;
|
||||
|
||||
return merkle.hashInternal(blake2b, left, right);
|
||||
};
|
||||
|
||||
if (totalTX === 0)
|
||||
throw new Error('Zero transactions.');
|
||||
|
||||
if (totalTX > consensus.MAX_BLOCK_SIZE / 60)
|
||||
throw new Error('Too many transactions.');
|
||||
|
||||
if (hashes.length > totalTX)
|
||||
throw new Error('Too many hashes.');
|
||||
|
||||
if (flags.length * 8 < hashes.length)
|
||||
throw new Error('Flags too small.');
|
||||
|
||||
while (width(height) > 1)
|
||||
height += 1;
|
||||
|
||||
const root = traverse(height, 0);
|
||||
|
||||
if (failed)
|
||||
throw new Error('Mutated merkle tree.');
|
||||
|
||||
if (((bitsUsed + 7) / 8 | 0) !== flags.length)
|
||||
throw new Error('Too many flag bits.');
|
||||
|
||||
if (hashUsed !== hashes.length)
|
||||
throw new Error('Incorrect number of hashes.');
|
||||
|
||||
return new PartialTree(root, matches, indexes, map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the coinbase height (always -1).
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getCoinbaseHeight() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect the block and return a more
|
||||
* user-friendly representation of the data.
|
||||
* @param {CoinView} [view]
|
||||
* @param {Number} [height]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format(view, height) {
|
||||
return {
|
||||
hash: this.hash().toString('hex'),
|
||||
height: height != null ? height : -1,
|
||||
date: util.date(this.time),
|
||||
version: this.version.toString(16),
|
||||
prevBlock: this.prevBlock.toString('hex'),
|
||||
merkleRoot: this.merkleRoot.toString('hex'),
|
||||
witnessRoot: this.witnessRoot.toString('hex'),
|
||||
treeRoot: this.treeRoot.toString('hex'),
|
||||
reservedRoot: this.reservedRoot.toString('hex'),
|
||||
time: this.time,
|
||||
bits: this.bits,
|
||||
nonce: this.nonce,
|
||||
extraNonce: this.extraNonce.toString('hex'),
|
||||
mask: this.mask.toString('hex'),
|
||||
totalTX: this.totalTX,
|
||||
hashes: this.hashes.map((hash) => {
|
||||
return hash.toString('hex');
|
||||
}),
|
||||
flags: this.flags,
|
||||
map: this.getTree().map,
|
||||
txs: this.txs.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get merkleblock size.
|
||||
* @returns {Number} Size.
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
size += this.sizeHead();
|
||||
size += 4;
|
||||
size += encoding.sizeVarint(this.hashes.length);
|
||||
size += this.hashes.length * 32;
|
||||
size += encoding.sizeVarint(this.flags.length);
|
||||
size += this.flags.length;
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the merkleblock to a buffer writer.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
this.writeHead(bw);
|
||||
|
||||
bw.writeU32(this.totalTX);
|
||||
|
||||
bw.writeVarint(this.hashes.length);
|
||||
|
||||
for (const hash of this.hashes)
|
||||
bw.writeHash(hash);
|
||||
|
||||
bw.writeVarBytes(this.flags);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from buffer reader.
|
||||
* @param {bio.BufferReader} br
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.readHead(br);
|
||||
|
||||
this.totalTX = br.readU32();
|
||||
|
||||
const count = br.readVarint();
|
||||
|
||||
for (let i = 0; i < count; i++)
|
||||
this.hashes.push(br.readHash());
|
||||
|
||||
this.flags = br.readVarBytes();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the block to an object suitable
|
||||
* for JSON serialization.
|
||||
* @param {Network} [network]
|
||||
* @param {CoinView} [view]
|
||||
* @param {Number} [height]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON(network, view, height) {
|
||||
return {
|
||||
hash: this.hash().toString('hex'),
|
||||
height: height,
|
||||
version: this.version,
|
||||
prevBlock: this.prevBlock,
|
||||
merkleRoot: this.merkleRoot,
|
||||
witnessRoot: this.witnessRoot,
|
||||
treeRoot: this.treeRoot,
|
||||
reservedRoot: this.reservedRoot,
|
||||
time: this.time,
|
||||
bits: this.bits,
|
||||
nonce: this.nonce,
|
||||
extraNonce: this.extraNonce.toString('hex'),
|
||||
mask: this.mask.toString('hex'),
|
||||
totalTX: this.totalTX,
|
||||
hashes: this.hashes.map((hash) => {
|
||||
return hash.toString('hex');
|
||||
}),
|
||||
flags: this.flags.toString('hex')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from json object.
|
||||
* @param {Object} json
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json, 'MerkleBlock data is required.');
|
||||
assert(Array.isArray(json.hashes));
|
||||
assert(typeof json.flags === 'string');
|
||||
assert((json.totalTX >>> 0) === json.totalTX);
|
||||
|
||||
this.parseJSON(json);
|
||||
|
||||
for (const hash of json.hashes)
|
||||
this.hashes.push(Buffer.from(hash, 'hex'));
|
||||
|
||||
this.flags = Buffer.from(json.flags, 'hex');
|
||||
|
||||
this.totalTX = json.totalTX;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a merkleblock from a {@link Block} object, passing
|
||||
* it through a filter first. This will build the partial
|
||||
* merkle tree.
|
||||
* @param {Block} block
|
||||
* @param {BloomFilter} filter
|
||||
* @returns {MerkleBlock}
|
||||
*/
|
||||
|
||||
static fromBlock(block, filter) {
|
||||
const matches = [];
|
||||
|
||||
for (const tx of block.txs)
|
||||
matches.push(tx.testAndMaybeUpdate(filter) ? 1 : 0);
|
||||
|
||||
return this.fromMatches(block, matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a merkleblock from an array of txids.
|
||||
* This will build the partial merkle tree.
|
||||
* @param {Block} block
|
||||
* @param {Hash[]} hashes
|
||||
* @returns {MerkleBlock}
|
||||
*/
|
||||
|
||||
static fromHashes(block, hashes) {
|
||||
const filter = new BufferSet();
|
||||
|
||||
for (const hash of hashes)
|
||||
filter.add(hash);
|
||||
|
||||
const matches = [];
|
||||
|
||||
for (const tx of block.txs) {
|
||||
const hash = tx.hash();
|
||||
matches.push(filter.has(hash) ? 1 : 0);
|
||||
}
|
||||
|
||||
return this.fromMatches(block, matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a merkleblock from an array of matches.
|
||||
* This will build the partial merkle tree.
|
||||
* @param {Block} block
|
||||
* @param {Number[]} matches
|
||||
* @returns {MerkleBlock}
|
||||
*/
|
||||
|
||||
static fromMatches(block, matches) {
|
||||
const txs = [];
|
||||
const leaves = [];
|
||||
const bits = [];
|
||||
const hashes = [];
|
||||
const totalTX = block.txs.length;
|
||||
const sentinel = merkle.hashEmpty(blake2b);
|
||||
|
||||
let height = 0;
|
||||
|
||||
const width = (height) => {
|
||||
return (totalTX + (1 << height) - 1) >>> height;
|
||||
};
|
||||
|
||||
const hash = (height, pos, leaves) => {
|
||||
if (height === 0)
|
||||
return merkle.hashLeaf(blake2b, leaves[pos]);
|
||||
|
||||
const left = hash(height - 1, pos * 2, leaves);
|
||||
|
||||
let right;
|
||||
|
||||
if (pos * 2 + 1 < width(height - 1))
|
||||
right = hash(height - 1, pos * 2 + 1, leaves);
|
||||
else
|
||||
right = sentinel;
|
||||
|
||||
return merkle.hashInternal(blake2b, left, right);
|
||||
};
|
||||
|
||||
const traverse = (height, pos, leaves, matches) => {
|
||||
let parent = 0;
|
||||
|
||||
for (let p = pos << height; p < ((pos + 1) << height) && p < totalTX; p++)
|
||||
parent |= matches[p];
|
||||
|
||||
bits.push(parent);
|
||||
|
||||
if (height === 0 && parent) {
|
||||
hashes.push(leaves[pos]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (height === 0 || !parent) {
|
||||
hashes.push(hash(height, pos, leaves));
|
||||
return;
|
||||
}
|
||||
|
||||
traverse(height - 1, pos * 2, leaves, matches);
|
||||
|
||||
if (pos * 2 + 1 < width(height - 1))
|
||||
traverse(height - 1, pos * 2 + 1, leaves, matches);
|
||||
};
|
||||
|
||||
for (let i = 0; i < block.txs.length; i++) {
|
||||
const tx = block.txs[i];
|
||||
|
||||
if (matches[i])
|
||||
txs.push(tx);
|
||||
|
||||
leaves.push(tx.hash());
|
||||
}
|
||||
|
||||
while (width(height) > 1)
|
||||
height += 1;
|
||||
|
||||
traverse(height, 0, leaves, matches);
|
||||
|
||||
const flags = Buffer.allocUnsafe((bits.length + 7) / 8 | 0);
|
||||
flags.fill(0);
|
||||
|
||||
for (let p = 0; p < bits.length; p++)
|
||||
flags[p / 8 | 0] |= bits[p] << (p % 8);
|
||||
|
||||
const mblock = new this();
|
||||
mblock._hash = block._hash;
|
||||
mblock.version = block.version;
|
||||
mblock.prevBlock = block.prevBlock;
|
||||
mblock.merkleRoot = block.merkleRoot;
|
||||
mblock.witnessRoot = block.witnessRoot;
|
||||
mblock.treeRoot = block.treeRoot;
|
||||
mblock.reservedRoot = block.reservedRoot;
|
||||
mblock.time = block.time;
|
||||
mblock.bits = block.bits;
|
||||
mblock.nonce = block.nonce;
|
||||
mblock.extraNonce = block.extraNonce;
|
||||
mblock.mask = block.mask;
|
||||
mblock.totalTX = totalTX;
|
||||
mblock.hashes = hashes;
|
||||
mblock.flags = flags;
|
||||
mblock.txs = txs;
|
||||
|
||||
return mblock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an object is a MerkleBlock.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isMerkleBlock(obj) {
|
||||
return obj instanceof MerkleBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the block to a headers object.
|
||||
* @returns {Headers}
|
||||
*/
|
||||
|
||||
toHeaders() {
|
||||
return Headers.fromBlock(this);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
class PartialTree {
|
||||
constructor(root, matches, indexes, map) {
|
||||
this.root = root || consensus.ZERO_HASH;
|
||||
this.matches = matches || [];
|
||||
this.indexes = indexes || [];
|
||||
this.map = map || new BufferMap();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = MerkleBlock;
|
||||
1493
docs/js-primitives/mtx.js
Normal file
1493
docs/js-primitives/mtx.js
Normal file
File diff suppressed because it is too large
Load diff
284
docs/js-primitives/outpoint.js
Normal file
284
docs/js-primitives/outpoint.js
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
/*!
|
||||
* outpoint.js - outpoint object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const util = require('../utils/util');
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').HexHash} HexHash */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('./tx')} TX */
|
||||
|
||||
/**
|
||||
* @typedef {Object} OutpointJSON
|
||||
* @property {HexHash} hash
|
||||
* @property {Number} index
|
||||
*/
|
||||
|
||||
/**
|
||||
* Outpoint
|
||||
* Represents a COutPoint.
|
||||
* @alias module:primitives.Outpoint
|
||||
* @property {Hash} hash
|
||||
* @property {Number} index
|
||||
*/
|
||||
|
||||
class Outpoint extends bio.Struct {
|
||||
/**
|
||||
* Create an outpoint.
|
||||
* @constructor
|
||||
* @param {Hash?} [hash]
|
||||
* @param {Number?} [index]
|
||||
*/
|
||||
|
||||
constructor(hash, index) {
|
||||
super();
|
||||
|
||||
this.hash = consensus.ZERO_HASH;
|
||||
this.index = 0xffffffff;
|
||||
|
||||
if (hash != null) {
|
||||
assert(Buffer.isBuffer(hash));
|
||||
assert((index >>> 0) === index, 'Index must be a uint32.');
|
||||
this.hash = hash;
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
assert(options, 'Outpoint data is required.');
|
||||
assert(Buffer.isBuffer(options.hash));
|
||||
assert((options.index >>> 0) === options.index, 'Index must be a uint32.');
|
||||
this.hash = options.hash;
|
||||
this.index = options.index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the outpoint.
|
||||
* @param {this} prevout
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
inject(prevout) {
|
||||
assert(prevout instanceof this.constructor);
|
||||
this.hash = prevout.hash;
|
||||
this.index = prevout.index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test equality against another outpoint.
|
||||
* @param {this} prevout
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
equals(prevout) {
|
||||
assert(prevout instanceof this.constructor);
|
||||
return this.hash.equals(prevout.hash)
|
||||
&& this.index === prevout.index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare against another outpoint (BIP69).
|
||||
* @param {this} prevout
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
compare(prevout) {
|
||||
assert(prevout instanceof this.constructor);
|
||||
|
||||
const cmp = this.hash.compare(prevout.hash);
|
||||
|
||||
if (cmp !== 0)
|
||||
return cmp;
|
||||
|
||||
return this.index - prevout.index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the outpoint is null (hash of zeroes
|
||||
* with max-u32 index). Used to detect coinbases.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isNull() {
|
||||
return this.index === 0xffffffff && this.hash.equals(consensus.ZERO_HASH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get little-endian hash.
|
||||
* @returns {HexHash}
|
||||
*/
|
||||
|
||||
txid() {
|
||||
return this.hash.toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize outpoint to a key
|
||||
* suitable for a hash table.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
toKey() {
|
||||
return Outpoint.toKey(this.hash, this.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from hash table key.
|
||||
* @param {Buffer} key
|
||||
* @returns {Outpoint}
|
||||
*/
|
||||
|
||||
fromKey(key) {
|
||||
assert(Buffer.isBuffer(key) && key.length === 36);
|
||||
this.hash = key.slice(0, 32);
|
||||
this.index = bio.readU32(key, 32);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate outpoint from hash table key.
|
||||
* @param {Buffer} key
|
||||
* @returns {Outpoint}
|
||||
*/
|
||||
|
||||
static fromKey(key) {
|
||||
return new this().fromKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write outpoint to a buffer writer.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeHash(this.hash);
|
||||
bw.writeU32(this.index);
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate size of outpoint.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 36;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from buffer reader.
|
||||
* @param {bio.BufferReader} br
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.hash = br.readHash();
|
||||
this.index = br.readU32();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from json object.
|
||||
* @param {OutpointJSON} json
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json, 'Outpoint data is required.');
|
||||
assert(json.hash, 'Hash is required.');
|
||||
assert((json.index >>> 0) === json.index, 'Index must be a uint32.');
|
||||
this.hash = util.parseHex(json.hash, 32);
|
||||
this.index = json.index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the outpoint to an object suitable
|
||||
* for JSON serialization.
|
||||
* @returns {OutpointJSON}
|
||||
*/
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
hash: this.hash.toString('hex'),
|
||||
index: this.index
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from tx.
|
||||
* @private
|
||||
* @param {TX} tx
|
||||
* @param {Number} index
|
||||
*/
|
||||
|
||||
fromTX(tx, index) {
|
||||
assert(tx);
|
||||
assert((index >>> 0) === index);
|
||||
this.hash = tx.hash();
|
||||
this.index = index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate outpoint from tx.
|
||||
* @param {TX} tx
|
||||
* @param {Number} index
|
||||
* @returns {Outpoint}
|
||||
*/
|
||||
|
||||
static fromTX(tx, index) {
|
||||
return new this().fromTX(tx, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize outpoint to a key
|
||||
* suitable for a hash table.
|
||||
* @param {Hash} hash
|
||||
* @param {Number} index
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
static toKey(hash, index) {
|
||||
return new Outpoint(hash, index).encode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the outpoint to a user-friendly string.
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
format() {
|
||||
return `<Outpoint: ${this.hash.toString('hex')}/${this.index}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test an object to see if it is an outpoint.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isOutpoint(obj) {
|
||||
return obj instanceof Outpoint;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Outpoint;
|
||||
307
docs/js-primitives/output.js
Normal file
307
docs/js-primitives/output.js
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
/*!
|
||||
* output.js - output object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const Amount = require('../ui/amount');
|
||||
const Network = require('../protocol/network');
|
||||
const Address = require('../primitives/address');
|
||||
const consensus = require('../protocol/consensus');
|
||||
const policy = require('../protocol/policy');
|
||||
const util = require('../utils/util');
|
||||
const Covenant = require('./covenant');
|
||||
|
||||
/** @typedef {import('../types').Amount} AmountValue */
|
||||
/** @typedef {import('../types').Rate} Rate */
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('./covenant').CovenantJSON} CovenantJSON */
|
||||
|
||||
/**
|
||||
* @typedef {Object} OutputJSON
|
||||
* @property {AmountValue} value
|
||||
* @property {String} address
|
||||
* @property {CovenantJSON} covenant
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a transaction output.
|
||||
* @alias module:primitives.Output
|
||||
* @property {AmountValue} value
|
||||
* @property {Address} address
|
||||
*/
|
||||
|
||||
class Output extends bio.Struct {
|
||||
/**
|
||||
* Create an output.
|
||||
* @constructor
|
||||
* @param {Object?} [options]
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.value = 0;
|
||||
this.address = new Address();
|
||||
this.covenant = new Covenant();
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
assert(options, 'Output data is required.');
|
||||
|
||||
if (options.value != null) {
|
||||
assert(util.isU64(options.value), 'Value must be a uint64.');
|
||||
this.value = options.value;
|
||||
}
|
||||
|
||||
if (options.address)
|
||||
this.address.fromOptions(options.address);
|
||||
|
||||
if (options.covenant)
|
||||
this.covenant.fromOptions(options.covenant);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from address/value pair.
|
||||
* @param {Address} address
|
||||
* @param {AmountValue} value
|
||||
* @returns {Output}
|
||||
*/
|
||||
|
||||
fromScript(address, value) {
|
||||
assert(util.isU64(value), 'Value must be a uint64.');
|
||||
|
||||
this.address = Address.fromOptions(address);
|
||||
this.value = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate output from address/value pair.
|
||||
* @param {Address} address
|
||||
* @param {AmountValue} value
|
||||
* @returns {Output}
|
||||
*/
|
||||
|
||||
static fromScript(address, value) {
|
||||
return new this().fromScript(address, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the output.
|
||||
* @param {this} output
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
inject(output) {
|
||||
assert(output instanceof this.constructor);
|
||||
this.value = output.value;
|
||||
this.address.inject(output.address);
|
||||
this.covenant.inject(output.covenant);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test equality against another output.
|
||||
* @param {Output} output
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
equals(output) {
|
||||
assert(output instanceof this.constructor);
|
||||
return this.value === output.value
|
||||
&& this.address.equals(output.address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare against another output (BIP69).
|
||||
* @param {Output} output
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
compare(output) {
|
||||
assert(output instanceof this.constructor);
|
||||
|
||||
const cmp = this.value - output.value;
|
||||
|
||||
if (cmp !== 0)
|
||||
return cmp;
|
||||
|
||||
return this.address.compare(output.address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address.
|
||||
* @returns {Address} address
|
||||
*/
|
||||
|
||||
getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address hash.
|
||||
* @returns {Hash}
|
||||
*/
|
||||
|
||||
getHash() {
|
||||
return this.address.getHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the input to a more user-friendly object.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format() {
|
||||
return {
|
||||
value: Amount.coin(this.value),
|
||||
address: this.address,
|
||||
covenant: this.covenant
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the output to an object suitable
|
||||
* for JSON serialization.
|
||||
* @param {Network} [network]
|
||||
* @returns {OutputJSON}
|
||||
*/
|
||||
|
||||
getJSON(network) {
|
||||
network = Network.get(network);
|
||||
|
||||
return {
|
||||
value: this.value,
|
||||
address: this.address.toString(network),
|
||||
covenant: this.covenant.toJSON()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the dust threshold for this
|
||||
* output, based on serialize size and rate.
|
||||
* @param {Rate?} [rate]
|
||||
* @returns {AmountValue}
|
||||
*/
|
||||
|
||||
getDustThreshold(rate) {
|
||||
if (!this.covenant.isDustworthy())
|
||||
return 0;
|
||||
|
||||
if (this.address.isUnspendable())
|
||||
return 0;
|
||||
|
||||
const scale = consensus.WITNESS_SCALE_FACTOR;
|
||||
|
||||
let size = this.getSize();
|
||||
|
||||
size += 32 + 4 + 1 + (107 / scale | 0) + 4;
|
||||
|
||||
return 3 * policy.getMinFee(size, rate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate size of serialized output.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 8 + this.address.getSize() + this.covenant.getVarSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the output should be considered dust.
|
||||
* @param {Rate?} [rate]
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isDust(rate) {
|
||||
return this.value < this.getDustThreshold(rate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the output is unspendable.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isUnspendable() {
|
||||
return this.address.isUnspendable() || this.covenant.isUnspendable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from a JSON object.
|
||||
* @param {OutputJSON} json
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json, 'Output data is required.');
|
||||
assert(util.isU64(json.value), 'Value must be a uint64.');
|
||||
|
||||
this.value = json.value;
|
||||
this.address.fromString(json.address);
|
||||
|
||||
if (json.covenant != null)
|
||||
this.covenant.fromJSON(json.covenant);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the output to a buffer writer.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeU64(this.value);
|
||||
this.address.write(bw);
|
||||
this.covenant.write(bw);
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from buffer reader.
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.value = br.readU64();
|
||||
this.address.read(br);
|
||||
this.covenant.read(br);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test an object to see if it is an Output.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isOutput(obj) {
|
||||
return obj instanceof Output;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Output;
|
||||
2096
docs/js-primitives/tx.js
Normal file
2096
docs/js-primitives/tx.js
Normal file
File diff suppressed because it is too large
Load diff
265
docs/js-primitives/txmeta.js
Normal file
265
docs/js-primitives/txmeta.js
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
/*!
|
||||
* txmeta.js - extended transaction object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const util = require('../utils/util');
|
||||
const TX = require('./tx');
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('../blockchain/chainentry')} ChainEntry */
|
||||
/** @typedef {import('../coins/coinview')} CoinView */
|
||||
/** @typedef {import('../protocol/network')} Network */
|
||||
|
||||
/**
|
||||
* TXMeta
|
||||
* An extended transaction object.
|
||||
* @alias module:primitives.TXMeta
|
||||
*/
|
||||
|
||||
class TXMeta extends bio.Struct {
|
||||
/**
|
||||
* Create an extended transaction.
|
||||
* @constructor
|
||||
* @param {Object?} [options]
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.tx = new TX();
|
||||
this.mtime = util.now();
|
||||
this.height = -1;
|
||||
/** @type {Hash} */
|
||||
this.block = null;
|
||||
this.time = 0;
|
||||
this.index = -1;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
if (options.tx) {
|
||||
assert(options.tx instanceof TX);
|
||||
this.tx = options.tx;
|
||||
}
|
||||
|
||||
if (options.mtime != null) {
|
||||
assert(util.isU64(options.mtime));
|
||||
this.mtime = options.mtime;
|
||||
}
|
||||
|
||||
if (options.height != null) {
|
||||
assert(options.height === -1
|
||||
|| (options.height >>> 0) === options.height);
|
||||
this.height = options.height;
|
||||
}
|
||||
|
||||
if (options.block !== undefined) {
|
||||
assert(options.block === null || Buffer.isBuffer(options.block));
|
||||
this.block = options.block;
|
||||
}
|
||||
|
||||
if (options.time != null) {
|
||||
assert(util.isU64(options.time));
|
||||
this.time = options.time;
|
||||
}
|
||||
|
||||
if (options.index != null) {
|
||||
assert(options.index === -1 || (options.index >>> 0) === options.index);
|
||||
this.index = options.index;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {TX} tx
|
||||
* @param {ChainEntry} entry
|
||||
* @param {Number} index
|
||||
*/
|
||||
|
||||
fromTX(tx, entry, index) {
|
||||
this.tx = tx;
|
||||
if (entry) {
|
||||
this.height = entry.height;
|
||||
this.block = entry.hash;
|
||||
this.time = entry.time;
|
||||
this.index = index;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate TXMeta from options.
|
||||
* @param {TX} tx
|
||||
* @param {ChainEntry} entry
|
||||
* @param {Number} index
|
||||
* @returns {TXMeta}
|
||||
*/
|
||||
|
||||
static fromTX(tx, entry, index) {
|
||||
return new this().fromTX(tx, entry, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect the transaction.
|
||||
* @param {CoinView} view
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format(view) {
|
||||
const data = this.tx.format(view, null, this.index);
|
||||
data.mtime = this.mtime;
|
||||
data.height = this.height;
|
||||
data.block = this.block ? this.block.toString('hex') : null;
|
||||
data.time = this.time;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the transaction to an object suitable
|
||||
* for JSON serialization.
|
||||
* @param {Network} [network]
|
||||
* @param {CoinView} [view]
|
||||
* @param {Number} [chainHeight]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON(network, view, chainHeight) {
|
||||
const json = this.tx.getJSON(network, view, null, this.index);
|
||||
json.mtime = this.mtime;
|
||||
json.height = this.height;
|
||||
json.block = this.block ? this.block.toString('hex') : null;
|
||||
json.time = this.time;
|
||||
json.confirmations = 0;
|
||||
|
||||
if (chainHeight != null && this.height !== -1)
|
||||
json.confirmations = chainHeight - this.height + 1;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from a json object.
|
||||
* @param {Object} json
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
this.tx.fromJSON(json);
|
||||
|
||||
assert(util.isU64(json.mtime));
|
||||
assert(json.height === -1 || (json.height >>> 0) === json.height);
|
||||
assert(!json.block || typeof json.block === 'string');
|
||||
assert(util.isU64(json.time));
|
||||
assert(json.index === -1 || (json.index >>> 0) === json.index);
|
||||
|
||||
this.mtime = json.mtime;
|
||||
this.height = json.height;
|
||||
this.block = json.block ? util.parseHex(json.block, 32) : null;
|
||||
this.index = json.index;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate serialization size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
|
||||
size += this.tx.getSize();
|
||||
size += 4;
|
||||
|
||||
if (this.block) {
|
||||
size += 1;
|
||||
size += 32;
|
||||
size += 4 * 3;
|
||||
} else {
|
||||
size += 1;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a transaction to "extended format".
|
||||
* This is the serialization format we use internally
|
||||
* to store transactions in the database. The extended
|
||||
* serialization includes the height, block hash, index,
|
||||
* timestamp, and pending-since time.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
this.tx.write(bw);
|
||||
|
||||
bw.writeU32(this.mtime);
|
||||
|
||||
if (this.block) {
|
||||
bw.writeU8(1);
|
||||
bw.writeHash(this.block);
|
||||
bw.writeU32(this.height);
|
||||
bw.writeU32(this.time);
|
||||
bw.writeU32(this.index);
|
||||
} else {
|
||||
bw.writeU8(0);
|
||||
}
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from "extended" serialization format.
|
||||
* @param {bio.BufferReader} br
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.tx.read(br);
|
||||
|
||||
this.mtime = br.readU32();
|
||||
|
||||
if (br.readU8() === 1) {
|
||||
this.block = br.readHash();
|
||||
this.height = br.readU32();
|
||||
this.time = br.readU32();
|
||||
this.index = br.readU32();
|
||||
if (this.index === 0xffffffff)
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an object is an TXMeta.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isTXMeta(obj) {
|
||||
return obj instanceof TXMeta;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = TXMeta;
|
||||
904
docs/js-wallet/account.js
Normal file
904
docs/js-wallet/account.js
Normal file
|
|
@ -0,0 +1,904 @@
|
|||
/*!
|
||||
* account.js - account object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const binary = require('../utils/binary');
|
||||
const Path = require('./path');
|
||||
const common = require('./common');
|
||||
const Script = require('../script/script');
|
||||
const WalletKey = require('./walletkey');
|
||||
const HDPublicKey = require('../hd/public');
|
||||
|
||||
/** @typedef {import('bdb').DB} DB */
|
||||
/** @typedef {ReturnType<DB['batch']>} Batch */
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('./walletdb')} WalletDB */
|
||||
/** @typedef {import('./masterkey')} MasterKey */
|
||||
/** @typedef {import('../primitives/address')} Address */
|
||||
|
||||
/**
|
||||
* Account
|
||||
* Represents a BIP44 Account belonging to a {@link Wallet}.
|
||||
* Note that this object does not enforce locks. Any method
|
||||
* that does a write is internal API only and will lead
|
||||
* to race conditions if used elsewhere.
|
||||
* @alias module:wallet.Account
|
||||
*/
|
||||
|
||||
class Account extends bio.Struct {
|
||||
/**
|
||||
* Create an account.
|
||||
* @constructor
|
||||
* @param {WalletDB} wdb
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
constructor(wdb, options) {
|
||||
super();
|
||||
|
||||
assert(wdb, 'Database is required.');
|
||||
|
||||
/** @type {WalletDB} */
|
||||
this.wdb = wdb;
|
||||
this.network = wdb.network;
|
||||
|
||||
this.wid = 0;
|
||||
/** @type {String|null} */
|
||||
this.id = null;
|
||||
this.accountIndex = 0;
|
||||
/** @type {String|null} */
|
||||
this.name = null;
|
||||
this.initialized = false;
|
||||
this.watchOnly = false;
|
||||
/** @type {Account.types} */
|
||||
this.type = Account.types.PUBKEYHASH;
|
||||
this.m = 1;
|
||||
this.n = 1;
|
||||
this.receiveDepth = 0;
|
||||
this.changeDepth = 0;
|
||||
this.lookahead = 200;
|
||||
/** @type {HDPublicKey|null} */
|
||||
this.accountKey = null;
|
||||
/** @type {HDPublicKey[]} */
|
||||
this.keys = [];
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {Object} options
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
assert(options, 'Options are required.');
|
||||
assert((options.wid >>> 0) === options.wid);
|
||||
assert(common.isName(options.id), 'Bad Wallet ID.');
|
||||
assert(HDPublicKey.isHDPublicKey(options.accountKey),
|
||||
'Account key is required.');
|
||||
assert((options.accountIndex >>> 0) === options.accountIndex,
|
||||
'Account index is required.');
|
||||
|
||||
this.wid = options.wid;
|
||||
this.id = options.id;
|
||||
|
||||
if (options.accountIndex != null) {
|
||||
assert((options.accountIndex >>> 0) === options.accountIndex);
|
||||
this.accountIndex = options.accountIndex;
|
||||
}
|
||||
|
||||
if (options.name != null) {
|
||||
assert(common.isName(options.name), 'Bad account name.');
|
||||
this.name = options.name;
|
||||
}
|
||||
|
||||
if (options.initialized != null) {
|
||||
assert(typeof options.initialized === 'boolean');
|
||||
this.initialized = options.initialized;
|
||||
}
|
||||
|
||||
if (options.watchOnly != null) {
|
||||
assert(typeof options.watchOnly === 'boolean');
|
||||
this.watchOnly = options.watchOnly;
|
||||
}
|
||||
|
||||
if (options.type != null) {
|
||||
if (typeof options.type === 'string') {
|
||||
this.type = Account.types[options.type.toUpperCase()];
|
||||
assert(this.type != null);
|
||||
} else {
|
||||
assert(typeof options.type === 'number');
|
||||
this.type = options.type;
|
||||
assert(Account.typesByVal[this.type]);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.m != null) {
|
||||
assert((options.m & 0xff) === options.m);
|
||||
this.m = options.m;
|
||||
}
|
||||
|
||||
if (options.n != null) {
|
||||
assert((options.n & 0xff) === options.n);
|
||||
this.n = options.n;
|
||||
}
|
||||
|
||||
if (options.receiveDepth != null) {
|
||||
assert((options.receiveDepth >>> 0) === options.receiveDepth);
|
||||
this.receiveDepth = options.receiveDepth;
|
||||
}
|
||||
|
||||
if (options.changeDepth != null) {
|
||||
assert((options.changeDepth >>> 0) === options.changeDepth);
|
||||
this.changeDepth = options.changeDepth;
|
||||
}
|
||||
|
||||
if (options.lookahead != null) {
|
||||
assert((options.lookahead >>> 0) === options.lookahead);
|
||||
assert(options.lookahead >= 0);
|
||||
this.lookahead = options.lookahead;
|
||||
}
|
||||
|
||||
this.accountKey = options.accountKey;
|
||||
|
||||
if (this.n > 1)
|
||||
this.type = Account.types.MULTISIG;
|
||||
|
||||
if (!this.name)
|
||||
this.name = this.accountIndex.toString(10);
|
||||
|
||||
if (this.m < 1 || this.m > this.n)
|
||||
throw new Error('m ranges between 1 and n');
|
||||
|
||||
if (options.keys) {
|
||||
assert(Array.isArray(options.keys));
|
||||
for (const key of options.keys)
|
||||
this.pushKey(key);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {WalletDB} wdb
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
static fromOptions(wdb, options) {
|
||||
return new this(wdb).fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to intialize the account (generating
|
||||
* the first addresses along with the lookahead
|
||||
* addresses). Called automatically from the
|
||||
* walletdb.
|
||||
* @param {Batch} b
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async init(b) {
|
||||
// Waiting for more keys.
|
||||
if (this.keys.length !== this.n - 1) {
|
||||
assert(!this.initialized);
|
||||
this.save(b);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(this.receiveDepth === 0);
|
||||
assert(this.changeDepth === 0);
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
await this.initDepth(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a public account key to the account (multisig).
|
||||
* Does not update the database.
|
||||
* @param {HDPublicKey} key - Account (bip44)
|
||||
* key (can be in base58 form).
|
||||
* @throws Error on non-hdkey/non-accountkey.
|
||||
* @returns {Boolean} - Whether the key was added.
|
||||
*/
|
||||
|
||||
pushKey(key) {
|
||||
if (typeof key === 'string')
|
||||
key = HDPublicKey.fromBase58(key, this.network);
|
||||
|
||||
if (!HDPublicKey.isHDPublicKey(key))
|
||||
throw new Error('Must add HD keys to wallet.');
|
||||
|
||||
if (!key.isAccount())
|
||||
throw new Error('Must add HD account keys to BIP44 wallet.');
|
||||
|
||||
if (this.type !== Account.types.MULTISIG)
|
||||
throw new Error('Cannot add keys to non-multisig wallet.');
|
||||
|
||||
if (key.equals(this.accountKey))
|
||||
throw new Error('Cannot add own key.');
|
||||
|
||||
const index = binary.insert(this.keys, key, cmp, true);
|
||||
|
||||
if (index === -1)
|
||||
return false;
|
||||
|
||||
if (this.keys.length > this.n - 1) {
|
||||
binary.remove(this.keys, key, cmp);
|
||||
throw new Error('Cannot add more keys.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a public account key to the account (multisig).
|
||||
* Does not update the database.
|
||||
* @param {HDPublicKey} key - Account (bip44)
|
||||
* key (can be in base58 form).
|
||||
* @throws Error on non-hdkey/non-accountkey.
|
||||
* @returns {Boolean} - Whether the key was removed.
|
||||
*/
|
||||
|
||||
spliceKey(key) {
|
||||
if (typeof key === 'string')
|
||||
key = HDPublicKey.fromBase58(key, this.network);
|
||||
|
||||
if (!HDPublicKey.isHDPublicKey(key))
|
||||
throw new Error('Must add HD keys to wallet.');
|
||||
|
||||
if (!key.isAccount())
|
||||
throw new Error('Must add HD account keys to BIP44 wallet.');
|
||||
|
||||
if (this.type !== Account.types.MULTISIG)
|
||||
throw new Error('Cannot remove keys from non-multisig wallet.');
|
||||
|
||||
if (this.keys.length === this.n - 1)
|
||||
throw new Error('Cannot remove key.');
|
||||
|
||||
return binary.remove(this.keys, key, cmp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a public account key to the account (multisig).
|
||||
* Saves the key in the wallet database.
|
||||
* @param {Batch} b
|
||||
* @param {HDPublicKey} key
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
|
||||
async addSharedKey(b, key) {
|
||||
const result = this.pushKey(key);
|
||||
|
||||
if (await this.hasDuplicate()) {
|
||||
this.spliceKey(key);
|
||||
throw new Error('Cannot add a key from another account.');
|
||||
}
|
||||
|
||||
// Try to initialize again.
|
||||
await this.init(b);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure accounts are not sharing keys.
|
||||
* @private
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
|
||||
async hasDuplicate() {
|
||||
if (this.keys.length !== this.n - 1)
|
||||
return false;
|
||||
|
||||
const ring = this.deriveReceive(0);
|
||||
const hash = ring.getScriptHash();
|
||||
|
||||
return this.wdb.hasPath(this.wid, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a public account key from the account (multisig).
|
||||
* Remove the key from the wallet database.
|
||||
* @param {Batch} b
|
||||
* @param {HDPublicKey} key
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
removeSharedKey(b, key) {
|
||||
const result = this.spliceKey(key);
|
||||
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
this.save(b);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new receiving address (increments receiveDepth).
|
||||
* @param {Batch} b
|
||||
* @returns {Promise<WalletKey>}
|
||||
*/
|
||||
|
||||
createReceive(b) {
|
||||
return this.createKey(b, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new change address (increments changeDepth).
|
||||
* @param {Batch} b
|
||||
* @returns {Promise<WalletKey>}
|
||||
*/
|
||||
|
||||
createChange(b) {
|
||||
return this.createKey(b, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new address (increments depth).
|
||||
* @param {Batch} b
|
||||
* @param {Number} branch
|
||||
* @returns {Promise<WalletKey>} - Returns {@link WalletKey}.
|
||||
*/
|
||||
|
||||
async createKey(b, branch) {
|
||||
let key, lookahead;
|
||||
|
||||
switch (branch) {
|
||||
case 0:
|
||||
key = this.deriveReceive(this.receiveDepth);
|
||||
lookahead = this.deriveReceive(this.receiveDepth + this.lookahead);
|
||||
await this.saveKey(b, lookahead);
|
||||
this.receiveDepth += 1;
|
||||
this.receive = key;
|
||||
break;
|
||||
case 1:
|
||||
key = this.deriveChange(this.changeDepth);
|
||||
lookahead = this.deriveChange(this.changeDepth + this.lookahead);
|
||||
await this.saveKey(b, lookahead);
|
||||
this.changeDepth += 1;
|
||||
this.change = key;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Bad branch: ${branch}.`);
|
||||
}
|
||||
|
||||
this.save(b);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a receiving address at `index`. Do not increment depth.
|
||||
* @param {Number} index
|
||||
* @param {MasterKey} [master]
|
||||
* @returns {WalletKey}
|
||||
*/
|
||||
|
||||
deriveReceive(index, master) {
|
||||
return this.deriveKey(0, index, master);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a change address at `index`. Do not increment depth.
|
||||
* @param {Number} index
|
||||
* @param {MasterKey} [master]
|
||||
* @returns {WalletKey}
|
||||
*/
|
||||
|
||||
deriveChange(index, master) {
|
||||
return this.deriveKey(1, index, master);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive an address from `path` object.
|
||||
* @param {Path} path
|
||||
* @param {MasterKey} master
|
||||
* @returns {WalletKey?}
|
||||
*/
|
||||
|
||||
derivePath(path, master) {
|
||||
switch (path.keyType) {
|
||||
case Path.types.HD: {
|
||||
return this.deriveKey(path.branch, path.index, master);
|
||||
}
|
||||
case Path.types.KEY: {
|
||||
assert(this.type === Account.types.PUBKEYHASH);
|
||||
|
||||
let data = path.data;
|
||||
|
||||
if (path.encrypted) {
|
||||
data = master.decipher(data, path.hash);
|
||||
if (!data)
|
||||
return null;
|
||||
}
|
||||
|
||||
return WalletKey.fromImport(this, data);
|
||||
}
|
||||
case Path.types.ADDRESS: {
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
throw new Error('Bad key type.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive an address at `index`. Do not increment depth.
|
||||
* @param {Number} branch
|
||||
* @param {Number} index
|
||||
* @param {MasterKey} [master]
|
||||
* @returns {WalletKey}
|
||||
*/
|
||||
|
||||
deriveKey(branch, index, master) {
|
||||
assert(typeof branch === 'number');
|
||||
|
||||
const keys = [];
|
||||
|
||||
let key;
|
||||
if (master && master.key && !this.watchOnly) {
|
||||
const type = this.network.keyPrefix.coinType;
|
||||
key = master.key.deriveAccount(44, type, this.accountIndex);
|
||||
key = key.derive(branch).derive(index);
|
||||
} else {
|
||||
key = this.accountKey.derive(branch).derive(index);
|
||||
}
|
||||
|
||||
const ring = WalletKey.fromHD(this, key, branch, index);
|
||||
|
||||
switch (this.type) {
|
||||
case Account.types.PUBKEYHASH:
|
||||
break;
|
||||
case Account.types.MULTISIG:
|
||||
keys.push(key.publicKey);
|
||||
|
||||
for (const shared of this.keys) {
|
||||
const key = shared.derive(branch).derive(index);
|
||||
keys.push(key.publicKey);
|
||||
}
|
||||
|
||||
ring.script = Script.fromMultisig(this.m, this.n, keys);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return ring;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the account to the database. Necessary
|
||||
* when address depth and keys change.
|
||||
* @param {Batch} b
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
save(b) {
|
||||
return this.wdb.saveAccount(b, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save addresses to path map.
|
||||
* @param {Batch} b
|
||||
* @param {WalletKey} ring
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
saveKey(b, ring) {
|
||||
return this.wdb.saveKey(b, this.wid, ring);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save paths to path map.
|
||||
* @param {Batch} b
|
||||
* @param {Path} path
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
savePath(b, path) {
|
||||
return this.wdb.savePath(b, this.wid, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize address depths (including lookahead).
|
||||
* @param {Batch} b
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async initDepth(b) {
|
||||
// Receive Address
|
||||
this.receiveDepth = 1;
|
||||
|
||||
for (let i = 0; i <= this.lookahead; i++) {
|
||||
const key = this.deriveReceive(i);
|
||||
await this.saveKey(b, key);
|
||||
}
|
||||
|
||||
// Change Address
|
||||
this.changeDepth = 1;
|
||||
|
||||
for (let i = 0; i <= this.lookahead; i++) {
|
||||
const key = this.deriveChange(i);
|
||||
await this.saveKey(b, key);
|
||||
}
|
||||
|
||||
this.save(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate new lookahead addresses if necessary.
|
||||
* @param {Batch} b
|
||||
* @param {Number} receive
|
||||
* @param {Number} change
|
||||
* @returns {Promise<WalletKey?>}
|
||||
*/
|
||||
|
||||
async syncDepth(b, receive, change) {
|
||||
let derived = false;
|
||||
let result = null;
|
||||
|
||||
if (receive > this.receiveDepth) {
|
||||
const depth = this.receiveDepth + this.lookahead;
|
||||
|
||||
assert(receive <= depth + 1);
|
||||
|
||||
for (let i = depth; i < receive + this.lookahead; i++) {
|
||||
const key = this.deriveReceive(i);
|
||||
await this.saveKey(b, key);
|
||||
result = key;
|
||||
}
|
||||
|
||||
this.receiveDepth = receive;
|
||||
|
||||
derived = true;
|
||||
}
|
||||
|
||||
if (change > this.changeDepth) {
|
||||
const depth = this.changeDepth + this.lookahead;
|
||||
|
||||
assert(change <= depth + 1);
|
||||
|
||||
for (let i = depth; i < change + this.lookahead; i++) {
|
||||
const key = this.deriveChange(i);
|
||||
await this.saveKey(b, key);
|
||||
}
|
||||
|
||||
this.changeDepth = change;
|
||||
|
||||
derived = true;
|
||||
}
|
||||
|
||||
if (derived)
|
||||
this.save(b);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate new lookahead addresses.
|
||||
* @param {Batch} b
|
||||
* @param {Number} lookahead
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async setLookahead(b, lookahead) {
|
||||
assert((lookahead >>> 0) === lookahead, 'Lookahead must be a number.');
|
||||
|
||||
if (lookahead === this.lookahead)
|
||||
return;
|
||||
|
||||
if (lookahead < this.lookahead) {
|
||||
const diff = this.lookahead - lookahead;
|
||||
|
||||
this.receiveDepth += diff;
|
||||
this.changeDepth += diff;
|
||||
|
||||
this.lookahead = lookahead;
|
||||
|
||||
this.save(b);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
const depth = this.receiveDepth + this.lookahead;
|
||||
const target = this.receiveDepth + lookahead;
|
||||
|
||||
for (let i = depth; i < target; i++) {
|
||||
const key = this.deriveReceive(i);
|
||||
await this.saveKey(b, key);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const depth = this.changeDepth + this.lookahead;
|
||||
const target = this.changeDepth + lookahead;
|
||||
|
||||
for (let i = depth; i < target; i++) {
|
||||
const key = this.deriveChange(i);
|
||||
await this.saveKey(b, key);
|
||||
}
|
||||
}
|
||||
|
||||
this.lookahead = lookahead;
|
||||
this.save(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current receive key.
|
||||
* @returns {WalletKey?}
|
||||
*/
|
||||
|
||||
receiveKey() {
|
||||
if (!this.initialized)
|
||||
return null;
|
||||
|
||||
return this.deriveReceive(this.receiveDepth - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current change key.
|
||||
* @returns {WalletKey?}
|
||||
*/
|
||||
|
||||
changeKey() {
|
||||
if (!this.initialized)
|
||||
return null;
|
||||
|
||||
return this.deriveChange(this.changeDepth - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current receive address.
|
||||
* @returns {Address?}
|
||||
*/
|
||||
|
||||
receiveAddress() {
|
||||
const key = this.receiveKey();
|
||||
|
||||
if (!key)
|
||||
return null;
|
||||
|
||||
return key.getAddress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current change address.
|
||||
* @returns {Address?}
|
||||
*/
|
||||
|
||||
changeAddress() {
|
||||
const key = this.changeKey();
|
||||
|
||||
if (!key)
|
||||
return null;
|
||||
|
||||
return key.getAddress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the account to a more inspection-friendly object.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format() {
|
||||
const receive = this.receiveAddress();
|
||||
const change = this.changeAddress();
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
wid: this.wid,
|
||||
name: this.name,
|
||||
network: this.network.type,
|
||||
initialized: this.initialized,
|
||||
watchOnly: this.watchOnly,
|
||||
type: Account.typesByVal[this.type].toLowerCase(),
|
||||
m: this.m,
|
||||
n: this.n,
|
||||
accountIndex: this.accountIndex,
|
||||
receiveDepth: this.receiveDepth,
|
||||
changeDepth: this.changeDepth,
|
||||
lookahead: this.lookahead,
|
||||
receiveAddress: receive ? receive.toString(this.network) : null,
|
||||
changeAddress: change ? change.toString(this.network) : null,
|
||||
accountKey: this.accountKey.toBase58(this.network),
|
||||
keys: this.keys.map(key => key.toBase58(this.network))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the account to an object suitable for
|
||||
* serialization.
|
||||
* @param {Object} [balance=null]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON(balance) {
|
||||
const receive = this.receiveAddress();
|
||||
const change = this.changeAddress();
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
initialized: this.initialized,
|
||||
watchOnly: this.watchOnly,
|
||||
type: Account.typesByVal[this.type].toLowerCase(),
|
||||
m: this.m,
|
||||
n: this.n,
|
||||
accountIndex: this.accountIndex,
|
||||
receiveDepth: this.receiveDepth,
|
||||
changeDepth: this.changeDepth,
|
||||
lookahead: this.lookahead,
|
||||
receiveAddress: receive ? receive.toString(this.network) : null,
|
||||
changeAddress: change ? change.toString(this.network) : null,
|
||||
accountKey: this.accountKey.toBase58(this.network),
|
||||
keys: this.keys.map(key => key.toBase58(this.network)),
|
||||
balance: balance ? balance.toJSON(true) : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate serialization size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
size += 91;
|
||||
size += this.keys.length * 74;
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the account.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
let flags = 0;
|
||||
|
||||
if (this.initialized)
|
||||
flags |= 1;
|
||||
|
||||
bw.writeU8(flags);
|
||||
bw.writeU8(this.type);
|
||||
bw.writeU8(this.m);
|
||||
bw.writeU8(this.n);
|
||||
bw.writeU32(this.receiveDepth);
|
||||
bw.writeU32(this.changeDepth);
|
||||
bw.writeU32(this.lookahead);
|
||||
writeKey(this.accountKey, bw);
|
||||
bw.writeU8(this.keys.length);
|
||||
|
||||
for (const key of this.keys)
|
||||
writeKey(key, bw);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized data.
|
||||
* @param {bio.BufferReader} br
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
const flags = br.readU8();
|
||||
|
||||
this.initialized = (flags & 1) !== 0;
|
||||
this.type = br.readU8();
|
||||
this.m = br.readU8();
|
||||
this.n = br.readU8();
|
||||
this.receiveDepth = br.readU32();
|
||||
this.changeDepth = br.readU32();
|
||||
this.lookahead = br.readU32();
|
||||
this.accountKey = readKey(br);
|
||||
|
||||
assert(this.type < Account.typesByVal.length);
|
||||
|
||||
const count = br.readU8();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const key = readKey(br);
|
||||
binary.insert(this.keys, key, cmp, true);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode account.
|
||||
* @param {WalletDB} wdb
|
||||
* @param {Buffer} data
|
||||
* @returns {Account}
|
||||
*/
|
||||
|
||||
static decode(wdb, data) {
|
||||
return new this(wdb).decode(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test an object to see if it is a Account.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isAccount(obj) {
|
||||
return obj instanceof Account;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Account types.
|
||||
* @enum {Number}
|
||||
* @default
|
||||
*/
|
||||
|
||||
Account.types = {
|
||||
PUBKEYHASH: 0,
|
||||
MULTISIG: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* Account types by value.
|
||||
* @const {Object}
|
||||
*/
|
||||
|
||||
Account.typesByVal = [
|
||||
'PUBKEYHASH',
|
||||
'MULTISIG'
|
||||
];
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
function cmp(a, b) {
|
||||
return a.compare(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HDPublicKey} key
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
function writeKey(key, bw) {
|
||||
bw.writeU8(key.depth);
|
||||
bw.writeU32BE(key.parentFingerPrint);
|
||||
bw.writeU32BE(key.childIndex);
|
||||
bw.writeBytes(key.chainCode);
|
||||
bw.writeBytes(key.publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {HDPublicKey}
|
||||
*/
|
||||
|
||||
function readKey(br) {
|
||||
const key = new HDPublicKey();
|
||||
key.depth = br.readU8();
|
||||
key.parentFingerPrint = br.readU32BE();
|
||||
key.childIndex = br.readU32BE();
|
||||
key.chainCode = br.readBytes(32);
|
||||
key.publicKey = br.readBytes(33);
|
||||
return key;
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Account;
|
||||
186
docs/js-wallet/client.js
Normal file
186
docs/js-wallet/client.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/*!
|
||||
* client.js - http client for wallets
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const NodeClient = require('../client/node');
|
||||
const TX = require('../primitives/tx');
|
||||
const Coin = require('../primitives/coin');
|
||||
const NameState = require('../covenants/namestate');
|
||||
const {encoding} = require('bufio');
|
||||
|
||||
const parsers = {
|
||||
'block connect': (entry, txs) => parseBlock(entry, txs),
|
||||
'block disconnect': entry => [parseEntry(entry)],
|
||||
'block rescan': (entry, txs) => parseBlock(entry, txs),
|
||||
'block rescan interactive': (entry, txs) => parseBlock(entry, txs),
|
||||
'chain reset': entry => [parseEntry(entry)],
|
||||
'tx': tx => [TX.decode(tx)]
|
||||
};
|
||||
|
||||
class WalletClient extends NodeClient {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
bind(event, handler) {
|
||||
const parser = parsers[event];
|
||||
|
||||
if (!parser) {
|
||||
super.bind(event, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
super.bind(event, (...args) => {
|
||||
return handler(...parser(...args));
|
||||
});
|
||||
}
|
||||
|
||||
hook(event, handler) {
|
||||
const parser = parsers[event];
|
||||
|
||||
if (!parser) {
|
||||
super.hook(event, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
super.hook(event, (...args) => {
|
||||
return handler(...parser(...args));
|
||||
});
|
||||
}
|
||||
|
||||
async getTip() {
|
||||
return parseEntry(await super.getTip());
|
||||
}
|
||||
|
||||
async getEntry(block) {
|
||||
if (Buffer.isBuffer(block))
|
||||
block = block.toString('hex');
|
||||
|
||||
return parseEntry(await super.getEntry(block));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entries.
|
||||
* @param {Number} [start=-1]
|
||||
* @param {Number} [end=-1]
|
||||
* @returns {Promise<Object[]>}
|
||||
*/
|
||||
|
||||
async getEntries(start, end) {
|
||||
const entries = await super.getEntries(start, end);
|
||||
return entries.map(parseEntry);
|
||||
}
|
||||
|
||||
async send(tx) {
|
||||
return super.send(tx.encode());
|
||||
}
|
||||
|
||||
async sendClaim(claim) {
|
||||
return super.sendClaim(claim.encode());
|
||||
}
|
||||
|
||||
async setFilter(filter) {
|
||||
return super.setFilter(filter.encode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescan for any missed transactions.
|
||||
* @param {Number|Hash} start - Start block.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async rescan(start) {
|
||||
return super.rescan(start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescan interactive for any missed transactions.
|
||||
* @param {Number|Hash} start - Start block.
|
||||
* @param {Boolean} [fullLock=false]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async rescanInteractive(start, fullLock) {
|
||||
return super.rescanInteractive(start, null, fullLock);
|
||||
}
|
||||
|
||||
async getNameStatus(nameHash) {
|
||||
const json = await super.getNameStatus(nameHash);
|
||||
return NameState.fromJSON(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Hash} hash
|
||||
* @param {Number} index
|
||||
* @returns {Promise<Coin>}
|
||||
*/
|
||||
|
||||
async getCoin(hash, index) {
|
||||
const json = super.getCoin(hash, index);
|
||||
return Coin.fromJSON(json);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
function parseEntry(data) {
|
||||
if (!data)
|
||||
return null;
|
||||
|
||||
// 32 hash
|
||||
// 4 height
|
||||
// 4 nonce
|
||||
// 8 time
|
||||
// 32 prev
|
||||
// 32 tree
|
||||
// 24 extranonce
|
||||
// 32 reserved
|
||||
// 32 witness
|
||||
// 32 merkle
|
||||
// 4 version
|
||||
// 4 bits
|
||||
// 32 mask
|
||||
// 32 chainwork
|
||||
// 304 TOTAL
|
||||
|
||||
assert(Buffer.isBuffer(data));
|
||||
// Just enough to read the three data below
|
||||
assert(data.length >= 80);
|
||||
|
||||
const hash = data.slice(0, 32);
|
||||
const height = encoding.readU32(data, 32);
|
||||
// skip nonce 4.
|
||||
const time = encoding.readU64(data, 40);
|
||||
const prevBlock = data.slice(48, 80);
|
||||
|
||||
return {
|
||||
hash,
|
||||
height,
|
||||
time,
|
||||
prevBlock
|
||||
};
|
||||
}
|
||||
|
||||
function parseBlock(entry, txs) {
|
||||
const block = parseEntry(entry);
|
||||
assert(block);
|
||||
const out = [];
|
||||
|
||||
for (const tx of txs)
|
||||
out.push(TX.decode(tx));
|
||||
|
||||
return [block, out];
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = WalletClient;
|
||||
164
docs/js-wallet/common.js
Normal file
164
docs/js-wallet/common.js
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/*!
|
||||
* common.js - commonly required functions for wallet.
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const {BufferMap} = require('buffer-map');
|
||||
|
||||
/** @typedef {import('../primitives/tx')} TX */
|
||||
/** @typedef {import('../primitives/txmeta')} TXMeta */
|
||||
/** @typedef {import('../primitives/coin')} Coin */
|
||||
|
||||
/**
|
||||
* @exports wallet/common
|
||||
*/
|
||||
|
||||
const common = exports;
|
||||
|
||||
/**
|
||||
* Test whether a string is eligible
|
||||
* to be used as a name or ID.
|
||||
* @param {String} key
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
common.isName = function isName(key) {
|
||||
if (typeof key !== 'string')
|
||||
return false;
|
||||
|
||||
if (key.length === 0)
|
||||
return false;
|
||||
|
||||
if (!/^[\-\._0-9A-Za-z]+$/.test(key))
|
||||
return false;
|
||||
|
||||
// Prevents __proto__
|
||||
// from being used.
|
||||
switch (key[0]) {
|
||||
case '_':
|
||||
case '-':
|
||||
case '.':
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (key[key.length - 1]) {
|
||||
case '_':
|
||||
case '-':
|
||||
case '.':
|
||||
return false;
|
||||
}
|
||||
|
||||
return key.length >= 1 && key.length <= 40;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort an array of transactions by time.
|
||||
* @param {TXMeta[]} txs
|
||||
* @returns {TXMeta[]}
|
||||
*/
|
||||
|
||||
common.sortTX = function sortTX(txs) {
|
||||
return txs.sort((a, b) => {
|
||||
return a.mtime - b.mtime;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort an array of coins by height.
|
||||
* @param {Coin[]} coins
|
||||
* @returns {Coin[]}
|
||||
*/
|
||||
|
||||
common.sortCoins = function sortCoins(coins) {
|
||||
return coins.sort((a, b) => {
|
||||
const ah = a.height === -1 ? 0x7fffffff : a.height;
|
||||
const bh = b.height === -1 ? 0x7fffffff : b.height;
|
||||
return ah - bh;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort an array of transactions in dependency order.
|
||||
* @param {TX[]} txs
|
||||
* @returns {TX[]}
|
||||
*/
|
||||
|
||||
common.sortDeps = function sortDeps(txs) {
|
||||
const map = new BufferMap();
|
||||
|
||||
for (const tx of txs) {
|
||||
const hash = tx.hash();
|
||||
map.set(hash, tx);
|
||||
}
|
||||
|
||||
const depMap = new BufferMap();
|
||||
const depCount = new BufferMap();
|
||||
const top = [];
|
||||
|
||||
for (const [hash, tx] of map) {
|
||||
depCount.set(hash, 0);
|
||||
|
||||
let hasDeps = false;
|
||||
|
||||
for (const input of tx.inputs) {
|
||||
const prev = input.prevout.hash;
|
||||
|
||||
if (!map.has(prev))
|
||||
continue;
|
||||
|
||||
const count = depCount.get(hash);
|
||||
depCount.set(hash, count + 1);
|
||||
hasDeps = true;
|
||||
|
||||
if (!depMap.has(prev))
|
||||
depMap.set(prev, []);
|
||||
|
||||
depMap.get(prev).push(tx);
|
||||
}
|
||||
|
||||
if (hasDeps)
|
||||
continue;
|
||||
|
||||
top.push(tx);
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
for (const tx of top) {
|
||||
const hash = tx.hash();
|
||||
const deps = depMap.get(hash);
|
||||
|
||||
result.push(tx);
|
||||
|
||||
if (!deps)
|
||||
continue;
|
||||
|
||||
for (const tx of deps) {
|
||||
const hash = tx.hash();
|
||||
|
||||
let count = depCount.get(hash);
|
||||
|
||||
if (--count === 0)
|
||||
top.push(tx);
|
||||
|
||||
depCount.set(hash, count);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wallet coin selection types.
|
||||
* @enum {String}
|
||||
*/
|
||||
|
||||
common.coinSelectionTypes = {
|
||||
DB_ALL: 'db-all',
|
||||
DB_VALUE: 'db-value',
|
||||
DB_SWEEPDUST: 'db-sweepdust',
|
||||
DB_AGE: 'db-age'
|
||||
};
|
||||
2004
docs/js-wallet/http.js
Normal file
2004
docs/js-wallet/http.js
Normal file
File diff suppressed because it is too large
Load diff
28
docs/js-wallet/index.js
Normal file
28
docs/js-wallet/index.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*!
|
||||
* wallet/index.js - wallet for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @module wallet
|
||||
*/
|
||||
|
||||
exports.Account = require('./account');
|
||||
exports.Client = require('./client');
|
||||
exports.common = require('./common');
|
||||
exports.HTTP = require('./http');
|
||||
exports.layout = require('./layout');
|
||||
exports.MasterKey = require('./masterkey');
|
||||
exports.NodeClient = require('./nodeclient');
|
||||
exports.Path = require('./path');
|
||||
exports.plugin = require('./plugin');
|
||||
exports.records = require('./records');
|
||||
exports.RPC = require('./rpc');
|
||||
exports.Node = require('./node');
|
||||
exports.TXDB = require('./txdb');
|
||||
exports.WalletDB = require('./walletdb');
|
||||
exports.Wallet = require('./wallet');
|
||||
exports.WalletKey = require('./walletkey');
|
||||
229
docs/js-wallet/layout.js
Normal file
229
docs/js-wallet/layout.js
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
/*!
|
||||
* layout.js - data layout for wallets
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const bdb = require('bdb');
|
||||
|
||||
/*
|
||||
* Wallet Database Layout:
|
||||
* WDB State
|
||||
* ---------
|
||||
* V -> db version
|
||||
* O -> flags
|
||||
* D -> wallet id depth
|
||||
* M -> migration state
|
||||
*
|
||||
* Chain Sync
|
||||
* ----------
|
||||
* R -> chain sync state
|
||||
* h[height] -> block hash
|
||||
*
|
||||
* WID mappings
|
||||
* --------
|
||||
* b[height] -> block->wid map
|
||||
* T[tx-hash] -> tx->wid map
|
||||
* o[tx-hash][index] -> outpoint->wid map
|
||||
* p[addr-hash] -> address->wid map
|
||||
* N[name-hash] -> name->wid map
|
||||
*
|
||||
* Wallet
|
||||
* ------
|
||||
* l[id] -> wid
|
||||
* w[wid] -> wallet
|
||||
* W[wid] -> wallet id
|
||||
*
|
||||
* Wallet Account
|
||||
* --------------
|
||||
* a[wid][index] -> account
|
||||
* i[wid][name] -> account index
|
||||
* n[wid][index] -> account name
|
||||
*
|
||||
* Wallet Path
|
||||
* -----------
|
||||
* P[wid][addr-hash] -> path data
|
||||
* r[wid][index][addr-hash] -> dummy (addr by account)
|
||||
*
|
||||
* TXDB
|
||||
* ----
|
||||
* t[wid]* -> txdb
|
||||
*/
|
||||
|
||||
exports.wdb = {
|
||||
// WDB State
|
||||
V: bdb.key('V'),
|
||||
O: bdb.key('O'),
|
||||
D: bdb.key('D'),
|
||||
M: bdb.key('M'),
|
||||
|
||||
// Chain Sync
|
||||
R: bdb.key('R'),
|
||||
h: bdb.key('h', ['uint32']),
|
||||
|
||||
// WID Mappings
|
||||
b: bdb.key('b', ['uint32']),
|
||||
T: bdb.key('T', ['hash256']),
|
||||
p: bdb.key('p', ['hash']),
|
||||
o: bdb.key('o', ['hash256', 'uint32']),
|
||||
N: bdb.key('N', ['hash256']),
|
||||
|
||||
// Wallet
|
||||
l: bdb.key('l', ['ascii']),
|
||||
w: bdb.key('w', ['uint32']),
|
||||
W: bdb.key('W', ['uint32']),
|
||||
|
||||
// Wallet Account
|
||||
a: bdb.key('a', ['uint32', 'uint32']),
|
||||
i: bdb.key('i', ['uint32', 'ascii']),
|
||||
n: bdb.key('n', ['uint32', 'uint32']),
|
||||
|
||||
// Wallet Path
|
||||
P: bdb.key('P', ['uint32', 'hash']),
|
||||
r: bdb.key('r', ['uint32', 'uint32', 'hash']),
|
||||
|
||||
// TXDB
|
||||
t: bdb.key('t', ['uint32'])
|
||||
};
|
||||
|
||||
/*
|
||||
* TXDB Database Layout:
|
||||
* Balance
|
||||
* -------
|
||||
* R -> wallet balance
|
||||
* r[account] -> account balance
|
||||
*
|
||||
* Coin
|
||||
* ----
|
||||
* c[tx-hash][index] -> coin
|
||||
* C[account][tx-hash][index] -> dummy (coin by account)
|
||||
* d[tx-hash][index] -> undo coin
|
||||
* s[tx-hash][index] -> spent by hash
|
||||
*
|
||||
* Transaction
|
||||
* -----------
|
||||
* t[tx-hash] -> extended tx
|
||||
* T[account][tx-hash] -> dummy (tx by account)
|
||||
*
|
||||
* Confirmed
|
||||
* ---------
|
||||
* b[height] -> block record
|
||||
* h[height][tx-hash] -> dummy (tx by height)
|
||||
* H[account][height][tx-hash] -> dummy (tx by height + account)
|
||||
*
|
||||
* Unconfirmed
|
||||
* -----------
|
||||
* p[hash] -> dummy (pending tx)
|
||||
* P[account][tx-hash] -> dummy (pending tx by account)
|
||||
*
|
||||
* Coin Selection
|
||||
* --------------
|
||||
* Sv[value][tx-hash][index] -> dummy (confirmed coins by Value)
|
||||
* SV[account][value][tx-hash][index] -> dummy
|
||||
* (confirmed coins by account + Value)
|
||||
*
|
||||
* Su[value][tx-hash][index] -> dummy (Unconfirmed coins by value)
|
||||
* SU[account][value][tx-hash][index] -> dummy
|
||||
* (Unconfirmed coins by account + value)
|
||||
*
|
||||
* Sh[tx-hash][index] -> dummy (coins by account + Height)
|
||||
* SH[account][height][tx-hash][index] -> dummy
|
||||
* (coins by account + Height)
|
||||
*
|
||||
* Count and Time Index
|
||||
* --------------------
|
||||
* Ol - Latest Unconfirmed Index
|
||||
* Oc[hash] - count (count for tx)
|
||||
* Ou[hash] - undo count (unconfirmed count for tx)
|
||||
* Ot[height][index] -> tx hash (tx by count)
|
||||
* OT[account][height][index] -> tx hash (tx by count + account)
|
||||
*
|
||||
* Count and Time Index - Confirmed
|
||||
* --------------------
|
||||
* Oi[time][height][index][hash] -> dummy (tx by time)
|
||||
* OI[account][time][height][index][hash] -> dummy (tx by time + account)
|
||||
*
|
||||
* Count and Time Index - Unconfirmed
|
||||
* --------------------
|
||||
* Om[time][count][hash] -> dummy (tx by time)
|
||||
* OM[account][time][count][hash] -> dummy (tx by time + account)
|
||||
* Oe[hash] -> undo time (unconfirmed time for tx)
|
||||
*
|
||||
* Names
|
||||
* -----
|
||||
* A[name-hash] -> name record (name record by name hash)
|
||||
* U[tx-hash] -> name undo record (name undo record by tx hash)
|
||||
* i[name-hash][tx-hash][index] -> bid (BlindBid by name + tx + index)
|
||||
* B[name-hash][tx-hash][index] -> reveal (BidReveal by name + tx + index)
|
||||
* E[name-hash][tx-hash][index] - bid to reveal out (by bid txhash + index)
|
||||
* v[blind-hash] -> blind (Blind Value by blind hash)
|
||||
* o[name-hash] -> tx hash OPEN only (tx hash by name hash)
|
||||
*/
|
||||
|
||||
exports.txdb = {
|
||||
prefix: bdb.key('t', ['uint32']),
|
||||
|
||||
// Balance
|
||||
R: bdb.key('R'),
|
||||
r: bdb.key('r', ['uint32']),
|
||||
|
||||
// Coin
|
||||
c: bdb.key('c', ['hash256', 'uint32']),
|
||||
C: bdb.key('C', ['uint32', 'hash256', 'uint32']),
|
||||
d: bdb.key('d', ['hash256', 'uint32']),
|
||||
s: bdb.key('s', ['hash256', 'uint32']),
|
||||
|
||||
// Coin Selector
|
||||
// confirmed by Value
|
||||
Sv: bdb.key('Sv', ['uint64', 'hash256', 'uint32']),
|
||||
// confirmed by account + Value
|
||||
SV: bdb.key('SV', ['uint32', 'uint64', 'hash256', 'uint32']),
|
||||
// Unconfirmed by value
|
||||
Su: bdb.key('Su', ['uint64', 'hash256', 'uint32']),
|
||||
// Unconfirmed by account + value
|
||||
SU: bdb.key('SU', ['uint32', 'uint64', 'hash256', 'uint32']),
|
||||
// by Height
|
||||
Sh: bdb.key('Sh', ['uint32', 'hash256', 'uint32']),
|
||||
// by account + Height
|
||||
SH: bdb.key('SH', ['uint32', 'uint32', 'hash256', 'uint32']),
|
||||
|
||||
// Transaction
|
||||
t: bdb.key('t', ['hash256']),
|
||||
T: bdb.key('T', ['uint32', 'hash256']),
|
||||
|
||||
// Confirmed
|
||||
b: bdb.key('b', ['uint32']),
|
||||
h: bdb.key('h', ['uint32', 'hash256']),
|
||||
H: bdb.key('H', ['uint32', 'uint32', 'hash256']),
|
||||
|
||||
// Unconfirmed
|
||||
p: bdb.key('p', ['hash256']),
|
||||
P: bdb.key('P', ['uint32', 'hash256']),
|
||||
|
||||
// Count and Time Index. (O for general prefix.)
|
||||
Ol: bdb.key('Ol'),
|
||||
Oc: bdb.key('Oc', ['hash256']),
|
||||
Ou: bdb.key('Ou', ['hash256']),
|
||||
Ot: bdb.key('Ot', ['uint32', 'uint32']),
|
||||
OT: bdb.key('OT', ['uint32', 'uint32', 'uint32']),
|
||||
|
||||
// Count and Time Index - Confirmed
|
||||
Oi: bdb.key('Oi', ['uint32', 'uint32', 'uint32', 'hash256']),
|
||||
OI: bdb.key('OI', ['uint32', 'uint32', 'uint32', 'uint32', 'hash256']),
|
||||
|
||||
// Count and Time Index - Unconfirmed
|
||||
Om: bdb.key('Om', ['uint32', 'uint32', 'hash256']),
|
||||
OM: bdb.key('OM', ['uint32', 'uint32', 'uint32', 'hash256']),
|
||||
Oe: bdb.key('Oe', ['hash256']),
|
||||
|
||||
// Names
|
||||
A: bdb.key('A', ['hash256']),
|
||||
U: bdb.key('U', ['hash256']),
|
||||
i: bdb.key('i', ['hash256', 'hash256', 'uint32']),
|
||||
B: bdb.key('B', ['hash256', 'hash256', 'uint32']),
|
||||
E: bdb.key('E', ['hash256', 'hash256', 'uint32']),
|
||||
v: bdb.key('v', ['hash256']),
|
||||
o: bdb.key('o', ['hash256'])
|
||||
};
|
||||
692
docs/js-wallet/masterkey.js
Normal file
692
docs/js-wallet/masterkey.js
Normal file
|
|
@ -0,0 +1,692 @@
|
|||
/*!
|
||||
* masterkey.js - master bip32 key object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const {Lock} = require('bmutex');
|
||||
const random = require('bcrypto/lib/random');
|
||||
const cleanse = require('bcrypto/lib/cleanse');
|
||||
const aes = require('bcrypto/lib/aes');
|
||||
const sha256 = require('bcrypto/lib/sha256');
|
||||
const secp256k1 = require('bcrypto/lib/secp256k1');
|
||||
const pbkdf2 = require('bcrypto/lib/pbkdf2');
|
||||
const scrypt = require('bcrypto/lib/scrypt');
|
||||
const util = require('../utils/util');
|
||||
const HDPrivateKey = require('../hd/private');
|
||||
const Mnemonic = require('../hd/mnemonic');
|
||||
const pkg = require('../pkg');
|
||||
const {encoding} = bio;
|
||||
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('../protocol/network')} Network */
|
||||
|
||||
/**
|
||||
* Master Key
|
||||
* Master BIP32 key which can exist
|
||||
* in a timed out encrypted state.
|
||||
* @alias module:wallet.MasterKey
|
||||
*/
|
||||
|
||||
class MasterKey extends bio.Struct {
|
||||
/**
|
||||
* Create a master key.
|
||||
* @constructor
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.encrypted = false;
|
||||
this.iv = null;
|
||||
this.ciphertext = null;
|
||||
this.key = null;
|
||||
this.mnemonic = null;
|
||||
|
||||
this.alg = MasterKey.alg.PBKDF2;
|
||||
this.n = 50000;
|
||||
this.r = 0;
|
||||
this.p = 0;
|
||||
|
||||
this.aesKey = null;
|
||||
this.timer = null;
|
||||
this.until = 0;
|
||||
this.locker = new Lock();
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from options object.
|
||||
* @param {Object} options
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
assert(options);
|
||||
|
||||
if (options.encrypted != null) {
|
||||
assert(typeof options.encrypted === 'boolean');
|
||||
this.encrypted = options.encrypted;
|
||||
}
|
||||
|
||||
if (options.iv) {
|
||||
assert(Buffer.isBuffer(options.iv));
|
||||
this.iv = options.iv;
|
||||
}
|
||||
|
||||
if (options.ciphertext) {
|
||||
assert(Buffer.isBuffer(options.ciphertext));
|
||||
this.ciphertext = options.ciphertext;
|
||||
}
|
||||
|
||||
if (options.key) {
|
||||
assert(HDPrivateKey.isHDPrivateKey(options.key));
|
||||
this.key = options.key;
|
||||
}
|
||||
|
||||
if (options.mnemonic) {
|
||||
assert(options.mnemonic instanceof Mnemonic);
|
||||
this.mnemonic = options.mnemonic;
|
||||
}
|
||||
|
||||
if (options.alg != null) {
|
||||
if (typeof options.alg === 'string') {
|
||||
this.alg = MasterKey.alg[options.alg.toUpperCase()];
|
||||
assert(this.alg != null, 'Unknown algorithm.');
|
||||
} else {
|
||||
assert(typeof options.alg === 'number');
|
||||
assert(MasterKey.algByVal[options.alg]);
|
||||
this.alg = options.alg;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.rounds != null) {
|
||||
assert((options.rounds >>> 0) === options.rounds);
|
||||
this.rounds = options.rounds;
|
||||
}
|
||||
|
||||
if (options.n != null) {
|
||||
assert((options.n >>> 0) === options.n);
|
||||
this.n = options.n;
|
||||
}
|
||||
|
||||
if (options.r != null) {
|
||||
assert((options.r >>> 0) === options.r);
|
||||
this.r = options.r;
|
||||
}
|
||||
|
||||
if (options.p != null) {
|
||||
assert((options.p >>> 0) === options.p);
|
||||
this.p = options.p;
|
||||
}
|
||||
|
||||
assert(this.encrypted ? !this.key : this.key);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the key and set a timeout to destroy decrypted data.
|
||||
* @param {Buffer|String} passphrase - Zero this yourself.
|
||||
* @param {Number} [timeout=60] timeout in seconds.
|
||||
* @returns {Promise<HDPrivateKey>}
|
||||
*/
|
||||
|
||||
async unlock(passphrase, timeout) {
|
||||
const _unlock = await this.locker.lock();
|
||||
try {
|
||||
return await this._unlock(passphrase, timeout);
|
||||
} finally {
|
||||
_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the key without a lock.
|
||||
* @private
|
||||
* @param {Buffer|String} passphrase - Zero this yourself.
|
||||
* @param {Number} [timeout=60] timeout in seconds.
|
||||
* @returns {Promise<HDPrivateKey>}
|
||||
*/
|
||||
|
||||
async _unlock(passphrase, timeout) {
|
||||
if (this.key) {
|
||||
if (this.encrypted) {
|
||||
assert(this.timer != null);
|
||||
this.start(timeout);
|
||||
}
|
||||
return this.key;
|
||||
}
|
||||
|
||||
if (!passphrase)
|
||||
throw new Error('No passphrase.');
|
||||
|
||||
assert(this.encrypted);
|
||||
|
||||
const key = await this.derive(passphrase);
|
||||
const data = aes.decipher(this.ciphertext, key, this.iv);
|
||||
|
||||
this.readKey(data);
|
||||
|
||||
this.start(timeout);
|
||||
|
||||
this.aesKey = key;
|
||||
|
||||
return this.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the destroy timer.
|
||||
* @private
|
||||
* @param {Number} [timeout=60] timeout in seconds.
|
||||
*/
|
||||
|
||||
start(timeout) {
|
||||
if (!timeout)
|
||||
timeout = 60;
|
||||
|
||||
this.stop();
|
||||
|
||||
if (timeout === -1)
|
||||
return;
|
||||
|
||||
assert((timeout >>> 0) === timeout);
|
||||
|
||||
this.until = util.now() + timeout;
|
||||
this.timer = setTimeout(() => this.lock(), timeout * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the destroy timer.
|
||||
* @private
|
||||
*/
|
||||
|
||||
stop() {
|
||||
if (this.timer != null) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
this.until = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive an aes key based on params.
|
||||
* @param {String|Buffer} passwd
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
|
||||
async derive(passwd) {
|
||||
const salt = MasterKey.SALT;
|
||||
const n = this.n;
|
||||
const r = this.r;
|
||||
const p = this.p;
|
||||
|
||||
if (typeof passwd === 'string')
|
||||
passwd = Buffer.from(passwd, 'utf8');
|
||||
|
||||
switch (this.alg) {
|
||||
case MasterKey.alg.PBKDF2:
|
||||
return pbkdf2.deriveAsync(sha256, passwd, salt, n, 32);
|
||||
case MasterKey.alg.SCRYPT:
|
||||
return scrypt.deriveAsync(passwd, salt, n, r, p, 32);
|
||||
default:
|
||||
throw new Error(`Unknown algorithm: ${this.alg}.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt data with in-memory aes key.
|
||||
* @param {Buffer} data
|
||||
* @param {Buffer} iv
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
encipher(data, iv) {
|
||||
if (!this.aesKey)
|
||||
return null;
|
||||
|
||||
return aes.encipher(data, this.aesKey, iv.slice(0, 16));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt data with in-memory aes key.
|
||||
* @param {Buffer} data
|
||||
* @param {Buffer} iv
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
decipher(data, iv) {
|
||||
if (!this.aesKey)
|
||||
return null;
|
||||
|
||||
return aes.decipher(data, this.aesKey, iv.slice(0, 16));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the key by zeroing the
|
||||
* privateKey and chainCode. Stop
|
||||
* the timer if there is one.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async lock() {
|
||||
const unlock = await this.locker.lock();
|
||||
try {
|
||||
return this._lock();
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the key by zeroing the
|
||||
* privateKey and chainCode. Stop
|
||||
* the timer if there is one.
|
||||
*/
|
||||
|
||||
_lock() {
|
||||
if (!this.encrypted) {
|
||||
assert(this.timer == null);
|
||||
assert(this.key);
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop();
|
||||
|
||||
if (this.key) {
|
||||
this.key.destroy(true);
|
||||
this.key = null;
|
||||
}
|
||||
|
||||
if (this.aesKey) {
|
||||
cleanse(this.aesKey);
|
||||
this.aesKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the key permanently.
|
||||
*/
|
||||
|
||||
async destroy() {
|
||||
await this.lock();
|
||||
this.locker.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the key permanently.
|
||||
* @param {Buffer|String} passphrase - Zero this yourself.
|
||||
* @param {Boolean} [clean=false]
|
||||
* @returns {Promise<Buffer|null>}
|
||||
*/
|
||||
|
||||
async decrypt(passphrase, clean) {
|
||||
const unlock = await this.locker.lock();
|
||||
try {
|
||||
return await this._decrypt(passphrase, clean);
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the key permanently without a lock.
|
||||
* @private
|
||||
* @param {Buffer|String} passphrase - Zero this yourself.
|
||||
* @param {Boolean} [clean=false]
|
||||
* @returns {Promise<Buffer|null>}
|
||||
*/
|
||||
|
||||
async _decrypt(passphrase, clean) {
|
||||
if (!this.encrypted)
|
||||
throw new Error('Master key is not encrypted.');
|
||||
|
||||
if (!passphrase)
|
||||
throw new Error('No passphrase provided.');
|
||||
|
||||
this._lock();
|
||||
|
||||
const key = await this.derive(passphrase);
|
||||
const data = aes.decipher(this.ciphertext, key, this.iv);
|
||||
|
||||
this.readKey(data);
|
||||
this.encrypted = false;
|
||||
this.iv = null;
|
||||
this.ciphertext = null;
|
||||
|
||||
if (!clean) {
|
||||
cleanse(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the key permanently.
|
||||
* @param {Buffer|String} passphrase - Zero this yourself.
|
||||
* @param {Boolean} [clean=false]
|
||||
* @returns {Promise<Buffer|null>}
|
||||
*/
|
||||
|
||||
async encrypt(passphrase, clean) {
|
||||
const unlock = await this.locker.lock();
|
||||
try {
|
||||
return await this._encrypt(passphrase, clean);
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the key permanently without a lock.
|
||||
* @private
|
||||
* @param {Buffer|String} passphrase - Zero this yourself.
|
||||
* @param {Boolean} [clean=false]
|
||||
* @returns {Promise<Buffer|null>}
|
||||
*/
|
||||
|
||||
async _encrypt(passphrase, clean) {
|
||||
if (this.encrypted)
|
||||
throw new Error('Master key is already encrypted.');
|
||||
|
||||
if (!passphrase)
|
||||
throw new Error('No passphrase provided.');
|
||||
|
||||
const raw = this.writeKey();
|
||||
const iv = random.randomBytes(16);
|
||||
|
||||
this.stop();
|
||||
|
||||
const key = await this.derive(passphrase);
|
||||
const data = aes.encipher(raw, key, iv);
|
||||
|
||||
this.key = null;
|
||||
this.mnemonic = null;
|
||||
this.encrypted = true;
|
||||
this.iv = iv;
|
||||
this.ciphertext = data;
|
||||
|
||||
if (!clean) {
|
||||
cleanse(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate key serialization size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
keySize() {
|
||||
let size = 0;
|
||||
|
||||
size += 64;
|
||||
size += 1;
|
||||
|
||||
if (this.mnemonic)
|
||||
size += this.mnemonic.getSize();
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize key and menmonic to a single buffer.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
writeKey() {
|
||||
const bw = bio.write(this.keySize());
|
||||
|
||||
bw.writeBytes(this.key.chainCode);
|
||||
bw.writeBytes(this.key.privateKey);
|
||||
|
||||
if (this.mnemonic) {
|
||||
bw.writeU8(1);
|
||||
this.mnemonic.write(bw);
|
||||
} else {
|
||||
bw.writeU8(0);
|
||||
}
|
||||
|
||||
return bw.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized key.
|
||||
* @param {Buffer} data
|
||||
*/
|
||||
|
||||
readKey(data) {
|
||||
const br = bio.read(data);
|
||||
|
||||
this.key = new HDPrivateKey();
|
||||
this.key.chainCode = br.readBytes(32);
|
||||
this.key.privateKey = br.readBytes(32);
|
||||
this.key.publicKey = secp256k1.publicKeyCreate(this.key.privateKey, true);
|
||||
|
||||
if (br.readU8() === 1)
|
||||
this.mnemonic = Mnemonic.read(br);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate serialization size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
|
||||
if (this.encrypted) {
|
||||
size += 1;
|
||||
size += encoding.sizeVarBytes(this.iv);
|
||||
size += encoding.sizeVarBytes(this.ciphertext);
|
||||
size += 13;
|
||||
return size;
|
||||
}
|
||||
|
||||
size += 1;
|
||||
size += this.keySize();
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the key in the form of:
|
||||
* `[enc-flag][iv?][ciphertext?][extended-key?]`
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
if (this.encrypted) {
|
||||
bw.writeU8(1);
|
||||
bw.writeVarBytes(this.iv);
|
||||
bw.writeVarBytes(this.ciphertext);
|
||||
|
||||
bw.writeU8(this.alg);
|
||||
bw.writeU32(this.n);
|
||||
bw.writeU32(this.r);
|
||||
bw.writeU32(this.p);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
bw.writeU8(0);
|
||||
|
||||
bw.writeBytes(this.key.chainCode);
|
||||
bw.writeBytes(this.key.privateKey);
|
||||
|
||||
if (this.mnemonic) {
|
||||
bw.writeU8(1);
|
||||
this.mnemonic.write(bw);
|
||||
} else {
|
||||
bw.writeU8(0);
|
||||
}
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized data.
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.encrypted = br.readU8() === 1;
|
||||
|
||||
if (this.encrypted) {
|
||||
this.iv = br.readVarBytes();
|
||||
this.ciphertext = br.readVarBytes();
|
||||
|
||||
this.alg = br.readU8();
|
||||
|
||||
assert(this.alg < MasterKey.algByVal.length);
|
||||
|
||||
this.n = br.readU32();
|
||||
this.r = br.readU32();
|
||||
this.p = br.readU32();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
this.key = new HDPrivateKey();
|
||||
this.key.chainCode = br.readBytes(32);
|
||||
this.key.privateKey = br.readBytes(32);
|
||||
this.key.publicKey = secp256k1.publicKeyCreate(this.key.privateKey, true);
|
||||
|
||||
if (br.readU8() === 1)
|
||||
this.mnemonic = Mnemonic.read(br);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from an HDPrivateKey.
|
||||
* @param {HDPrivateKey} key
|
||||
* @param {Mnemonic?} mnemonic
|
||||
*/
|
||||
|
||||
fromKey(key, mnemonic) {
|
||||
this.encrypted = false;
|
||||
this.iv = null;
|
||||
this.ciphertext = null;
|
||||
this.key = key;
|
||||
this.mnemonic = mnemonic || null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate master key from an HDPrivateKey.
|
||||
* @param {HDPrivateKey} key
|
||||
* @param {Mnemonic?} mnemonic
|
||||
* @returns {MasterKey}
|
||||
*/
|
||||
|
||||
static fromKey(key, mnemonic) {
|
||||
return new this().fromKey(key, mnemonic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert master key to a jsonifiable object.
|
||||
* @param {Network?} [network]
|
||||
* @param {Boolean?} [unsafe] - Whether to include
|
||||
* the key data in the JSON.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON(network, unsafe) {
|
||||
if (this.encrypted) {
|
||||
return {
|
||||
encrypted: true,
|
||||
until: this.until,
|
||||
iv: this.iv.toString('hex'),
|
||||
ciphertext: unsafe ? this.ciphertext.toString('hex') : undefined,
|
||||
algorithm: MasterKey.algByVal[this.alg].toLowerCase(),
|
||||
n: this.n,
|
||||
r: this.r,
|
||||
p: this.p
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
encrypted: false,
|
||||
key: unsafe ? this.key.getJSON(network) : undefined,
|
||||
mnemonic: unsafe && this.mnemonic ? this.mnemonic.toJSON() : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect the key.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
format() {
|
||||
const json = this.getJSON(null, true);
|
||||
|
||||
if (this.key)
|
||||
json.key = this.key.toJSON();
|
||||
|
||||
if (this.mnemonic)
|
||||
json.mnemonic = this.mnemonic.toJSON();
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an object is a MasterKey.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isMasterKey(obj) {
|
||||
return obj instanceof MasterKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Key derivation salt.
|
||||
* @const {Buffer}
|
||||
* @default
|
||||
*/
|
||||
|
||||
MasterKey.SALT = Buffer.from(pkg.name, 'ascii');
|
||||
|
||||
/**
|
||||
* Key derivation algorithms.
|
||||
* @enum {Number}
|
||||
* @default
|
||||
*/
|
||||
|
||||
MasterKey.alg = {
|
||||
PBKDF2: 0,
|
||||
SCRYPT: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* Key derivation algorithms by value.
|
||||
* @enum {String}
|
||||
* @default
|
||||
*/
|
||||
|
||||
MasterKey.algByVal = [
|
||||
'PBKDF2',
|
||||
'SCRYPT'
|
||||
];
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = MasterKey;
|
||||
1773
docs/js-wallet/migrations.js
Normal file
1773
docs/js-wallet/migrations.js
Normal file
File diff suppressed because it is too large
Load diff
147
docs/js-wallet/node.js
Normal file
147
docs/js-wallet/node.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/*!
|
||||
* server.js - wallet server for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const Node = require('../node/node');
|
||||
const WalletDB = require('./walletdb');
|
||||
const HTTP = require('./http');
|
||||
const Client = require('./client');
|
||||
const RPC = require('./rpc');
|
||||
const pkg = require('../pkg');
|
||||
|
||||
/**
|
||||
* Wallet Node
|
||||
* @extends Node
|
||||
*/
|
||||
|
||||
class WalletNode extends Node {
|
||||
/**
|
||||
* Create a wallet node.
|
||||
* @constructor
|
||||
* @param {Object?} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super(pkg.name, 'hsw.conf', 'wallet.log', options);
|
||||
|
||||
this.opened = false;
|
||||
|
||||
this.client = new Client({
|
||||
network: this.network,
|
||||
url: this.config.str('node-url'),
|
||||
host: this.config.str('node-host'),
|
||||
port: this.config.uint('node-port', this.network.rpcPort),
|
||||
ssl: this.config.bool('node-ssl'),
|
||||
apiKey: this.config.str('node-api-key')
|
||||
});
|
||||
|
||||
this.wdb = new WalletDB({
|
||||
network: this.network,
|
||||
logger: this.logger,
|
||||
workers: this.workers,
|
||||
client: this.client,
|
||||
prefix: this.config.prefix,
|
||||
memory: this.config.bool('memory'),
|
||||
maxFiles: this.config.uint('max-files'),
|
||||
cacheSize: this.config.mb('cache-size'),
|
||||
wipeNoReally: this.config.bool('wipe-no-really'),
|
||||
spv: this.config.bool('spv'),
|
||||
walletMigrate: this.config.uint('migrate'),
|
||||
icannlockup: this.config.bool('icannlockup', true),
|
||||
migrateNoRescan: this.config.bool('migrate-no-rescan', false),
|
||||
preloadAll: this.config.bool('preload-all', false),
|
||||
maxHistoryTXs: this.config.uint('max-history-txs', 100)
|
||||
});
|
||||
|
||||
this.rpc = new RPC(this);
|
||||
|
||||
this.http = new HTTP({
|
||||
network: this.network,
|
||||
logger: this.logger,
|
||||
node: this,
|
||||
prefix: this.config.prefix,
|
||||
ssl: this.config.bool('ssl'),
|
||||
keyFile: this.config.path('ssl-key'),
|
||||
certFile: this.config.path('ssl-cert'),
|
||||
host: this.config.str('http-host'),
|
||||
port: this.config.uint('http-port'),
|
||||
apiKey: this.config.str('api-key'),
|
||||
walletAuth: this.config.bool('wallet-auth'),
|
||||
noAuth: this.config.bool('no-auth'),
|
||||
cors: this.config.bool('cors'),
|
||||
adminToken: this.config.str('admin-token')
|
||||
});
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the node.
|
||||
* @private
|
||||
*/
|
||||
|
||||
init() {
|
||||
this.wdb.on('error', err => this.error(err));
|
||||
this.http.on('error', err => this.error(err));
|
||||
|
||||
this.loadPlugins();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the node and all its child objects,
|
||||
* wait for the database to load.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async open() {
|
||||
assert(!this.opened, 'WalletNode is already open.');
|
||||
this.opened = true;
|
||||
|
||||
await this.handlePreopen();
|
||||
await this.wdb.open();
|
||||
|
||||
this.rpc.wallet = this.wdb.primary;
|
||||
|
||||
await this.openPlugins();
|
||||
|
||||
await this.http.open();
|
||||
await this.wdb.connect();
|
||||
await this.handleOpen();
|
||||
|
||||
this.logger.info('Wallet node is loaded.');
|
||||
this.emit('open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the node, wait for the database to close.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async close() {
|
||||
assert(this.opened, 'WalletNode is not open.');
|
||||
this.opened = false;
|
||||
|
||||
await this.handlePreclose();
|
||||
await this.http.close();
|
||||
|
||||
await this.closePlugins();
|
||||
|
||||
this.rpc.wallet = null;
|
||||
|
||||
await this.wdb.disconnect();
|
||||
await this.wdb.close();
|
||||
await this.handleClose();
|
||||
this.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = WalletNode;
|
||||
360
docs/js-wallet/nodeclient.js
Normal file
360
docs/js-wallet/nodeclient.js
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
/*!
|
||||
* nodeclient.js - node client for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const blacklist = require('bsock/lib/blacklist');
|
||||
const AsyncEmitter = require('bevent');
|
||||
|
||||
/** @typedef {import('bfilter').BloomFilter} BloomFilter */
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../primitives/tx')} TX */
|
||||
/** @typedef {import('../primitives/claim')} Claim */
|
||||
/** @typedef {import('../covenants/namestate')} NameState */
|
||||
/** @typedef {import('../blockchain/chainentry')} ChainEntry */
|
||||
/** @typedef {import('../node/fullnode')} FullNode */
|
||||
/** @typedef {import('../node/spvnode')} SPVNode */
|
||||
/** @typedef {FullNode|SPVNode} Node */
|
||||
|
||||
/**
|
||||
* Node Client
|
||||
* @alias module:node.NodeClient
|
||||
*/
|
||||
|
||||
class NodeClient extends AsyncEmitter {
|
||||
/**
|
||||
* Create a node client.
|
||||
* @constructor
|
||||
* @param {Node} node
|
||||
*/
|
||||
|
||||
constructor(node) {
|
||||
super();
|
||||
|
||||
/** @type {Node} */
|
||||
this.node = node;
|
||||
this.network = node.network;
|
||||
this.filter = null;
|
||||
this.opened = false;
|
||||
this.hooks = new Map();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the client.
|
||||
*/
|
||||
|
||||
init() {
|
||||
this.node.chain.on('connect', async (entry, block) => {
|
||||
if (!this.opened)
|
||||
return;
|
||||
|
||||
await this.emitAsync('block connect', entry, block.txs);
|
||||
});
|
||||
|
||||
this.node.chain.on('disconnect', async (entry, block) => {
|
||||
if (!this.opened)
|
||||
return;
|
||||
|
||||
await this.emitAsync('block disconnect', entry);
|
||||
});
|
||||
|
||||
this.node.on('tx', (tx) => {
|
||||
if (!this.opened)
|
||||
return;
|
||||
|
||||
this.emit('tx', tx);
|
||||
});
|
||||
|
||||
this.node.on('reset', (tip) => {
|
||||
if (!this.opened)
|
||||
return;
|
||||
|
||||
this.emit('chain reset', tip);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the client.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
async open() {
|
||||
assert(!this.opened, 'NodeClient is already open.');
|
||||
this.opened = true;
|
||||
setImmediate(() => this.emit('connect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the client.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
async close() {
|
||||
assert(this.opened, 'NodeClient is not open.');
|
||||
this.opened = false;
|
||||
setImmediate(() => this.emit('disconnect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener.
|
||||
* @param {String} type
|
||||
* @param {Function} handler
|
||||
*/
|
||||
|
||||
bind(type, handler) {
|
||||
return this.on(type, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a hook.
|
||||
* @param {String} event
|
||||
* @param {Function} handler
|
||||
*/
|
||||
|
||||
hook(event, handler) {
|
||||
assert(typeof event === 'string', 'Event must be a string.');
|
||||
assert(typeof handler === 'function', 'Handler must be a function.');
|
||||
assert(!this.hooks.has(event), 'Hook already bound.');
|
||||
assert(!Object.prototype.hasOwnProperty.call(blacklist, event),
|
||||
'Blacklisted event.');
|
||||
this.hooks.set(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a hook.
|
||||
* @param {String} event
|
||||
*/
|
||||
|
||||
unhook(event) {
|
||||
assert(typeof event === 'string', 'Event must be a string.');
|
||||
assert(!Object.prototype.hasOwnProperty.call(blacklist, event),
|
||||
'Blacklisted event.');
|
||||
this.hooks.delete(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a hook.
|
||||
* @param {String} event
|
||||
* @param {...Object} args
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
handleCall(event, ...args) {
|
||||
const hook = this.hooks.get(event);
|
||||
|
||||
if (!hook)
|
||||
throw new Error('No hook available.');
|
||||
|
||||
return hook(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chain tip.
|
||||
* @returns {Promise<ChainEntry>}
|
||||
*/
|
||||
|
||||
async getTip() {
|
||||
return this.node.chain.tip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chain entry.
|
||||
* @param {Hash} hash
|
||||
* @returns {Promise<ChainEntry?>}
|
||||
*/
|
||||
|
||||
async getEntry(hash) {
|
||||
const entry = await this.node.chain.getEntry(hash);
|
||||
|
||||
if (!entry)
|
||||
return null;
|
||||
|
||||
if (!await this.node.chain.isMainChain(entry))
|
||||
return null;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a transaction. Do not wait for promise.
|
||||
* @param {TX} tx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
async send(tx) {
|
||||
this.node.relay(tx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a claim. Do not wait for promise.
|
||||
* @param {Claim} claim
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
async sendClaim(claim) {
|
||||
this.node.relayClaim(claim);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bloom filter.
|
||||
* @param {BloomFilter} filter
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
async setFilter(filter) {
|
||||
this.filter = filter;
|
||||
this.node.pool.setFilter(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to filter.
|
||||
* @param {Buffer} data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
async addFilter(data) {
|
||||
// `data` is ignored because pool.spvFilter === walletDB.filter
|
||||
// and therefore is already updated.
|
||||
// Argument is kept here to be consistent with API in
|
||||
// wallet/client.js (client/node.js) and wallet/nullclient.js
|
||||
this.node.pool.queueFilterLoad();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset filter.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
async resetFilter() {
|
||||
this.node.pool.queueFilterLoad();
|
||||
}
|
||||
|
||||
/**
|
||||
* Esimate smart fee.
|
||||
* @param {Number?} blocks
|
||||
* @returns {Promise<Number>}
|
||||
*/
|
||||
|
||||
async estimateFee(blocks) {
|
||||
if (!this.node.fees)
|
||||
return this.network.feeRate;
|
||||
|
||||
return this.node.fees.estimateFee(blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hash range.
|
||||
* @param {Number} start
|
||||
* @param {Number} end
|
||||
* @returns {Promise<Hash[]>}
|
||||
*/
|
||||
|
||||
async getHashes(start = -1, end = -1) {
|
||||
return this.node.chain.getHashes(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entries range.
|
||||
* @param {Number} start
|
||||
* @param {Number} end
|
||||
* @returns {Promise<ChainEntry[]>}
|
||||
*/
|
||||
|
||||
async getEntries(start = -1, end = -1) {
|
||||
return this.node.chain.getEntries(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescan for any missed transactions.
|
||||
* @param {Number|Hash} start - Start block.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
async rescan(start) {
|
||||
if (this.node.spv)
|
||||
return this.node.chain.reset(start);
|
||||
|
||||
return this.node.chain.scan(start, this.filter, (entry, txs) => {
|
||||
return this.handleCall('block rescan', entry, txs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescan interactive for any missed transactions.
|
||||
* @param {Number|Hash} start - Start block.
|
||||
* @param {Boolean} [fullLock=false]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
async rescanInteractive(start, fullLock = true) {
|
||||
if (this.node.spv)
|
||||
return this.node.chain.reset(start);
|
||||
|
||||
const iter = async (entry, txs) => {
|
||||
return await this.handleCall('block rescan interactive', entry, txs);
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.node.scanInteractive(
|
||||
start,
|
||||
this.filter,
|
||||
iter,
|
||||
fullLock
|
||||
);
|
||||
} catch (e) {
|
||||
await this.handleCall('block rescan interactive abort', e.message);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name state.
|
||||
* @param {Buffer} nameHash
|
||||
* @returns {Promise<NameState>}
|
||||
*/
|
||||
|
||||
async getNameStatus(nameHash) {
|
||||
return this.node.getNameStatus(nameHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UTXO.
|
||||
* @param {Hash} hash
|
||||
* @param {Number} index
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
async getCoin(hash, index) {
|
||||
return this.node.getCoin(hash, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block header.
|
||||
* @param {Hash|Number} block
|
||||
* @returns {Promise<ChainEntry>}
|
||||
*/
|
||||
|
||||
async getBlockHeader(block) {
|
||||
if (typeof block === 'string')
|
||||
block = Buffer.from(block, 'hex');
|
||||
|
||||
const entry = await this.node.chain.getEntry(block);
|
||||
|
||||
if (!entry)
|
||||
return null;
|
||||
|
||||
return entry.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = NodeClient;
|
||||
229
docs/js-wallet/nullclient.js
Normal file
229
docs/js-wallet/nullclient.js
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
/*!
|
||||
* nullclient.js - node client for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const EventEmitter = require('events');
|
||||
const NameState = require('../covenants/namestate');
|
||||
const Block = require('../primitives/block');
|
||||
|
||||
/**
|
||||
* Null Client
|
||||
* Sort of a fake local client for separation of concerns.
|
||||
* @alias module:node.NullClient
|
||||
*/
|
||||
|
||||
class NullClient extends EventEmitter {
|
||||
/**
|
||||
* Create a client.
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
constructor(wdb) {
|
||||
super();
|
||||
|
||||
this.wdb = wdb;
|
||||
this.network = wdb.network;
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the client.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async open() {
|
||||
assert(!this.opened, 'NullClient is already open.');
|
||||
this.opened = true;
|
||||
setImmediate(() => this.emit('connect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the client.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async close() {
|
||||
assert(this.opened, 'NullClient is not open.');
|
||||
this.opened = false;
|
||||
setImmediate(() => this.emit('disconnect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener.
|
||||
* @param {String} type
|
||||
* @param {Function} handler
|
||||
*/
|
||||
|
||||
bind(type, handler) {
|
||||
return this.on(type, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener.
|
||||
* @param {String} type
|
||||
* @param {Function} handler
|
||||
*/
|
||||
|
||||
hook(type, handler) {
|
||||
return this.on(type, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chain tip.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async getTip() {
|
||||
const {hash, height, time} = this.network.genesis;
|
||||
return { hash, height, time };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chain entry.
|
||||
* @param {Hash} hash
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async getEntry(hash) {
|
||||
return { hash, height: 0, time: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a transaction. Do not wait for promise.
|
||||
* @param {TX} tx
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async send(tx) {
|
||||
this.wdb.emit('send', tx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a claim. Do not wait for promise.
|
||||
* @param {Claim} claim
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async sendClaim(claim) {
|
||||
this.wdb.emit('send claim', claim);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bloom filter.
|
||||
* @param {Bloom} filter
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async setFilter(filter) {
|
||||
this.wdb.emit('set filter', filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to filter.
|
||||
* @param {Buffer} data
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async addFilter(data) {
|
||||
this.wdb.emit('add filter', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset filter.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async resetFilter() {
|
||||
this.wdb.emit('reset filter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Esimate smart fee.
|
||||
* @param {Number?} blocks
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async estimateFee(blocks) {
|
||||
return this.network.feeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hash range.
|
||||
* @param {Number} start
|
||||
* @param {Number} end
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async getHashes(start = -1, end = -1) {
|
||||
return [this.network.genesis.hash];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entries.
|
||||
* @param {Number} [start=-1]
|
||||
* @param {Number} [end=-1]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async getEntries(start = -1, end = -1) {
|
||||
const genesisBlock = Block.decode(this.network.genesisBlock);
|
||||
const entry = {
|
||||
hash: genesisBlock.hash(),
|
||||
height: 0,
|
||||
time: genesisBlock.time
|
||||
};
|
||||
return [entry];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescan for any missed transactions.
|
||||
* @param {Number|Hash} start - Start block.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async rescan(start) {
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescan interactive for any missed transactions.
|
||||
* @param {Number|Hash} start - Start block.
|
||||
* @param {Boolean} [fullLock=false]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async rescanInteractive(start, fullLock) {
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get opening bid height.
|
||||
* @param {Buffer} nameHash
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
async getNameStatus(nameHash) {
|
||||
return new NameState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block header.
|
||||
* @param {Hash|Number} block
|
||||
* @returns {Promise<ChainEntry?>}
|
||||
*/
|
||||
|
||||
async getBlockHeader(block) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = NullClient;
|
||||
346
docs/js-wallet/path.js
Normal file
346
docs/js-wallet/path.js
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
/*!
|
||||
* path.js - path object for wallets
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const Address = require('../primitives/address');
|
||||
const Network = require('../protocol/network');
|
||||
const {encoding} = bio;
|
||||
|
||||
/** @typedef {import('../types').NetworkType} NetworkType */
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('./account')} Account */
|
||||
|
||||
/**
|
||||
* Path
|
||||
* @alias module:wallet.Path
|
||||
* @property {String} name - Account name.
|
||||
* @property {Number} account - Account index.
|
||||
* @property {Number} branch - Branch index.
|
||||
* @property {Number} index - Address index.
|
||||
*/
|
||||
|
||||
class Path extends bio.Struct {
|
||||
/**
|
||||
* Create a path.
|
||||
* @constructor
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.keyType = Path.types.HD;
|
||||
|
||||
/** @type {String|null} */
|
||||
this.name = null; // Passed in by caller.
|
||||
this.account = 0;
|
||||
|
||||
this.version = 0;
|
||||
this.branch = -1;
|
||||
this.index = -1;
|
||||
|
||||
this.encrypted = false;
|
||||
this.data = null;
|
||||
|
||||
/** @type {Hash|null} */
|
||||
this.hash = null; // Passed in by caller.
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate path from options object.
|
||||
* @param {Object} options
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromOptions(options) {
|
||||
this.keyType = options.keyType;
|
||||
|
||||
this.name = options.name;
|
||||
this.account = options.account;
|
||||
this.branch = options.branch;
|
||||
this.index = options.index;
|
||||
|
||||
this.encrypted = options.encrypted;
|
||||
this.data = options.data;
|
||||
|
||||
this.version = options.version;
|
||||
this.hash = options.hash;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the path object.
|
||||
* @param {this} path
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
inject(path) {
|
||||
this.keyType = path.keyType;
|
||||
|
||||
this.name = path.name;
|
||||
this.account = path.account;
|
||||
this.branch = path.branch;
|
||||
this.index = path.index;
|
||||
|
||||
this.encrypted = path.encrypted;
|
||||
this.data = path.data;
|
||||
|
||||
this.version = path.version;
|
||||
this.hash = path.hash;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized data.
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.account = br.readU32();
|
||||
this.keyType = br.readU8();
|
||||
this.version = br.readU8();
|
||||
|
||||
switch (this.keyType) {
|
||||
case Path.types.HD:
|
||||
this.branch = br.readU32();
|
||||
this.index = br.readU32();
|
||||
break;
|
||||
case Path.types.KEY:
|
||||
this.encrypted = br.readU8() === 1;
|
||||
this.data = br.readVarBytes();
|
||||
break;
|
||||
case Path.types.ADDRESS:
|
||||
// Hash will be passed in by caller.
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate serialization size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
|
||||
size += 6;
|
||||
|
||||
switch (this.keyType) {
|
||||
case Path.types.HD:
|
||||
size += 8;
|
||||
break;
|
||||
case Path.types.KEY:
|
||||
size += 1;
|
||||
size += encoding.sizeVarBytes(this.data);
|
||||
break;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize path.
|
||||
* @param {bio.BufferWriter} bw
|
||||
* @returns {bio.BufferWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeU32(this.account);
|
||||
bw.writeU8(this.keyType);
|
||||
bw.writeU8(this.version);
|
||||
|
||||
switch (this.keyType) {
|
||||
case Path.types.HD:
|
||||
assert(!this.data);
|
||||
assert(this.index !== -1);
|
||||
bw.writeU32(this.branch);
|
||||
bw.writeU32(this.index);
|
||||
break;
|
||||
case Path.types.KEY:
|
||||
assert(this.data);
|
||||
assert(this.index === -1);
|
||||
bw.writeU8(this.encrypted ? 1 : 0);
|
||||
bw.writeVarBytes(this.data);
|
||||
break;
|
||||
case Path.types.ADDRESS:
|
||||
assert(!this.data);
|
||||
assert(this.index === -1);
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from address.
|
||||
* @param {Account} account
|
||||
* @param {Address} address
|
||||
*/
|
||||
|
||||
fromAddress(account, address) {
|
||||
this.keyType = Path.types.ADDRESS;
|
||||
this.name = account.name;
|
||||
this.account = account.accountIndex;
|
||||
this.version = address.version;
|
||||
this.hash = address.getHash();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate path from address.
|
||||
* @param {Account} account
|
||||
* @param {Address} address
|
||||
* @returns {Path}
|
||||
*/
|
||||
|
||||
static fromAddress(account, address) {
|
||||
return new this().fromAddress(account, address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert path object to string derivation path.
|
||||
* @param {(NetworkType|Network)?} [network] - Network type.
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
toPath(network) {
|
||||
if (this.keyType !== Path.types.HD)
|
||||
return null;
|
||||
|
||||
let prefix = 'm';
|
||||
|
||||
if (network) {
|
||||
const purpose = 44;
|
||||
network = Network.get(network);
|
||||
prefix += `/${purpose}'/${network.keyPrefix.coinType}'`;
|
||||
}
|
||||
|
||||
return `${prefix}/${this.account}'/${this.branch}/${this.index}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert path object to an address (currently unused).
|
||||
* @returns {Address}
|
||||
*/
|
||||
|
||||
toAddress() {
|
||||
return Address.fromHash(this.hash, this.version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert path to a json-friendly object.
|
||||
* @param {(NetworkType|Network)?} [network] - Network type.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON(network) {
|
||||
return {
|
||||
name: this.name,
|
||||
account: this.account,
|
||||
change: this.branch === 1,
|
||||
derivation: this.toPath(network)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from a json object.
|
||||
* @param {Object} json
|
||||
* @returns {Path}
|
||||
*/
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this().fromJSON(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from a json object.
|
||||
* @param {Object} json
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
assert(json && typeof json === 'object');
|
||||
assert(json.derivation && typeof json.derivation === 'string');
|
||||
|
||||
// Note: this object is mutated below.
|
||||
const path = json.derivation.split('/');
|
||||
|
||||
// Note: "m/X'/X'/X'/X/X" or "m/X'/X/X".
|
||||
assert (path.length === 4 || path.length === 6);
|
||||
|
||||
const index = parseInt(path.pop(), 10);
|
||||
const branch = parseInt(path.pop(), 10);
|
||||
const account = parseInt(path.pop(), 10);
|
||||
|
||||
assert(account === json.account);
|
||||
assert(branch === 0 || branch === 1);
|
||||
assert(Boolean(branch) === json.change);
|
||||
assert((index >>> 0) === index);
|
||||
|
||||
this.name = json.name;
|
||||
this.account = account;
|
||||
this.branch = branch;
|
||||
this.index = index;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect the path.
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
format() {
|
||||
return `<Path: ${this.name}:${this.toPath()}>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Path types.
|
||||
* @enum {Number}
|
||||
* @default
|
||||
*/
|
||||
|
||||
Path.types = {
|
||||
HD: 0,
|
||||
KEY: 1,
|
||||
ADDRESS: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* Path types.
|
||||
* @enum {Number}
|
||||
* @default
|
||||
*/
|
||||
|
||||
Path.typesByVal = [
|
||||
'HD',
|
||||
'KEY',
|
||||
'ADDRESS'
|
||||
];
|
||||
|
||||
/**
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Path;
|
||||
95
docs/js-wallet/paths.js
Normal file
95
docs/js-wallet/paths.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/*!
|
||||
* paths.js - paths object for hsd
|
||||
* Copyright (c) 2019, Boyma Fahnbulleh (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
|
||||
/** @typedef {import('./path')} Path */
|
||||
|
||||
/**
|
||||
* Paths
|
||||
* Represents the HD paths for coins in a single transaction.
|
||||
* @alias module:wallet.Paths
|
||||
* @property {Map[]} outputs - Paths.
|
||||
*/
|
||||
|
||||
class Paths {
|
||||
/**
|
||||
* Create paths
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
this.paths = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single entry to the collection.
|
||||
* @param {Number} index
|
||||
* @param {Path} path
|
||||
* @returns {Path}
|
||||
*/
|
||||
|
||||
add(index, path) {
|
||||
assert((index >>> 0) === index);
|
||||
assert(path);
|
||||
this.paths.set(index, path);
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the collection has a path.
|
||||
* @param {Number} index
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
has(index) {
|
||||
return this.paths.has(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a path.
|
||||
* @param {Number} index
|
||||
* @returns {Path|null}
|
||||
*/
|
||||
|
||||
get(index) {
|
||||
return this.paths.get(index) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a path and return it.
|
||||
* @param {Number} index
|
||||
* @returns {Path|null}
|
||||
*/
|
||||
|
||||
remove(index) {
|
||||
const path = this.get(index);
|
||||
|
||||
if (!path)
|
||||
return null;
|
||||
|
||||
this.paths.delete(index);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether there are paths.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isEmpty() {
|
||||
return this.paths.size === 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Paths;
|
||||
128
docs/js-wallet/plugin.js
Normal file
128
docs/js-wallet/plugin.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/*!
|
||||
* plugin.js - wallet plugin for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const WalletDB = require('./walletdb');
|
||||
const NodeClient = require('./nodeclient');
|
||||
const HTTP = require('./http');
|
||||
const RPC = require('./rpc');
|
||||
|
||||
/** @typedef {import('../node/fullnode')} FullNode */
|
||||
/** @typedef {import('../node/spvnode')} SPVNode */
|
||||
/** @typedef {FullNode|SPVNode} Node */
|
||||
|
||||
/**
|
||||
* @exports wallet/plugin
|
||||
*/
|
||||
|
||||
const plugin = exports;
|
||||
|
||||
/**
|
||||
* Plugin
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
|
||||
class Plugin extends EventEmitter {
|
||||
/**
|
||||
* Create a plugin.
|
||||
* @constructor
|
||||
* @param {Node} node
|
||||
*/
|
||||
|
||||
constructor(node) {
|
||||
super();
|
||||
|
||||
this.config = node.config.filter('wallet', {
|
||||
// Allow configurations to propagate from the hsd.conf
|
||||
// with 'wallet-' prefix.
|
||||
data: true
|
||||
});
|
||||
this.config.open('hsw.conf');
|
||||
|
||||
this.network = node.network;
|
||||
this.logger = node.logger;
|
||||
|
||||
this.client = new NodeClient(node);
|
||||
|
||||
this.wdb = new WalletDB({
|
||||
network: this.network,
|
||||
logger: this.logger,
|
||||
workers: this.workers,
|
||||
client: this.client,
|
||||
prefix: this.config.prefix,
|
||||
memory: this.config.bool('memory', node.memory),
|
||||
maxFiles: this.config.uint('max-files'),
|
||||
cacheSize: this.config.mb('cache-size'),
|
||||
wipeNoReally: this.config.bool('wipe-no-really'),
|
||||
spv: node.spv,
|
||||
walletMigrate: this.config.uint('migrate'),
|
||||
icannlockup: this.config.bool('icannlockup', true),
|
||||
migrateNoRescan: this.config.bool('migrate-no-rescan', false),
|
||||
preloadAll: this.config.bool('preload-all', false),
|
||||
maxHistoryTXs: this.config.uint('max-history-txs', 100)
|
||||
});
|
||||
|
||||
this.rpc = new RPC(this);
|
||||
|
||||
this.http = new HTTP({
|
||||
network: this.network,
|
||||
logger: this.logger,
|
||||
node: this,
|
||||
ssl: this.config.bool('ssl'),
|
||||
keyFile: this.config.path('ssl-key'),
|
||||
certFile: this.config.path('ssl-cert'),
|
||||
host: this.config.str('http-host'),
|
||||
port: this.config.uint('http-port'),
|
||||
apiKey: this.config.str('api-key', node.config.str('api-key')),
|
||||
walletAuth: this.config.bool('wallet-auth'),
|
||||
noAuth: this.config.bool('no-auth'),
|
||||
cors: this.config.bool('cors'),
|
||||
adminToken: this.config.str('admin-token')
|
||||
});
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.wdb.on('error', err => this.emit('error', err));
|
||||
this.http.on('error', err => this.emit('error', err));
|
||||
}
|
||||
|
||||
async open() {
|
||||
await this.wdb.open();
|
||||
this.rpc.wallet = this.wdb.primary;
|
||||
await this.http.open();
|
||||
await this.wdb.connect();
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.http.close();
|
||||
this.rpc.wallet = null;
|
||||
await this.wdb.disconnect();
|
||||
await this.wdb.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin name.
|
||||
* @const {String}
|
||||
*/
|
||||
|
||||
plugin.id = 'walletdb';
|
||||
|
||||
/**
|
||||
* Plugin initialization.
|
||||
* @param {Node} node
|
||||
* @returns {Plugin}
|
||||
*/
|
||||
|
||||
plugin.init = function init(node) {
|
||||
return new Plugin(node);
|
||||
};
|
||||
|
||||
plugin.Plugin = Plugin;
|
||||
533
docs/js-wallet/records.js
Normal file
533
docs/js-wallet/records.js
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
/*!
|
||||
* records.js - walletdb records
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @module wallet/records
|
||||
*/
|
||||
|
||||
const assert = require('bsert');
|
||||
const bio = require('bufio');
|
||||
const util = require('../utils/util');
|
||||
const TX = require('../primitives/tx');
|
||||
const consensus = require('../protocol/consensus');
|
||||
|
||||
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../blockchain/chainentry')} ChainEntry */
|
||||
|
||||
/**
|
||||
* Chain State
|
||||
*/
|
||||
|
||||
class ChainState extends bio.Struct {
|
||||
/**
|
||||
* Create a chain state.
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.startHeight = 0;
|
||||
this.startHash = consensus.ZERO_HASH;
|
||||
this.height = 0;
|
||||
this.marked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the state.
|
||||
* @param {ChainState} state
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
inject(state) {
|
||||
this.startHeight = state.startHeight;
|
||||
this.startHash = state.startHash;
|
||||
this.height = state.height;
|
||||
this.marked = state.marked;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 41;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized data.
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.startHeight = br.readU32();
|
||||
this.startHash = br.readHash();
|
||||
this.height = br.readU32();
|
||||
this.marked = br.readU8() === 1;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the chain state.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeU32(this.startHeight);
|
||||
bw.writeHash(this.startHash);
|
||||
bw.writeU32(this.height);
|
||||
bw.writeU8(this.marked ? 1 : 0);
|
||||
return bw;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block Meta
|
||||
*/
|
||||
|
||||
class BlockMeta extends bio.Struct {
|
||||
/**
|
||||
* Create block meta.
|
||||
* @constructor
|
||||
* @param {Hash} [hash]
|
||||
* @param {Number} [height]
|
||||
* @param {Number} [time]
|
||||
*/
|
||||
|
||||
constructor(hash, height, time) {
|
||||
super();
|
||||
this.hash = hash || consensus.ZERO_HASH;
|
||||
this.height = height != null ? height : -1;
|
||||
this.time = time || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the block.
|
||||
* @param {BlockMeta} meta
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
inject(meta) {
|
||||
this.hash = meta.hash;
|
||||
this.height = meta.height;
|
||||
this.time = meta.time;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode hash and time.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
toHashAndTime() {
|
||||
const data = Buffer.allocUnsafe(32 + 8);
|
||||
bio.writeBytes(data, this.hash, 0);
|
||||
bio.writeU64(data, this.time, 32);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode hash and time.
|
||||
* @param {Buffer} data
|
||||
* @param {Number} height
|
||||
* @returns {BlockMeta}
|
||||
*/
|
||||
|
||||
fromHashAndTime(data, height) {
|
||||
this.hash = data.slice(0, 32);
|
||||
this.time = bio.readU64(data, 32);
|
||||
this.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate block meta from hash and time.
|
||||
* @param {Buffer} data
|
||||
* @param {Number} height
|
||||
* @returns {BlockMeta}
|
||||
*/
|
||||
|
||||
static fromHashAndTime(data, height) {
|
||||
return new this().fromHashAndTime(data, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate block meta from chain entry.
|
||||
* @private
|
||||
* @param {ChainEntry} entry
|
||||
*/
|
||||
|
||||
fromEntry(entry) {
|
||||
this.hash = entry.hash;
|
||||
this.height = entry.height;
|
||||
this.time = entry.time;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate block meta from chain entry.
|
||||
* @param {ChainEntry} entry
|
||||
* @returns {BlockMeta}
|
||||
*/
|
||||
|
||||
static fromEntry(entry) {
|
||||
return new this().fromEntry(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate block meta from serialized tip data.
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.hash = br.readHash();
|
||||
this.height = br.readU32();
|
||||
this.time = br.readU64();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 44;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the block meta.
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeHash(this.hash);
|
||||
bw.writeU32(this.height);
|
||||
bw.writeU64(this.time);
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate block meta from json object.
|
||||
* @param {Object} json
|
||||
*/
|
||||
|
||||
fromJSON(json) {
|
||||
this.hash = util.parseHex(json.hash, 32);
|
||||
this.height = json.height;
|
||||
this.time = json.time;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the block meta to a more json-friendly object.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON() {
|
||||
return {
|
||||
hash: this.hash.toString('hex'),
|
||||
height: this.height,
|
||||
time: this.time
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TX Record
|
||||
*/
|
||||
|
||||
class TXRecord extends bio.Struct {
|
||||
/**
|
||||
* Create tx record.
|
||||
* @constructor
|
||||
* @param {Number} mtime
|
||||
* @param {TX} [tx]
|
||||
* @param {BlockMeta} [block]
|
||||
*/
|
||||
|
||||
constructor(mtime, tx, block) {
|
||||
super();
|
||||
|
||||
if (mtime == null)
|
||||
mtime = util.now();
|
||||
|
||||
assert(typeof mtime === 'number');
|
||||
|
||||
this.tx = null;
|
||||
this.hash = null;
|
||||
this.mtime = mtime;
|
||||
this.height = -1;
|
||||
/** @type {Hash?} */
|
||||
this.block = null;
|
||||
this.index = -1;
|
||||
this.time = 0;
|
||||
|
||||
if (tx)
|
||||
this.fromTX(tx, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from tx and block.
|
||||
* @param {TX} tx
|
||||
* @param {BlockMeta} [block]
|
||||
* @returns {TXRecord}
|
||||
*/
|
||||
|
||||
fromTX(tx, block) {
|
||||
this.tx = tx;
|
||||
this.hash = tx.hash();
|
||||
|
||||
if (block)
|
||||
this.setBlock(block);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate tx record from tx and block.
|
||||
* @param {TX} [tx]
|
||||
* @param {BlockMeta} [block]
|
||||
* @param {Number} [mtime]
|
||||
* @returns {TXRecord}
|
||||
*/
|
||||
|
||||
static fromTX(tx, block, mtime) {
|
||||
return new this(mtime).fromTX(tx, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set block data (confirm).
|
||||
* @param {BlockMeta} block
|
||||
*/
|
||||
|
||||
setBlock(block) {
|
||||
this.height = block.height;
|
||||
this.block = block.hash;
|
||||
this.time = block.time;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset block (unconfirm).
|
||||
*/
|
||||
|
||||
unsetBlock() {
|
||||
this.height = -1;
|
||||
this.block = null;
|
||||
this.time = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert tx record to a block meta.
|
||||
* @returns {BlockMeta?}
|
||||
*/
|
||||
|
||||
getBlock() {
|
||||
if (this.height === -1)
|
||||
return null;
|
||||
|
||||
return new BlockMeta(this.block, this.height, this.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate current number of transaction confirmations.
|
||||
* @param {Number} height - Current chain height.
|
||||
* @returns {Number} confirmations
|
||||
*/
|
||||
|
||||
getDepth(height) {
|
||||
assert(typeof height === 'number', 'Must pass in height.');
|
||||
|
||||
if (this.height === -1)
|
||||
return 0;
|
||||
|
||||
if (height < this.height)
|
||||
return 0;
|
||||
|
||||
return height - this.height + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get serialization size.
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
let size = 0;
|
||||
|
||||
size += this.tx.getSize();
|
||||
size += 4;
|
||||
|
||||
if (this.block) {
|
||||
size += 1;
|
||||
size += 32;
|
||||
size += 4 * 3;
|
||||
} else {
|
||||
size += 1;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a transaction to "extended format".
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
let index = this.index;
|
||||
|
||||
this.tx.write(bw);
|
||||
|
||||
bw.writeU32(this.mtime);
|
||||
|
||||
if (this.block) {
|
||||
if (index === -1)
|
||||
index = 0x7fffffff;
|
||||
|
||||
bw.writeU8(1);
|
||||
bw.writeHash(this.block);
|
||||
bw.writeU32(this.height);
|
||||
bw.writeU32(this.time);
|
||||
bw.writeU32(index);
|
||||
} else {
|
||||
bw.writeU8(0);
|
||||
}
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from "extended" format.
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
this.tx = new TX();
|
||||
this.tx.read(br);
|
||||
|
||||
this.hash = this.tx.hash();
|
||||
this.mtime = br.readU32();
|
||||
|
||||
if (br.readU8() === 1) {
|
||||
this.block = br.readHash();
|
||||
this.height = br.readU32();
|
||||
this.time = br.readU32();
|
||||
this.index = br.readU32();
|
||||
if (this.index === 0x7fffffff)
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Record
|
||||
*/
|
||||
|
||||
class MapRecord extends bio.Struct {
|
||||
/**
|
||||
* Create map record.
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {Set<Number>} */
|
||||
this.wids = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} wid
|
||||
* @returns {Boolean} - Whether the map did not contain the wid.
|
||||
*/
|
||||
|
||||
add(wid) {
|
||||
if (this.wids.has(wid))
|
||||
return false;
|
||||
|
||||
this.wids.add(wid);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} wid
|
||||
* @returns {Boolean} - Whether the map contained the wid.
|
||||
*/
|
||||
|
||||
remove(wid) {
|
||||
return this.wids.delete(wid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} wid
|
||||
* @returns {Boolean} - Whether the map contains the wid.
|
||||
*/
|
||||
|
||||
has(wid) {
|
||||
return this.wids.has(wid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BufioWriter} bw
|
||||
* @returns {BufioWriter}
|
||||
*/
|
||||
|
||||
write(bw) {
|
||||
bw.writeU32(this.wids.size);
|
||||
|
||||
for (const wid of this.wids)
|
||||
bw.writeU32(wid);
|
||||
|
||||
return bw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
|
||||
getSize() {
|
||||
return 4 + this.wids.size * 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bio.BufferReader} br
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
read(br) {
|
||||
const count = br.readU32();
|
||||
|
||||
for (let i = 0; i < count; i++)
|
||||
this.wids.add(br.readU32());
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
exports.ChainState = ChainState;
|
||||
exports.BlockMeta = BlockMeta;
|
||||
exports.TXRecord = TXRecord;
|
||||
exports.MapRecord = MapRecord;
|
||||
2937
docs/js-wallet/rpc.js
Normal file
2937
docs/js-wallet/rpc.js
Normal file
File diff suppressed because it is too large
Load diff
5305
docs/js-wallet/txdb.js
Normal file
5305
docs/js-wallet/txdb.js
Normal file
File diff suppressed because it is too large
Load diff
5894
docs/js-wallet/wallet.js
Normal file
5894
docs/js-wallet/wallet.js
Normal file
File diff suppressed because it is too large
Load diff
203
docs/js-wallet/walletcoinview.js
Normal file
203
docs/js-wallet/walletcoinview.js
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
/*!
|
||||
* walletcoinview.js - wallet coin viewpoint object for hsd
|
||||
* Copyright (c) 2019, Boyma Fahnbulleh (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('bsert');
|
||||
const {BufferMap} = require('buffer-map');
|
||||
const Paths = require('./paths');
|
||||
const CoinView = require('../coins/coinview');
|
||||
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../primitives/outpoint')} Outpoint */
|
||||
/** @typedef {import('../primitives/input')} Input */
|
||||
/** @typedef {import('../primitives/coin')} Coin */
|
||||
/** @typedef {import('../coins/coins')} Coins */
|
||||
/** @typedef {import('./path')} Path */
|
||||
|
||||
/**
|
||||
* Wallet Coin View
|
||||
* Represents a wallet, coin viewpoint: a snapshot of {@link Coins} objects
|
||||
* and the HD paths for their associated keys.
|
||||
* @alias module:wallet.WalletCoinView
|
||||
*/
|
||||
|
||||
class WalletCoinView extends CoinView {
|
||||
/**
|
||||
* Create a wallet coin view.
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.paths = new BufferMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from coin view object.
|
||||
* @private
|
||||
* @param {CoinView} view
|
||||
*/
|
||||
|
||||
fromCoinView(view) {
|
||||
assert(view instanceof CoinView, 'View must be instance of CoinView');
|
||||
this.map = view.map;
|
||||
this.undo = view.undo;
|
||||
this.bits = view.bits;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate wallet coin view from coin view.
|
||||
* @param {CoinView} view
|
||||
* @returns {WalletCoinView}
|
||||
*/
|
||||
|
||||
static fromCoinView(view) {
|
||||
return new this().fromCoinView(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add paths to the collection.
|
||||
* @param {Hash} hash
|
||||
* @param {Paths} paths
|
||||
* @returns {Paths|null}
|
||||
*/
|
||||
|
||||
addPaths(hash, paths) {
|
||||
this.paths.set(hash, paths);
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paths.
|
||||
* @param {Hash} hash
|
||||
* @returns {Paths} paths
|
||||
*/
|
||||
|
||||
getPaths(hash) {
|
||||
return this.paths.get(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the view has a paths entry.
|
||||
* @param {Hash} hash
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
hasPaths(hash) {
|
||||
return this.paths.has(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure existence of paths object in the collection.
|
||||
* @param {Hash} hash
|
||||
* @returns {Paths}
|
||||
*/
|
||||
|
||||
ensurePaths(hash) {
|
||||
const paths = this.paths.get(hash);
|
||||
|
||||
if (paths)
|
||||
return paths;
|
||||
|
||||
return this.addPaths(hash, new Paths());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove paths from the collection.
|
||||
* @param {Hash} hash
|
||||
* @returns {Paths|null}
|
||||
*/
|
||||
|
||||
removePaths(hash) {
|
||||
const paths = this.paths.get(hash);
|
||||
|
||||
if (!paths)
|
||||
return null;
|
||||
|
||||
this.paths.delete(hash);
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an HD path to the collection.
|
||||
* @param {Outpoint} prevout
|
||||
* @param {Path} path
|
||||
* @returns {Path|null}
|
||||
*/
|
||||
|
||||
addPath(prevout, path) {
|
||||
const {hash, index} = prevout;
|
||||
const paths = this.ensurePaths(hash);
|
||||
return paths.add(index, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an HD path by prevout.
|
||||
* @param {Outpoint} prevout
|
||||
* @returns {Path|null}
|
||||
*/
|
||||
|
||||
getPath(prevout) {
|
||||
const {hash, index} = prevout;
|
||||
const paths = this.getPaths(hash);
|
||||
|
||||
if (!paths)
|
||||
return null;
|
||||
|
||||
return paths.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an HD path.
|
||||
* @param {Outpoint} prevout
|
||||
* @returns {Path|null}
|
||||
*/
|
||||
|
||||
removePath(prevout) {
|
||||
const {hash, index} = prevout;
|
||||
const paths = this.getPaths(hash);
|
||||
|
||||
if (!paths)
|
||||
return null;
|
||||
|
||||
return paths.remove(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the view has a path by prevout.
|
||||
* @param {Outpoint} prevout
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
hasPath(prevout) {
|
||||
const {hash, index} = prevout;
|
||||
const paths = this.getPaths(hash);
|
||||
|
||||
if (!paths)
|
||||
return false;
|
||||
|
||||
return paths.has(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single path by input.
|
||||
* @param {Input} input
|
||||
* @returns {Path|null}
|
||||
*/
|
||||
|
||||
getPathFor(input) {
|
||||
return this.getPath(input.prevout);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = WalletCoinView;
|
||||
3084
docs/js-wallet/walletdb.js
Normal file
3084
docs/js-wallet/walletdb.js
Normal file
File diff suppressed because it is too large
Load diff
190
docs/js-wallet/walletkey.js
Normal file
190
docs/js-wallet/walletkey.js
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/*!
|
||||
* walletkey.js - walletkey object for hsd
|
||||
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/handshake-org/hsd
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const KeyRing = require('../primitives/keyring');
|
||||
const Path = require('./path');
|
||||
|
||||
/** @typedef {import('../hd/private')} HDPrivateKey */
|
||||
/** @typedef {import('../hd/public')} HDPublicKey */
|
||||
/** @typedef {import('../protocol/network')} Network */
|
||||
/** @typedef {import('./account')} Account */
|
||||
|
||||
/**
|
||||
* Wallet Key
|
||||
* Represents a key ring which amounts to an address.
|
||||
* @alias module:wallet.WalletKey
|
||||
* @extends KeyRing
|
||||
*/
|
||||
|
||||
class WalletKey extends KeyRing {
|
||||
/**
|
||||
* Create a wallet key.
|
||||
* @constructor
|
||||
* @param {Object?} options
|
||||
*/
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.keyType = Path.types.HD;
|
||||
|
||||
this.name = null;
|
||||
this.account = -1;
|
||||
this.branch = -1;
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an WalletKey to a more json-friendly object.
|
||||
* @param {Network} [network]
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
getJSON(network) {
|
||||
return {
|
||||
name: this.name,
|
||||
account: this.account,
|
||||
branch: this.branch,
|
||||
index: this.index,
|
||||
publicKey: this.publicKey.toString('hex'),
|
||||
script: this.script ? this.script.toHex() : null,
|
||||
address: this.getAddress().toString(network)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from hd key.
|
||||
* @param {Account} account
|
||||
* @param {HDPrivateKey|HDPublicKey} key
|
||||
* @param {Number} branch
|
||||
* @param {Number} index
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
fromHD(account, key, branch, index) {
|
||||
this.keyType = Path.types.HD;
|
||||
this.name = account.name;
|
||||
this.account = account.accountIndex;
|
||||
this.branch = branch;
|
||||
this.index = index;
|
||||
|
||||
if (key.privateKey)
|
||||
return this.fromPrivate(key.privateKey);
|
||||
|
||||
return this.fromPublic(key.publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a wallet key from hd key.
|
||||
* @param {Account} account
|
||||
* @param {HDPrivateKey|HDPublicKey} key
|
||||
* @param {Number} branch
|
||||
* @param {Number} index
|
||||
* @returns {WalletKey}
|
||||
*/
|
||||
|
||||
static fromHD(account, key, branch, index) {
|
||||
return new this().fromHD(account, key, branch, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from imported data.
|
||||
* @param {Account} account
|
||||
* @param {Buffer} data
|
||||
* @returns {WalletKey}
|
||||
*/
|
||||
|
||||
fromImport(account, data) {
|
||||
this.keyType = Path.types.KEY;
|
||||
this.name = account.name;
|
||||
this.account = account.accountIndex;
|
||||
return this.decode(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a wallet key from imported data.
|
||||
* @param {Account} account
|
||||
* @param {Buffer} data
|
||||
* @returns {WalletKey}
|
||||
*/
|
||||
|
||||
static fromImport(account, data) {
|
||||
return new this().fromImport(account, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from key.
|
||||
* @private
|
||||
* @param {Account} account
|
||||
* @param {KeyRing} ring
|
||||
* @returns {WalletKey}
|
||||
*/
|
||||
|
||||
fromRing(account, ring) {
|
||||
this.keyType = Path.types.KEY;
|
||||
this.name = account.name;
|
||||
this.account = account.accountIndex;
|
||||
return this.fromOptions(ring);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a wallet key from regular key.
|
||||
* @param {Account} account
|
||||
* @param {KeyRing} ring
|
||||
* @returns {WalletKey}
|
||||
*/
|
||||
|
||||
static fromRing(account, ring) {
|
||||
return new this().fromRing(account, ring);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert wallet key to a path.
|
||||
* @returns {Path}
|
||||
*/
|
||||
|
||||
toPath() {
|
||||
const path = new Path();
|
||||
|
||||
path.name = this.name;
|
||||
path.account = this.account;
|
||||
|
||||
switch (this.keyType) {
|
||||
case Path.types.HD:
|
||||
path.branch = this.branch;
|
||||
path.index = this.index;
|
||||
break;
|
||||
case Path.types.KEY:
|
||||
path.data = this.encode();
|
||||
break;
|
||||
}
|
||||
|
||||
path.keyType = this.keyType;
|
||||
|
||||
path.version = this.getVersion();
|
||||
path.hash = this.getHash();
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an object is a WalletKey.
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
static isWalletKey(obj) {
|
||||
return obj instanceof WalletKey;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = WalletKey;
|
||||
Loading…
Add table
Reference in a new issue