@polkadot-apps/utils 0.2.1 → 0.4.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/README.md +77 -0
- package/dist/balance.d.ts +52 -0
- package/dist/balance.js +77 -0
- package/dist/hashing.d.ts +54 -0
- package/dist/hashing.js +149 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -3
- package/dist/planck.d.ts +36 -0
- package/dist/planck.js +110 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,23 @@ const text = utf8ToBytes("hello"); // Uint8Array [104, 101, 108, 108, 111]
|
|
|
23
23
|
const combined = concatBytes(header, payload);
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
### Hashing
|
|
27
|
+
|
|
28
|
+
Deterministic 32-byte hash functions used across the Polkadot ecosystem.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { blake2b256, sha256, keccak256, bytesToHex } from "@polkadot-apps/utils";
|
|
32
|
+
|
|
33
|
+
const hash = blake2b256(new TextEncoder().encode("hello"));
|
|
34
|
+
console.log(bytesToHex(hash)); // 64-char hex string
|
|
35
|
+
|
|
36
|
+
// SHA2-256 (bulletin-deploy default)
|
|
37
|
+
const sha = sha256(data);
|
|
38
|
+
|
|
39
|
+
// Keccak-256 (Ethereum compatibility)
|
|
40
|
+
const kek = keccak256(data);
|
|
41
|
+
```
|
|
42
|
+
|
|
26
43
|
### Token formatting
|
|
27
44
|
|
|
28
45
|
Convert between raw planck values (the smallest indivisible token unit on Substrate chains) and human-readable decimal strings.
|
|
@@ -45,6 +62,37 @@ formatPlanck(1_000_000_000_000n, 12); // "1.0"
|
|
|
45
62
|
parseToPlanck("1.0", 12); // 1_000_000_000_000n
|
|
46
63
|
```
|
|
47
64
|
|
|
65
|
+
### Display formatting
|
|
66
|
+
|
|
67
|
+
Format planck values for display with locale-aware thousand separators, configurable decimal precision, and optional token symbol.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { formatBalance } from "@polkadot-apps/utils";
|
|
71
|
+
|
|
72
|
+
formatBalance(10_000_000_000n); // "1"
|
|
73
|
+
formatBalance(15_000_000_000n, { symbol: "DOT" }); // "1.5 DOT"
|
|
74
|
+
formatBalance(10_000_000_000_000n, { symbol: "DOT" }); // "1,000 DOT"
|
|
75
|
+
formatBalance(12_345_678_900n, { maxDecimals: 2 }); // "1.23"
|
|
76
|
+
formatBalance(0n, { symbol: "DOT" }); // "0 DOT"
|
|
77
|
+
|
|
78
|
+
// Custom chain decimals and locale
|
|
79
|
+
formatBalance(1_000_000_000_000n, { decimals: 12, symbol: "KSM", locale: "de-DE" });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Unlike the `Number()` approach used in some apps, `formatBalance` preserves full BigInt precision for balances of any size.
|
|
83
|
+
|
|
84
|
+
### Balance querying
|
|
85
|
+
|
|
86
|
+
Query on-chain balances with a typed convenience wrapper. Works with any PAPI typed API via structural typing — no extra dependencies.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { getBalance, formatBalance } from "@polkadot-apps/utils";
|
|
90
|
+
|
|
91
|
+
const balance = await getBalance(api.assetHub, aliceAddress);
|
|
92
|
+
console.log(formatBalance(balance.free, { symbol: "DOT" })); // "1,000.5 DOT"
|
|
93
|
+
console.log(formatBalance(balance.reserved, { symbol: "DOT" })); // "50 DOT"
|
|
94
|
+
```
|
|
95
|
+
|
|
48
96
|
## API
|
|
49
97
|
|
|
50
98
|
### Encoding
|
|
@@ -56,12 +104,27 @@ parseToPlanck("1.0", 12); // 1_000_000_000_000n
|
|
|
56
104
|
| `utf8ToBytes` | `(str: string)` | `Uint8Array` |
|
|
57
105
|
| `concatBytes` | `(...arrays: Uint8Array[])` | `Uint8Array` |
|
|
58
106
|
|
|
107
|
+
### Hashing
|
|
108
|
+
|
|
109
|
+
| Function | Signature | Returns | Description |
|
|
110
|
+
|---|---|---|---|
|
|
111
|
+
| `blake2b256` | `(data: Uint8Array)` | `Uint8Array` (32 bytes) | BLAKE2b-256 — Polkadot default |
|
|
112
|
+
| `sha256` | `(data: Uint8Array)` | `Uint8Array` (32 bytes) | SHA2-256 — bulletin-deploy default |
|
|
113
|
+
| `keccak256` | `(data: Uint8Array)` | `Uint8Array` (32 bytes) | Keccak-256 — Ethereum compatibility |
|
|
114
|
+
|
|
59
115
|
### Token formatting
|
|
60
116
|
|
|
61
117
|
| Function | Signature | Returns |
|
|
62
118
|
|---|---|---|
|
|
63
119
|
| `formatPlanck` | `(planck: bigint, decimals?: number)` | `string` |
|
|
64
120
|
| `parseToPlanck` | `(amount: string, decimals?: number)` | `bigint` |
|
|
121
|
+
| `formatBalance` | `(planck: bigint, options?: FormatBalanceOptions)` | `string` |
|
|
122
|
+
|
|
123
|
+
### Balance querying
|
|
124
|
+
|
|
125
|
+
| Function | Signature | Returns |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `getBalance` | `(api: BalanceApi, address: string)` | `Promise<AccountBalance>` |
|
|
65
128
|
|
|
66
129
|
**`formatPlanck(planck, decimals = 10)`**
|
|
67
130
|
|
|
@@ -77,11 +140,25 @@ Parse a decimal string into its planck bigint representation. If the fractional
|
|
|
77
140
|
- Throws `Error` if `amount` is empty or contains invalid characters.
|
|
78
141
|
- Throws `RangeError` if `amount` is negative or `decimals` is invalid.
|
|
79
142
|
|
|
143
|
+
**`formatBalance(planck, options?)`**
|
|
144
|
+
|
|
145
|
+
Format a planck value for display with locale-aware thousand separators. Builds on `formatPlanck` for BigInt precision.
|
|
146
|
+
|
|
147
|
+
Options: `{ decimals?: number, maxDecimals?: number, symbol?: string, locale?: string }`. Defaults: `decimals = 10`, `maxDecimals = 4`, no symbol, user's locale.
|
|
148
|
+
|
|
149
|
+
- Throws `RangeError` if `planck < 0n` or `decimals` is invalid (delegated to `formatPlanck`).
|
|
150
|
+
|
|
151
|
+
**`getBalance(api, address): Promise<AccountBalance>`**
|
|
152
|
+
|
|
153
|
+
Query the free, reserved, and frozen balances for an address. Returns `{ free: bigint, reserved: bigint, frozen: bigint }`. Uses structural typing — works with any PAPI typed API that has `System.Account`.
|
|
154
|
+
|
|
80
155
|
## Common mistakes
|
|
81
156
|
|
|
82
157
|
- **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
158
|
- **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
159
|
- **Assuming `parseToPlanck` rounds excess decimals.** It truncates, not rounds. `parseToPlanck("1.999999999999", 10)` gives the same result as `parseToPlanck("1.9999999999", 10)`.
|
|
160
|
+
- **Using `Number()` to format large balances.** `Number(raw) / 10**decimals` loses precision for values > 2^53 planck (~900 DOT). Use `formatBalance` which preserves full BigInt precision.
|
|
161
|
+
- **Passing the ChainAPI wrapper to `getBalance`.** Pass the chain-specific TypedApi (e.g., `api.assetHub`), not the multi-chain `ChainAPI` object.
|
|
85
162
|
|
|
86
163
|
## License
|
|
87
164
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Balance breakdown from a Substrate `System.Account` query. */
|
|
2
|
+
export interface AccountBalance {
|
|
3
|
+
/** Available (transferable) balance in planck. */
|
|
4
|
+
free: bigint;
|
|
5
|
+
/** Reserved (locked by governance, staking, etc.) balance in planck. */
|
|
6
|
+
reserved: bigint;
|
|
7
|
+
/** Frozen (non-transferable but still counted) balance in planck. */
|
|
8
|
+
frozen: bigint;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Minimal structural type for a PAPI typed API with `System.Account`.
|
|
12
|
+
*
|
|
13
|
+
* Structural so it works with any chain that has the System pallet, without
|
|
14
|
+
* importing chain-specific descriptors.
|
|
15
|
+
*/
|
|
16
|
+
export interface BalanceApi {
|
|
17
|
+
query: {
|
|
18
|
+
System: {
|
|
19
|
+
Account: {
|
|
20
|
+
getValue(address: string): Promise<{
|
|
21
|
+
data: {
|
|
22
|
+
free: bigint;
|
|
23
|
+
reserved: bigint;
|
|
24
|
+
frozen: bigint;
|
|
25
|
+
};
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Query the free, reserved, and frozen balances for an on-chain address.
|
|
33
|
+
*
|
|
34
|
+
* Thin typed wrapper around `System.Account.getValue` that returns a clean
|
|
35
|
+
* {@link AccountBalance} object. Uses structural typing so it works with any
|
|
36
|
+
* PAPI typed API that has the System pallet — no chain-specific imports needed.
|
|
37
|
+
*
|
|
38
|
+
* @param api - A PAPI typed API with `query.System.Account`. Pass the chain-specific
|
|
39
|
+
* API (e.g., `api.assetHub`), not the multi-chain `ChainAPI` wrapper.
|
|
40
|
+
* @param address - The SS58 address to query.
|
|
41
|
+
* @returns The account's balance breakdown.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { getBalance } from "@polkadot-apps/utils";
|
|
46
|
+
* import { formatBalance } from "@polkadot-apps/utils";
|
|
47
|
+
*
|
|
48
|
+
* const balance = await getBalance(api.assetHub, aliceAddress);
|
|
49
|
+
* console.log(formatBalance(balance.free, { symbol: "DOT" })); // "1,000.5 DOT"
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function getBalance(api: BalanceApi, address: string): Promise<AccountBalance>;
|
package/dist/balance.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query the free, reserved, and frozen balances for an on-chain address.
|
|
3
|
+
*
|
|
4
|
+
* Thin typed wrapper around `System.Account.getValue` that returns a clean
|
|
5
|
+
* {@link AccountBalance} object. Uses structural typing so it works with any
|
|
6
|
+
* PAPI typed API that has the System pallet — no chain-specific imports needed.
|
|
7
|
+
*
|
|
8
|
+
* @param api - A PAPI typed API with `query.System.Account`. Pass the chain-specific
|
|
9
|
+
* API (e.g., `api.assetHub`), not the multi-chain `ChainAPI` wrapper.
|
|
10
|
+
* @param address - The SS58 address to query.
|
|
11
|
+
* @returns The account's balance breakdown.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { getBalance } from "@polkadot-apps/utils";
|
|
16
|
+
* import { formatBalance } from "@polkadot-apps/utils";
|
|
17
|
+
*
|
|
18
|
+
* const balance = await getBalance(api.assetHub, aliceAddress);
|
|
19
|
+
* console.log(formatBalance(balance.free, { symbol: "DOT" })); // "1,000.5 DOT"
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export async function getBalance(api, address) {
|
|
23
|
+
const account = await api.query.System.Account.getValue(address);
|
|
24
|
+
return {
|
|
25
|
+
free: account.data.free,
|
|
26
|
+
reserved: account.data.reserved,
|
|
27
|
+
frozen: account.data.frozen,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (import.meta.vitest) {
|
|
31
|
+
const { describe, test, expect } = import.meta.vitest;
|
|
32
|
+
function createMockApi(data) {
|
|
33
|
+
return {
|
|
34
|
+
query: {
|
|
35
|
+
System: {
|
|
36
|
+
Account: {
|
|
37
|
+
getValue: async () => ({ data }),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
describe("getBalance", () => {
|
|
44
|
+
test("returns correct AccountBalance from API", async () => {
|
|
45
|
+
const api = createMockApi({
|
|
46
|
+
free: 10000000000n,
|
|
47
|
+
reserved: 5000000000n,
|
|
48
|
+
frozen: 1000000000n,
|
|
49
|
+
});
|
|
50
|
+
const balance = await getBalance(api, "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY");
|
|
51
|
+
expect(balance.free).toBe(10000000000n);
|
|
52
|
+
expect(balance.reserved).toBe(5000000000n);
|
|
53
|
+
expect(balance.frozen).toBe(1000000000n);
|
|
54
|
+
});
|
|
55
|
+
test("propagates errors from getValue", async () => {
|
|
56
|
+
const api = {
|
|
57
|
+
query: {
|
|
58
|
+
System: {
|
|
59
|
+
Account: {
|
|
60
|
+
getValue: async () => {
|
|
61
|
+
throw new Error("RPC connection failed");
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
await expect(getBalance(api, "5GrwvaEF...")).rejects.toThrow("RPC connection failed");
|
|
68
|
+
});
|
|
69
|
+
test("works with zero balances", async () => {
|
|
70
|
+
const api = createMockApi({ free: 0n, reserved: 0n, frozen: 0n });
|
|
71
|
+
const balance = await getBalance(api, "5GrwvaEF...");
|
|
72
|
+
expect(balance.free).toBe(0n);
|
|
73
|
+
expect(balance.reserved).toBe(0n);
|
|
74
|
+
expect(balance.frozen).toBe(0n);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute a 32-byte BLAKE2b-256 hash.
|
|
3
|
+
*
|
|
4
|
+
* This is the default hash algorithm used by the Polkadot ecosystem and the
|
|
5
|
+
* Bulletin Chain. Deterministic: same input always produces the same output.
|
|
6
|
+
*
|
|
7
|
+
* @param data - Arbitrary bytes to hash.
|
|
8
|
+
* @returns 32-byte BLAKE2b-256 digest.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { blake2b256, bytesToHex } from "@polkadot-apps/utils";
|
|
13
|
+
*
|
|
14
|
+
* const hash = blake2b256(new TextEncoder().encode("hello"));
|
|
15
|
+
* console.log(bytesToHex(hash)); // 64-char hex string
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function blake2b256(data: Uint8Array): Uint8Array;
|
|
19
|
+
/**
|
|
20
|
+
* Compute a 32-byte SHA2-256 hash.
|
|
21
|
+
*
|
|
22
|
+
* Used by bulletin-deploy and supported by the Bulletin Chain as an
|
|
23
|
+
* alternative hashing algorithm.
|
|
24
|
+
*
|
|
25
|
+
* @param data - Arbitrary bytes to hash.
|
|
26
|
+
* @returns 32-byte SHA2-256 digest.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { sha256, bytesToHex } from "@polkadot-apps/utils";
|
|
31
|
+
*
|
|
32
|
+
* const hash = sha256(new TextEncoder().encode("hello"));
|
|
33
|
+
* console.log(bytesToHex(hash)); // 64-char hex string
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function sha256(data: Uint8Array): Uint8Array;
|
|
37
|
+
/**
|
|
38
|
+
* Compute a 32-byte Keccak-256 hash.
|
|
39
|
+
*
|
|
40
|
+
* Used for Ethereum-compatible operations (address derivation, EVM function
|
|
41
|
+
* selectors) and supported by the Bulletin Chain for cross-chain compatibility.
|
|
42
|
+
*
|
|
43
|
+
* @param data - Arbitrary bytes to hash.
|
|
44
|
+
* @returns 32-byte Keccak-256 digest.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { keccak256, bytesToHex } from "@polkadot-apps/utils";
|
|
49
|
+
*
|
|
50
|
+
* const hash = keccak256(new TextEncoder().encode("hello"));
|
|
51
|
+
* console.log(bytesToHex(hash)); // 64-char hex string
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function keccak256(data: Uint8Array): Uint8Array;
|
package/dist/hashing.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { blake2b } from "@noble/hashes/blake2.js";
|
|
2
|
+
import { sha256 as _sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
+
import { bytesToHex as _bytesToHex } from "@noble/hashes/utils.js";
|
|
4
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
5
|
+
/**
|
|
6
|
+
* Compute a 32-byte BLAKE2b-256 hash.
|
|
7
|
+
*
|
|
8
|
+
* This is the default hash algorithm used by the Polkadot ecosystem and the
|
|
9
|
+
* Bulletin Chain. Deterministic: same input always produces the same output.
|
|
10
|
+
*
|
|
11
|
+
* @param data - Arbitrary bytes to hash.
|
|
12
|
+
* @returns 32-byte BLAKE2b-256 digest.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { blake2b256, bytesToHex } from "@polkadot-apps/utils";
|
|
17
|
+
*
|
|
18
|
+
* const hash = blake2b256(new TextEncoder().encode("hello"));
|
|
19
|
+
* console.log(bytesToHex(hash)); // 64-char hex string
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function blake2b256(data) {
|
|
23
|
+
return blake2b(data, { dkLen: 32 });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Compute a 32-byte SHA2-256 hash.
|
|
27
|
+
*
|
|
28
|
+
* Used by bulletin-deploy and supported by the Bulletin Chain as an
|
|
29
|
+
* alternative hashing algorithm.
|
|
30
|
+
*
|
|
31
|
+
* @param data - Arbitrary bytes to hash.
|
|
32
|
+
* @returns 32-byte SHA2-256 digest.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { sha256, bytesToHex } from "@polkadot-apps/utils";
|
|
37
|
+
*
|
|
38
|
+
* const hash = sha256(new TextEncoder().encode("hello"));
|
|
39
|
+
* console.log(bytesToHex(hash)); // 64-char hex string
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function sha256(data) {
|
|
43
|
+
return _sha256(data);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Compute a 32-byte Keccak-256 hash.
|
|
47
|
+
*
|
|
48
|
+
* Used for Ethereum-compatible operations (address derivation, EVM function
|
|
49
|
+
* selectors) and supported by the Bulletin Chain for cross-chain compatibility.
|
|
50
|
+
*
|
|
51
|
+
* @param data - Arbitrary bytes to hash.
|
|
52
|
+
* @returns 32-byte Keccak-256 digest.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { keccak256, bytesToHex } from "@polkadot-apps/utils";
|
|
57
|
+
*
|
|
58
|
+
* const hash = keccak256(new TextEncoder().encode("hello"));
|
|
59
|
+
* console.log(bytesToHex(hash)); // 64-char hex string
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function keccak256(data) {
|
|
63
|
+
return keccak_256(data);
|
|
64
|
+
}
|
|
65
|
+
if (import.meta.vitest) {
|
|
66
|
+
const { describe, test, expect } = import.meta.vitest;
|
|
67
|
+
describe("blake2b256", () => {
|
|
68
|
+
test("produces a 32-byte hash", () => {
|
|
69
|
+
const hash = blake2b256(new TextEncoder().encode("hello"));
|
|
70
|
+
expect(hash).toBeInstanceOf(Uint8Array);
|
|
71
|
+
expect(hash.length).toBe(32);
|
|
72
|
+
});
|
|
73
|
+
test("deterministic — same input, same output", () => {
|
|
74
|
+
const data = new TextEncoder().encode("test");
|
|
75
|
+
expect(blake2b256(data)).toEqual(blake2b256(data));
|
|
76
|
+
});
|
|
77
|
+
test("different inputs produce different hashes", () => {
|
|
78
|
+
const a = blake2b256(new Uint8Array([1]));
|
|
79
|
+
const b = blake2b256(new Uint8Array([2]));
|
|
80
|
+
expect(a).not.toEqual(b);
|
|
81
|
+
});
|
|
82
|
+
test("empty input produces valid 32-byte hash", () => {
|
|
83
|
+
const hash = blake2b256(new Uint8Array(0));
|
|
84
|
+
expect(hash.length).toBe(32);
|
|
85
|
+
});
|
|
86
|
+
test("matches direct @noble/hashes import", () => {
|
|
87
|
+
const data = new TextEncoder().encode("wrapper transparency check");
|
|
88
|
+
const direct = blake2b(data, { dkLen: 32 });
|
|
89
|
+
expect(blake2b256(data)).toEqual(direct);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("sha256", () => {
|
|
93
|
+
test("produces a 32-byte hash", () => {
|
|
94
|
+
const hash = sha256(new TextEncoder().encode("hello"));
|
|
95
|
+
expect(hash).toBeInstanceOf(Uint8Array);
|
|
96
|
+
expect(hash.length).toBe(32);
|
|
97
|
+
});
|
|
98
|
+
test("deterministic — same input, same output", () => {
|
|
99
|
+
const data = new TextEncoder().encode("test");
|
|
100
|
+
expect(sha256(data)).toEqual(sha256(data));
|
|
101
|
+
});
|
|
102
|
+
test("different inputs produce different hashes", () => {
|
|
103
|
+
const a = sha256(new Uint8Array([1]));
|
|
104
|
+
const b = sha256(new Uint8Array([2]));
|
|
105
|
+
expect(a).not.toEqual(b);
|
|
106
|
+
});
|
|
107
|
+
test("empty input produces valid 32-byte hash", () => {
|
|
108
|
+
const hash = sha256(new Uint8Array(0));
|
|
109
|
+
expect(hash.length).toBe(32);
|
|
110
|
+
});
|
|
111
|
+
test("differs from blake2b256 for same input", () => {
|
|
112
|
+
const data = new TextEncoder().encode("cross-check");
|
|
113
|
+
expect(sha256(data)).not.toEqual(blake2b256(data));
|
|
114
|
+
});
|
|
115
|
+
test("matches known SHA-256 test vector", () => {
|
|
116
|
+
const hash = sha256(new TextEncoder().encode("hello"));
|
|
117
|
+
expect(_bytesToHex(hash)).toBe("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe("keccak256", () => {
|
|
121
|
+
test("produces a 32-byte hash", () => {
|
|
122
|
+
const hash = keccak256(new TextEncoder().encode("hello"));
|
|
123
|
+
expect(hash).toBeInstanceOf(Uint8Array);
|
|
124
|
+
expect(hash.length).toBe(32);
|
|
125
|
+
});
|
|
126
|
+
test("deterministic — same input, same output", () => {
|
|
127
|
+
const data = new TextEncoder().encode("test");
|
|
128
|
+
expect(keccak256(data)).toEqual(keccak256(data));
|
|
129
|
+
});
|
|
130
|
+
test("different inputs produce different hashes", () => {
|
|
131
|
+
const a = keccak256(new Uint8Array([1]));
|
|
132
|
+
const b = keccak256(new Uint8Array([2]));
|
|
133
|
+
expect(a).not.toEqual(b);
|
|
134
|
+
});
|
|
135
|
+
test("empty input produces valid 32-byte hash", () => {
|
|
136
|
+
const hash = keccak256(new Uint8Array(0));
|
|
137
|
+
expect(hash.length).toBe(32);
|
|
138
|
+
});
|
|
139
|
+
test("differs from sha256 and blake2b256 for same input", () => {
|
|
140
|
+
const data = new TextEncoder().encode("cross-check");
|
|
141
|
+
expect(keccak256(data)).not.toEqual(sha256(data));
|
|
142
|
+
expect(keccak256(data)).not.toEqual(blake2b256(data));
|
|
143
|
+
});
|
|
144
|
+
test("matches known Keccak-256 test vector", () => {
|
|
145
|
+
const hash = keccak256(new TextEncoder().encode("hello"));
|
|
146
|
+
expect(_bytesToHex(hash)).toBe("1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @polkadot-apps/utils — Encoding
|
|
2
|
+
* @polkadot-apps/utils — Encoding, hashing, token formatting, and balance querying for the Polkadot app ecosystem.
|
|
3
3
|
*
|
|
4
4
|
* Provides general-purpose byte encoding/decoding (`bytesToHex`, `hexToBytes`, `utf8ToBytes`,
|
|
5
|
-
* `concatBytes`)
|
|
6
|
-
*
|
|
5
|
+
* `concatBytes`), 32-byte hash functions (`blake2b256`, `sha256`, `keccak256`),
|
|
6
|
+
* Substrate token formatting (`formatPlanck`, `parseToPlanck`, `formatBalance`),
|
|
7
|
+
* and typed balance queries (`getBalance`).
|
|
8
|
+
* All functions are framework-agnostic.
|
|
7
9
|
*
|
|
8
10
|
* @packageDocumentation
|
|
9
11
|
*/
|
|
10
12
|
export * from "./encoding.js";
|
|
13
|
+
export * from "./hashing.js";
|
|
11
14
|
export * from "./planck.js";
|
|
15
|
+
export * from "./balance.js";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @polkadot-apps/utils — Encoding
|
|
2
|
+
* @polkadot-apps/utils — Encoding, hashing, token formatting, and balance querying for the Polkadot app ecosystem.
|
|
3
3
|
*
|
|
4
4
|
* Provides general-purpose byte encoding/decoding (`bytesToHex`, `hexToBytes`, `utf8ToBytes`,
|
|
5
|
-
* `concatBytes`)
|
|
6
|
-
*
|
|
5
|
+
* `concatBytes`), 32-byte hash functions (`blake2b256`, `sha256`, `keccak256`),
|
|
6
|
+
* Substrate token formatting (`formatPlanck`, `parseToPlanck`, `formatBalance`),
|
|
7
|
+
* and typed balance queries (`getBalance`).
|
|
8
|
+
* All functions are framework-agnostic.
|
|
7
9
|
*
|
|
8
10
|
* @packageDocumentation
|
|
9
11
|
*/
|
|
10
12
|
export * from "./encoding.js";
|
|
13
|
+
export * from "./hashing.js";
|
|
11
14
|
export * from "./planck.js";
|
|
15
|
+
export * from "./balance.js";
|
package/dist/planck.d.ts
CHANGED
|
@@ -45,3 +45,39 @@ export declare function formatPlanck(planck: bigint, decimals?: number): string;
|
|
|
45
45
|
* ```
|
|
46
46
|
*/
|
|
47
47
|
export declare function parseToPlanck(amount: string, decimals?: number): bigint;
|
|
48
|
+
/** Options for {@link formatBalance}. */
|
|
49
|
+
export interface FormatBalanceOptions {
|
|
50
|
+
/** Token decimals. Default: 10 (DOT). */
|
|
51
|
+
decimals?: number;
|
|
52
|
+
/** Maximum fraction digits to display. Default: 4. */
|
|
53
|
+
maxDecimals?: number;
|
|
54
|
+
/** Token symbol to append (e.g., `"DOT"`, `"PAS"`). Omitted by default. */
|
|
55
|
+
symbol?: string;
|
|
56
|
+
/** BCP 47 locale tag for grouping and decimal separators (e.g., `"en-US"` → `","` grouping + `"."` decimal, `"de-DE"` → `"."` grouping + `","` decimal). Default: user's locale. */
|
|
57
|
+
locale?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Format a planck value for display with locale-aware thousand separators,
|
|
61
|
+
* decimal truncation, and an optional token symbol.
|
|
62
|
+
*
|
|
63
|
+
* Builds on {@link formatPlanck} for BigInt-safe conversion, then applies
|
|
64
|
+
* presentation formatting. Unlike {@link formatPlanck}, trailing `.0` is
|
|
65
|
+
* omitted — display values show `"1,000"` not `"1,000.0"`.
|
|
66
|
+
*
|
|
67
|
+
* @param planck - The raw planck value as a bigint. Must be non-negative.
|
|
68
|
+
* @param options - Formatting options.
|
|
69
|
+
* @returns A display-ready string (e.g. `"1,000.5 DOT"`).
|
|
70
|
+
* @throws {RangeError} If `planck` is negative or `decimals` is invalid (delegated to {@link formatPlanck}).
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { formatBalance } from "@polkadot-apps/utils";
|
|
75
|
+
*
|
|
76
|
+
* formatBalance(10_000_000_000n); // "1"
|
|
77
|
+
* formatBalance(15_000_000_000n, { symbol: "DOT" }); // "1.5 DOT"
|
|
78
|
+
* formatBalance(10_000_000_000_000n, { symbol: "DOT" }); // "1,000 DOT"
|
|
79
|
+
* formatBalance(12_345_678_900n, { maxDecimals: 2 }); // "1.23"
|
|
80
|
+
* formatBalance(0n, { symbol: "DOT" }); // "0 DOT"
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function formatBalance(planck: bigint, options?: FormatBalanceOptions): string;
|
package/dist/planck.js
CHANGED
|
@@ -104,6 +104,54 @@ export function parseToPlanck(amount, decimals = 10) {
|
|
|
104
104
|
const fraction = decimals > 0 ? BigInt(paddedFraction) : 0n;
|
|
105
105
|
return whole + fraction;
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Format a planck value for display with locale-aware thousand separators,
|
|
109
|
+
* decimal truncation, and an optional token symbol.
|
|
110
|
+
*
|
|
111
|
+
* Builds on {@link formatPlanck} for BigInt-safe conversion, then applies
|
|
112
|
+
* presentation formatting. Unlike {@link formatPlanck}, trailing `.0` is
|
|
113
|
+
* omitted — display values show `"1,000"` not `"1,000.0"`.
|
|
114
|
+
*
|
|
115
|
+
* @param planck - The raw planck value as a bigint. Must be non-negative.
|
|
116
|
+
* @param options - Formatting options.
|
|
117
|
+
* @returns A display-ready string (e.g. `"1,000.5 DOT"`).
|
|
118
|
+
* @throws {RangeError} If `planck` is negative or `decimals` is invalid (delegated to {@link formatPlanck}).
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* import { formatBalance } from "@polkadot-apps/utils";
|
|
123
|
+
*
|
|
124
|
+
* formatBalance(10_000_000_000n); // "1"
|
|
125
|
+
* formatBalance(15_000_000_000n, { symbol: "DOT" }); // "1.5 DOT"
|
|
126
|
+
* formatBalance(10_000_000_000_000n, { symbol: "DOT" }); // "1,000 DOT"
|
|
127
|
+
* formatBalance(12_345_678_900n, { maxDecimals: 2 }); // "1.23"
|
|
128
|
+
* formatBalance(0n, { symbol: "DOT" }); // "0 DOT"
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function formatBalance(planck, options) {
|
|
132
|
+
const decimals = options?.decimals ?? 10;
|
|
133
|
+
const maxDecimals = options?.maxDecimals ?? 4;
|
|
134
|
+
const symbol = options?.symbol;
|
|
135
|
+
const locale = options?.locale;
|
|
136
|
+
if (maxDecimals < 0 || !Number.isInteger(maxDecimals)) {
|
|
137
|
+
throw new RangeError(`maxDecimals must be a non-negative integer, got ${maxDecimals}`);
|
|
138
|
+
}
|
|
139
|
+
const raw = formatPlanck(planck, decimals);
|
|
140
|
+
const dotIndex = raw.indexOf(".");
|
|
141
|
+
const wholePart = dotIndex === -1 ? raw : raw.slice(0, dotIndex);
|
|
142
|
+
const fractionPart = dotIndex === -1 ? "" : raw.slice(dotIndex + 1);
|
|
143
|
+
const formatter = new Intl.NumberFormat(locale, { useGrouping: true });
|
|
144
|
+
// Format whole part with locale-aware grouping (BigInt overload avoids precision loss)
|
|
145
|
+
const formattedWhole = formatter.format(BigInt(wholePart));
|
|
146
|
+
// Extract the locale's decimal separator (e.g., "." for en-US, "," for de-DE)
|
|
147
|
+
const decimalSep = formatter.formatToParts(1.1).find((p) => p.type === "decimal")?.value ?? ".";
|
|
148
|
+
// Truncate fraction to maxDecimals, trim trailing zeros
|
|
149
|
+
const truncated = fractionPart.slice(0, maxDecimals);
|
|
150
|
+
const trimmed = truncated.replace(/0+$/, "");
|
|
151
|
+
const fractionSuffix = trimmed ? `${decimalSep}${trimmed}` : "";
|
|
152
|
+
const symbolSuffix = symbol ? ` ${symbol}` : "";
|
|
153
|
+
return `${formattedWhole}${fractionSuffix}${symbolSuffix}`;
|
|
154
|
+
}
|
|
107
155
|
if (import.meta.vitest) {
|
|
108
156
|
const { describe, test, expect } = import.meta.vitest;
|
|
109
157
|
describe("formatPlanck", () => {
|
|
@@ -191,4 +239,66 @@ if (import.meta.vitest) {
|
|
|
191
239
|
expect(parseToPlanck(formatted)).toBe(original);
|
|
192
240
|
});
|
|
193
241
|
});
|
|
242
|
+
describe("formatBalance", () => {
|
|
243
|
+
test("formats with default options (no symbol, max 4 decimals)", () => {
|
|
244
|
+
expect(formatBalance(15000000000n)).toBe("1.5");
|
|
245
|
+
expect(formatBalance(12345678900n)).toBe("1.2345");
|
|
246
|
+
});
|
|
247
|
+
test("applies thousand separators", () => {
|
|
248
|
+
expect(formatBalance(10000000000000n, { locale: "en-US" })).toBe("1,000");
|
|
249
|
+
expect(formatBalance(1234567000000000n, { locale: "en-US", symbol: "DOT" })).toBe("123,456.7 DOT");
|
|
250
|
+
});
|
|
251
|
+
test("truncates fraction to maxDecimals", () => {
|
|
252
|
+
expect(formatBalance(12345678900n, { maxDecimals: 2 })).toBe("1.23");
|
|
253
|
+
expect(formatBalance(12345678900n, { maxDecimals: 8 })).toBe("1.23456789");
|
|
254
|
+
});
|
|
255
|
+
test("appends symbol", () => {
|
|
256
|
+
expect(formatBalance(15000000000n, { symbol: "DOT" })).toBe("1.5 DOT");
|
|
257
|
+
expect(formatBalance(15000000000n, { symbol: "PAS" })).toBe("1.5 PAS");
|
|
258
|
+
});
|
|
259
|
+
test("omits fraction when all zeros after truncation", () => {
|
|
260
|
+
expect(formatBalance(10000000000n)).toBe("1");
|
|
261
|
+
expect(formatBalance(20000000000n, { symbol: "DOT" })).toBe("2 DOT");
|
|
262
|
+
});
|
|
263
|
+
test("respects maxDecimals: 0", () => {
|
|
264
|
+
expect(formatBalance(15000000000n, { maxDecimals: 0 })).toBe("1");
|
|
265
|
+
expect(formatBalance(19999999999n, { maxDecimals: 0, symbol: "DOT" })).toBe("1 DOT");
|
|
266
|
+
});
|
|
267
|
+
test("handles zero", () => {
|
|
268
|
+
expect(formatBalance(0n)).toBe("0");
|
|
269
|
+
expect(formatBalance(0n, { symbol: "DOT" })).toBe("0 DOT");
|
|
270
|
+
});
|
|
271
|
+
test("handles sub-unit amounts", () => {
|
|
272
|
+
// 1 planck is below 4-decimal display threshold → shows "0"
|
|
273
|
+
expect(formatBalance(1n)).toBe("0");
|
|
274
|
+
// 0.0001 DOT is exactly at the threshold
|
|
275
|
+
expect(formatBalance(1000000n)).toBe("0.0001");
|
|
276
|
+
// With more maxDecimals, sub-unit amounts become visible
|
|
277
|
+
expect(formatBalance(1n, { maxDecimals: 10 })).toBe("0.0000000001");
|
|
278
|
+
});
|
|
279
|
+
test("uses locale-correct decimal separator", () => {
|
|
280
|
+
// German uses . for grouping and , for decimal
|
|
281
|
+
expect(formatBalance(15000000000n, { locale: "de-DE" })).toBe("1,5");
|
|
282
|
+
expect(formatBalance(10000000000000n, { locale: "de-DE" })).toBe("1.000");
|
|
283
|
+
// With fraction
|
|
284
|
+
expect(formatBalance(10005000000000n, { locale: "de-DE" })).toBe("1.000,5");
|
|
285
|
+
});
|
|
286
|
+
test("preserves BigInt precision for large amounts", () => {
|
|
287
|
+
// 2^53 + 1 in planck — would lose precision with Number()
|
|
288
|
+
const largePlanck = 90071992547409920000n; // ~9 billion DOT
|
|
289
|
+
const result = formatBalance(largePlanck, { locale: "en-US", symbol: "DOT" });
|
|
290
|
+
expect(result).toContain("9,007,199,254");
|
|
291
|
+
expect(result).toContain("DOT");
|
|
292
|
+
});
|
|
293
|
+
test("throws on negative planck (delegates to formatPlanck)", () => {
|
|
294
|
+
expect(() => formatBalance(-1n)).toThrow(RangeError);
|
|
295
|
+
});
|
|
296
|
+
test("throws on invalid decimals (delegates to formatPlanck)", () => {
|
|
297
|
+
expect(() => formatBalance(0n, { decimals: -1 })).toThrow(RangeError);
|
|
298
|
+
});
|
|
299
|
+
test("throws on invalid maxDecimals", () => {
|
|
300
|
+
expect(() => formatBalance(0n, { maxDecimals: -1 })).toThrow(RangeError);
|
|
301
|
+
expect(() => formatBalance(0n, { maxDecimals: 1.5 })).toThrow(RangeError);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
194
304
|
}
|
package/package.json
CHANGED