Source: primitives/address.js

/*!
 * address.js - address object for bcoin
 * Copyright (c) 2014-2015, Fedor Indutny (MIT License)
 * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

const assert = require('bsert');
const bio = require('bufio');
const base58 = require('bcrypto/lib/encoding/base58');
const bech32 = require('bcrypto/lib/encoding/bech32');
const sha256 = require('bcrypto/lib/sha256');
const hash160 = require('bcrypto/lib/hash160');
const hash256 = require('bcrypto/lib/hash256');
const Network = require('../protocol/network');
const consensus = require('../protocol/consensus');
const {inspectSymbol} = require('../utils');

/*
 * Constants
 */

const ZERO_HASH160 = Buffer.alloc(20, 0x00);

/**
 * Address
 * Represents an address.
 * @alias module:primitives.Address
 * @property {Buffer} hash
 * @property {AddressPrefix} type
 * @property {Number} version
 */

class Address {
  /**
   * Create an address.
   * @constructor
   * @param {Object?} options
   */

  constructor(options, network) {
    this.type = Address.types.PUBKEYHASH;
    this.version = -1;
    this.hash = ZERO_HASH160;

    if (options)
      this.fromOptions(options, network);
  }

  /**
   * Inject properties from options object.
   * @private
   * @param {Object} options
   */

  fromOptions(options, network) {
    if (typeof options === 'string')
      return this.fromString(options, network);

    assert(options);

    const {hash, type, version} = options;

    return this.fromHash(hash, type, version);
  }

  /**
   * Insantiate address from options.
   * @param {Object} options
   * @returns {Address}
   */

  static fromOptions(options, network) {
    return new this().fromOptions(options, network);
  }

  /**
   * Get the address hash.
   * @param {String?} enc - Can be `"hex"` or `null`.
   * @returns {Hash|Buffer}
   */

  getHash(enc) {
    if (enc === 'hex')
      return this.hash.toString('hex');
    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 equality against another address.
   * @param {Address} addr
   * @returns {Boolean}
   */

  equals(addr) {
    assert(addr instanceof Address);

    return this.type === addr.type
      && this.version === addr.version
      && this.hash.equals(addr.hash);
  }

  /**
   * Get the address type as a string.
   * @returns {String}
   */

  getType() {
    return Address.typesByVal[this.type].toLowerCase();
  }

  /**
   * Get a network address prefix for the address.
   * @param {Network?} network
   * @returns {Number}
   */

  getPrefix(network) {
    network = Network.get(network);

    const prefixes = network.addressPrefix;

    switch (this.type) {
      case Address.types.PUBKEYHASH:
        return prefixes.pubkeyhash;
      case Address.types.SCRIPTHASH:
        return prefixes.scripthash;
      case Address.types.WITNESS:
        if (this.hash.length === 20)
          return prefixes.witnesspubkeyhash;

        if (this.hash.length === 32)
          return prefixes.witnessscripthash;

        break;
    }

    return -1;
  }

  /**
   * Calculate size of serialized address.
   * @returns {Number}
   */

  getSize() {
    let size = 5 + this.hash.length;

    if (this.version !== -1)
      size += 2;

    return size;
  }

  /**
   * Compile the address object to its raw serialization.
   * @param {{NetworkType|Network)?} network
   * @returns {Buffer}
   * @throws Error on bad hash/prefix.
   */

  toRaw(network) {
    const size = this.getSize();
    const bw = bio.write(size);
    const prefix = this.getPrefix(network);

    assert(prefix !== -1, 'Not a valid address prefix.');

    bw.writeU8(prefix);

    if (this.version !== -1) {
      bw.writeU8(this.version);
      bw.writeU8(0);
    }

    bw.writeBytes(this.hash);
    bw.writeChecksum(hash256.digest);

    return bw.render();
  }

  /**
   * Compile the address object to a base58 address.
   * @param {{NetworkType|Network)?} network
   * @returns {AddressString}
   * @throws Error on bad hash/prefix.
   */

  toBase58(network) {
    return base58.encode(this.toRaw(network));
  }

  /**
   * Compile the address object to a bech32 address.
   * @param {{NetworkType|Network)?} network
   * @returns {String}
   * @throws Error on bad hash/prefix.
   */

  toBech32(network) {
    const version = this.version;
    const hash = this.hash;

    assert(version !== -1,
      'Cannot convert non-program address to bech32.');

    network = Network.get(network);

    const hrp = network.addressPrefix.bech32;

    return bech32.encode(hrp, version, hash);
  }

  /**
   * Inject properties from string.
   * @private
   * @param {String} addr
   * @param {(Network|NetworkType)?} network
   * @returns {Address}
   */

  fromString(addr, network) {
    assert(typeof addr === 'string');
    assert(addr.length > 0);
    assert(addr.length <= 100);

    // If the address is mixed case,
    // it can only ever be base58.
    if (isMixedCase(addr))
      return this.fromBase58(addr, network);

    // Otherwise, it's most likely bech32.
    try {
      return this.fromBech32(addr, network);
    } catch (e) {
      return this.fromBase58(addr, network);
    }
  }

  /**
   * Instantiate address from string.
   * @param {String} addr
   * @param {(Network|NetworkType)?} network
   * @returns {Address}
   */

  static fromString(addr, network) {
    return new this().fromString(addr, network);
  }

  /**
   * Convert the Address to a string.
   * @param {(Network|NetworkType)?} network
   * @returns {AddressString}
   */

  toString(network) {
    if (this.version !== -1)
      return this.toBech32(network);
    return this.toBase58(network);
  }

  /**
   * Inspect the Address.
   * @returns {Object}
   */

  [inspectSymbol]() {
    return '<Address:'
      + ` type=${this.getType()}`
      + ` version=${this.version}`
      + ` str=${this.toString()}`
      + '>';
  }

  /**
   * Inject properties from serialized data.
   * @private
   * @param {Buffer} data
   * @throws Parse error
   */

  fromRaw(data, network) {
    const br = bio.read(data, true);
    const prefix = br.readU8();

    network = Network.fromAddress(prefix, network);

    const type = Address.getType(prefix, network);

    let version = -1;
    if (type === Address.types.WITNESS) {
      if (data.length > 38)
        throw new Error('Address is too long.');

      version = br.readU8();

      if (br.readU8() !== 0)
        throw new Error('Address version padding is non-zero.');
    } else {
      if (data.length !== 25)
        throw new Error('Address is too long.');
    }

    const hash = br.readBytes(br.left() - 4);

    br.verifyChecksum(hash256.digest);

    return this.fromHash(hash, type, version);
  }

  /**
   * Create an address object from a serialized address.
   * @param {Buffer} data
   * @returns {Address}
   * @throws Parse error.
   */

  static fromRaw(data, network) {
    return new this().fromRaw(data, network);
  }

  /**
   * Inject properties from base58 address.
   * @private
   * @param {AddressString} data
   * @param {Network?} network
   * @throws Parse error
   */

  fromBase58(data, network) {
    assert(typeof data === 'string');

    if (data.length > 55)
      throw new Error('Address is too long.');

    return this.fromRaw(base58.decode(data), network);
  }

  /**
   * Create an address object from a base58 address.
   * @param {AddressString} data
   * @param {Network?} network
   * @returns {Address}
   * @throws Parse error.
   */

  static fromBase58(data, network) {
    return new this().fromBase58(data, network);
  }

  /**
   * Inject properties from bech32 address.
   * @private
   * @param {String} data
   * @param {Network?} network
   * @throws Parse error
   */

  fromBech32(data, network) {
    const type = Address.types.WITNESS;

    assert(typeof data === 'string');

    const [hrp, version, hash] = bech32.decode(data);

    // make sure HRP is correct.
    Network.fromBech32(hrp, network);

    return this.fromHash(hash, type, version);
  }

  /**
   * Create an address object from a bech32 address.
   * @param {String} data
   * @param {Network?} network
   * @returns {Address}
   * @throws Parse error.
   */

  static fromBech32(data, network) {
    return new this().fromBech32(data, network);
  }

  /**
   * Inject properties from output script.
   * @private
   * @param {Script} script
   */

  fromScript(script) {
    const pk = script.getPubkey();

    if (pk) {
      this.hash = hash160.digest(pk);
      this.type = Address.types.PUBKEYHASH;
      this.version = -1;
      return this;
    }

    const pkh = script.getPubkeyhash();

    if (pkh) {
      this.hash = pkh;
      this.type = Address.types.PUBKEYHASH;
      this.version = -1;
      return this;
    }

    const sh = script.getScripthash();

    if (sh) {
      this.hash = sh;
      this.type = Address.types.SCRIPTHASH;
      this.version = -1;
      return this;
    }

    const program = script.getProgram();

    if (program && !program.isMalformed()) {
      this.hash = program.data;
      this.type = Address.types.WITNESS;
      this.version = program.version;
      return this;
    }

    // Put this last: it's the slowest to check.
    if (script.isMultisig()) {
      this.hash = script.hash160();
      this.type = Address.types.SCRIPTHASH;
      this.version = -1;
      return this;
    }

    return null;
  }

  /**
   * Inject properties from witness.
   * @private
   * @param {Witness} witness
   */

  fromWitness(witness) {
    const [, pk] = witness.getPubkeyhashInput();

    // We're pretty much screwed here
    // since we can't get the version.
    if (pk) {
      this.hash = hash160.digest(pk);
      this.type = Address.types.WITNESS;
      this.version = 0;
      return this;
    }

    const redeem = witness.getScripthashInput();

    if (redeem) {
      this.hash = sha256.digest(redeem);
      this.type = Address.types.WITNESS;
      this.version = 0;
      return this;
    }

    return null;
  }

  /**
   * Inject properties from input script.
   * @private
   * @param {Script} script
   */

  fromInputScript(script) {
    const [, pk] = script.getPubkeyhashInput();

    if (pk) {
      this.hash = hash160.digest(pk);
      this.type = Address.types.PUBKEYHASH;
      this.version = -1;
      return this;
    }

    const redeem = script.getScripthashInput();

    if (redeem) {
      this.hash = hash160.digest(redeem);
      this.type = Address.types.SCRIPTHASH;
      this.version = -1;
      return this;
    }

    return null;
  }

  /**
   * Create an Address from a witness.
   * Attempt to extract address
   * properties from a witness.
   * @param {Witness}
   * @returns {Address|null}
   */

  static fromWitness(witness) {
    return new this().fromWitness(witness);
  }

  /**
   * Create an Address from an input script.
   * Attempt to extract address
   * properties from an input script.
   * @param {Script}
   * @returns {Address|null}
   */

  static fromInputScript(script) {
    return new this().fromInputScript(script);
  }

  /**
   * Create an Address from an output script.
   * Parse an output script and extract address
   * properties. Converts pubkey and multisig
   * scripts to pubkeyhash and scripthash addresses.
   * @param {Script}
   * @returns {Address|null}
   */

  static fromScript(script) {
    return new this().fromScript(script);
  }

  /**
   * Inject properties from a hash.
   * @private
   * @param {Buffer|Hash} hash
   * @param {AddressPrefix} type
   * @param {Number} [version=-1]
   * @throws on bad hash size
   */

  fromHash(hash, type, version) {
    if (typeof type === 'string') {
      type = Address.types[type.toUpperCase()];
      assert(type != null, 'Not a valid address type.');
    }

    if (type == null)
      type = Address.types.PUBKEYHASH;

    if (version == null)
      version = -1;

    assert(Buffer.isBuffer(hash));
    assert((type >>> 0) === type);
    assert((version | 0) === version);

    assert(type >= Address.types.PUBKEYHASH && type <= Address.types.WITNESS,
      'Not a valid address type.');

    if (version === -1) {
      assert(type !== Address.types.WITNESS, 'Wrong version (witness)');
      assert(hash.length === 20, 'Hash is the wrong size.');
    } else {
      assert(type === Address.types.WITNESS, 'Wrong version (non-witness).');
      assert(version >= 0 && version <= 16, 'Bad program version.');
      if (version === 0 && type === Address.types.WITNESS) {
        assert(hash.length === 20 || hash.length === 32,
          'Witness program hash is the wrong size.');
      }
      assert(hash.length >= 2 && hash.length <= 40, 'Hash is the wrong size.');
    }

    this.hash = hash;
    this.type = type;
    this.version = version;

    return this;
  }

  /**
   * Create a naked address from hash/type/version.
   * @param {Hash} hash
   * @param {AddressPrefix} type
   * @param {Number} [version=-1]
   * @returns {Address}
   * @throws on bad hash size
   */

  static fromHash(hash, type, version) {
    return new this().fromHash(hash, type, version);
  }

  /**
   * Inject properties from pubkeyhash.
   * @private
   * @param {Buffer} hash
   * @returns {Address}
   */

  fromPubkeyhash(hash) {
    const type = Address.types.PUBKEYHASH;
    assert(hash.length === 20, 'P2PKH must be 20 bytes.');
    return this.fromHash(hash, type, -1);
  }

  /**
   * Instantiate address from pubkeyhash.
   * @param {Buffer} hash
   * @returns {Address}
   */

  static fromPubkeyhash(hash) {
    return new this().fromPubkeyhash(hash);
  }

  /**
   * Inject properties from scripthash.
   * @private
   * @param {Buffer} hash
   * @returns {Address}
   */

  fromScripthash(hash) {
    const type = Address.types.SCRIPTHASH;
    assert(hash && hash.length === 20, 'P2SH must be 20 bytes.');
    return this.fromHash(hash, type, -1);
  }

  /**
   * Instantiate address from scripthash.
   * @param {Buffer} hash
   * @returns {Address}
   */

  static fromScripthash(hash) {
    return new this().fromScripthash(hash);
  }

  /**
   * Inject properties from witness pubkeyhash.
   * @private
   * @param {Buffer} hash
   * @returns {Address}
   */

  fromWitnessPubkeyhash(hash) {
    const type = Address.types.WITNESS;
    assert(hash && hash.length === 20, 'P2WPKH must be 20 bytes.');
    return this.fromHash(hash, type, 0);
  }

  /**
   * Instantiate address from witness pubkeyhash.
   * @param {Buffer} hash
   * @returns {Address}
   */

  static fromWitnessPubkeyhash(hash) {
    return new this().fromWitnessPubkeyhash(hash);
  }

  /**
   * Inject properties from witness scripthash.
   * @private
   * @param {Buffer} hash
   * @returns {Address}
   */

  fromWitnessScripthash(hash) {
    const type = Address.types.WITNESS;
    assert(hash && hash.length === 32, 'P2WPKH must be 32 bytes.');
    return this.fromHash(hash, type, 0);
  }

  /**
   * Instantiate address from witness scripthash.
   * @param {Buffer} hash
   * @returns {Address}
   */

  static fromWitnessScripthash(hash) {
    return new this().fromWitnessScripthash(hash);
  }

  /**
   * Inject properties from witness program.
   * @private
   * @param {Number} version
   * @param {Buffer} hash
   * @returns {Address}
   */

  fromProgram(version, hash) {
    const type = Address.types.WITNESS;

    assert(version >= 0, 'Bad version for witness program.');

    return this.fromHash(hash, type, version);
  }

  /**
   * Instantiate address from witness program.
   * @param {Number} version
   * @param {Buffer} hash
   * @returns {Address}
   */

  static fromProgram(version, hash) {
    return new this().fromProgram(version, hash);
  }

  /**
   * Test whether the address is pubkeyhash.
   * @returns {Boolean}
   */

  isPubkeyhash() {
    return this.type === Address.types.PUBKEYHASH;
  }

  /**
   * Test whether the address is scripthash.
   * @returns {Boolean}
   */

  isScripthash() {
    return this.type === Address.types.SCRIPTHASH;
  }

  /**
   * Test whether the address is witness pubkeyhash.
   * @returns {Boolean}
   */

  isWitnessPubkeyhash() {
    return this.version === 0 && this.hash.length === 20;
  }

  /**
   * Test whether the address is witness scripthash.
   * @returns {Boolean}
   */

  isWitnessScripthash() {
    return this.version === 0 && this.hash.length === 32;
  }

  /**
   * Test whether the address is a witness program.
   * @returns {Boolean}
   */

  isProgram() {
    return this.version !== -1;
  }

  /**
   * Test whether the address is an unknown witness program.
   * @returns {Boolean}
   */

  isUnknown() {
    if (this.version === -1)
      return false;

    if (this.version > 0)
      return true;

    return this.hash.length !== 20 && this.hash.length !== 32;
  }

  /**
   * Get the hash of a base58 address or address-related object.
   * @param {String|Address|Hash} data
   * @param {String?} enc - Can be `"hex"` or `null`.
   * @returns {Hash}
   */

  static getHash(data, enc) {
    if (!data)
      throw new Error('Object is not an address.');

    let hash;

    if (Buffer.isBuffer(data)) {
      if (data.length !== 20 && data.length !== 32)
        throw new Error('Object is not an address.');
      hash = data;
    } else if (data instanceof Address) {
      hash = data.hash;
    } else {
      throw new Error('Object is not an address.');
    }

    if (enc === 'hex')
      return hash.toString('hex');

    return hash;
  }

  /**
   * Get an address type for a specified network address prefix.
   * @param {Number} prefix
   * @param {Network} network
   * @returns {AddressType}
   */

  static getType(prefix, network) {
    const prefixes = network.addressPrefix;

    switch (prefix) {
      case prefixes.pubkeyhash:
        return Address.types.PUBKEYHASH;
      case prefixes.scripthash:
        return Address.types.SCRIPTHASH;
      case prefixes.witnesspubkeyhash:
      case prefixes.witnessscripthash:
        return Address.types.WITNESS;
      default:
        throw new Error('Unknown address prefix.');
    }
  }
}

/**
 * Address types.
 * @enum {Number}
 */

Address.types = {
  PUBKEYHASH: 0,
  SCRIPTHASH: 1,
  WITNESS: 2
};

/**
 * Address types by value.
 * @const {Object}
 */

Address.typesByVal = [
  'PUBKEYHASH',
  'SCRIPTHASH',
  'WITNESS'
];

/*
 * Helpers
 */

function isMixedCase(str) {
  let lower = false;
  let upper = false;

  for (let i = 0; i < str.length; i++) {
    const ch = str.charCodeAt(i);

    if (ch >= 0x30 && ch <= 0x39)
      continue;

    if (ch & 32) {
      assert(ch >= 0x61 && ch <= 0x7a);
      lower = true;
    } else {
      assert(ch >= 0x41 && ch <= 0x5a);
      upper = true;
    }

    if (lower && upper)
      return true;
  }

  return false;
}

/*
 * Expose
 */

module.exports = Address;