saepenatus
Version:
Web3-Onboard makes it simple to connect Ethereum hardware and software wallets to your dapp. Features standardised spec compliant web3 providers for all supported wallets, framework agnostic modern javascript UI with code splitting, CSS customization, mul
412 lines (307 loc) • 13.9 kB
text/typescript
"use strict";
import { arrayify, BytesLike, hexZeroPad, isBytes } from "@ethersproject/bytes";
import { Logger } from "@ethersproject/logger";
import { version } from "./_version";
const logger = new Logger(version);
import { BigNumber, BigNumberish, isBigNumberish } from "./bignumber";
const _constructorGuard = { };
const Zero = BigNumber.from(0);
const NegativeOne = BigNumber.from(-1);
function throwFault(message: string, fault: string, operation: string, value?: any): never {
const params: any = { fault: fault, operation: operation };
if (value !== undefined) { params.value = value; }
return logger.throwError(message, Logger.errors.NUMERIC_FAULT, params);
}
// Constant to pull zeros from for multipliers
let zeros = "0";
while (zeros.length < 256) { zeros += zeros; }
// Returns a string "1" followed by decimal "0"s
function getMultiplier(decimals: BigNumberish): string {
if (typeof(decimals) !== "number") {
try {
decimals = BigNumber.from(decimals).toNumber();
} catch (e) { }
}
if (typeof(decimals) === "number" && decimals >= 0 && decimals <= 256 && !(decimals % 1)) {
return ("1" + zeros.substring(0, decimals));
}
return logger.throwArgumentError("invalid decimal size", "decimals", decimals);
}
export function formatFixed(value: BigNumberish, decimals?: string | BigNumberish): string {
if (decimals == null) { decimals = 0; }
const multiplier = getMultiplier(decimals);
// Make sure wei is a big number (convert as necessary)
value = BigNumber.from(value);
const negative = value.lt(Zero);
if (negative) { value = value.mul(NegativeOne); }
let fraction = value.mod(multiplier).toString();
while (fraction.length < multiplier.length - 1) { fraction = "0" + fraction; }
// Strip training 0
fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1];
const whole = value.div(multiplier).toString();
if (multiplier.length === 1) {
value = whole;
} else {
value = whole + "." + fraction;
}
if (negative) { value = "-" + value; }
return value;
}
export function parseFixed(value: string, decimals?: BigNumberish): BigNumber {
if (decimals == null) { decimals = 0; }
const multiplier = getMultiplier(decimals);
if (typeof(value) !== "string" || !value.match(/^-?[0-9.]+$/)) {
logger.throwArgumentError("invalid decimal value", "value", value);
}
// Is it negative?
const negative = (value.substring(0, 1) === "-");
if (negative) { value = value.substring(1); }
if (value === ".") {
logger.throwArgumentError("missing value", "value", value);
}
// Split it into a whole and fractional part
const comps = value.split(".");
if (comps.length > 2) {
logger.throwArgumentError("too many decimal points", "value", value);
}
let whole = comps[0], fraction = comps[1];
if (!whole) { whole = "0"; }
if (!fraction) { fraction = "0"; }
// Trim trailing zeros
while (fraction[fraction.length - 1] === "0") {
fraction = fraction.substring(0, fraction.length - 1);
}
// Check the fraction doesn't exceed our decimals size
if (fraction.length > multiplier.length - 1) {
throwFault("fractional component exceeds decimals", "underflow", "parseFixed");
}
// If decimals is 0, we have an empty string for fraction
if (fraction === "") { fraction = "0"; }
// Fully pad the string with zeros to get to wei
while (fraction.length < multiplier.length - 1) { fraction += "0"; }
const wholeValue = BigNumber.from(whole);
const fractionValue = BigNumber.from(fraction);
let wei = (wholeValue.mul(multiplier)).add(fractionValue);
if (negative) { wei = wei.mul(NegativeOne); }
return wei;
}
export class FixedFormat {
readonly signed: boolean;
readonly width: number;
readonly decimals: number;
readonly name: string;
readonly _multiplier: string;
constructor(constructorGuard: any, signed: boolean, width: number, decimals: number) {
if (constructorGuard !== _constructorGuard) {
logger.throwError("cannot use FixedFormat constructor; use FixedFormat.from", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "new FixedFormat"
});
}
this.signed = signed;
this.width = width;
this.decimals = decimals;
this.name = (signed ? "": "u") + "fixed" + String(width) + "x" + String(decimals);
this._multiplier = getMultiplier(decimals);
Object.freeze(this);
}
static from(value: any): FixedFormat {
if (value instanceof FixedFormat) { return value; }
if (typeof(value) === "number") {
value = `fixed128x${value}`
}
let signed = true;
let width = 128;
let decimals = 18;
if (typeof(value) === "string") {
if (value === "fixed") {
// defaults...
} else if (value === "ufixed") {
signed = false;
} else {
const match = value.match(/^(u?)fixed([0-9]+)x([0-9]+)$/);
if (!match) { logger.throwArgumentError("invalid fixed format", "format", value); }
signed = (match[1] !== "u");
width = parseInt(match[2]);
decimals = parseInt(match[3]);
}
} else if (value) {
const check = (key: string, type: string, defaultValue: any): any => {
if (value[key] == null) { return defaultValue; }
if (typeof(value[key]) !== type) {
logger.throwArgumentError("invalid fixed format (" + key + " not " + type +")", "format." + key, value[key]);
}
return value[key];
}
signed = check("signed", "boolean", signed);
width = check("width", "number", width);
decimals = check("decimals", "number", decimals);
}
if (width % 8) {
logger.throwArgumentError("invalid fixed format width (not byte aligned)", "format.width", width);
}
if (decimals > 80) {
logger.throwArgumentError("invalid fixed format (decimals too large)", "format.decimals", decimals);
}
return new FixedFormat(_constructorGuard, signed, width, decimals);
}
}
export class FixedNumber {
readonly format: FixedFormat;
readonly _hex: string;
readonly _value: string;
readonly _isFixedNumber: boolean;
constructor(constructorGuard: any, hex: string, value: string, format?: FixedFormat) {
logger.checkNew(new.target, FixedNumber);
if (constructorGuard !== _constructorGuard) {
logger.throwError("cannot use FixedNumber constructor; use FixedNumber.from", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "new FixedFormat"
});
}
this.format = format;
this._hex = hex;
this._value = value;
this._isFixedNumber = true;
Object.freeze(this);
}
_checkFormat(other: FixedNumber): void {
if (this.format.name !== other.format.name) {
logger.throwArgumentError("incompatible format; use fixedNumber.toFormat", "other", other);
}
}
addUnsafe(other: FixedNumber): FixedNumber {
this._checkFormat(other);
const a = parseFixed(this._value, this.format.decimals);
const b = parseFixed(other._value, other.format.decimals);
return FixedNumber.fromValue(a.add(b), this.format.decimals, this.format);
}
subUnsafe(other: FixedNumber): FixedNumber {
this._checkFormat(other);
const a = parseFixed(this._value, this.format.decimals);
const b = parseFixed(other._value, other.format.decimals);
return FixedNumber.fromValue(a.sub(b), this.format.decimals, this.format);
}
mulUnsafe(other: FixedNumber): FixedNumber {
this._checkFormat(other);
const a = parseFixed(this._value, this.format.decimals);
const b = parseFixed(other._value, other.format.decimals);
return FixedNumber.fromValue(a.mul(b).div(this.format._multiplier), this.format.decimals, this.format);
}
divUnsafe(other: FixedNumber): FixedNumber {
this._checkFormat(other);
const a = parseFixed(this._value, this.format.decimals);
const b = parseFixed(other._value, other.format.decimals);
return FixedNumber.fromValue(a.mul(this.format._multiplier).div(b), this.format.decimals, this.format);
}
floor(): FixedNumber {
const comps = this.toString().split(".");
if (comps.length === 1) { comps.push("0"); }
let result = FixedNumber.from(comps[0], this.format);
const hasFraction = !comps[1].match(/^(0*)$/);
if (this.isNegative() && hasFraction) {
result = result.subUnsafe(ONE.toFormat(result.format));
}
return result;
}
ceiling(): FixedNumber {
const comps = this.toString().split(".");
if (comps.length === 1) { comps.push("0"); }
let result = FixedNumber.from(comps[0], this.format);
const hasFraction = !comps[1].match(/^(0*)$/);
if (!this.isNegative() && hasFraction) {
result = result.addUnsafe(ONE.toFormat(result.format));
}
return result;
}
// @TODO: Support other rounding algorithms
round(decimals?: number): FixedNumber {
if (decimals == null) { decimals = 0; }
// If we are already in range, we're done
const comps = this.toString().split(".");
if (comps.length === 1) { comps.push("0"); }
if (decimals < 0 || decimals > 80 || (decimals % 1)) {
logger.throwArgumentError("invalid decimal count", "decimals", decimals);
}
if (comps[1].length <= decimals) { return this; }
const factor = FixedNumber.from("1" + zeros.substring(0, decimals), this.format);
const bump = BUMP.toFormat(this.format);
return this.mulUnsafe(factor).addUnsafe(bump).floor().divUnsafe(factor);
}
isZero(): boolean {
return (this._value === "0.0" || this._value === "0");
}
isNegative(): boolean {
return (this._value[0] === "-");
}
toString(): string { return this._value; }
toHexString(width?: number): string {
if (width == null) { return this._hex; }
if (width % 8) { logger.throwArgumentError("invalid byte width", "width", width); }
const hex = BigNumber.from(this._hex).fromTwos(this.format.width).toTwos(width).toHexString();
return hexZeroPad(hex, width / 8);
}
toUnsafeFloat(): number { return parseFloat(this.toString()); }
toFormat(format: FixedFormat | string): FixedNumber {
return FixedNumber.fromString(this._value, format);
}
static fromValue(value: BigNumber, decimals?: BigNumberish, format?: FixedFormat | string | number): FixedNumber {
// If decimals looks more like a format, and there is no format, shift the parameters
if (format == null && decimals != null && !isBigNumberish(decimals)) {
format = decimals;
decimals = null;
}
if (decimals == null) { decimals = 0; }
if (format == null) { format = "fixed"; }
return FixedNumber.fromString(formatFixed(value, decimals), FixedFormat.from(format));
}
static fromString(value: string, format?: FixedFormat | string | number): FixedNumber {
if (format == null) { format = "fixed"; }
const fixedFormat = FixedFormat.from(format);
const numeric = parseFixed(value, fixedFormat.decimals);
if (!fixedFormat.signed && numeric.lt(Zero)) {
throwFault("unsigned value cannot be negative", "overflow", "value", value);
}
let hex: string = null;
if (fixedFormat.signed) {
hex = numeric.toTwos(fixedFormat.width).toHexString();
} else {
hex = numeric.toHexString();
hex = hexZeroPad(hex, fixedFormat.width / 8);
}
const decimal = formatFixed(numeric, fixedFormat.decimals);
return new FixedNumber(_constructorGuard, hex, decimal, fixedFormat);
}
static fromBytes(value: BytesLike, format?: FixedFormat | string | number): FixedNumber {
if (format == null) { format = "fixed"; }
const fixedFormat = FixedFormat.from(format);
if (arrayify(value).length > fixedFormat.width / 8) {
throw new Error("overflow");
}
let numeric = BigNumber.from(value);
if (fixedFormat.signed) { numeric = numeric.fromTwos(fixedFormat.width); }
const hex = numeric.toTwos((fixedFormat.signed ? 0: 1) + fixedFormat.width).toHexString();
const decimal = formatFixed(numeric, fixedFormat.decimals);
return new FixedNumber(_constructorGuard, hex, decimal, fixedFormat);
}
static from(value: any, format?: FixedFormat | string | number) {
if (typeof(value) === "string") {
return FixedNumber.fromString(value, format);
}
if (isBytes(value)) {
return FixedNumber.fromBytes(value, format);
}
try {
return FixedNumber.fromValue(value, 0, format);
} catch (error) {
// Allow NUMERIC_FAULT to bubble up
if (error.code !== Logger.errors.INVALID_ARGUMENT) {
throw error;
}
}
return logger.throwArgumentError("invalid FixedNumber value", "value", value);
}
static isFixedNumber(value: any): value is FixedNumber {
return !!(value && value._isFixedNumber);
}
}
const ONE = FixedNumber.from(1);
const BUMP = FixedNumber.from("0.5");