numbers/xdr_large_int.js

/* eslint no-bitwise: ["error", {"allow": [">>"]}] */
import { Hyper, UnsignedHyper } from '@stellar/js-xdr';

import { Uint128 } from './uint128';
import { Uint256 } from './uint256';
import { Int128 } from './int128';
import { Int256 } from './int256';

import xdr from '../xdr';

/**
 * A wrapper class to represent large XDR-encodable integers.
 *
 * This operates at a lower level than {@link ScInt} by forcing you to specify
 * the type / width / size in bits of the integer you're targeting, regardless
 * of the input value(s) you provide.
 *
 * @param {string}  type - force a specific data type. the type choices are:
 *    'i64', 'u64', 'i128', 'u128', 'i256', and 'u256' (default: the smallest
 *    one that fits the `value`) (see {@link XdrLargeInt.isType})
 * @param {number|bigint|string|Array<number|bigint|string>} values   a list of
 *    integer-like values interpreted in big-endian order
 */
export class XdrLargeInt {
  /** @type {xdr.LargeInt} */
  int; // child class of a jsXdr.LargeInt

  /** @type {string} */
  type;

  constructor(type, values) {
    if (!(values instanceof Array)) {
      values = [values];
    }

    // normalize values to one type
    values = values.map((i) => {
      // micro-optimization to no-op on the likeliest input value:
      if (typeof i === 'bigint') {
        return i;
      }
      if (i instanceof XdrLargeInt) {
        return i.toBigInt();
      }
      return BigInt(i);
    });

    switch (type) {
      case 'i64':
        this.int = new Hyper(values);
        break;
      case 'i128':
        this.int = new Int128(values);
        break;
      case 'i256':
        this.int = new Int256(values);
        break;
      case 'u64':
        this.int = new UnsignedHyper(values);
        break;
      case 'u128':
        this.int = new Uint128(values);
        break;
      case 'u256':
        this.int = new Uint256(values);
        break;
      default:
        throw TypeError(`invalid type: ${type}`);
    }

    this.type = type;
  }

  /**
   * @returns {number}
   * @throws {RangeError} if the value can't fit into a Number
   */
  toNumber() {
    const bi = this.int.toBigInt();
    if (bi > Number.MAX_SAFE_INTEGER || bi < Number.MIN_SAFE_INTEGER) {
      throw RangeError(
        `value ${bi} not in range for Number ` +
          `[${Number.MAX_SAFE_INTEGER}, ${Number.MIN_SAFE_INTEGER}]`
      );
    }

    return Number(bi);
  }

  /** @returns {bigint} */
  toBigInt() {
    return this.int.toBigInt();
  }

  /** @returns {xdr.ScVal} the integer encoded with `ScValType = I64` */
  toI64() {
    this._sizeCheck(64);
    const v = this.toBigInt();
    if (BigInt.asIntN(64, v) !== v) {
      throw RangeError(`value too large for i64: ${v}`);
    }

    return xdr.ScVal.scvI64(new xdr.Int64(v));
  }

  /** @returns {xdr.ScVal} the integer encoded with `ScValType = U64` */
  toU64() {
    this._sizeCheck(64);
    return xdr.ScVal.scvU64(
      new xdr.Uint64(BigInt.asUintN(64, this.toBigInt())) // reiterpret as unsigned
    );
  }

  /**
   * @returns {xdr.ScVal} the integer encoded with `ScValType = I128`
   * @throws {RangeError} if the value cannot fit in 128 bits
   */
  toI128() {
    this._sizeCheck(128);

    const v = this.int.toBigInt();
    const hi64 = BigInt.asIntN(64, v >> 64n); // encode top 64 w/ sign bit
    const lo64 = BigInt.asUintN(64, v); // grab btm 64, encode sign

    return xdr.ScVal.scvI128(
      new xdr.Int128Parts({
        hi: new xdr.Int64(hi64),
        lo: new xdr.Uint64(lo64)
      })
    );
  }

  /**
   * @returns {xdr.ScVal} the integer encoded with `ScValType = U128`
   * @throws {RangeError} if the value cannot fit in 128 bits
   */
  toU128() {
    this._sizeCheck(128);
    const v = this.int.toBigInt();

    return xdr.ScVal.scvU128(
      new xdr.UInt128Parts({
        hi: new xdr.Uint64(BigInt.asUintN(64, v >> 64n)),
        lo: new xdr.Uint64(BigInt.asUintN(64, v))
      })
    );
  }

  /** @returns {xdr.ScVal} the integer encoded with `ScValType = I256` */
  toI256() {
    const v = this.int.toBigInt();
    const hiHi64 = BigInt.asIntN(64, v >> 192n); // keep sign bit
    const hiLo64 = BigInt.asUintN(64, v >> 128n);
    const loHi64 = BigInt.asUintN(64, v >> 64n);
    const loLo64 = BigInt.asUintN(64, v);

    return xdr.ScVal.scvI256(
      new xdr.Int256Parts({
        hiHi: new xdr.Int64(hiHi64),
        hiLo: new xdr.Uint64(hiLo64),
        loHi: new xdr.Uint64(loHi64),
        loLo: new xdr.Uint64(loLo64)
      })
    );
  }

  /** @returns {xdr.ScVal} the integer encoded with `ScValType = U256` */
  toU256() {
    const v = this.int.toBigInt();
    const hiHi64 = BigInt.asUintN(64, v >> 192n); // encode sign bit
    const hiLo64 = BigInt.asUintN(64, v >> 128n);
    const loHi64 = BigInt.asUintN(64, v >> 64n);
    const loLo64 = BigInt.asUintN(64, v);

    return xdr.ScVal.scvU256(
      new xdr.UInt256Parts({
        hiHi: new xdr.Uint64(hiHi64),
        hiLo: new xdr.Uint64(hiLo64),
        loHi: new xdr.Uint64(loHi64),
        loLo: new xdr.Uint64(loLo64)
      })
    );
  }

  /** @returns {xdr.ScVal} the smallest interpretation of the stored value */
  toScVal() {
    switch (this.type) {
      case 'i64':
        return this.toI64();
      case 'i128':
        return this.toI128();
      case 'i256':
        return this.toI256();
      case 'u64':
        return this.toU64();
      case 'u128':
        return this.toU128();
      case 'u256':
        return this.toU256();
      default:
        throw TypeError(`invalid type: ${this.type}`);
    }
  }

  valueOf() {
    return this.int.valueOf();
  }

  toString() {
    return this.int.toString();
  }

  toJSON() {
    return {
      value: this.toBigInt().toString(),
      type: this.type
    };
  }

  _sizeCheck(bits) {
    if (this.int.size > bits) {
      throw RangeError(`value too large for ${bits} bits (${this.type})`);
    }
  }

  static isType(type) {
    switch (type) {
      case 'i64':
      case 'i128':
      case 'i256':
      case 'u64':
      case 'u128':
      case 'u256':
        return true;
      default:
        return false;
    }
  }

  /**
   * Convert the raw `ScValType` string (e.g. 'scvI128', generated by the XDR)
   * to a type description for {@link XdrLargeInt} construction (e.g. 'i128')
   *
   * @param {string} scvType  the `xdr.ScValType` as a string
   * @returns {string} a suitable equivalent type to construct this object
   */
  static getType(scvType) {
    return scvType.slice(3).toLowerCase();
  }
}