@koralabs/kora-labs-common 6.8.1 → 6.9.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/mpt/index.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { Trie } from '@aiken-lang/merkle-patricia-forestry';
2
+ import * as labelSet from './labelSet';
3
+ import * as registryValue from './registryValue';
4
+ export { labelSet, registryValue };
5
+ export { Trie };
6
+ /**
7
+ * THE canonical minting-data MPT (a real {@link Trie}, for proof generation during mint/burn).
8
+ * Consumers that only need the root hash should use {@link computeMintingDataRoot}; consumers that
9
+ * generate proofs (the minting engine's verifyRootHash) keep the returned Trie. Value = "" per the
10
+ * validator's `update_root` — see {@link computeMintingDataRoot} for the full rationale.
11
+ */
12
+ export declare const buildMintingDataTrie: (handleNames: string[]) => Promise<Trie>;
13
+ /**
14
+ * THE canonical minting-data MPT root. Every Node service (api.handle.me, the minting engine,
15
+ * handle.me/bff) and the on-chain mirror (@koralabs/handles-decentralized-minting) MUST compute
16
+ * the root through this one function, so the off-chain root can never drift from what the
17
+ * validator maintains. (It drifted before because api + engine each re-derived it independently.)
18
+ *
19
+ * Value = "" (empty) per the legacy/DeMi mint validator's `update_root`:
20
+ * `mpt.insert(root, handle_name, #"", proof)`
21
+ * Labels (001/002…) are NOT in the root until the in-band `MintLabelAssets` path is deployed;
22
+ * until then the off-chain root MUST use empty values or it diverges from chain on every labeled
23
+ * handle. When labels go in-band, change ONLY this function (accept a registry arg, set
24
+ * value = registryValue.encode(freeNames, labels)) and every consumer moves in lockstep.
25
+ *
26
+ * Order-independent (MPT root is determined by the key/value set), so callers need not sort;
27
+ * duplicate keys are de-duped to avoid a Trie insert throw.
28
+ */
29
+ export declare const computeMintingDataRoot: (handleNames: string[]) => Promise<string>;
package/mpt/index.js ADDED
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.computeMintingDataRoot = exports.buildMintingDataTrie = exports.Trie = exports.registryValue = exports.labelSet = void 0;
27
+ // Server-only (Node) MPT module. Pulls @aiken-lang/merkle-patricia-forestry (native `level`/`blake2b`)
28
+ // so it is NOT re-exported from the package root — import it from the subpath:
29
+ // import { computeMintingDataRoot } from '@koralabs/kora-labs-common/mpt';
30
+ // Mirrors the './aws' server-only convention; keeps the package root isomorphic/browser-safe.
31
+ const merkle_patricia_forestry_1 = require("@aiken-lang/merkle-patricia-forestry");
32
+ Object.defineProperty(exports, "Trie", { enumerable: true, get: function () { return merkle_patricia_forestry_1.Trie; } });
33
+ const labelSet = __importStar(require("./labelSet"));
34
+ exports.labelSet = labelSet;
35
+ const registryValue = __importStar(require("./registryValue"));
36
+ exports.registryValue = registryValue;
37
+ const EMPTY_ROOT_HEX = Buffer.alloc(32).toString('hex');
38
+ /**
39
+ * THE canonical minting-data MPT (a real {@link Trie}, for proof generation during mint/burn).
40
+ * Consumers that only need the root hash should use {@link computeMintingDataRoot}; consumers that
41
+ * generate proofs (the minting engine's verifyRootHash) keep the returned Trie. Value = "" per the
42
+ * validator's `update_root` — see {@link computeMintingDataRoot} for the full rationale.
43
+ */
44
+ const buildMintingDataTrie = async (handleNames) => {
45
+ const uniqueKeys = Array.from(new Set(handleNames));
46
+ return merkle_patricia_forestry_1.Trie.fromList(uniqueKeys.map((key) => ({ key, value: '' })));
47
+ };
48
+ exports.buildMintingDataTrie = buildMintingDataTrie;
49
+ /**
50
+ * THE canonical minting-data MPT root. Every Node service (api.handle.me, the minting engine,
51
+ * handle.me/bff) and the on-chain mirror (@koralabs/handles-decentralized-minting) MUST compute
52
+ * the root through this one function, so the off-chain root can never drift from what the
53
+ * validator maintains. (It drifted before because api + engine each re-derived it independently.)
54
+ *
55
+ * Value = "" (empty) per the legacy/DeMi mint validator's `update_root`:
56
+ * `mpt.insert(root, handle_name, #"", proof)`
57
+ * Labels (001/002…) are NOT in the root until the in-band `MintLabelAssets` path is deployed;
58
+ * until then the off-chain root MUST use empty values or it diverges from chain on every labeled
59
+ * handle. When labels go in-band, change ONLY this function (accept a registry arg, set
60
+ * value = registryValue.encode(freeNames, labels)) and every consumer moves in lockstep.
61
+ *
62
+ * Order-independent (MPT root is determined by the key/value set), so callers need not sort;
63
+ * duplicate keys are de-duped to avoid a Trie insert throw.
64
+ */
65
+ const computeMintingDataRoot = async (handleNames) => {
66
+ var _a, _b;
67
+ const trie = await (0, exports.buildMintingDataTrie)(handleNames);
68
+ return (_b = (_a = trie.hash) === null || _a === void 0 ? void 0 : _a.toString('hex')) !== null && _b !== void 0 ? _b : EMPTY_ROOT_HEX;
69
+ };
70
+ exports.computeMintingDataRoot = computeMintingDataRoot;
@@ -0,0 +1,12 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ /** Does the canonical label set contain `label`? */
4
+ export declare const contains: (set: string, label: string) => boolean;
5
+ /** Insert `label`, keeping the set sorted ascending. Throws if already present. */
6
+ export declare const insert: (set: string, label: string) => string;
7
+ /** Remove `label`. Throws if absent. */
8
+ export declare const remove: (set: string, label: string) => string;
9
+ /** Apply a +1 (add) / -1 (remove) delta — couples the value change to the tx mint/burn. */
10
+ export declare const apply: (set: string, label: string, amount: bigint) => string;
11
+ /** The raw value bytes to store in the Trie (UTF-8-safe). */
12
+ export declare const valueBuffer: (set: string) => Buffer;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ // WS1 — TypeScript port of the on-chain `label_set.ak` value encoding. The MPT value for a
3
+ // handle key is the canonical (sorted) concatenation of fixed 4-byte CIP-67 label prefixes,
4
+ // represented here as a lowercase hex string ("" = empty set). This MUST stay byte-identical
5
+ // to the aiken encoding so the off-chain `old_value`/`new_value` match what the validator
6
+ // reconstructs (`mpt.update` verifies the old bytes are in the trie).
7
+ //
8
+ // CANONICAL HOME: this lived (duplicated) in @koralabs/handles-decentralized-minting/store and
9
+ // was re-derived again in api.handle.me + the minting engine. Those independent copies drifted
10
+ // (the root went out of sync across services). It now lives ONCE here so every Node service
11
+ // computes the registry value identically. See ./index.ts `computeMintingDataRoot`.
12
+ //
13
+ // Trie note: the JS Merkle-Patricia-Forestry treats string values as UTF-8, so insert the
14
+ // value bytes via `Buffer.from(hex, "hex")` (see `valueBuffer`), not the hex string itself.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.valueBuffer = exports.apply = exports.remove = exports.insert = exports.contains = void 0;
17
+ const LABEL_WIDTH_HEX = 8; // 4 bytes
18
+ const chunks = (set) => {
19
+ const out = [];
20
+ for (let i = 0; i < set.length; i += LABEL_WIDTH_HEX) {
21
+ out.push(set.slice(i, i + LABEL_WIDTH_HEX));
22
+ }
23
+ return out;
24
+ };
25
+ const normalizeLabel = (label) => {
26
+ const l = label.toLowerCase();
27
+ if (l.length !== LABEL_WIDTH_HEX) {
28
+ throw new Error(`label must be ${LABEL_WIDTH_HEX} hex chars (4 bytes): ${label}`);
29
+ }
30
+ return l;
31
+ };
32
+ /** Does the canonical label set contain `label`? */
33
+ const contains = (set, label) => chunks(set.toLowerCase()).includes(normalizeLabel(label));
34
+ exports.contains = contains;
35
+ /** Insert `label`, keeping the set sorted ascending. Throws if already present. */
36
+ const insert = (set, label) => {
37
+ const l = normalizeLabel(label);
38
+ const cs = chunks(set.toLowerCase());
39
+ if (cs.includes(l))
40
+ throw new Error('LABEL_ALREADY_PRESENT');
41
+ cs.push(l);
42
+ // fixed-width hex sorts lexicographically == on-chain byte order
43
+ cs.sort();
44
+ return cs.join('');
45
+ };
46
+ exports.insert = insert;
47
+ /** Remove `label`. Throws if absent. */
48
+ const remove = (set, label) => {
49
+ const l = normalizeLabel(label);
50
+ const cs = chunks(set.toLowerCase());
51
+ const idx = cs.indexOf(l);
52
+ if (idx < 0)
53
+ throw new Error('LABEL_ABSENT');
54
+ cs.splice(idx, 1);
55
+ return cs.join('');
56
+ };
57
+ exports.remove = remove;
58
+ /** Apply a +1 (add) / -1 (remove) delta — couples the value change to the tx mint/burn. */
59
+ const apply = (set, label, amount) => {
60
+ // BigInt(1)/BigInt(-1) rather than 1n/-1n literals: klc targets es2018.
61
+ if (amount === BigInt(1))
62
+ return (0, exports.insert)(set, label);
63
+ if (amount === BigInt(-1))
64
+ return (0, exports.remove)(set, label);
65
+ throw new Error('INVALID_AMOUNT');
66
+ };
67
+ exports.apply = apply;
68
+ /** The raw value bytes to store in the Trie (UTF-8-safe). */
69
+ const valueBuffer = (set) => Buffer.from(set, 'hex');
70
+ exports.valueBuffer = valueBuffer;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Byte-identical to on-chain `registry_value.encode(free_names, labels)`.
3
+ * free_names == [] -> labels (an empty handle stays "", any pure-label handle is WS1-identical)
4
+ * free_names != [] -> "ff" ++ serialise_data(free_names) ++ labels
5
+ */
6
+ export declare const encode: (freeNames: string[], labels: string) => string;
7
+ /** A free slot is available iff the root holds fewer than the configured free count. */
8
+ export declare const hasFreeSlot: (freeNames: string[], freeVirtualCount: number) => boolean;
9
+ /** Is this sub-name one of the root's current free names? */
10
+ export declare const hasFreeName: (freeNames: string[], name: string) => boolean;
11
+ /**
12
+ * Add a sub-name to the free set (a free private-virtual MINT). Prepends (mirrors aiken
13
+ * `list.push`) and is idempotent — the prepend order is part of the encoded bytes, so it MUST
14
+ * match the contract's `add_free_name`.
15
+ */
16
+ export declare const addFreeName: (freeNames: string[], name: string) => string[];
17
+ /**
18
+ * Remove a sub-name from the free set (a free private-virtual BURN → reopen its slot). No-op if
19
+ * the name isn't in the set (a paid sub's burn doesn't touch the allowance). Preserves order.
20
+ */
21
+ export declare const removeFreeName: (freeNames: string[], name: string) => string[];
22
+ /** Parse a stored comma-hex free-name list ("" -> []). */
23
+ export declare const parseFreeNames: (csv: string | null | undefined) => string[];
24
+ /** Serialize a free-name set back to comma-hex for storage. */
25
+ export declare const serializeFreeNames: (freeNames: string[]) => string;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ // DSH-402 — TypeScript port of the on-chain `registry_value.ak`. The MPT value at a ROOT
3
+ // handle key carries, alongside the WS1 label set, the SET OF NAMES of the root's currently-held
4
+ // FREE private virtual subs (at most `free_virtual_count`). This MUST stay byte-identical to the
5
+ // aiken `encode` so the off-chain `old_value`/`new_value` match what the validator reconstructs
6
+ // (`mpt.update` verifies the old bytes are in the trie). If these diverge, every free-virtual
7
+ // `mpt.update` fails on-chain.
8
+ //
9
+ // CANONICAL HOME: see ./labelSet.ts — this is the single shared definition for all Node services.
10
+ //
11
+ // Representation: free names and labels are lowercase hex strings; `encode` returns a hex string
12
+ // ("" = empty value). Names are sub-handle name bytes; labels are concatenated 4-byte CIP-67
13
+ // prefixes (each starting 0x00), so a leading 0xff marker can never collide with a label set.
14
+ //
15
+ // Encoding (mirrors `registry_value.ak`):
16
+ // - free_names == [] -> labels (backward compatible with WS1)
17
+ // - free_names != [] -> 0xff ++ serialise_data(free_names) ++ labels
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.serializeFreeNames = exports.parseFreeNames = exports.removeFreeName = exports.addFreeName = exports.hasFreeName = exports.hasFreeSlot = exports.encode = void 0;
20
+ // CBOR header for a Plutus `Data` byte string (major type 2). Handle/sub names are CIP-67 asset
21
+ // names (<= 32 bytes), so a single definite-length chunk always suffices; Plutus only chunks
22
+ // byte strings longer than 64 bytes, which a valid handle name can never be — throw rather than
23
+ // emit divergent bytes for an impossible input.
24
+ const cborByteString = (hex) => {
25
+ const byteLen = hex.length / 2;
26
+ if (!Number.isInteger(byteLen)) {
27
+ throw new Error(`free name is not valid hex (odd length): ${hex}`);
28
+ }
29
+ if (byteLen < 24)
30
+ return (0x40 | byteLen).toString(16).padStart(2, '0') + hex;
31
+ if (byteLen <= 64)
32
+ return '58' + byteLen.toString(16).padStart(2, '0') + hex;
33
+ throw new Error(`free name exceeds 64 bytes (Plutus would chunk): ${hex}`);
34
+ };
35
+ // Plutus `Data` CBOR of a non-empty `List<ByteArray>` (indefinite-length array).
36
+ const serialiseData = (freeNames) => '9f' + freeNames.map((n) => cborByteString(n.toLowerCase())).join('') + 'ff';
37
+ /**
38
+ * Byte-identical to on-chain `registry_value.encode(free_names, labels)`.
39
+ * free_names == [] -> labels (an empty handle stays "", any pure-label handle is WS1-identical)
40
+ * free_names != [] -> "ff" ++ serialise_data(free_names) ++ labels
41
+ */
42
+ const encode = (freeNames, labels) => {
43
+ const l = labels.toLowerCase();
44
+ return freeNames.length === 0 ? l : 'ff' + serialiseData(freeNames) + l;
45
+ };
46
+ exports.encode = encode;
47
+ /** A free slot is available iff the root holds fewer than the configured free count. */
48
+ const hasFreeSlot = (freeNames, freeVirtualCount) => freeNames.length < freeVirtualCount;
49
+ exports.hasFreeSlot = hasFreeSlot;
50
+ /** Is this sub-name one of the root's current free names? */
51
+ const hasFreeName = (freeNames, name) => freeNames.map((n) => n.toLowerCase()).includes(name.toLowerCase());
52
+ exports.hasFreeName = hasFreeName;
53
+ /**
54
+ * Add a sub-name to the free set (a free private-virtual MINT). Prepends (mirrors aiken
55
+ * `list.push`) and is idempotent — the prepend order is part of the encoded bytes, so it MUST
56
+ * match the contract's `add_free_name`.
57
+ */
58
+ const addFreeName = (freeNames, name) => {
59
+ const n = name.toLowerCase();
60
+ const lower = freeNames.map((x) => x.toLowerCase());
61
+ return lower.includes(n) ? lower : [n, ...lower];
62
+ };
63
+ exports.addFreeName = addFreeName;
64
+ /**
65
+ * Remove a sub-name from the free set (a free private-virtual BURN → reopen its slot). No-op if
66
+ * the name isn't in the set (a paid sub's burn doesn't touch the allowance). Preserves order.
67
+ */
68
+ const removeFreeName = (freeNames, name) => {
69
+ const n = name.toLowerCase();
70
+ return freeNames.map((x) => x.toLowerCase()).filter((x) => x !== n);
71
+ };
72
+ exports.removeFreeName = removeFreeName;
73
+ /** Parse a stored comma-hex free-name list ("" -> []). */
74
+ const parseFreeNames = (csv) => (csv ? csv.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean) : []);
75
+ exports.parseFreeNames = parseFreeNames;
76
+ /** Serialize a free-name set back to comma-hex for storage. */
77
+ const serializeFreeNames = (freeNames) => freeNames.map((n) => n.toLowerCase()).join(',');
78
+ exports.serializeFreeNames = serializeFreeNames;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koralabs/kora-labs-common",
3
- "version": "6.8.1",
3
+ "version": "6.9.1",
4
4
  "description": "Kora Labs Common Utilities",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -37,6 +37,7 @@
37
37
  "author": "",
38
38
  "license": "ISC",
39
39
  "devDependencies": {
40
+ "@aiken-lang/merkle-patricia-forestry": "^1.2.0",
40
41
  "@aws-sdk/client-dynamodb": "^3.549.0",
41
42
  "@aws-sdk/lib-dynamodb": "^3.549.0",
42
43
  "@types/jest": "^30.0.0",
@@ -63,5 +64,13 @@
63
64
  },
64
65
  "overrides": {
65
66
  "js-yaml": "^4.2.0"
67
+ },
68
+ "peerDependencies": {
69
+ "@aiken-lang/merkle-patricia-forestry": "^1.2.0"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@aiken-lang/merkle-patricia-forestry": {
73
+ "optional": true
74
+ }
66
75
  }
67
76
  }