566 lines
11 KiB
JavaScript
566 lines
11 KiB
JavaScript
/*!
|
|
* 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;
|