@meantjs/format 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +4 -0
- package/package.json +1 -1
- package/src/amount.mjs +35 -6
- package/src/formatter.mjs +23 -10
- package/src/index.d.mts +9 -4
- package/src/time.mjs +3 -3
- package/src/util.mjs +32 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
The five Candid Profile library packages (`@meantjs/profile`, `/format`, `/consent`, `/tools`, `/reconcile`) release together under one version and pin each other exactly; a version appears here even when this package did not change. Release steps: `docs/releasing.md` in the repository.
|
|
4
4
|
|
|
5
|
+
## 0.2.0 — 2026-10-28
|
|
6
|
+
|
|
7
|
+
- EVM integer types (spec §15, D-054): `amount` on `uint8`…`uint256` and `int8`…`int256`, `timestamp` and `duration`
|
|
8
|
+
on `uint32`…`uint256` and `int64`…`int256`, with the bounds of their bits; `bytes` on `bytes` and `bytes1`…`bytes32`
|
|
9
|
+
(a `bytesN` takes exactly N bytes). A resolved-form node with `abi` is read like one with `candid`.
|
|
10
|
+
- `amount` takes `threshold` and `message`: `displayAmount` shows a value at or above the threshold as the message
|
|
11
|
+
(default "Unlimited") and the symbol. Parsing is unchanged.
|
|
12
|
+
|
|
5
13
|
## 0.1.0 — 2026-10-28
|
|
6
14
|
|
|
7
15
|
First published version.
|
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@ npm install @meantjs/format
|
|
|
17
17
|
- **No dependencies** at run time or for tests. Node >= 22.
|
|
18
18
|
- **No I/O.** Dynamic tag arguments (`{ "from": "query:icrc1_decimals" }`) are resolved by the caller and passed in.
|
|
19
19
|
- The choices it made where §5.3 was open were accepted as D-024 and are now in spec §5.3.
|
|
20
|
+
- **EVM types** (spec §15, experimental): a position of an EVM contract's resolved form (`{ abi: "uint256", tags }`)
|
|
21
|
+
works like a Candid one. `amount` takes `uint8`…`uint256` and `int8`…`int256`, `timestamp` and `duration`
|
|
22
|
+
`uint32`…`uint256` and `int64`…`int256`, each with the bounds of its bits; `bytes` takes `bytes` and `bytesN`
|
|
23
|
+
(exactly N bytes). `amount`'s `threshold` and `message` show a value at or above the threshold as `Unlimited USDC`.
|
|
20
24
|
|
|
21
25
|
```sh
|
|
22
26
|
npm test -w packages/format # or: npm test at the repo root
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meantjs/format",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Exact value conversion for Candid Profile value tags (amount, timestamp, duration, account, subaccount, bytes): parse human input, display canonical text, never round.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
package/src/amount.mjs
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
// extra digits are zeros (the literal reading of "more than d fractional digits MUST be rejected").
|
|
6
6
|
|
|
7
7
|
import { CODES, fail } from "./errors.mjs";
|
|
8
|
-
import { UNSAFE_DISPLAY_CHAR, checkRange, intType, requireString, toBigInt } from "./util.mjs";
|
|
8
|
+
import { ABI_INT_TYPES, UNSAFE_DISPLAY_CHAR, checkRange, intType, requireString, toBigInt, typeWord } from "./util.mjs";
|
|
9
9
|
|
|
10
|
-
export const AMOUNT_TYPES = ["nat", "int", "nat8", "nat16", "nat32", "nat64", "int8", "int16", "int32", "int64"];
|
|
10
|
+
export const AMOUNT_TYPES = ["nat", "int", "nat8", "nat16", "nat32", "nat64", "int8", "int16", "int32", "int64", ...ABI_INT_TYPES.filter((t) => !/^int(?:8|16|32|64)$/.test(t))];
|
|
11
|
+
|
|
12
|
+
/** The default `message` of an amount at or above its `threshold` (spec section 5.3; ERC-7730's tokenAmount default). */
|
|
13
|
+
export const DEFAULT_THRESHOLD_MESSAGE = "Unlimited";
|
|
11
14
|
|
|
12
15
|
// number, then (after whitespace) an optional symbol. "1.", ".5", "1,5", "1e3" and "+1" do not match.
|
|
13
16
|
const AMOUNT_RE = /^(-?)([0-9]+)(?:\.([0-9]+))?(?:\s+(\S(?:.*\S)?))?$/s;
|
|
@@ -27,6 +30,27 @@ export function checkSymbol(symbol) {
|
|
|
27
30
|
return symbol;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/** @returns {bigint | undefined} a validated threshold: a canonical decimal integer string or a bigint, not negative */
|
|
34
|
+
export function checkThreshold(threshold) {
|
|
35
|
+
if (threshold === undefined || threshold === null) return undefined;
|
|
36
|
+
if (typeof threshold === "bigint" ? threshold < 0n : typeof threshold !== "string" || !/^(?:0|[1-9][0-9]{0,77})$/.test(threshold)) {
|
|
37
|
+
fail(CODES.ARG, `amount threshold must be a decimal string of base units (0 or more, at most 78 digits) or a bigint, got ${typeof threshold === "string" ? JSON.stringify(threshold) : String(threshold)}`);
|
|
38
|
+
}
|
|
39
|
+
return BigInt(threshold);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @returns {string | undefined} a validated threshold message: 1 to 32 characters, nothing invisible */
|
|
43
|
+
export function checkMessage(message) {
|
|
44
|
+
if (message === undefined || message === null) return undefined;
|
|
45
|
+
if (typeof message !== "string") fail(CODES.ARG, `amount message must be a string, got ${typeof message}`);
|
|
46
|
+
const n = [...message].length;
|
|
47
|
+
if (n < 1 || n > 32) fail(CODES.ARG, `amount message must be 1 to 32 characters, got ${n}`);
|
|
48
|
+
if (message !== message.trim() || UNSAFE_DISPLAY_CHAR.test(message) || !message.isWellFormed()) {
|
|
49
|
+
fail(CODES.ARG, `amount message ${JSON.stringify(message)} has surrounding whitespace, control or invisible characters`);
|
|
50
|
+
}
|
|
51
|
+
return message;
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
export function checkDecimals(decimals) {
|
|
31
55
|
let d = decimals;
|
|
32
56
|
if (typeof d === "string" && /^(?:0|[1-9][0-9]{0,2})$/.test(d)) d = Number(d);
|
|
@@ -55,7 +79,7 @@ export function parseAmount(input, { decimals, symbol, candid = "nat" }) {
|
|
|
55
79
|
);
|
|
56
80
|
}
|
|
57
81
|
const [, sign, whole, frac = "", given] = m;
|
|
58
|
-
if (sign && !type.signed) fail(CODES.NEGATIVE, `negative amounts are not allowed here (the
|
|
82
|
+
if (sign && !type.signed) fail(CODES.NEGATIVE, `negative amounts are not allowed here (the ${typeWord(candid)} ${candid} is unsigned)`);
|
|
59
83
|
if (given !== undefined) {
|
|
60
84
|
if (expected === undefined) fail(CODES.SYMBOL_MISMATCH, `this amount has no symbol; remove "${given}" and give the number only`);
|
|
61
85
|
if (asciiLower(given) !== asciiLower(expected)) fail(CODES.SYMBOL_MISMATCH, `the symbol "${given}" is not this token's symbol "${expected}"`);
|
|
@@ -73,18 +97,23 @@ export function parseAmount(input, { decimals, symbol, candid = "nat" }) {
|
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
/**
|
|
76
|
-
* Display a base-unit integer as "1.5 ICP": trailing fractional zeros removed, symbol appended.
|
|
100
|
+
* Display a base-unit integer as "1.5 ICP": trailing fractional zeros removed, symbol appended. A value at or above
|
|
101
|
+
* `threshold` displays as `message` (default "Unlimited") and the symbol: "Unlimited USDC" (spec section 5.3).
|
|
77
102
|
*
|
|
78
103
|
* @param {string | bigint} value
|
|
79
|
-
* @param {{ decimals: number | bigint | string, symbol?: string, candid?: string }} opts
|
|
104
|
+
* @param {{ decimals: number | bigint | string, symbol?: string, candid?: string, threshold?: string | bigint, message?: string }} opts
|
|
80
105
|
* @returns {string}
|
|
81
106
|
*/
|
|
82
|
-
export function displayAmount(value, { decimals, symbol, candid = "nat" }) {
|
|
107
|
+
export function displayAmount(value, { decimals, symbol, candid = "nat", threshold, message }) {
|
|
83
108
|
const d = checkDecimals(decimals);
|
|
84
109
|
const sym = checkSymbol(symbol);
|
|
110
|
+
const limit = checkThreshold(threshold);
|
|
111
|
+
const text = checkMessage(message);
|
|
112
|
+
if (text !== undefined && limit === undefined) fail(CODES.ARG, "amount message needs a threshold");
|
|
85
113
|
const type = intType(candid, AMOUNT_TYPES, "amount");
|
|
86
114
|
const v = toBigInt(value, "amount");
|
|
87
115
|
checkRange(v, type, "amount");
|
|
116
|
+
if (limit !== undefined && v >= limit) return `${text ?? DEFAULT_THRESHOLD_MESSAGE}${sym ? ` ${sym}` : ""}`;
|
|
88
117
|
const digits = (v < 0n ? -v : v).toString().padStart(d + 1, "0");
|
|
89
118
|
const whole = digits.slice(0, digits.length - d);
|
|
90
119
|
const frac = digits.slice(digits.length - d).replace(/0+$/, "");
|
package/src/formatter.mjs
CHANGED
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
// resolved by the caller (the library does no I/O) and passed in `refs`, keyed by the `from` string.
|
|
7
7
|
|
|
8
8
|
import { CODES, FormatError, fail } from "./errors.mjs";
|
|
9
|
-
import { AMOUNT_TYPES, checkDecimals, checkSymbol, displayAmount, parseAmount } from "./amount.mjs";
|
|
9
|
+
import { AMOUNT_TYPES, DEFAULT_THRESHOLD_MESSAGE, checkDecimals, checkMessage, checkSymbol, checkThreshold, displayAmount, parseAmount } from "./amount.mjs";
|
|
10
10
|
import { TIME_TYPES, displayDuration, displayTimestamp, parseDuration, parseTimestamp, unitFactor } from "./time.mjs";
|
|
11
11
|
import { displayAccount, parseAccount } from "./account.mjs";
|
|
12
12
|
import { displaySubaccount, parseSubaccount } from "./subaccount.mjs";
|
|
13
13
|
import { BYTES_ENCODINGS, checkMaxLen, displayBytes, parseBytes } from "./bytes.mjs";
|
|
14
|
+
import { INT_TYPES, listTypes } from "./util.mjs";
|
|
15
|
+
|
|
16
|
+
/** The byte types: Candid's blob, the EVM ABI's bytes and bytes1 to bytes32 (spec section 15). */
|
|
17
|
+
const BYTE_TYPES = ["blob", "bytes", ...Array.from({ length: 32 }, (_, i) => `bytes${i + 1}`)];
|
|
14
18
|
|
|
15
19
|
/** The tags that give a position a text form, in spec §5.1. */
|
|
16
20
|
export const VALUE_TAGS = Object.freeze(["amount", "timestamp", "duration", "account", "subaccount", "bytes"]);
|
|
@@ -28,7 +32,7 @@ function resolveArg(arg, refs, where) {
|
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
function requireCandid(node, allowed, tag) {
|
|
31
|
-
if (!allowed.includes(node.candid)) fail(CODES.TAG_TYPE, `the ${tag} tag applies to ${allowed
|
|
35
|
+
if (!allowed.includes(node.candid)) fail(CODES.TAG_TYPE, `the ${tag} tag applies to ${listTypes(allowed)}, not ${node.candid}`);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
function requireUnit(unit, tag) {
|
|
@@ -68,7 +72,8 @@ function requireUnit(unit, tag) {
|
|
|
68
72
|
* @returns {Formatter | null} null when the position has no value tag (render it as Candid text, spec §6)
|
|
69
73
|
*/
|
|
70
74
|
export function formatterFor(node, { refs = {}, now: defaultNow } = {}) {
|
|
71
|
-
if (node
|
|
75
|
+
if (node !== null && typeof node === "object" && node.candid === undefined && typeof node.abi === "string") node = { ...node, candid: node.abi }; // an EVM position (spec section 15)
|
|
76
|
+
if (node === null || typeof node !== "object" || typeof node.candid !== "string") fail(CODES.TYPE, "node must be a resolved-form position with a `candid` type (or an EVM position's `abi` type)");
|
|
72
77
|
const tags = node.tags ?? {};
|
|
73
78
|
const present = VALUE_TAGS.filter((t) => Object.hasOwn(tags, t));
|
|
74
79
|
if (present.length === 0) return null;
|
|
@@ -86,8 +91,11 @@ export function formatterFor(node, { refs = {}, now: defaultNow } = {}) {
|
|
|
86
91
|
if (raw.decimals === undefined) fail(CODES.ARG, "the amount tag needs decimals");
|
|
87
92
|
const decimals = checkDecimals(arg("decimals"));
|
|
88
93
|
const symbol = checkSymbol(arg("symbol"));
|
|
89
|
-
const
|
|
90
|
-
|
|
94
|
+
const threshold = checkThreshold(raw.threshold);
|
|
95
|
+
const message = checkMessage(raw.message);
|
|
96
|
+
if (message !== undefined && threshold === undefined) fail(CODES.ARG, "amount message needs a threshold");
|
|
97
|
+
const o = { decimals, symbol, candid: node.candid, threshold, message };
|
|
98
|
+
args = { decimals, symbol, ...(threshold === undefined ? {} : { threshold: threshold.toString(), message: message ?? DEFAULT_THRESHOLD_MESSAGE }) };
|
|
91
99
|
parse = (input) => parseAmount(input, o);
|
|
92
100
|
display = (value) => displayAmount(value, o);
|
|
93
101
|
break;
|
|
@@ -127,12 +135,17 @@ export function formatterFor(node, { refs = {}, now: defaultNow } = {}) {
|
|
|
127
135
|
break;
|
|
128
136
|
}
|
|
129
137
|
case "bytes": {
|
|
130
|
-
requireCandid(node,
|
|
138
|
+
requireCandid(node, BYTE_TYPES, tag);
|
|
131
139
|
const encoding = arg("encoding");
|
|
132
140
|
if (!BYTES_ENCODINGS.includes(encoding)) fail(CODES.ARG, `the bytes tag needs encoding utf8, hex or base64, got ${JSON.stringify(encoding)}`);
|
|
133
141
|
const maxLen = raw.max_len;
|
|
134
142
|
args = { encoding, ...(maxLen === undefined ? {} : { max_len: maxLen }) };
|
|
135
|
-
|
|
143
|
+
const size = /^bytes(\d+)$/.exec(node.candid)?.[1]; // bytesN holds exactly N bytes
|
|
144
|
+
parse = (input) => {
|
|
145
|
+
const b = parseBytes(input, { encoding, maxLen });
|
|
146
|
+
if (size !== undefined && b.length !== Number(size)) fail(CODES.LENGTH, `${node.candid} holds exactly ${size} bytes, got ${b.length}`);
|
|
147
|
+
return b;
|
|
148
|
+
};
|
|
136
149
|
display = (value) => displayBytes(value, { encoding });
|
|
137
150
|
break;
|
|
138
151
|
}
|
|
@@ -165,10 +178,10 @@ export function formatterFor(node, { refs = {}, now: defaultNow } = {}) {
|
|
|
165
178
|
// ---------------------------------------------------------------- describe
|
|
166
179
|
|
|
167
180
|
/** The Candid types each value tag applies to (the `requireCandid` calls in `formatterFor`). */
|
|
168
|
-
const CANDID_OF = { amount: AMOUNT_TYPES, timestamp: TIME_TYPES, duration: TIME_TYPES, account: ["record"], subaccount: ["blob"], bytes:
|
|
181
|
+
const CANDID_OF = { amount: AMOUNT_TYPES, timestamp: TIME_TYPES, duration: TIME_TYPES, account: ["record"], subaccount: ["blob"], bytes: BYTE_TYPES };
|
|
169
182
|
/** 2026-01-01T00:00:00Z in ns: the reference instant relative examples are checked against. */
|
|
170
183
|
const DESCRIBE_NOW = 1_767_225_600_000_000_000n;
|
|
171
|
-
|
|
184
|
+
|
|
172
185
|
// ECMAScript syntax characters only: escaping anything else (such as "-") is an error under the `u` flag, which JSON
|
|
173
186
|
// Schema validators such as Ajv use.
|
|
174
187
|
const reEscape = (s) => s.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&");
|
|
@@ -178,7 +191,7 @@ const quoted = (xs) => xs.map((x) => JSON.stringify(x)).join(", ");
|
|
|
178
191
|
|
|
179
192
|
/** The grammar in words (no examples), candidate examples, and a pattern where the grammar is regular. */
|
|
180
193
|
function grammar(tag, args, candid) {
|
|
181
|
-
const signed =
|
|
194
|
+
const signed = INT_TYPES[candid]?.signed === true;
|
|
182
195
|
const finer = (what) => (args.unit === "ns" ? "" : ` Stored in ${args.unit}: a ${what} finer than that is an error, not rounded.`);
|
|
183
196
|
switch (tag) {
|
|
184
197
|
case "amount": {
|
package/src/index.d.mts
CHANGED
|
@@ -52,8 +52,12 @@ export declare class FormatError extends Error {
|
|
|
52
52
|
export interface AmountOptions {
|
|
53
53
|
decimals: number | bigint | string;
|
|
54
54
|
symbol?: string;
|
|
55
|
-
/** The Candid type, for the sign and range (default `nat`). */
|
|
55
|
+
/** The Candid type, or an EVM ABI integer type (`uint256`), for the sign and range (default `nat`). */
|
|
56
56
|
candid?: string;
|
|
57
|
+
/** Display only: a value at or above it (base units) displays as `message` and the symbol, "Unlimited USDC". */
|
|
58
|
+
threshold?: string | bigint;
|
|
59
|
+
/** What a value at or above `threshold` displays as (default "Unlimited"); needs `threshold`. */
|
|
60
|
+
message?: string;
|
|
57
61
|
}
|
|
58
62
|
/** "1.5" or "1.5 ICP" to base units as a decimal string. Excess precision throws; nothing is rounded. */
|
|
59
63
|
export function parseAmount(input: string, options: AmountOptions): string;
|
|
@@ -111,6 +115,7 @@ export type ParsedValue = string | Account | Uint8Array;
|
|
|
111
115
|
/** One position's formatter: `formatterFor` returns it with the tag's arguments resolved. */
|
|
112
116
|
export interface Formatter {
|
|
113
117
|
readonly tag: ValueTag;
|
|
118
|
+
/** The position's Candid type, or its EVM ABI type (`uint256`, `bytes32`) for an EVM position. */
|
|
114
119
|
readonly candid: string;
|
|
115
120
|
readonly args: Readonly<Record<string, unknown>>;
|
|
116
121
|
/** Throws FormatError. */
|
|
@@ -122,9 +127,9 @@ export interface Formatter {
|
|
|
122
127
|
describe(): Description;
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
/** A position of a resolved form, as far as formatting reads it. */
|
|
126
|
-
export
|
|
127
|
-
|
|
130
|
+
/** A position of a resolved form, as far as formatting reads it: `candid`, or `abi` for an EVM contract's (spec section 15). */
|
|
131
|
+
export type FormatNode = FormatNodeFields & ({ candid: string } | { abi: string; candid?: undefined });
|
|
132
|
+
interface FormatNodeFields {
|
|
128
133
|
tags?: Record<string, { args?: Record<string, unknown> } | undefined>;
|
|
129
134
|
[key: string]: unknown;
|
|
130
135
|
}
|
package/src/time.mjs
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// millisecond precision.
|
|
11
11
|
|
|
12
12
|
import { CODES, fail } from "./errors.mjs";
|
|
13
|
-
import { checkRange, intType, requireString, toBigInt } from "./util.mjs";
|
|
13
|
+
import { ABI_INT_TYPES, checkRange, intType, requireString, toBigInt, typeWord } from "./util.mjs";
|
|
14
14
|
|
|
15
|
-
export const TIME_TYPES = ["nat", "int", "nat32", "nat64", "int64"];
|
|
15
|
+
export const TIME_TYPES = ["nat", "int", "nat32", "nat64", "int64", ...ABI_INT_TYPES.filter((t) => (t.startsWith("u") ? Number(t.slice(4)) >= 32 : Number(t.slice(3)) > 64))];
|
|
16
16
|
|
|
17
17
|
const NS_PER = Object.freeze({ ns: 1n, us: 1_000n, ms: 1_000_000n, s: 1_000_000_000n });
|
|
18
18
|
const NS_PER_S = 1_000_000_000n;
|
|
@@ -227,7 +227,7 @@ export function parseDuration(input, { unit, candid = "nat64" } = {}) {
|
|
|
227
227
|
const type = intType(candid, TIME_TYPES, "duration");
|
|
228
228
|
const text = requireString(input, "duration").trim();
|
|
229
229
|
const { negative, ns } = parseDurationNs(text, "a duration");
|
|
230
|
-
if (negative && ns !== 0n && !type.signed) fail(CODES.NEGATIVE, `negative durations are not allowed here (the
|
|
230
|
+
if (negative && ns !== 0n && !type.signed) fail(CODES.NEGATIVE, `negative durations are not allowed here (the ${typeWord(candid)} ${candid} is unsigned)`);
|
|
231
231
|
const v = nsToUnit(negative ? -ns : ns, unit);
|
|
232
232
|
checkRange(v, type, "duration");
|
|
233
233
|
return v.toString();
|
package/src/util.mjs
CHANGED
|
@@ -5,7 +5,11 @@ export const MAX_INPUT_CHARS = 1024;
|
|
|
5
5
|
|
|
6
6
|
const DEC_INT = /^-?(?:0|[1-9][0-9]*)$/;
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The integer types the value tags apply to, with their inclusive bounds (null = unbounded): Candid's, then the EVM ABI's
|
|
10
|
+
* uint8 to uint256 and int8 to int256 (spec section 15). int8 to int64 are both Candid and ABI types, with the same
|
|
11
|
+
* bounds; the others carry `abi: true`, which only changes the wording of messages.
|
|
12
|
+
*/
|
|
9
13
|
export const INT_TYPES = Object.freeze({
|
|
10
14
|
nat: { signed: false, min: 0n, max: null },
|
|
11
15
|
nat8: { signed: false, min: 0n, max: 2n ** 8n - 1n },
|
|
@@ -17,12 +21,34 @@ export const INT_TYPES = Object.freeze({
|
|
|
17
21
|
int16: { signed: true, min: -(2n ** 15n), max: 2n ** 15n - 1n },
|
|
18
22
|
int32: { signed: true, min: -(2n ** 31n), max: 2n ** 31n - 1n },
|
|
19
23
|
int64: { signed: true, min: -(2n ** 63n), max: 2n ** 63n - 1n },
|
|
24
|
+
...Object.fromEntries(
|
|
25
|
+
Array.from({ length: 32 }, (_, i) => BigInt(8 * (i + 1))).flatMap((b) => [
|
|
26
|
+
[`uint${b}`, { signed: false, min: 0n, max: 2n ** b - 1n, abi: true }],
|
|
27
|
+
...(b <= 64n && (b & (b - 1n)) === 0n ? [] : [[`int${b}`, { signed: true, min: -(2n ** (b - 1n)), max: 2n ** (b - 1n) - 1n, abi: true }]]),
|
|
28
|
+
]),
|
|
29
|
+
),
|
|
20
30
|
});
|
|
21
31
|
|
|
32
|
+
/** The EVM ABI integer types (uint8 to uint256, int8 to int256), in that order. */
|
|
33
|
+
export const ABI_INT_TYPES = Object.freeze(Array.from({ length: 32 }, (_, i) => 8 * (i + 1)).flatMap((b) => [`uint${b}`, `int${b}`]));
|
|
34
|
+
|
|
35
|
+
/** How a message names the type: "ABI type" for the EVM-only integer types, "Candid type" otherwise. */
|
|
36
|
+
export const typeWord = (name) => (INT_TYPES[name]?.abi ? "ABI type" : "Candid type");
|
|
37
|
+
|
|
38
|
+
/** A list of types for a message: the Candid types by name, the ABI's integer and byte families by range. */
|
|
39
|
+
export function listTypes(types) {
|
|
40
|
+
const candid = types.filter((t) => !INT_TYPES[t]?.abi && !/^bytes\d*$/.test(t));
|
|
41
|
+
const abi = [];
|
|
42
|
+
if (types.includes("uint8")) abi.push("uint8 to uint256", "int8 to int256");
|
|
43
|
+
else if (types.includes("uint32")) abi.push("uint32 to uint256", "int72 to int256");
|
|
44
|
+
if (types.includes("bytes")) abi.push("bytes", "bytes1 to bytes32");
|
|
45
|
+
return [...candid, ...abi].join(", ");
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
/** @param {string} candid @param {string[]} allowed */
|
|
23
49
|
export function intType(candid, allowed, tag) {
|
|
24
50
|
if (!allowed.includes(candid)) {
|
|
25
|
-
fail(CODES.TAG_TYPE, `the ${tag} tag applies to ${allowed
|
|
51
|
+
fail(CODES.TAG_TYPE, `the ${tag} tag applies to ${listTypes(allowed)}, not ${candid}`);
|
|
26
52
|
}
|
|
27
53
|
return INT_TYPES[candid];
|
|
28
54
|
}
|
|
@@ -53,9 +79,10 @@ export function toBigInt(value, what = "value") {
|
|
|
53
79
|
}
|
|
54
80
|
|
|
55
81
|
export function checkRange(v, type, what) {
|
|
56
|
-
|
|
57
|
-
if (type.
|
|
58
|
-
if (type.
|
|
82
|
+
const word = type.abi ? "ABI type" : "Candid type";
|
|
83
|
+
if (!type.signed && v < 0n) fail(CODES.RANGE, `${what} ${v} is negative but the ${word} is unsigned`);
|
|
84
|
+
if (type.min !== null && v < type.min) fail(CODES.RANGE, `${what} ${v} is below the minimum ${type.min} of the ${word}`);
|
|
85
|
+
if (type.max !== null && v > type.max) fail(CODES.RANGE, `${what} ${v} is above the maximum ${type.max} of the ${word}`);
|
|
59
86
|
}
|
|
60
87
|
|
|
61
88
|
// ---------------------------------------------------------------- bytes helpers
|