@polkadot-apps/utils 0.2.1

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/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @polkadot-apps/utils
2
+
3
+ Encoding utilities and token formatting for the `@polkadot-apps` ecosystem.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @polkadot-apps/utils
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ### Encoding
14
+
15
+ General-purpose byte encoding/decoding functions for working with hex strings, UTF-8 text, and byte arrays.
16
+
17
+ ```typescript
18
+ import { bytesToHex, hexToBytes, utf8ToBytes, concatBytes } from "@polkadot-apps/utils";
19
+
20
+ const hex = bytesToHex(new Uint8Array([0xab, 0xcd])); // "abcd"
21
+ const bytes = hexToBytes("abcd"); // Uint8Array [0xab, 0xcd]
22
+ const text = utf8ToBytes("hello"); // Uint8Array [104, 101, 108, 108, 111]
23
+ const combined = concatBytes(header, payload);
24
+ ```
25
+
26
+ ### Token formatting
27
+
28
+ Convert between raw planck values (the smallest indivisible token unit on Substrate chains) and human-readable decimal strings.
29
+
30
+ ```typescript
31
+ import { formatPlanck, parseToPlanck } from "@polkadot-apps/utils";
32
+
33
+ // Format planck to human-readable (default: 10 decimals for DOT)
34
+ formatPlanck(10_000_000_000n); // "1.0"
35
+ formatPlanck(15_000_000_000n); // "1.5"
36
+ formatPlanck(12_345_678_900n); // "1.23456789"
37
+ formatPlanck(0n); // "0.0"
38
+
39
+ // Parse human-readable to planck
40
+ parseToPlanck("1.5"); // 15_000_000_000n
41
+ parseToPlanck("100"); // 1_000_000_000_000n
42
+
43
+ // Custom decimals for other chains
44
+ formatPlanck(1_000_000_000_000n, 12); // "1.0"
45
+ parseToPlanck("1.0", 12); // 1_000_000_000_000n
46
+ ```
47
+
48
+ ## API
49
+
50
+ ### Encoding
51
+
52
+ | Function | Signature | Returns |
53
+ |---|---|---|
54
+ | `bytesToHex` | `(bytes: Uint8Array)` | `string` (lowercase, no `0x` prefix) |
55
+ | `hexToBytes` | `(hex: string)` | `Uint8Array` (no `0x` prefix expected) |
56
+ | `utf8ToBytes` | `(str: string)` | `Uint8Array` |
57
+ | `concatBytes` | `(...arrays: Uint8Array[])` | `Uint8Array` |
58
+
59
+ ### Token formatting
60
+
61
+ | Function | Signature | Returns |
62
+ |---|---|---|
63
+ | `formatPlanck` | `(planck: bigint, decimals?: number)` | `string` |
64
+ | `parseToPlanck` | `(amount: string, decimals?: number)` | `bigint` |
65
+
66
+ **`formatPlanck(planck, decimals = 10)`**
67
+
68
+ Convert a planck bigint to a human-readable decimal string. Trailing zeros are trimmed but at least one fractional digit is always shown (e.g. `"1.0"`, not `"1"`).
69
+
70
+ - Throws `RangeError` if `planck < 0n`.
71
+ - Throws `RangeError` if `decimals` is not a non-negative integer.
72
+
73
+ **`parseToPlanck(amount, decimals = 10)`**
74
+
75
+ Parse a decimal string into its planck bigint representation. If the fractional part exceeds `decimals`, excess digits are truncated with a warning.
76
+
77
+ - Throws `Error` if `amount` is empty or contains invalid characters.
78
+ - Throws `RangeError` if `amount` is negative or `decimals` is invalid.
79
+
80
+ ## Common mistakes
81
+
82
+ - **Passing a `0x`-prefixed string to `hexToBytes`.** The `@noble/hashes` implementation expects raw hex without a prefix. Strip it first: `hexToBytes(hex.slice(2))`.
83
+ - **Using `formatPlanck` with the wrong `decimals` for a chain.** DOT uses 10, KSM uses 12, many parachains use 18. Always check the chain's token metadata.
84
+ - **Assuming `parseToPlanck` rounds excess decimals.** It truncates, not rounds. `parseToPlanck("1.999999999999", 10)` gives the same result as `parseToPlanck("1.9999999999", 10)`.
85
+
86
+ ## License
87
+
88
+ Apache-2.0
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Convert a `Uint8Array` to its lowercase hexadecimal string representation.
3
+ *
4
+ * @param bytes - The bytes to encode.
5
+ * @returns Hex string (no `0x` prefix).
6
+ */
7
+ export { bytesToHex } from "@noble/hashes/utils.js";
8
+ /**
9
+ * Decode a hexadecimal string into a `Uint8Array`.
10
+ *
11
+ * @param hex - Hex string to decode (no `0x` prefix expected).
12
+ * @returns The decoded bytes.
13
+ */
14
+ export { hexToBytes } from "@noble/hashes/utils.js";
15
+ /**
16
+ * Encode a UTF-8 string into a `Uint8Array`.
17
+ *
18
+ * @param str - The string to encode.
19
+ * @returns UTF-8 encoded bytes.
20
+ */
21
+ export { utf8ToBytes } from "@noble/hashes/utils.js";
22
+ /**
23
+ * Concatenate multiple `Uint8Array` instances into a single `Uint8Array`.
24
+ *
25
+ * @param arrays - The byte arrays to concatenate.
26
+ * @returns A new `Uint8Array` containing all input bytes in order.
27
+ */
28
+ export { concatBytes } from "@noble/hashes/utils.js";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Convert a `Uint8Array` to its lowercase hexadecimal string representation.
3
+ *
4
+ * @param bytes - The bytes to encode.
5
+ * @returns Hex string (no `0x` prefix).
6
+ */
7
+ export { bytesToHex } from "@noble/hashes/utils.js";
8
+ /**
9
+ * Decode a hexadecimal string into a `Uint8Array`.
10
+ *
11
+ * @param hex - Hex string to decode (no `0x` prefix expected).
12
+ * @returns The decoded bytes.
13
+ */
14
+ export { hexToBytes } from "@noble/hashes/utils.js";
15
+ /**
16
+ * Encode a UTF-8 string into a `Uint8Array`.
17
+ *
18
+ * @param str - The string to encode.
19
+ * @returns UTF-8 encoded bytes.
20
+ */
21
+ export { utf8ToBytes } from "@noble/hashes/utils.js";
22
+ /**
23
+ * Concatenate multiple `Uint8Array` instances into a single `Uint8Array`.
24
+ *
25
+ * @param arrays - The byte arrays to concatenate.
26
+ * @returns A new `Uint8Array` containing all input bytes in order.
27
+ */
28
+ export { concatBytes } from "@noble/hashes/utils.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @polkadot-apps/utils — Encoding utilities and token formatting for the Polkadot app ecosystem.
3
+ *
4
+ * Provides general-purpose byte encoding/decoding (`bytesToHex`, `hexToBytes`, `utf8ToBytes`,
5
+ * `concatBytes`) and Substrate token formatting (`formatPlanck`, `parseToPlanck`).
6
+ * All functions are synchronous and framework-agnostic.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ export * from "./encoding.js";
11
+ export * from "./planck.js";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @polkadot-apps/utils — Encoding utilities and token formatting for the Polkadot app ecosystem.
3
+ *
4
+ * Provides general-purpose byte encoding/decoding (`bytesToHex`, `hexToBytes`, `utf8ToBytes`,
5
+ * `concatBytes`) and Substrate token formatting (`formatPlanck`, `parseToPlanck`).
6
+ * All functions are synchronous and framework-agnostic.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ export * from "./encoding.js";
11
+ export * from "./planck.js";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Convert a planck (smallest indivisible token unit) value to a human-readable decimal string.
3
+ *
4
+ * Substrate chains store all token amounts as integer planck values. This function
5
+ * converts them to human-readable form (e.g. `10_000_000_000n` → `"1.0"` for DOT
6
+ * with 10 decimals). Trailing zeros are trimmed but at least one fractional digit
7
+ * is always shown.
8
+ *
9
+ * @param planck - The raw planck value as a bigint. Must be non-negative.
10
+ * @param decimals - Number of decimal places for the token (default: 10 for DOT).
11
+ * @returns A decimal string representation (e.g. `"1.5"`, `"0.0001"`).
12
+ * @throws {RangeError} If `planck` is negative or `decimals` is invalid.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { formatPlanck } from "@polkadot-apps/utils";
17
+ *
18
+ * formatPlanck(10_000_000_000n); // "1.0" (10 decimals, DOT default)
19
+ * formatPlanck(15_000_000_000n); // "1.5"
20
+ * formatPlanck(1_000_000_000_000n, 12); // "1.0" (12 decimals, e.g. Polkadot relay)
21
+ * formatPlanck(0n); // "0.0"
22
+ * ```
23
+ */
24
+ export declare function formatPlanck(planck: bigint, decimals?: number): string;
25
+ /**
26
+ * Parse a human-readable decimal token amount into its planck (smallest unit) representation.
27
+ *
28
+ * Converts a string like `"1.5"` into the corresponding bigint planck value
29
+ * (e.g. `15_000_000_000n` for 10 decimals). If the input has more fractional
30
+ * digits than `decimals`, excess digits are silently truncated with a warning log.
31
+ *
32
+ * @param amount - A non-negative decimal string (e.g. `"1.5"`, `"100"`, `"0.001"`).
33
+ * @param decimals - Number of decimal places for the token (default: 10 for DOT).
34
+ * @returns The planck value as a bigint.
35
+ * @throws {Error} If `amount` is empty, negative, or contains invalid characters.
36
+ * @throws {RangeError} If `decimals` is invalid.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { parseToPlanck } from "@polkadot-apps/utils";
41
+ *
42
+ * parseToPlanck("1.5"); // 15_000_000_000n (10 decimals, DOT default)
43
+ * parseToPlanck("100"); // 1_000_000_000_000n
44
+ * parseToPlanck("0.001", 12); // 1_000_000_000n (12 decimals)
45
+ * ```
46
+ */
47
+ export declare function parseToPlanck(amount: string, decimals?: number): bigint;
package/dist/planck.js ADDED
@@ -0,0 +1,194 @@
1
+ import { createLogger } from "@polkadot-apps/logger";
2
+ const log = createLogger("utils");
3
+ const MAX_REASONABLE_DECIMALS = 30;
4
+ /**
5
+ * Validate that `decimals` is a non-negative integer.
6
+ *
7
+ * @param decimals - The token decimal count to validate.
8
+ * @throws {RangeError} If `decimals` is negative, fractional, or not a safe integer.
9
+ */
10
+ function validateDecimals(decimals) {
11
+ if (!Number.isInteger(decimals) || decimals < 0) {
12
+ throw new RangeError(`decimals must be a non-negative integer, got ${decimals}`);
13
+ }
14
+ if (decimals > MAX_REASONABLE_DECIMALS) {
15
+ log.warn("Unusually large decimals value — possible bug", { decimals });
16
+ }
17
+ }
18
+ /**
19
+ * Convert a planck (smallest indivisible token unit) value to a human-readable decimal string.
20
+ *
21
+ * Substrate chains store all token amounts as integer planck values. This function
22
+ * converts them to human-readable form (e.g. `10_000_000_000n` → `"1.0"` for DOT
23
+ * with 10 decimals). Trailing zeros are trimmed but at least one fractional digit
24
+ * is always shown.
25
+ *
26
+ * @param planck - The raw planck value as a bigint. Must be non-negative.
27
+ * @param decimals - Number of decimal places for the token (default: 10 for DOT).
28
+ * @returns A decimal string representation (e.g. `"1.5"`, `"0.0001"`).
29
+ * @throws {RangeError} If `planck` is negative or `decimals` is invalid.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { formatPlanck } from "@polkadot-apps/utils";
34
+ *
35
+ * formatPlanck(10_000_000_000n); // "1.0" (10 decimals, DOT default)
36
+ * formatPlanck(15_000_000_000n); // "1.5"
37
+ * formatPlanck(1_000_000_000_000n, 12); // "1.0" (12 decimals, e.g. Polkadot relay)
38
+ * formatPlanck(0n); // "0.0"
39
+ * ```
40
+ */
41
+ export function formatPlanck(planck, decimals = 10) {
42
+ validateDecimals(decimals);
43
+ if (planck < 0n) {
44
+ throw new RangeError(`planck must be non-negative, got ${planck}`);
45
+ }
46
+ if (decimals === 0) {
47
+ return `${planck}.0`;
48
+ }
49
+ const divisor = 10n ** BigInt(decimals);
50
+ const whole = planck / divisor;
51
+ const remainder = planck % divisor;
52
+ const fractionStr = remainder.toString().padStart(decimals, "0");
53
+ // Trim trailing zeros but keep at least 1 fractional digit
54
+ const trimmed = fractionStr.replace(/0+$/, "") || "0";
55
+ return `${whole}.${trimmed}`;
56
+ }
57
+ /**
58
+ * Parse a human-readable decimal token amount into its planck (smallest unit) representation.
59
+ *
60
+ * Converts a string like `"1.5"` into the corresponding bigint planck value
61
+ * (e.g. `15_000_000_000n` for 10 decimals). If the input has more fractional
62
+ * digits than `decimals`, excess digits are silently truncated with a warning log.
63
+ *
64
+ * @param amount - A non-negative decimal string (e.g. `"1.5"`, `"100"`, `"0.001"`).
65
+ * @param decimals - Number of decimal places for the token (default: 10 for DOT).
66
+ * @returns The planck value as a bigint.
67
+ * @throws {Error} If `amount` is empty, negative, or contains invalid characters.
68
+ * @throws {RangeError} If `decimals` is invalid.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * import { parseToPlanck } from "@polkadot-apps/utils";
73
+ *
74
+ * parseToPlanck("1.5"); // 15_000_000_000n (10 decimals, DOT default)
75
+ * parseToPlanck("100"); // 1_000_000_000_000n
76
+ * parseToPlanck("0.001", 12); // 1_000_000_000n (12 decimals)
77
+ * ```
78
+ */
79
+ export function parseToPlanck(amount, decimals = 10) {
80
+ validateDecimals(decimals);
81
+ if (amount === "") {
82
+ throw new Error("amount must not be empty");
83
+ }
84
+ if (amount.startsWith("-")) {
85
+ throw new RangeError(`amount must be non-negative, got "${amount}"`);
86
+ }
87
+ // Validate characters: only digits and at most one dot
88
+ if (!/^\d+\.?\d*$/.test(amount)) {
89
+ throw new Error(`amount contains invalid characters: "${amount}"`);
90
+ }
91
+ const parts = amount.split(".");
92
+ const wholePart = parts[0];
93
+ let fractionPart = parts[1] ?? "";
94
+ if (fractionPart.length > decimals) {
95
+ log.warn("Fractional digits exceed token decimals — truncating", {
96
+ amount,
97
+ decimals,
98
+ excessDigits: fractionPart.length - decimals,
99
+ });
100
+ fractionPart = fractionPart.slice(0, decimals);
101
+ }
102
+ const paddedFraction = fractionPart.padEnd(decimals, "0");
103
+ const whole = BigInt(wholePart) * 10n ** BigInt(decimals);
104
+ const fraction = decimals > 0 ? BigInt(paddedFraction) : 0n;
105
+ return whole + fraction;
106
+ }
107
+ if (import.meta.vitest) {
108
+ const { describe, test, expect } = import.meta.vitest;
109
+ describe("formatPlanck", () => {
110
+ test("formats 1 DOT with default 10 decimals", () => {
111
+ expect(formatPlanck(10000000000n)).toBe("1.0");
112
+ });
113
+ test("formats fractional amounts", () => {
114
+ expect(formatPlanck(15000000000n)).toBe("1.5");
115
+ expect(formatPlanck(12345678900n)).toBe("1.23456789");
116
+ });
117
+ test("formats zero", () => {
118
+ expect(formatPlanck(0n)).toBe("0.0");
119
+ });
120
+ test("formats sub-unit amounts", () => {
121
+ expect(formatPlanck(1n)).toBe("0.0000000001");
122
+ expect(formatPlanck(100n)).toBe("0.00000001");
123
+ });
124
+ test("formats large amounts", () => {
125
+ expect(formatPlanck(1000000000000000n)).toBe("100000.0");
126
+ });
127
+ test("trims trailing zeros but keeps at least one", () => {
128
+ expect(formatPlanck(20000000000n)).toBe("2.0");
129
+ expect(formatPlanck(10100000000n)).toBe("1.01");
130
+ });
131
+ test("handles custom decimals", () => {
132
+ expect(formatPlanck(1000000000000n, 12)).toBe("1.0");
133
+ expect(formatPlanck(1500000n, 6)).toBe("1.5");
134
+ });
135
+ test("handles zero decimals", () => {
136
+ expect(formatPlanck(42n, 0)).toBe("42.0");
137
+ });
138
+ test("throws on negative planck", () => {
139
+ expect(() => formatPlanck(-1n)).toThrow(RangeError);
140
+ });
141
+ test("throws on invalid decimals", () => {
142
+ expect(() => formatPlanck(0n, -1)).toThrow(RangeError);
143
+ expect(() => formatPlanck(0n, 1.5)).toThrow(RangeError);
144
+ });
145
+ });
146
+ describe("parseToPlanck", () => {
147
+ test("parses whole number", () => {
148
+ expect(parseToPlanck("1")).toBe(10000000000n);
149
+ });
150
+ test("parses fractional amount", () => {
151
+ expect(parseToPlanck("1.5")).toBe(15000000000n);
152
+ });
153
+ test("parses zero", () => {
154
+ expect(parseToPlanck("0")).toBe(0n);
155
+ expect(parseToPlanck("0.0")).toBe(0n);
156
+ });
157
+ test("parses small fractions", () => {
158
+ expect(parseToPlanck("0.0000000001")).toBe(1n);
159
+ });
160
+ test("handles custom decimals", () => {
161
+ expect(parseToPlanck("1.0", 12)).toBe(1000000000000n);
162
+ expect(parseToPlanck("1.5", 6)).toBe(1500000n);
163
+ });
164
+ test("handles zero decimals", () => {
165
+ expect(parseToPlanck("42", 0)).toBe(42n);
166
+ });
167
+ test("truncates excess fractional digits", () => {
168
+ // 10 decimals, input has 12 fractional digits → truncate last 2
169
+ expect(parseToPlanck("1.123456789012")).toBe(11234567890n);
170
+ });
171
+ test("pads short fractional parts", () => {
172
+ expect(parseToPlanck("1.5")).toBe(15000000000n);
173
+ });
174
+ test("throws on empty string", () => {
175
+ expect(() => parseToPlanck("")).toThrow("must not be empty");
176
+ });
177
+ test("throws on negative amount", () => {
178
+ expect(() => parseToPlanck("-1")).toThrow(RangeError);
179
+ });
180
+ test("throws on invalid characters", () => {
181
+ expect(() => parseToPlanck("abc")).toThrow("invalid characters");
182
+ expect(() => parseToPlanck("1.2.3")).toThrow("invalid characters");
183
+ expect(() => parseToPlanck("1e10")).toThrow("invalid characters");
184
+ });
185
+ test("throws on invalid decimals", () => {
186
+ expect(() => parseToPlanck("1", -1)).toThrow(RangeError);
187
+ });
188
+ test("round-trips with formatPlanck", () => {
189
+ const original = 12345678901n;
190
+ const formatted = formatPlanck(original);
191
+ expect(parseToPlanck(formatted)).toBe(original);
192
+ });
193
+ });
194
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@polkadot-apps/utils",
3
+ "description": "Encoding utilities and token formatting for the @polkadot-apps ecosystem",
4
+ "version": "0.2.1",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "source": "./src/index.ts",
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@noble/hashes": "^2.0.1",
24
+ "@polkadot-apps/logger": "0.1.5"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.9.3"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "clean": "rm -rf dist"
32
+ }
33
+ }