UNPKG

13.9 kBPlain TextView Raw
1"use strict";
2
3import { arrayify, BytesLike, hexZeroPad, isBytes } from "@ethersproject/bytes";
4
5import { Logger } from "@ethersproject/logger";
6import { version } from "./_version";
7const logger = new Logger(version);
8
9import { BigNumber, BigNumberish, isBigNumberish } from "./bignumber";
10
11const _constructorGuard = { };
12
13const Zero = BigNumber.from(0);
14const NegativeOne = BigNumber.from(-1);
15
16function throwFault(message: string, fault: string, operation: string, value?: any): never {
17 const params: any = { fault: fault, operation: operation };
18 if (value !== undefined) { params.value = value; }
19 return logger.throwError(message, Logger.errors.NUMERIC_FAULT, params);
20}
21
22// Constant to pull zeros from for multipliers
23let zeros = "0";
24while (zeros.length < 256) { zeros += zeros; }
25
26// Returns a string "1" followed by decimal "0"s
27function getMultiplier(decimals: BigNumberish): string {
28
29 if (typeof(decimals) !== "number") {
30 try {
31 decimals = BigNumber.from(decimals).toNumber();
32 } catch (e) { }
33 }
34
35 if (typeof(decimals) === "number" && decimals >= 0 && decimals <= 256 && !(decimals % 1)) {
36 return ("1" + zeros.substring(0, decimals));
37 }
38
39 return logger.throwArgumentError("invalid decimal size", "decimals", decimals);
40}
41
42export function formatFixed(value: BigNumberish, decimals?: string | BigNumberish): string {
43 if (decimals == null) { decimals = 0; }
44 const multiplier = getMultiplier(decimals);
45
46 // Make sure wei is a big number (convert as necessary)
47 value = BigNumber.from(value);
48
49 const negative = value.lt(Zero);
50 if (negative) { value = value.mul(NegativeOne); }
51
52 let fraction = value.mod(multiplier).toString();
53 while (fraction.length < multiplier.length - 1) { fraction = "0" + fraction; }
54
55 // Strip training 0
56 fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1];
57
58 const whole = value.div(multiplier).toString();
59 if (multiplier.length === 1) {
60 value = whole;
61 } else {
62 value = whole + "." + fraction;
63 }
64
65 if (negative) { value = "-" + value; }
66
67 return value;
68}
69
70export function parseFixed(value: string, decimals?: BigNumberish): BigNumber {
71
72 if (decimals == null) { decimals = 0; }
73 const multiplier = getMultiplier(decimals);
74
75 if (typeof(value) !== "string" || !value.match(/^-?[0-9.]+$/)) {
76 logger.throwArgumentError("invalid decimal value", "value", value);
77 }
78
79 // Is it negative?
80 const negative = (value.substring(0, 1) === "-");
81 if (negative) { value = value.substring(1); }
82
83 if (value === ".") {
84 logger.throwArgumentError("missing value", "value", value);
85 }
86
87 // Split it into a whole and fractional part
88 const comps = value.split(".");
89 if (comps.length > 2) {
90 logger.throwArgumentError("too many decimal points", "value", value);
91 }
92
93 let whole = comps[0], fraction = comps[1];
94 if (!whole) { whole = "0"; }
95 if (!fraction) { fraction = "0"; }
96
97 // Trim trailing zeros
98 while (fraction[fraction.length - 1] === "0") {
99 fraction = fraction.substring(0, fraction.length - 1);
100 }
101
102 // Check the fraction doesn't exceed our decimals size
103 if (fraction.length > multiplier.length - 1) {
104 throwFault("fractional component exceeds decimals", "underflow", "parseFixed");
105 }
106
107 // If decimals is 0, we have an empty string for fraction
108 if (fraction === "") { fraction = "0"; }
109
110 // Fully pad the string with zeros to get to wei
111 while (fraction.length < multiplier.length - 1) { fraction += "0"; }
112
113 const wholeValue = BigNumber.from(whole);
114 const fractionValue = BigNumber.from(fraction);
115
116 let wei = (wholeValue.mul(multiplier)).add(fractionValue);
117
118 if (negative) { wei = wei.mul(NegativeOne); }
119
120 return wei;
121}
122
123
124export class FixedFormat {
125 readonly signed: boolean;
126 readonly width: number;
127 readonly decimals: number;
128 readonly name: string;
129 readonly _multiplier: string;
130
131 constructor(constructorGuard: any, signed: boolean, width: number, decimals: number) {
132 if (constructorGuard !== _constructorGuard) {
133 logger.throwError("cannot use FixedFormat constructor; use FixedFormat.from", Logger.errors.UNSUPPORTED_OPERATION, {
134 operation: "new FixedFormat"
135 });
136 }
137
138 this.signed = signed;
139 this.width = width;
140 this.decimals = decimals;
141
142 this.name = (signed ? "": "u") + "fixed" + String(width) + "x" + String(decimals);
143
144 this._multiplier = getMultiplier(decimals);
145
146 Object.freeze(this);
147 }
148
149 static from(value: any): FixedFormat {
150 if (value instanceof FixedFormat) { return value; }
151
152 if (typeof(value) === "number") {
153 value = `fixed128x${value}`
154 }
155
156 let signed = true;
157 let width = 128;
158 let decimals = 18;
159
160 if (typeof(value) === "string") {
161 if (value === "fixed") {
162 // defaults...
163 } else if (value === "ufixed") {
164 signed = false;
165 } else {
166 const match = value.match(/^(u?)fixed([0-9]+)x([0-9]+)$/);
167 if (!match) { logger.throwArgumentError("invalid fixed format", "format", value); }
168 signed = (match[1] !== "u");
169 width = parseInt(match[2]);
170 decimals = parseInt(match[3]);
171 }
172 } else if (value) {
173 const check = (key: string, type: string, defaultValue: any): any => {
174 if (value[key] == null) { return defaultValue; }
175 if (typeof(value[key]) !== type) {
176 logger.throwArgumentError("invalid fixed format (" + key + " not " + type +")", "format." + key, value[key]);
177 }
178 return value[key];
179 }
180 signed = check("signed", "boolean", signed);
181 width = check("width", "number", width);
182 decimals = check("decimals", "number", decimals);
183 }
184
185 if (width % 8) {
186 logger.throwArgumentError("invalid fixed format width (not byte aligned)", "format.width", width);
187 }
188
189 if (decimals > 80) {
190 logger.throwArgumentError("invalid fixed format (decimals too large)", "format.decimals", decimals);
191 }
192
193 return new FixedFormat(_constructorGuard, signed, width, decimals);
194 }
195}
196
197export class FixedNumber {
198 readonly format: FixedFormat;
199 readonly _hex: string;
200 readonly _value: string;
201
202 readonly _isFixedNumber: boolean;
203
204 constructor(constructorGuard: any, hex: string, value: string, format?: FixedFormat) {
205 logger.checkNew(new.target, FixedNumber);
206
207 if (constructorGuard !== _constructorGuard) {
208 logger.throwError("cannot use FixedNumber constructor; use FixedNumber.from", Logger.errors.UNSUPPORTED_OPERATION, {
209 operation: "new FixedFormat"
210 });
211 }
212
213 this.format = format;
214 this._hex = hex;
215 this._value = value;
216
217 this._isFixedNumber = true;
218
219 Object.freeze(this);
220 }
221
222 _checkFormat(other: FixedNumber): void {
223 if (this.format.name !== other.format.name) {
224 logger.throwArgumentError("incompatible format; use fixedNumber.toFormat", "other", other);
225 }
226 }
227
228 addUnsafe(other: FixedNumber): FixedNumber {
229 this._checkFormat(other);
230 const a = parseFixed(this._value, this.format.decimals);
231 const b = parseFixed(other._value, other.format.decimals);
232 return FixedNumber.fromValue(a.add(b), this.format.decimals, this.format);
233 }
234
235 subUnsafe(other: FixedNumber): FixedNumber {
236 this._checkFormat(other);
237 const a = parseFixed(this._value, this.format.decimals);
238 const b = parseFixed(other._value, other.format.decimals);
239 return FixedNumber.fromValue(a.sub(b), this.format.decimals, this.format);
240 }
241
242 mulUnsafe(other: FixedNumber): FixedNumber {
243 this._checkFormat(other);
244 const a = parseFixed(this._value, this.format.decimals);
245 const b = parseFixed(other._value, other.format.decimals);
246 return FixedNumber.fromValue(a.mul(b).div(this.format._multiplier), this.format.decimals, this.format);
247 }
248
249 divUnsafe(other: FixedNumber): FixedNumber {
250 this._checkFormat(other);
251 const a = parseFixed(this._value, this.format.decimals);
252 const b = parseFixed(other._value, other.format.decimals);
253 return FixedNumber.fromValue(a.mul(this.format._multiplier).div(b), this.format.decimals, this.format);
254 }
255
256 floor(): FixedNumber {
257 const comps = this.toString().split(".");
258 if (comps.length === 1) { comps.push("0"); }
259
260 let result = FixedNumber.from(comps[0], this.format);
261
262 const hasFraction = !comps[1].match(/^(0*)$/);
263 if (this.isNegative() && hasFraction) {
264 result = result.subUnsafe(ONE.toFormat(result.format));
265 }
266
267 return result;
268 }
269
270 ceiling(): FixedNumber {
271 const comps = this.toString().split(".");
272 if (comps.length === 1) { comps.push("0"); }
273
274 let result = FixedNumber.from(comps[0], this.format);
275
276 const hasFraction = !comps[1].match(/^(0*)$/);
277 if (!this.isNegative() && hasFraction) {
278 result = result.addUnsafe(ONE.toFormat(result.format));
279 }
280
281 return result;
282 }
283
284 // @TODO: Support other rounding algorithms
285 round(decimals?: number): FixedNumber {
286 if (decimals == null) { decimals = 0; }
287
288 // If we are already in range, we're done
289 const comps = this.toString().split(".");
290 if (comps.length === 1) { comps.push("0"); }
291
292 if (decimals < 0 || decimals > 80 || (decimals % 1)) {
293 logger.throwArgumentError("invalid decimal count", "decimals", decimals);
294 }
295
296 if (comps[1].length <= decimals) { return this; }
297
298 const factor = FixedNumber.from("1" + zeros.substring(0, decimals), this.format);
299 const bump = BUMP.toFormat(this.format);
300
301 return this.mulUnsafe(factor).addUnsafe(bump).floor().divUnsafe(factor);
302 }
303
304 isZero(): boolean {
305 return (this._value === "0.0" || this._value === "0");
306 }
307
308 isNegative(): boolean {
309 return (this._value[0] === "-");
310 }
311
312 toString(): string { return this._value; }
313
314 toHexString(width?: number): string {
315 if (width == null) { return this._hex; }
316 if (width % 8) { logger.throwArgumentError("invalid byte width", "width", width); }
317 const hex = BigNumber.from(this._hex).fromTwos(this.format.width).toTwos(width).toHexString();
318 return hexZeroPad(hex, width / 8);
319 }
320
321 toUnsafeFloat(): number { return parseFloat(this.toString()); }
322
323 toFormat(format: FixedFormat | string): FixedNumber {
324 return FixedNumber.fromString(this._value, format);
325 }
326
327
328 static fromValue(value: BigNumber, decimals?: BigNumberish, format?: FixedFormat | string | number): FixedNumber {
329 // If decimals looks more like a format, and there is no format, shift the parameters
330 if (format == null && decimals != null && !isBigNumberish(decimals)) {
331 format = decimals;
332 decimals = null;
333 }
334
335 if (decimals == null) { decimals = 0; }
336 if (format == null) { format = "fixed"; }
337
338 return FixedNumber.fromString(formatFixed(value, decimals), FixedFormat.from(format));
339 }
340
341
342 static fromString(value: string, format?: FixedFormat | string | number): FixedNumber {
343 if (format == null) { format = "fixed"; }
344
345 const fixedFormat = FixedFormat.from(format);
346
347 const numeric = parseFixed(value, fixedFormat.decimals);
348
349 if (!fixedFormat.signed && numeric.lt(Zero)) {
350 throwFault("unsigned value cannot be negative", "overflow", "value", value);
351 }
352
353 let hex: string = null;
354 if (fixedFormat.signed) {
355 hex = numeric.toTwos(fixedFormat.width).toHexString();
356 } else {
357 hex = numeric.toHexString();
358 hex = hexZeroPad(hex, fixedFormat.width / 8);
359 }
360
361 const decimal = formatFixed(numeric, fixedFormat.decimals);
362
363 return new FixedNumber(_constructorGuard, hex, decimal, fixedFormat);
364 }
365
366 static fromBytes(value: BytesLike, format?: FixedFormat | string | number): FixedNumber {
367 if (format == null) { format = "fixed"; }
368
369 const fixedFormat = FixedFormat.from(format);
370
371 if (arrayify(value).length > fixedFormat.width / 8) {
372 throw new Error("overflow");
373 }
374
375 let numeric = BigNumber.from(value);
376 if (fixedFormat.signed) { numeric = numeric.fromTwos(fixedFormat.width); }
377
378 const hex = numeric.toTwos((fixedFormat.signed ? 0: 1) + fixedFormat.width).toHexString();
379 const decimal = formatFixed(numeric, fixedFormat.decimals);
380
381 return new FixedNumber(_constructorGuard, hex, decimal, fixedFormat);
382 }
383
384 static from(value: any, format?: FixedFormat | string | number) {
385 if (typeof(value) === "string") {
386 return FixedNumber.fromString(value, format);
387 }
388
389 if (isBytes(value)) {
390 return FixedNumber.fromBytes(value, format);
391 }
392
393 try {
394 return FixedNumber.fromValue(value, 0, format);
395 } catch (error) {
396 // Allow NUMERIC_FAULT to bubble up
397 if (error.code !== Logger.errors.INVALID_ARGUMENT) {
398 throw error;
399 }
400 }
401
402 return logger.throwArgumentError("invalid FixedNumber value", "value", value);
403 }
404
405 static isFixedNumber(value: any): value is FixedNumber {
406 return !!(value && value._isFixedNumber);
407 }
408}
409
410const ONE = FixedNumber.from(1);
411const BUMP = FixedNumber.from("0.5");