@andamio/core 0.2.0 → 0.3.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/dist/index.js +90 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +90 -35
- package/dist/index.mjs.map +1 -1
- package/dist/utils/hashing/index.d.mts +11 -9
- package/dist/utils/hashing/index.d.ts +11 -9
- package/dist/utils/hashing/index.js +90 -35
- package/dist/utils/hashing/index.js.map +1 -1
- package/dist/utils/hashing/index.mjs +90 -35
- package/dist/utils/hashing/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/utils/hashing/task-hash.test.ts +145 -55
- package/src/utils/hashing/task-hash.ts +145 -84
|
@@ -75,7 +75,7 @@ function concatUint8Arrays(arrays) {
|
|
|
75
75
|
}
|
|
76
76
|
function computeTaskHash(task) {
|
|
77
77
|
validateTaskData(task);
|
|
78
|
-
const bytes =
|
|
78
|
+
const bytes = encodeTaskAsPlutusData(task);
|
|
79
79
|
return blake.blake2bHex(bytes, void 0, 32);
|
|
80
80
|
}
|
|
81
81
|
function verifyTaskHash(task, expectedHash) {
|
|
@@ -90,18 +90,94 @@ function isValidTaskHash(hash) {
|
|
|
90
90
|
}
|
|
91
91
|
function debugTaskBytes(task) {
|
|
92
92
|
validateTaskData(task);
|
|
93
|
-
const bytes =
|
|
93
|
+
const bytes = encodeTaskAsPlutusData(task);
|
|
94
94
|
return uint8ArrayToHex(bytes);
|
|
95
95
|
}
|
|
96
|
-
function
|
|
96
|
+
function encodeTaskAsPlutusData(task) {
|
|
97
97
|
const normalizedContent = task.project_content.normalize("NFC");
|
|
98
|
+
const contentBytes = new TextEncoder().encode(normalizedContent);
|
|
98
99
|
return concatUint8Arrays2([
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
// Tag 121 (Plutus Data Constructor 0) + indefinite array start
|
|
101
|
+
new Uint8Array([216, 121, 159]),
|
|
102
|
+
// Field 1: project_content (ByteArray)
|
|
103
|
+
encodeCborBytes(contentBytes),
|
|
104
|
+
// Field 2: deadline (Int)
|
|
105
|
+
encodeCborUint(task.expiration_time),
|
|
106
|
+
// Field 3: lovelace_am (Int)
|
|
107
|
+
encodeCborUint(task.lovelace_amount),
|
|
108
|
+
// Field 4: tokens (List<FlatValue>)
|
|
109
|
+
encodeTokensList(task.native_assets),
|
|
110
|
+
// Break (end of indefinite array)
|
|
111
|
+
new Uint8Array([255])
|
|
103
112
|
]);
|
|
104
113
|
}
|
|
114
|
+
function encodeTokensList(assets) {
|
|
115
|
+
if (assets.length === 0) {
|
|
116
|
+
return new Uint8Array([128]);
|
|
117
|
+
}
|
|
118
|
+
const parts = [new Uint8Array([159])];
|
|
119
|
+
for (const [policyId, tokenName, quantity] of assets) {
|
|
120
|
+
parts.push(new Uint8Array([216, 121, 159]));
|
|
121
|
+
parts.push(encodeCborBytes(hexToBytes(policyId)));
|
|
122
|
+
parts.push(encodeCborBytes(hexToBytes(tokenName)));
|
|
123
|
+
parts.push(encodeCborUint(quantity));
|
|
124
|
+
parts.push(new Uint8Array([255]));
|
|
125
|
+
}
|
|
126
|
+
parts.push(new Uint8Array([255]));
|
|
127
|
+
return concatUint8Arrays2(parts);
|
|
128
|
+
}
|
|
129
|
+
var MAX_UINT64 = 18446744073709551615n;
|
|
130
|
+
function encodeCborUint(n) {
|
|
131
|
+
if (n < 0n) {
|
|
132
|
+
throw new Error("Negative integers not supported");
|
|
133
|
+
}
|
|
134
|
+
if (n > MAX_UINT64) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Integer exceeds maximum CBOR uint64 value (got ${n}, max ${MAX_UINT64})`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (n < 24n) {
|
|
140
|
+
return new Uint8Array([Number(n)]);
|
|
141
|
+
} else if (n < 256n) {
|
|
142
|
+
return new Uint8Array([24, Number(n)]);
|
|
143
|
+
} else if (n < 65536n) {
|
|
144
|
+
return new Uint8Array([25, Number(n >> 8n) & 255, Number(n) & 255]);
|
|
145
|
+
} else if (n < 4294967296n) {
|
|
146
|
+
return new Uint8Array([
|
|
147
|
+
26,
|
|
148
|
+
Number(n >> 24n & 0xffn),
|
|
149
|
+
Number(n >> 16n & 0xffn),
|
|
150
|
+
Number(n >> 8n & 0xffn),
|
|
151
|
+
Number(n & 0xffn)
|
|
152
|
+
]);
|
|
153
|
+
} else {
|
|
154
|
+
return new Uint8Array([
|
|
155
|
+
27,
|
|
156
|
+
Number(n >> 56n & 0xffn),
|
|
157
|
+
Number(n >> 48n & 0xffn),
|
|
158
|
+
Number(n >> 40n & 0xffn),
|
|
159
|
+
Number(n >> 32n & 0xffn),
|
|
160
|
+
Number(n >> 24n & 0xffn),
|
|
161
|
+
Number(n >> 16n & 0xffn),
|
|
162
|
+
Number(n >> 8n & 0xffn),
|
|
163
|
+
Number(n & 0xffn)
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function encodeCborBytes(bytes) {
|
|
168
|
+
const len = bytes.length;
|
|
169
|
+
let header;
|
|
170
|
+
if (len < 24) {
|
|
171
|
+
header = new Uint8Array([64 + len]);
|
|
172
|
+
} else if (len < 256) {
|
|
173
|
+
header = new Uint8Array([88, len]);
|
|
174
|
+
} else if (len < 65536) {
|
|
175
|
+
header = new Uint8Array([89, len >> 8 & 255, len & 255]);
|
|
176
|
+
} else {
|
|
177
|
+
throw new Error("Byte string too long for CBOR encoding");
|
|
178
|
+
}
|
|
179
|
+
return concatUint8Arrays2([header, bytes]);
|
|
180
|
+
}
|
|
105
181
|
function validateTaskData(task) {
|
|
106
182
|
if (task.project_content.length > 140) {
|
|
107
183
|
throw new Error(
|
|
@@ -136,33 +212,6 @@ function validateTaskData(task) {
|
|
|
136
212
|
}
|
|
137
213
|
}
|
|
138
214
|
}
|
|
139
|
-
function intToBytesLittleEndian(n) {
|
|
140
|
-
if (n < 0n) {
|
|
141
|
-
throw new Error("Negative integers not supported");
|
|
142
|
-
}
|
|
143
|
-
if (n === 0n) {
|
|
144
|
-
return new Uint8Array([0]);
|
|
145
|
-
}
|
|
146
|
-
const bytes = [];
|
|
147
|
-
let remaining = n;
|
|
148
|
-
while (remaining > 0n) {
|
|
149
|
-
bytes.push(Number(remaining & 0xffn));
|
|
150
|
-
remaining = remaining >> 8n;
|
|
151
|
-
}
|
|
152
|
-
return new Uint8Array(bytes);
|
|
153
|
-
}
|
|
154
|
-
function combineNativeAssets(assets) {
|
|
155
|
-
if (assets.length === 0) {
|
|
156
|
-
return new Uint8Array([]);
|
|
157
|
-
}
|
|
158
|
-
const chunks = [];
|
|
159
|
-
for (const [policyId, tokenName, quantity] of assets) {
|
|
160
|
-
chunks.push(hexToBytes(policyId));
|
|
161
|
-
chunks.push(hexToBytes(tokenName));
|
|
162
|
-
chunks.push(intToBytesLittleEndian(quantity));
|
|
163
|
-
}
|
|
164
|
-
return concatUint8Arrays2(chunks);
|
|
165
|
-
}
|
|
166
215
|
function hexToBytes(hex) {
|
|
167
216
|
if (hex.length === 0) {
|
|
168
217
|
return new Uint8Array([]);
|
|
@@ -172,7 +221,13 @@ function hexToBytes(hex) {
|
|
|
172
221
|
}
|
|
173
222
|
const bytes = new Uint8Array(hex.length / 2);
|
|
174
223
|
for (let i = 0; i < bytes.length; i++) {
|
|
175
|
-
|
|
224
|
+
const byte = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
225
|
+
if (Number.isNaN(byte)) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Invalid hex character at position ${i * 2}: "${hex.slice(i * 2, i * 2 + 2)}"`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
bytes[i] = byte;
|
|
176
231
|
}
|
|
177
232
|
return bytes;
|
|
178
233
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/hashing/slt-hash.ts","../../../src/utils/hashing/task-hash.ts","../../../src/utils/hashing/commitment-hash.ts"],"names":["blake","concatUint8Arrays"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * SLT Hash Utility\n *\n * Computes the module token name (hash) from a list of Student Learning Targets (SLTs).\n * This hash is used as the token name when minting module tokens on-chain.\n *\n * The algorithm matches the on-chain Plutus validator:\n * ```haskell\n * sltsToBbs MintModuleV2{slts} = blake2b_256 $ serialiseData $ toBuiltinData $ map stringToBuiltinByteString slts\n * ```\n *\n * Serialization format:\n * 1. Convert each SLT string to UTF-8 bytes\n * 2. Encode as CBOR indefinite-length array of byte strings\n * 3. Hash with Blake2b-256 (32 bytes / 256 bits)\n *\n * @module @andamio/core/hashing\n */\n\nimport blake from \"blakejs\";\n\n/**\n * Plutus chunk size for byte strings.\n * Strings longer than this are encoded as indefinite-length chunked byte strings.\n */\nconst PLUTUS_CHUNK_SIZE = 64;\n\n/**\n * Compute the module hash matching Plutus on-chain encoding.\n *\n * Plutus's `stringToBuiltinByteString` chunks byte strings at 64 bytes.\n * This function replicates that behavior:\n * - Strings <= 64 bytes: encoded as regular CBOR byte strings\n * - Strings > 64 bytes: encoded as indefinite-length chunked byte strings\n *\n * @param slts - Array of Student Learning Target strings\n * @returns 64-character hex string (256-bit Blake2b hash)\n *\n * @example\n * ```typescript\n * import { computeSltHash } from \"@andamio/core/hashing\";\n *\n * const slts = [\n * \"I can mint an access token.\",\n * \"I can complete an assignment to earn a credential.\"\n * ];\n *\n * const moduleHash = computeSltHash(slts);\n * // Returns: \"8dcbe1b925d87e6c547bbd8071c23a712db4c32751454b0948f8c846e9246b5c\"\n * ```\n */\nexport function computeSltHash(slts: string[]): string {\n const sltBytes = slts.map((slt) => new TextEncoder().encode(slt));\n const cborData = encodeAsPlutusArray(sltBytes);\n return blake.blake2bHex(cborData, undefined, 32);\n}\n\n/**\n * @deprecated Use `computeSltHash` instead. This alias is kept for backwards compatibility.\n */\nexport const computeSltHashDefinite = computeSltHash;\n\n/**\n * Verify that a given hash matches the computed hash for SLTs.\n *\n * @param slts - Array of Student Learning Target strings\n * @param expectedHash - The hash to verify (64-character hex string)\n * @returns true if the computed hash matches the expected hash\n */\nexport function verifySltHash(slts: string[], expectedHash: string): boolean {\n const computedHash = computeSltHash(slts);\n return computedHash.toLowerCase() === expectedHash.toLowerCase();\n}\n\n/**\n * Validate that a string is a valid SLT hash format.\n *\n * SLT hashes are 64-character hexadecimal strings (256-bit Blake2b hash).\n *\n * @param hash - String to validate\n * @returns true if the string is a valid SLT hash format\n */\nexport function isValidSltHash(hash: string): boolean {\n if (hash.length !== 64) {\n return false;\n }\n return /^[0-9a-fA-F]{64}$/.test(hash);\n}\n\n// =============================================================================\n// Plutus-Compatible Encoding (Internal)\n// =============================================================================\n\n/**\n * Encode an array of byte buffers matching Plutus serialization.\n *\n * Uses indefinite-length array with chunked byte strings for long values.\n *\n * @internal\n */\nfunction encodeAsPlutusArray(items: Uint8Array[]): Uint8Array {\n const chunks: Uint8Array[] = [];\n\n // Start indefinite array\n chunks.push(new Uint8Array([0x9f]));\n\n // Encode each item (with chunking for long strings)\n for (const item of items) {\n chunks.push(encodePlutusBuiltinByteString(item));\n }\n\n // End indefinite array\n chunks.push(new Uint8Array([0xff]));\n\n return concatUint8Arrays(chunks);\n}\n\n/**\n * Encode a byte buffer matching Plutus's stringToBuiltinByteString.\n *\n * - Strings <= 64 bytes: regular CBOR byte string\n * - Strings > 64 bytes: indefinite-length chunked byte string (64-byte chunks)\n *\n * @internal\n */\nfunction encodePlutusBuiltinByteString(buffer: Uint8Array): Uint8Array {\n if (buffer.length <= PLUTUS_CHUNK_SIZE) {\n // Short string: encode normally\n return encodeCBORByteString(buffer);\n }\n\n // Long string: use indefinite-length chunked encoding\n const chunks: Uint8Array[] = [];\n chunks.push(new Uint8Array([0x5f])); // Start indefinite byte string\n\n for (let i = 0; i < buffer.length; i += PLUTUS_CHUNK_SIZE) {\n const chunk = buffer.subarray(i, Math.min(i + PLUTUS_CHUNK_SIZE, buffer.length));\n chunks.push(encodeCBORByteString(chunk));\n }\n\n chunks.push(new Uint8Array([0xff])); // Break\n return concatUint8Arrays(chunks);\n}\n\n/**\n * Encode a byte buffer as a CBOR byte string (definite length).\n *\n * @internal\n */\nfunction encodeCBORByteString(buffer: Uint8Array): Uint8Array {\n const len = buffer.length;\n\n // CBOR byte string encoding (major type 2 = 0x40):\n // - 0-23 bytes: length inline (0x40 + len)\n // - 24-255 bytes: 0x58 + 1-byte length\n // - 256-65535 bytes: 0x59 + 2-byte length (big-endian)\n if (len <= 23) {\n const result = new Uint8Array(1 + len);\n result[0] = 0x40 + len;\n result.set(buffer, 1);\n return result;\n } else if (len <= 255) {\n const result = new Uint8Array(2 + len);\n result[0] = 0x58;\n result[1] = len;\n result.set(buffer, 2);\n return result;\n } else if (len <= 65535) {\n const result = new Uint8Array(3 + len);\n result[0] = 0x59;\n result[1] = len >> 8;\n result[2] = len & 0xff;\n result.set(buffer, 3);\n return result;\n }\n throw new Error(\"Byte string too long for CBOR encoding\");\n}\n\n/**\n * Concatenate multiple Uint8Arrays into one\n *\n * @internal\n */\nfunction concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n","/**\n * Task Hash Utility\n *\n * Computes the task token name (hash) from task data.\n * This hash is used as the task_hash on-chain (content-addressed identifier).\n *\n * The algorithm matches the on-chain Aiken hash_project_data function using\n * raw byte concatenation and little-endian integer encoding.\n *\n * @module @andamio/core/hashing\n */\n\nimport blake from \"blakejs\";\n\n/**\n * Native asset in Cardano format: [policyId, tokenName, quantity]\n *\n * @example\n * ```typescript\n * const asset: NativeAsset = [\n * \"abc123def456...\".repeat(4), // 56 hex chars (28 bytes) - policy ID\n * \"746f6b656e\", // hex-encoded token name\n * 1000n // quantity as bigint\n * ];\n * ```\n */\nexport type NativeAsset = [\n policyId: string, // 56 hex chars (28 bytes)\n tokenName: string, // hex encoded (0-64 chars / 0-32 bytes)\n quantity: bigint, // arbitrary precision integer\n];\n\n/**\n * Task data structure matching the Aiken ProjectData type.\n *\n * Fields are concatenated in this order for hashing:\n * 1. project_content (UTF-8 bytes, NFC normalized)\n * 2. expiration_time (little-endian, minimal bytes)\n * 3. lovelace_amount (little-endian, minimal bytes)\n * 4. native_assets (raw concatenation of each asset's bytes)\n */\nexport interface TaskData {\n /** Task description (max 140 characters) */\n project_content: string;\n /** Unix timestamp in milliseconds */\n expiration_time: bigint;\n /** Lovelace amount (micro-ADA) */\n lovelace_amount: bigint;\n /** Native assets attached to task */\n native_assets: readonly NativeAsset[];\n}\n\n/**\n * Compute the task hash (token name / task_hash) from task data.\n *\n * This produces the same hash as the on-chain Aiken hash_project_data function,\n * allowing clients to pre-compute or verify task hashes.\n *\n * @param task - Task data object\n * @returns 64-character hex string (256-bit Blake2b hash)\n * @throws Error if task data validation fails\n *\n * @example\n * ```typescript\n * import { computeTaskHash } from \"@andamio/core/hashing\";\n *\n * const task = {\n * project_content: \"Open Task #1\",\n * expiration_time: 1769027280000n,\n * lovelace_amount: 15000000n,\n * native_assets: []\n * };\n *\n * const taskHash = computeTaskHash(task);\n * // Returns the on-chain task_hash\n * ```\n */\nexport function computeTaskHash(task: TaskData): string {\n // Validate inputs\n validateTaskData(task);\n\n // Encode task as raw bytes matching Aiken format\n const bytes = encodeTaskAsRawBytes(task);\n\n // Hash with Blake2b-256\n return blake.blake2bHex(bytes, undefined, 32);\n}\n\n/**\n * Verify that a given hash matches the computed hash for a task.\n *\n * @param task - Task data object\n * @param expectedHash - The hash to verify (64-character hex string)\n * @returns true if the computed hash matches the expected hash\n */\nexport function verifyTaskHash(task: TaskData, expectedHash: string): boolean {\n const computedHash = computeTaskHash(task);\n return computedHash.toLowerCase() === expectedHash.toLowerCase();\n}\n\n/**\n * Validate that a string is a valid task hash format.\n *\n * Task hashes are 64-character hexadecimal strings (256-bit Blake2b hash).\n */\nexport function isValidTaskHash(hash: string): boolean {\n if (hash.length !== 64) {\n return false;\n }\n return /^[0-9a-fA-F]{64}$/.test(hash);\n}\n\n/**\n * Debug function to show the raw byte encoding of a task.\n * Useful for comparing against on-chain data.\n *\n * @param task - Task data object\n * @returns Hex string of the encoded data (before hashing)\n */\nexport function debugTaskBytes(task: TaskData): string {\n validateTaskData(task);\n const bytes = encodeTaskAsRawBytes(task);\n return uint8ArrayToHex(bytes);\n}\n\n// =============================================================================\n// Raw Byte Encoding (Internal) - Matches Aiken hash_project_data\n// =============================================================================\n\n/**\n * Encode task data as raw bytes matching Aiken's hash_project_data format.\n *\n * Format: project_content ++ int_to_bbs(deadline) ++ int_to_bbs(lovelace) ++ combine_flat_val(tokens)\n *\n * @internal\n */\nfunction encodeTaskAsRawBytes(task: TaskData): Uint8Array {\n // Normalize Unicode for consistent hashing\n const normalizedContent = task.project_content.normalize(\"NFC\");\n\n return concatUint8Arrays([\n new TextEncoder().encode(normalizedContent),\n intToBytesLittleEndian(task.expiration_time),\n intToBytesLittleEndian(task.lovelace_amount),\n combineNativeAssets(task.native_assets),\n ]);\n}\n\n/**\n * Validate TaskData before hashing.\n *\n * @throws Error if validation fails\n * @internal\n */\nfunction validateTaskData(task: TaskData): void {\n // Validate project_content\n if (task.project_content.length > 140) {\n throw new Error(\n `project_content exceeds 140 characters (got ${task.project_content.length})`,\n );\n }\n\n // Validate numeric fields\n if (task.expiration_time < 0n) {\n throw new Error(\"expiration_time must be non-negative\");\n }\n if (task.lovelace_amount < 0n) {\n throw new Error(\"lovelace_amount must be non-negative\");\n }\n\n // Validate native assets\n for (const [policyId, tokenName, quantity] of task.native_assets) {\n if (policyId.length !== 56) {\n throw new Error(\n `policyId must be 56 hex chars (got ${policyId.length})`,\n );\n }\n if (!/^[0-9a-fA-F]*$/.test(policyId)) {\n throw new Error(\"policyId contains invalid hex characters\");\n }\n if (tokenName.length > 64 || tokenName.length % 2 !== 0) {\n throw new Error(\n `tokenName must be 0-64 hex chars with even length (got ${tokenName.length})`,\n );\n }\n if (tokenName.length > 0 && !/^[0-9a-fA-F]*$/.test(tokenName)) {\n throw new Error(\"tokenName contains invalid hex characters\");\n }\n if (quantity < 0n) {\n throw new Error(\"asset quantity must be non-negative\");\n }\n }\n}\n\n/**\n * Convert a non-negative bigint to little-endian byte representation.\n * Matches Aiken's integer_to_bytearray(False, 0, int).\n *\n * - False = little-endian byte order\n * - 0 = minimal byte length (no zero-padding)\n *\n * @param n - Non-negative bigint to convert\n * @returns Uint8Array with little-endian byte representation\n * @internal\n */\nfunction intToBytesLittleEndian(n: bigint): Uint8Array {\n if (n < 0n) {\n throw new Error(\"Negative integers not supported\");\n }\n if (n === 0n) {\n return new Uint8Array([0]);\n }\n\n const bytes: number[] = [];\n let remaining = n;\n\n while (remaining > 0n) {\n bytes.push(Number(remaining & 0xffn));\n remaining = remaining >> 8n;\n }\n\n return new Uint8Array(bytes);\n}\n\n/**\n * Combine native assets into raw bytes matching Aiken's combine_flat_val.\n * Format: policy_id ++ token_name ++ quantity for each asset.\n *\n * @param assets - Array of native assets\n * @returns Uint8Array of concatenated asset bytes\n * @internal\n */\nfunction combineNativeAssets(assets: readonly NativeAsset[]): Uint8Array {\n if (assets.length === 0) {\n return new Uint8Array([]);\n }\n\n const chunks: Uint8Array[] = [];\n for (const [policyId, tokenName, quantity] of assets) {\n chunks.push(hexToBytes(policyId));\n chunks.push(hexToBytes(tokenName));\n chunks.push(intToBytesLittleEndian(quantity));\n }\n return concatUint8Arrays(chunks);\n}\n\n/**\n * Convert hex string to Uint8Array.\n *\n * @param hex - Hexadecimal string (must be even length)\n * @returns Uint8Array of bytes\n * @internal\n */\nfunction hexToBytes(hex: string): Uint8Array {\n if (hex.length === 0) {\n return new Uint8Array([]);\n }\n\n // Validation already done in validateTaskData, but defensive check\n if (hex.length % 2 !== 0) {\n throw new Error(`Invalid hex string: odd length (${hex.length})`);\n }\n\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n }\n return bytes;\n}\n\n/**\n * Concatenate multiple Uint8Arrays into one.\n *\n * @internal\n */\nfunction concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * Convert Uint8Array to hex string.\n *\n * @internal\n */\nfunction uint8ArrayToHex(arr: Uint8Array): string {\n return Array.from(arr)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n","/**\n * Commitment Hash Utility\n *\n * Functions for computing and verifying hashes of commitment evidence.\n * The hash is stored on-chain as `commitment_hash` in the course state datum,\n * while the full evidence (Tiptap JSON document) is stored in the database.\n *\n * This provides:\n * - Compact on-chain storage (64-char hex hash vs full JSON)\n * - Tamper-evidence (can verify DB content matches on-chain commitment)\n * - Privacy (evidence details not exposed on-chain)\n *\n * @module @andamio/core/hashing\n */\n\nimport blake from \"blakejs\";\n\n/**\n * Tiptap document structure (simplified)\n * The actual structure can be more complex with nested content\n */\nexport type TiptapDoc = {\n type: \"doc\";\n content?: TiptapNode[];\n [key: string]: unknown;\n};\n\nexport type TiptapNode = {\n type: string;\n content?: TiptapNode[];\n text?: string;\n marks?: TiptapMark[];\n attrs?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\nexport type TiptapMark = {\n type: string;\n attrs?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\n/**\n * Normalizes a value for consistent hashing.\n *\n * Normalization rules:\n * - Objects: Sort keys alphabetically, recursively normalize values\n * - Arrays: Preserve order, recursively normalize items\n * - Strings: Trim whitespace\n * - Numbers/Booleans/null: Keep as-is\n * - undefined: Convert to null\n *\n * @param value - Any JSON-serializable value\n * @returns Normalized value\n */\nexport function normalizeForHashing(value: unknown): unknown {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (typeof value === \"string\") {\n return value.trim();\n }\n\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(normalizeForHashing);\n }\n\n if (typeof value === \"object\") {\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(value as Record<string, unknown>).sort();\n for (const key of keys) {\n const val = (value as Record<string, unknown>)[key];\n if (val !== undefined) {\n sorted[key] = normalizeForHashing(val);\n }\n }\n return sorted;\n }\n\n return value;\n}\n\n/**\n * Computes the commitment hash from evidence content.\n *\n * The hash is computed as:\n * 1. Normalize the evidence (sort keys, trim strings, etc.)\n * 2. Serialize to JSON string (deterministic due to normalization)\n * 3. Apply Blake2b-256 hash\n *\n * @param evidence - The evidence content (Tiptap JSON document or any JSON-serializable data)\n * @returns 64-character lowercase hex string (Blake2b-256 hash)\n *\n * @example\n * ```typescript\n * import { computeCommitmentHash } from \"@andamio/core/hashing\";\n *\n * const evidence = {\n * type: \"doc\",\n * content: [\n * { type: \"paragraph\", content: [{ type: \"text\", text: \"My submission\" }] }\n * ]\n * };\n *\n * const hash = computeCommitmentHash(evidence);\n * // Use this hash as commitment_hash in the transaction\n * ```\n */\nexport function computeCommitmentHash(evidence: unknown): string {\n const normalized = normalizeForHashing(evidence);\n const jsonString = JSON.stringify(normalized);\n const bytes = new TextEncoder().encode(jsonString);\n return blake.blake2bHex(bytes, undefined, 32);\n}\n\n/**\n * Verifies that evidence content matches an expected hash.\n *\n * Use this to verify that database evidence matches the on-chain commitment.\n *\n * @param evidence - The evidence content to verify\n * @param expectedHash - The expected hash (from on-chain data)\n * @returns True if the evidence produces the expected hash\n */\nexport function verifyCommitmentHash(\n evidence: unknown,\n expectedHash: string\n): boolean {\n const computedHash = computeCommitmentHash(evidence);\n return computedHash.toLowerCase() === expectedHash.toLowerCase();\n}\n\n/**\n * Validates that a string is a valid commitment hash format.\n *\n * A valid hash is a 64-character hexadecimal string (Blake2b-256 output).\n *\n * @param hash - The string to validate\n * @returns True if the string is a valid hash format\n */\nexport function isValidCommitmentHash(hash: string): boolean {\n if (typeof hash !== \"string\") {\n return false;\n }\n if (hash.length !== 64) {\n return false;\n }\n return /^[0-9a-fA-F]{64}$/.test(hash);\n}\n\n/**\n * Result of comparing evidence with an on-chain hash\n */\nexport type EvidenceVerificationResult = {\n /** Whether the evidence matches the on-chain hash */\n isValid: boolean;\n /** The hash computed from the evidence */\n computedHash: string;\n /** The expected hash (from on-chain) */\n expectedHash: string;\n /** Human-readable status message */\n message: string;\n};\n\n/**\n * Performs a detailed verification of evidence against an on-chain hash.\n *\n * Returns a detailed result object with both hashes and a status message,\n * useful for debugging and user feedback.\n *\n * @param evidence - The evidence content to verify\n * @param onChainHash - The hash from on-chain data\n * @returns Detailed verification result\n */\nexport function verifyEvidenceDetailed(\n evidence: unknown,\n onChainHash: string\n): EvidenceVerificationResult {\n if (!isValidCommitmentHash(onChainHash)) {\n return {\n isValid: false,\n computedHash: \"\",\n expectedHash: onChainHash,\n message: `Invalid on-chain hash format: expected 64 hex characters, got \"${onChainHash}\"`,\n };\n }\n\n const computedHash = computeCommitmentHash(evidence);\n const isValid = computedHash.toLowerCase() === onChainHash.toLowerCase();\n\n return {\n isValid,\n computedHash,\n expectedHash: onChainHash.toLowerCase(),\n message: isValid\n ? \"Evidence matches on-chain commitment\"\n : \"Evidence does not match on-chain commitment - content may have been modified\",\n };\n}\n\n// =============================================================================\n// Backwards Compatibility Aliases (deprecated)\n// =============================================================================\n\n/**\n * @deprecated Use `computeCommitmentHash` instead.\n */\nexport const computeAssignmentInfoHash = computeCommitmentHash;\n\n/**\n * @deprecated Use `verifyCommitmentHash` instead.\n */\nexport const verifyAssignmentInfoHash = verifyCommitmentHash;\n\n/**\n * @deprecated Use `isValidCommitmentHash` instead.\n */\nexport const isValidAssignmentInfoHash = isValidCommitmentHash;\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/hashing/slt-hash.ts","../../../src/utils/hashing/task-hash.ts","../../../src/utils/hashing/commitment-hash.ts"],"names":["blake","concatUint8Arrays"],"mappings":";;;AAyBA,IAAM,iBAAA,GAAoB,EAAA;AA0BnB,SAAS,eAAe,IAAA,EAAwB;AACrD,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,CAAC,GAAA,KAAQ,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,GAAG,CAAC,CAAA;AAChE,EAAA,MAAM,QAAA,GAAW,oBAAoB,QAAQ,CAAA;AAC7C,EAAA,OAAO,KAAA,CAAM,UAAA,CAAW,QAAA,EAAU,MAAA,EAAW,EAAE,CAAA;AACjD;AAKO,IAAM,sBAAA,GAAyB;AAS/B,SAAS,aAAA,CAAc,MAAgB,YAAA,EAA+B;AAC3E,EAAA,MAAM,YAAA,GAAe,eAAe,IAAI,CAAA;AACxC,EAAA,OAAO,YAAA,CAAa,WAAA,EAAY,KAAM,YAAA,CAAa,WAAA,EAAY;AACjE;AAUO,SAAS,eAAe,IAAA,EAAuB;AACpD,EAAA,IAAI,IAAA,CAAK,WAAW,EAAA,EAAI;AACtB,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,mBAAA,CAAoB,KAAK,IAAI,CAAA;AACtC;AAaA,SAAS,oBAAoB,KAAA,EAAiC;AAC5D,EAAA,MAAM,SAAuB,EAAC;AAG9B,EAAA,MAAA,CAAO,KAAK,IAAI,UAAA,CAAW,CAAC,GAAI,CAAC,CAAC,CAAA;AAGlC,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAA,CAAO,IAAA,CAAK,6BAAA,CAA8B,IAAI,CAAC,CAAA;AAAA,EACjD;AAGA,EAAA,MAAA,CAAO,KAAK,IAAI,UAAA,CAAW,CAAC,GAAI,CAAC,CAAC,CAAA;AAElC,EAAA,OAAO,kBAAkB,MAAM,CAAA;AACjC;AAUA,SAAS,8BAA8B,MAAA,EAAgC;AACrE,EAAA,IAAI,MAAA,CAAO,UAAU,iBAAA,EAAmB;AAEtC,IAAA,OAAO,qBAAqB,MAAM,CAAA;AAAA,EACpC;AAGA,EAAA,MAAM,SAAuB,EAAC;AAC9B,EAAA,MAAA,CAAO,KAAK,IAAI,UAAA,CAAW,CAAC,EAAI,CAAC,CAAC,CAAA;AAElC,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,MAAA,CAAO,MAAA,EAAQ,KAAK,iBAAA,EAAmB;AACzD,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,QAAA,CAAS,CAAA,EAAG,IAAA,CAAK,IAAI,CAAA,GAAI,iBAAA,EAAmB,MAAA,CAAO,MAAM,CAAC,CAAA;AAC/E,IAAA,MAAA,CAAO,IAAA,CAAK,oBAAA,CAAqB,KAAK,CAAC,CAAA;AAAA,EACzC;AAEA,EAAA,MAAA,CAAO,KAAK,IAAI,UAAA,CAAW,CAAC,GAAI,CAAC,CAAC,CAAA;AAClC,EAAA,OAAO,kBAAkB,MAAM,CAAA;AACjC;AAOA,SAAS,qBAAqB,MAAA,EAAgC;AAC5D,EAAA,MAAM,MAAM,MAAA,CAAO,MAAA;AAMnB,EAAA,IAAI,OAAO,EAAA,EAAI;AACb,IAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,CAAA,GAAI,GAAG,CAAA;AACrC,IAAA,MAAA,CAAO,CAAC,IAAI,EAAA,GAAO,GAAA;AACnB,IAAA,MAAA,CAAO,GAAA,CAAI,QAAQ,CAAC,CAAA;AACpB,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,MAAA,IAAW,OAAO,GAAA,EAAK;AACrB,IAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,CAAA,GAAI,GAAG,CAAA;AACrC,IAAA,MAAA,CAAO,CAAC,CAAA,GAAI,EAAA;AACZ,IAAA,MAAA,CAAO,CAAC,CAAA,GAAI,GAAA;AACZ,IAAA,MAAA,CAAO,GAAA,CAAI,QAAQ,CAAC,CAAA;AACpB,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,MAAA,IAAW,OAAO,KAAA,EAAO;AACvB,IAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,CAAA,GAAI,GAAG,CAAA;AACrC,IAAA,MAAA,CAAO,CAAC,CAAA,GAAI,EAAA;AACZ,IAAA,MAAA,CAAO,CAAC,IAAI,GAAA,IAAO,CAAA;AACnB,IAAA,MAAA,CAAO,CAAC,IAAI,GAAA,GAAM,GAAA;AAClB,IAAA,MAAA,CAAO,GAAA,CAAI,QAAQ,CAAC,CAAA;AACpB,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAC1D;AAOA,SAAS,kBAAkB,MAAA,EAAkC;AAC3D,EAAA,MAAM,WAAA,GAAc,OAAO,MAAA,CAAO,CAAC,KAAK,GAAA,KAAQ,GAAA,GAAM,GAAA,CAAI,MAAA,EAAQ,CAAC,CAAA;AACnE,EAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,WAAW,CAAA;AACzC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACxB,IAAA,MAAA,CAAO,GAAA,CAAI,KAAK,MAAM,CAAA;AACtB,IAAA,MAAA,IAAU,GAAA,CAAI,MAAA;AAAA,EAChB;AACA,EAAA,OAAO,MAAA;AACT;ACjHO,SAAS,gBAAgB,IAAA,EAAwB;AACtD,EAAA,gBAAA,CAAiB,IAAI,CAAA;AACrB,EAAA,MAAM,KAAA,GAAQ,uBAAuB,IAAI,CAAA;AACzC,EAAA,OAAOA,KAAAA,CAAM,UAAA,CAAW,KAAA,EAAO,MAAA,EAAW,EAAE,CAAA;AAC9C;AASO,SAAS,cAAA,CAAe,MAAgB,YAAA,EAA+B;AAC5E,EAAA,MAAM,YAAA,GAAe,gBAAgB,IAAI,CAAA;AACzC,EAAA,OAAO,YAAA,CAAa,WAAA,EAAY,KAAM,YAAA,CAAa,WAAA,EAAY;AACjE;AAOO,SAAS,gBAAgB,IAAA,EAAuB;AACrD,EAAA,IAAI,IAAA,CAAK,WAAW,EAAA,EAAI;AACtB,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,mBAAA,CAAoB,KAAK,IAAI,CAAA;AACtC;AASO,SAAS,eAAe,IAAA,EAAwB;AACrD,EAAA,gBAAA,CAAiB,IAAI,CAAA;AACrB,EAAA,MAAM,KAAA,GAAQ,uBAAuB,IAAI,CAAA;AACzC,EAAA,OAAO,gBAAgB,KAAK,CAAA;AAC9B;AAgBA,SAAS,uBAAuB,IAAA,EAA4B;AAC1D,EAAA,MAAM,iBAAA,GAAoB,IAAA,CAAK,eAAA,CAAgB,SAAA,CAAU,KAAK,CAAA;AAC9D,EAAA,MAAM,YAAA,GAAe,IAAI,WAAA,EAAY,CAAE,OAAO,iBAAiB,CAAA;AAE/D,EAAA,OAAOC,kBAAAA,CAAkB;AAAA;AAAA,IAEvB,IAAI,UAAA,CAAW,CAAC,GAAA,EAAM,GAAA,EAAK,GAAI,CAAC,CAAA;AAAA;AAAA,IAEhC,gBAAgB,YAAY,CAAA;AAAA;AAAA,IAE5B,cAAA,CAAe,KAAK,eAAe,CAAA;AAAA;AAAA,IAEnC,cAAA,CAAe,KAAK,eAAe,CAAA;AAAA;AAAA,IAEnC,gBAAA,CAAiB,KAAK,aAAa,CAAA;AAAA;AAAA,IAEnC,IAAI,UAAA,CAAW,CAAC,GAAI,CAAC;AAAA,GACtB,CAAA;AACH;AAYA,SAAS,iBAAiB,MAAA,EAA4C;AACpE,EAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AAEvB,IAAA,OAAO,IAAI,UAAA,CAAW,CAAC,GAAI,CAAC,CAAA;AAAA,EAC9B;AAGA,EAAA,MAAM,QAAsB,CAAC,IAAI,WAAW,CAAC,GAAI,CAAC,CAAC,CAAA;AAEnD,EAAA,KAAA,MAAW,CAAC,QAAA,EAAU,SAAA,EAAW,QAAQ,KAAK,MAAA,EAAQ;AAEpD,IAAA,KAAA,CAAM,IAAA,CAAK,IAAI,UAAA,CAAW,CAAC,KAAM,GAAA,EAAK,GAAI,CAAC,CAAC,CAAA;AAC5C,IAAA,KAAA,CAAM,IAAA,CAAK,eAAA,CAAgB,UAAA,CAAW,QAAQ,CAAC,CAAC,CAAA;AAChD,IAAA,KAAA,CAAM,IAAA,CAAK,eAAA,CAAgB,UAAA,CAAW,SAAS,CAAC,CAAC,CAAA;AACjD,IAAA,KAAA,CAAM,IAAA,CAAK,cAAA,CAAe,QAAQ,CAAC,CAAA;AACnC,IAAA,KAAA,CAAM,KAAK,IAAI,UAAA,CAAW,CAAC,GAAI,CAAC,CAAC,CAAA;AAAA,EACnC;AAEA,EAAA,KAAA,CAAM,KAAK,IAAI,UAAA,CAAW,CAAC,GAAI,CAAC,CAAC,CAAA;AACjC,EAAA,OAAOA,mBAAkB,KAAK,CAAA;AAChC;AAMA,IAAM,UAAA,GAAa,qBAAA;AAOnB,SAAS,eAAe,CAAA,EAAuB;AAC7C,EAAA,IAAI,IAAI,EAAA,EAAI;AACV,IAAA,MAAM,IAAI,MAAM,iCAAiC,CAAA;AAAA,EACnD;AACA,EAAA,IAAI,IAAI,UAAA,EAAY;AAClB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,+CAAA,EAAkD,CAAC,CAAA,MAAA,EAAS,UAAU,CAAA,CAAA;AAAA,KACxE;AAAA,EACF;AAEA,EAAA,IAAI,IAAI,GAAA,EAAK;AACX,IAAA,OAAO,IAAI,UAAA,CAAW,CAAC,MAAA,CAAO,CAAC,CAAC,CAAC,CAAA;AAAA,EACnC,CAAA,MAAA,IAAW,IAAI,IAAA,EAAM;AACnB,IAAA,OAAO,IAAI,UAAA,CAAW,CAAC,IAAM,MAAA,CAAO,CAAC,CAAC,CAAC,CAAA;AAAA,EACzC,CAAA,MAAA,IAAW,IAAI,MAAA,EAAQ;AACrB,IAAA,OAAO,IAAI,UAAA,CAAW,CAAC,EAAA,EAAM,MAAA,CAAO,CAAA,IAAK,EAAE,CAAA,GAAI,GAAA,EAAM,MAAA,CAAO,CAAC,CAAA,GAAI,GAAI,CAAC,CAAA;AAAA,EACxE,CAAA,MAAA,IAAW,IAAI,WAAA,EAAa;AAC1B,IAAA,OAAO,IAAI,UAAA,CAAW;AAAA,MACpB,EAAA;AAAA,MACA,MAAA,CAAQ,CAAA,IAAK,GAAA,GAAO,KAAK,CAAA;AAAA,MACzB,MAAA,CAAQ,CAAA,IAAK,GAAA,GAAO,KAAK,CAAA;AAAA,MACzB,MAAA,CAAQ,CAAA,IAAK,EAAA,GAAM,KAAK,CAAA;AAAA,MACxB,MAAA,CAAO,IAAI,KAAK;AAAA,KACjB,CAAA;AAAA,EACH,CAAA,MAAO;AAEL,IAAA,OAAO,IAAI,UAAA,CAAW;AAAA,MACpB,EAAA;AAAA,MACA,MAAA,CAAQ,CAAA,IAAK,GAAA,GAAO,KAAK,CAAA;AAAA,MACzB,MAAA,CAAQ,CAAA,IAAK,GAAA,GAAO,KAAK,CAAA;AAAA,MACzB,MAAA,CAAQ,CAAA,IAAK,GAAA,GAAO,KAAK,CAAA;AAAA,MACzB,MAAA,CAAQ,CAAA,IAAK,GAAA,GAAO,KAAK,CAAA;AAAA,MACzB,MAAA,CAAQ,CAAA,IAAK,GAAA,GAAO,KAAK,CAAA;AAAA,MACzB,MAAA,CAAQ,CAAA,IAAK,GAAA,GAAO,KAAK,CAAA;AAAA,MACzB,MAAA,CAAQ,CAAA,IAAK,EAAA,GAAM,KAAK,CAAA;AAAA,MACxB,MAAA,CAAO,IAAI,KAAK;AAAA,KACjB,CAAA;AAAA,EACH;AACF;AAOA,SAAS,gBAAgB,KAAA,EAA+B;AACtD,EAAA,MAAM,MAAM,KAAA,CAAM,MAAA;AAClB,EAAA,IAAI,MAAA;AAEJ,EAAA,IAAI,MAAM,EAAA,EAAI;AACZ,IAAA,MAAA,GAAS,IAAI,UAAA,CAAW,CAAC,EAAA,GAAO,GAAG,CAAC,CAAA;AAAA,EACtC,CAAA,MAAA,IAAW,MAAM,GAAA,EAAK;AACpB,IAAA,MAAA,GAAS,IAAI,UAAA,CAAW,CAAC,EAAA,EAAM,GAAG,CAAC,CAAA;AAAA,EACrC,CAAA,MAAA,IAAW,MAAM,KAAA,EAAO;AACtB,IAAA,MAAA,GAAS,IAAI,WAAW,CAAC,EAAA,EAAO,OAAO,CAAA,GAAK,GAAA,EAAM,GAAA,GAAM,GAAI,CAAC,CAAA;AAAA,EAC/D,CAAA,MAAO;AACL,IAAA,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAAA,EAC1D;AAEA,EAAA,OAAOA,kBAAAA,CAAkB,CAAC,MAAA,EAAQ,KAAK,CAAC,CAAA;AAC1C;AAQA,SAAS,iBAAiB,IAAA,EAAsB;AAC9C,EAAA,IAAI,IAAA,CAAK,eAAA,CAAgB,MAAA,GAAS,GAAA,EAAK;AACrC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,4CAAA,EAA+C,IAAA,CAAK,eAAA,CAAgB,MAAM,CAAA,CAAA;AAAA,KAC5E;AAAA,EACF;AAEA,EAAA,IAAI,IAAA,CAAK,kBAAkB,EAAA,EAAI;AAC7B,IAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,EACxD;AACA,EAAA,IAAI,IAAA,CAAK,kBAAkB,EAAA,EAAI;AAC7B,IAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,EACxD;AAEA,EAAA,KAAA,MAAW,CAAC,QAAA,EAAU,SAAA,EAAW,QAAQ,CAAA,IAAK,KAAK,aAAA,EAAe;AAChE,IAAA,IAAI,QAAA,CAAS,WAAW,EAAA,EAAI;AAC1B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,mCAAA,EAAsC,SAAS,MAAM,CAAA,CAAA;AAAA,OACvD;AAAA,IACF;AACA,IAAA,IAAI,CAAC,gBAAA,CAAiB,IAAA,CAAK,QAAQ,CAAA,EAAG;AACpC,MAAA,MAAM,IAAI,MAAM,0CAA0C,CAAA;AAAA,IAC5D;AACA,IAAA,IAAI,UAAU,MAAA,GAAS,EAAA,IAAM,SAAA,CAAU,MAAA,GAAS,MAAM,CAAA,EAAG;AACvD,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,uDAAA,EAA0D,UAAU,MAAM,CAAA,CAAA;AAAA,OAC5E;AAAA,IACF;AACA,IAAA,IAAI,UAAU,MAAA,GAAS,CAAA,IAAK,CAAC,gBAAA,CAAiB,IAAA,CAAK,SAAS,CAAA,EAAG;AAC7D,MAAA,MAAM,IAAI,MAAM,2CAA2C,CAAA;AAAA,IAC7D;AACA,IAAA,IAAI,WAAW,EAAA,EAAI;AACjB,MAAA,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAAA,IACvD;AAAA,EACF;AACF;AAOA,SAAS,WAAW,GAAA,EAAyB;AAC3C,EAAA,IAAI,GAAA,CAAI,WAAW,CAAA,EAAG;AACpB,IAAA,OAAO,IAAI,UAAA,CAAW,EAAE,CAAA;AAAA,EAC1B;AAEA,EAAA,IAAI,GAAA,CAAI,MAAA,GAAS,CAAA,KAAM,CAAA,EAAG;AACxB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,gCAAA,EAAmC,GAAA,CAAI,MAAM,CAAA,CAAA,CAAG,CAAA;AAAA,EAClE;AAEA,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,GAAA,CAAI,SAAS,CAAC,CAAA;AAC3C,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAM,IAAA,GAAO,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,CAAA,GAAI,GAAG,CAAA,GAAI,CAAA,GAAI,CAAC,CAAA,EAAG,EAAE,CAAA;AACrD,IAAA,IAAI,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA,EAAG;AACtB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,kCAAA,EAAqC,CAAA,GAAI,CAAC,CAAA,GAAA,EAAM,GAAA,CAAI,KAAA,CAAM,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,GAAI,CAAC,CAAC,CAAA,CAAA;AAAA,OAC7E;AAAA,IACF;AACA,IAAA,KAAA,CAAM,CAAC,CAAA,GAAI,IAAA;AAAA,EACb;AACA,EAAA,OAAO,KAAA;AACT;AAOA,SAASA,mBAAkB,MAAA,EAAkC;AAC3D,EAAA,MAAM,WAAA,GAAc,OAAO,MAAA,CAAO,CAAC,KAAK,GAAA,KAAQ,GAAA,GAAM,GAAA,CAAI,MAAA,EAAQ,CAAC,CAAA;AACnE,EAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,WAAW,CAAA;AACzC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACxB,IAAA,MAAA,CAAO,GAAA,CAAI,KAAK,MAAM,CAAA;AACtB,IAAA,MAAA,IAAU,GAAA,CAAI,MAAA;AAAA,EAChB;AACA,EAAA,OAAO,MAAA;AACT;AAOA,SAAS,gBAAgB,GAAA,EAAyB;AAChD,EAAA,OAAO,MAAM,IAAA,CAAK,GAAG,CAAA,CAClB,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAC1C,KAAK,EAAE,CAAA;AACZ;AC7SO,SAAS,oBAAoB,KAAA,EAAyB;AAC3D,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW;AACzC,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,OAAO,MAAM,IAAA,EAAK;AAAA,EACpB;AAEA,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,OAAO,UAAU,SAAA,EAAW;AAC3D,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,IAAA,OAAO,KAAA,CAAM,IAAI,mBAAmB,CAAA;AAAA,EACtC;AAEA,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,MAAM,SAAkC,EAAC;AACzC,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,KAAgC,EAAE,IAAA,EAAK;AAChE,IAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,MAAA,MAAM,GAAA,GAAO,MAAkC,GAAG,CAAA;AAClD,MAAA,IAAI,QAAQ,MAAA,EAAW;AACrB,QAAA,MAAA,CAAO,GAAG,CAAA,GAAI,mBAAA,CAAoB,GAAG,CAAA;AAAA,MACvC;AAAA,IACF;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO,KAAA;AACT;AA4BO,SAAS,sBAAsB,QAAA,EAA2B;AAC/D,EAAA,MAAM,UAAA,GAAa,oBAAoB,QAAQ,CAAA;AAC/C,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,SAAA,CAAU,UAAU,CAAA;AAC5C,EAAA,MAAM,KAAA,GAAQ,IAAI,WAAA,EAAY,CAAE,OAAO,UAAU,CAAA;AACjD,EAAA,OAAOD,KAAAA,CAAM,UAAA,CAAW,KAAA,EAAO,MAAA,EAAW,EAAE,CAAA;AAC9C;AAWO,SAAS,oBAAA,CACd,UACA,YAAA,EACS;AACT,EAAA,MAAM,YAAA,GAAe,sBAAsB,QAAQ,CAAA;AACnD,EAAA,OAAO,YAAA,CAAa,WAAA,EAAY,KAAM,YAAA,CAAa,WAAA,EAAY;AACjE;AAUO,SAAS,sBAAsB,IAAA,EAAuB;AAC3D,EAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,IAAA,CAAK,WAAW,EAAA,EAAI;AACtB,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,mBAAA,CAAoB,KAAK,IAAI,CAAA;AACtC;AA0BO,SAAS,sBAAA,CACd,UACA,WAAA,EAC4B;AAC5B,EAAA,IAAI,CAAC,qBAAA,CAAsB,WAAW,CAAA,EAAG;AACvC,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,KAAA;AAAA,MACT,YAAA,EAAc,EAAA;AAAA,MACd,YAAA,EAAc,WAAA;AAAA,MACd,OAAA,EAAS,kEAAkE,WAAW,CAAA,CAAA;AAAA,KACxF;AAAA,EACF;AAEA,EAAA,MAAM,YAAA,GAAe,sBAAsB,QAAQ,CAAA;AACnD,EAAA,MAAM,OAAA,GAAU,YAAA,CAAa,WAAA,EAAY,KAAM,YAAY,WAAA,EAAY;AAEvE,EAAA,OAAO;AAAA,IACL,OAAA;AAAA,IACA,YAAA;AAAA,IACA,YAAA,EAAc,YAAY,WAAA,EAAY;AAAA,IACtC,OAAA,EAAS,UACL,sCAAA,GACA;AAAA,GACN;AACF;AASO,IAAM,yBAAA,GAA4B;AAKlC,IAAM,wBAAA,GAA2B;AAKjC,IAAM,yBAAA,GAA4B","file":"index.mjs","sourcesContent":["/**\n * SLT Hash Utility\n *\n * Computes the module token name (hash) from a list of Student Learning Targets (SLTs).\n * This hash is used as the token name when minting module tokens on-chain.\n *\n * The algorithm matches the on-chain Plutus validator:\n * ```haskell\n * sltsToBbs MintModuleV2{slts} = blake2b_256 $ serialiseData $ toBuiltinData $ map stringToBuiltinByteString slts\n * ```\n *\n * Serialization format:\n * 1. Convert each SLT string to UTF-8 bytes\n * 2. Encode as CBOR indefinite-length array of byte strings\n * 3. Hash with Blake2b-256 (32 bytes / 256 bits)\n *\n * @module @andamio/core/hashing\n */\n\nimport blake from \"blakejs\";\n\n/**\n * Plutus chunk size for byte strings.\n * Strings longer than this are encoded as indefinite-length chunked byte strings.\n */\nconst PLUTUS_CHUNK_SIZE = 64;\n\n/**\n * Compute the module hash matching Plutus on-chain encoding.\n *\n * Plutus's `stringToBuiltinByteString` chunks byte strings at 64 bytes.\n * This function replicates that behavior:\n * - Strings <= 64 bytes: encoded as regular CBOR byte strings\n * - Strings > 64 bytes: encoded as indefinite-length chunked byte strings\n *\n * @param slts - Array of Student Learning Target strings\n * @returns 64-character hex string (256-bit Blake2b hash)\n *\n * @example\n * ```typescript\n * import { computeSltHash } from \"@andamio/core/hashing\";\n *\n * const slts = [\n * \"I can mint an access token.\",\n * \"I can complete an assignment to earn a credential.\"\n * ];\n *\n * const moduleHash = computeSltHash(slts);\n * // Returns: \"8dcbe1b925d87e6c547bbd8071c23a712db4c32751454b0948f8c846e9246b5c\"\n * ```\n */\nexport function computeSltHash(slts: string[]): string {\n const sltBytes = slts.map((slt) => new TextEncoder().encode(slt));\n const cborData = encodeAsPlutusArray(sltBytes);\n return blake.blake2bHex(cborData, undefined, 32);\n}\n\n/**\n * @deprecated Use `computeSltHash` instead. This alias is kept for backwards compatibility.\n */\nexport const computeSltHashDefinite = computeSltHash;\n\n/**\n * Verify that a given hash matches the computed hash for SLTs.\n *\n * @param slts - Array of Student Learning Target strings\n * @param expectedHash - The hash to verify (64-character hex string)\n * @returns true if the computed hash matches the expected hash\n */\nexport function verifySltHash(slts: string[], expectedHash: string): boolean {\n const computedHash = computeSltHash(slts);\n return computedHash.toLowerCase() === expectedHash.toLowerCase();\n}\n\n/**\n * Validate that a string is a valid SLT hash format.\n *\n * SLT hashes are 64-character hexadecimal strings (256-bit Blake2b hash).\n *\n * @param hash - String to validate\n * @returns true if the string is a valid SLT hash format\n */\nexport function isValidSltHash(hash: string): boolean {\n if (hash.length !== 64) {\n return false;\n }\n return /^[0-9a-fA-F]{64}$/.test(hash);\n}\n\n// =============================================================================\n// Plutus-Compatible Encoding (Internal)\n// =============================================================================\n\n/**\n * Encode an array of byte buffers matching Plutus serialization.\n *\n * Uses indefinite-length array with chunked byte strings for long values.\n *\n * @internal\n */\nfunction encodeAsPlutusArray(items: Uint8Array[]): Uint8Array {\n const chunks: Uint8Array[] = [];\n\n // Start indefinite array\n chunks.push(new Uint8Array([0x9f]));\n\n // Encode each item (with chunking for long strings)\n for (const item of items) {\n chunks.push(encodePlutusBuiltinByteString(item));\n }\n\n // End indefinite array\n chunks.push(new Uint8Array([0xff]));\n\n return concatUint8Arrays(chunks);\n}\n\n/**\n * Encode a byte buffer matching Plutus's stringToBuiltinByteString.\n *\n * - Strings <= 64 bytes: regular CBOR byte string\n * - Strings > 64 bytes: indefinite-length chunked byte string (64-byte chunks)\n *\n * @internal\n */\nfunction encodePlutusBuiltinByteString(buffer: Uint8Array): Uint8Array {\n if (buffer.length <= PLUTUS_CHUNK_SIZE) {\n // Short string: encode normally\n return encodeCBORByteString(buffer);\n }\n\n // Long string: use indefinite-length chunked encoding\n const chunks: Uint8Array[] = [];\n chunks.push(new Uint8Array([0x5f])); // Start indefinite byte string\n\n for (let i = 0; i < buffer.length; i += PLUTUS_CHUNK_SIZE) {\n const chunk = buffer.subarray(i, Math.min(i + PLUTUS_CHUNK_SIZE, buffer.length));\n chunks.push(encodeCBORByteString(chunk));\n }\n\n chunks.push(new Uint8Array([0xff])); // Break\n return concatUint8Arrays(chunks);\n}\n\n/**\n * Encode a byte buffer as a CBOR byte string (definite length).\n *\n * @internal\n */\nfunction encodeCBORByteString(buffer: Uint8Array): Uint8Array {\n const len = buffer.length;\n\n // CBOR byte string encoding (major type 2 = 0x40):\n // - 0-23 bytes: length inline (0x40 + len)\n // - 24-255 bytes: 0x58 + 1-byte length\n // - 256-65535 bytes: 0x59 + 2-byte length (big-endian)\n if (len <= 23) {\n const result = new Uint8Array(1 + len);\n result[0] = 0x40 + len;\n result.set(buffer, 1);\n return result;\n } else if (len <= 255) {\n const result = new Uint8Array(2 + len);\n result[0] = 0x58;\n result[1] = len;\n result.set(buffer, 2);\n return result;\n } else if (len <= 65535) {\n const result = new Uint8Array(3 + len);\n result[0] = 0x59;\n result[1] = len >> 8;\n result[2] = len & 0xff;\n result.set(buffer, 3);\n return result;\n }\n throw new Error(\"Byte string too long for CBOR encoding\");\n}\n\n/**\n * Concatenate multiple Uint8Arrays into one\n *\n * @internal\n */\nfunction concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n","/**\n * Task Hash Utility\n *\n * Computes the task token name (hash) from task data.\n * This hash is used as the task_hash on-chain (content-addressed identifier).\n *\n * The algorithm matches the on-chain Aiken/Haskell hash_project_data function:\n * - Serialize task data as Plutus Data (CBOR with tag 121 for Constructor 0)\n * - Use indefinite-length arrays for constructor fields\n * - Hash with Blake2b-256\n *\n * @module @andamio/core/hashing\n */\n\nimport blake from \"blakejs\";\n\n/**\n * Native asset in Cardano format: [policyId, tokenName, quantity]\n *\n * @example\n * ```typescript\n * const asset: NativeAsset = [\n * \"abc123def456...\".repeat(4), // 56 hex chars (28 bytes) - policy ID\n * \"746f6b656e\", // hex-encoded token name\n * 1000n // quantity as bigint\n * ];\n * ```\n */\nexport type NativeAsset = [\n policyId: string, // 56 hex chars (28 bytes)\n tokenName: string, // hex encoded (0-64 chars / 0-32 bytes)\n quantity: bigint, // arbitrary precision integer\n];\n\n/**\n * Task data structure matching the Aiken ProjectData type.\n *\n * Serialized as Plutus Data Constructor 0 (CBOR tag 121) with fields:\n * 1. project_content (ByteArray - UTF-8 encoded, NFC normalized)\n * 2. deadline (Int - milliseconds)\n * 3. lovelace_am (Int - micro-ADA)\n * 4. tokens (List<FlatValue> - native assets)\n */\nexport interface TaskData {\n /** Task description (max 140 characters) */\n project_content: string;\n /** Unix timestamp in milliseconds */\n expiration_time: bigint;\n /** Lovelace amount (micro-ADA) */\n lovelace_amount: bigint;\n /** Native assets attached to task */\n native_assets: readonly NativeAsset[];\n}\n\n/**\n * Compute the task hash (token name / task_hash) from task data.\n *\n * This produces the same hash as the on-chain Aiken hash_project_data function,\n * allowing clients to pre-compute or verify task hashes.\n *\n * @param task - Task data object\n * @returns 64-character hex string (256-bit Blake2b hash)\n * @throws Error if task data validation fails\n *\n * @example\n * ```typescript\n * import { computeTaskHash } from \"@andamio/core/hashing\";\n *\n * const task = {\n * project_content: \"Open Task #1\",\n * expiration_time: 1769027280000n,\n * lovelace_amount: 15000000n,\n * native_assets: []\n * };\n *\n * const taskHash = computeTaskHash(task);\n * // Returns the on-chain task_hash\n * ```\n */\nexport function computeTaskHash(task: TaskData): string {\n validateTaskData(task);\n const bytes = encodeTaskAsPlutusData(task);\n return blake.blake2bHex(bytes, undefined, 32);\n}\n\n/**\n * Verify that a given hash matches the computed hash for a task.\n *\n * @param task - Task data object\n * @param expectedHash - The hash to verify (64-character hex string)\n * @returns true if the computed hash matches the expected hash\n */\nexport function verifyTaskHash(task: TaskData, expectedHash: string): boolean {\n const computedHash = computeTaskHash(task);\n return computedHash.toLowerCase() === expectedHash.toLowerCase();\n}\n\n/**\n * Validate that a string is a valid task hash format.\n *\n * Task hashes are 64-character hexadecimal strings (256-bit Blake2b hash).\n */\nexport function isValidTaskHash(hash: string): boolean {\n if (hash.length !== 64) {\n return false;\n }\n return /^[0-9a-fA-F]{64}$/.test(hash);\n}\n\n/**\n * Debug function to show the CBOR encoding of a task.\n * Useful for comparing against on-chain data.\n *\n * @param task - Task data object\n * @returns Hex string of the CBOR-encoded Plutus Data (before hashing)\n */\nexport function debugTaskBytes(task: TaskData): string {\n validateTaskData(task);\n const bytes = encodeTaskAsPlutusData(task);\n return uint8ArrayToHex(bytes);\n}\n\n// =============================================================================\n// Plutus Data CBOR Encoding (Internal)\n// =============================================================================\n\n/**\n * Encode task data as Plutus Data matching Aiken/Haskell serialization.\n *\n * Format: tag(121) + indefinite-array + [content, deadline, lovelace, tokens] + break\n *\n * The key insight is that Haskell's `serialiseData . toBuiltinData` uses\n * indefinite-length CBOR arrays (0x9f ... 0xff) for Plutus Data constructors.\n *\n * @internal\n */\nfunction encodeTaskAsPlutusData(task: TaskData): Uint8Array {\n const normalizedContent = task.project_content.normalize(\"NFC\");\n const contentBytes = new TextEncoder().encode(normalizedContent);\n\n return concatUint8Arrays([\n // Tag 121 (Plutus Data Constructor 0) + indefinite array start\n new Uint8Array([0xd8, 121, 0x9f]),\n // Field 1: project_content (ByteArray)\n encodeCborBytes(contentBytes),\n // Field 2: deadline (Int)\n encodeCborUint(task.expiration_time),\n // Field 3: lovelace_am (Int)\n encodeCborUint(task.lovelace_amount),\n // Field 4: tokens (List<FlatValue>)\n encodeTokensList(task.native_assets),\n // Break (end of indefinite array)\n new Uint8Array([0xff]),\n ]);\n}\n\n/**\n * Encode a list of native assets as Plutus Data.\n *\n * Each FlatValue is encoded as a Plutus Data constructor with:\n * - PolicyId (ByteArray)\n * - AssetName (ByteArray)\n * - Quantity (Int)\n *\n * @internal\n */\nfunction encodeTokensList(assets: readonly NativeAsset[]): Uint8Array {\n if (assets.length === 0) {\n // Empty definite-length array\n return new Uint8Array([0x80]);\n }\n\n // Encode as indefinite-length array of FlatValue constructors\n const parts: Uint8Array[] = [new Uint8Array([0x9f])]; // indefinite array start\n\n for (const [policyId, tokenName, quantity] of assets) {\n // Each FlatValue is Constructor 0 with 3 fields\n parts.push(new Uint8Array([0xd8, 121, 0x9f])); // tag 121, indefinite array\n parts.push(encodeCborBytes(hexToBytes(policyId)));\n parts.push(encodeCborBytes(hexToBytes(tokenName)));\n parts.push(encodeCborUint(quantity));\n parts.push(new Uint8Array([0xff])); // break\n }\n\n parts.push(new Uint8Array([0xff])); // break (end of list)\n return concatUint8Arrays(parts);\n}\n\n/**\n * Maximum value for CBOR uint64 encoding.\n * @internal\n */\nconst MAX_UINT64 = 18446744073709551615n; // 2^64 - 1\n\n/**\n * Encode CBOR unsigned integer (major type 0).\n *\n * @internal\n */\nfunction encodeCborUint(n: bigint): Uint8Array {\n if (n < 0n) {\n throw new Error(\"Negative integers not supported\");\n }\n if (n > MAX_UINT64) {\n throw new Error(\n `Integer exceeds maximum CBOR uint64 value (got ${n}, max ${MAX_UINT64})`,\n );\n }\n\n if (n < 24n) {\n return new Uint8Array([Number(n)]);\n } else if (n < 256n) {\n return new Uint8Array([0x18, Number(n)]);\n } else if (n < 65536n) {\n return new Uint8Array([0x19, Number(n >> 8n) & 0xff, Number(n) & 0xff]);\n } else if (n < 4294967296n) {\n return new Uint8Array([\n 0x1a,\n Number((n >> 24n) & 0xffn),\n Number((n >> 16n) & 0xffn),\n Number((n >> 8n) & 0xffn),\n Number(n & 0xffn),\n ]);\n } else {\n // 8-byte unsigned integer\n return new Uint8Array([\n 0x1b,\n Number((n >> 56n) & 0xffn),\n Number((n >> 48n) & 0xffn),\n Number((n >> 40n) & 0xffn),\n Number((n >> 32n) & 0xffn),\n Number((n >> 24n) & 0xffn),\n Number((n >> 16n) & 0xffn),\n Number((n >> 8n) & 0xffn),\n Number(n & 0xffn),\n ]);\n }\n}\n\n/**\n * Encode CBOR byte string (major type 2).\n *\n * @internal\n */\nfunction encodeCborBytes(bytes: Uint8Array): Uint8Array {\n const len = bytes.length;\n let header: Uint8Array;\n\n if (len < 24) {\n header = new Uint8Array([0x40 + len]);\n } else if (len < 256) {\n header = new Uint8Array([0x58, len]);\n } else if (len < 65536) {\n header = new Uint8Array([0x59, (len >> 8) & 0xff, len & 0xff]);\n } else {\n throw new Error(\"Byte string too long for CBOR encoding\");\n }\n\n return concatUint8Arrays([header, bytes]);\n}\n\n/**\n * Validate TaskData before hashing.\n *\n * @throws Error if validation fails\n * @internal\n */\nfunction validateTaskData(task: TaskData): void {\n if (task.project_content.length > 140) {\n throw new Error(\n `project_content exceeds 140 characters (got ${task.project_content.length})`,\n );\n }\n\n if (task.expiration_time < 0n) {\n throw new Error(\"expiration_time must be non-negative\");\n }\n if (task.lovelace_amount < 0n) {\n throw new Error(\"lovelace_amount must be non-negative\");\n }\n\n for (const [policyId, tokenName, quantity] of task.native_assets) {\n if (policyId.length !== 56) {\n throw new Error(\n `policyId must be 56 hex chars (got ${policyId.length})`,\n );\n }\n if (!/^[0-9a-fA-F]*$/.test(policyId)) {\n throw new Error(\"policyId contains invalid hex characters\");\n }\n if (tokenName.length > 64 || tokenName.length % 2 !== 0) {\n throw new Error(\n `tokenName must be 0-64 hex chars with even length (got ${tokenName.length})`,\n );\n }\n if (tokenName.length > 0 && !/^[0-9a-fA-F]*$/.test(tokenName)) {\n throw new Error(\"tokenName contains invalid hex characters\");\n }\n if (quantity < 0n) {\n throw new Error(\"asset quantity must be non-negative\");\n }\n }\n}\n\n/**\n * Convert hex string to Uint8Array.\n *\n * @internal\n */\nfunction hexToBytes(hex: string): Uint8Array {\n if (hex.length === 0) {\n return new Uint8Array([]);\n }\n\n if (hex.length % 2 !== 0) {\n throw new Error(`Invalid hex string: odd length (${hex.length})`);\n }\n\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n const byte = parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n if (Number.isNaN(byte)) {\n throw new Error(\n `Invalid hex character at position ${i * 2}: \"${hex.slice(i * 2, i * 2 + 2)}\"`,\n );\n }\n bytes[i] = byte;\n }\n return bytes;\n}\n\n/**\n * Concatenate multiple Uint8Arrays into one.\n *\n * @internal\n */\nfunction concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * Convert Uint8Array to hex string.\n *\n * @internal\n */\nfunction uint8ArrayToHex(arr: Uint8Array): string {\n return Array.from(arr)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n","/**\n * Commitment Hash Utility\n *\n * Functions for computing and verifying hashes of commitment evidence.\n * The hash is stored on-chain as `commitment_hash` in the course state datum,\n * while the full evidence (Tiptap JSON document) is stored in the database.\n *\n * This provides:\n * - Compact on-chain storage (64-char hex hash vs full JSON)\n * - Tamper-evidence (can verify DB content matches on-chain commitment)\n * - Privacy (evidence details not exposed on-chain)\n *\n * @module @andamio/core/hashing\n */\n\nimport blake from \"blakejs\";\n\n/**\n * Tiptap document structure (simplified)\n * The actual structure can be more complex with nested content\n */\nexport type TiptapDoc = {\n type: \"doc\";\n content?: TiptapNode[];\n [key: string]: unknown;\n};\n\nexport type TiptapNode = {\n type: string;\n content?: TiptapNode[];\n text?: string;\n marks?: TiptapMark[];\n attrs?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\nexport type TiptapMark = {\n type: string;\n attrs?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\n/**\n * Normalizes a value for consistent hashing.\n *\n * Normalization rules:\n * - Objects: Sort keys alphabetically, recursively normalize values\n * - Arrays: Preserve order, recursively normalize items\n * - Strings: Trim whitespace\n * - Numbers/Booleans/null: Keep as-is\n * - undefined: Convert to null\n *\n * @param value - Any JSON-serializable value\n * @returns Normalized value\n */\nexport function normalizeForHashing(value: unknown): unknown {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (typeof value === \"string\") {\n return value.trim();\n }\n\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(normalizeForHashing);\n }\n\n if (typeof value === \"object\") {\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(value as Record<string, unknown>).sort();\n for (const key of keys) {\n const val = (value as Record<string, unknown>)[key];\n if (val !== undefined) {\n sorted[key] = normalizeForHashing(val);\n }\n }\n return sorted;\n }\n\n return value;\n}\n\n/**\n * Computes the commitment hash from evidence content.\n *\n * The hash is computed as:\n * 1. Normalize the evidence (sort keys, trim strings, etc.)\n * 2. Serialize to JSON string (deterministic due to normalization)\n * 3. Apply Blake2b-256 hash\n *\n * @param evidence - The evidence content (Tiptap JSON document or any JSON-serializable data)\n * @returns 64-character lowercase hex string (Blake2b-256 hash)\n *\n * @example\n * ```typescript\n * import { computeCommitmentHash } from \"@andamio/core/hashing\";\n *\n * const evidence = {\n * type: \"doc\",\n * content: [\n * { type: \"paragraph\", content: [{ type: \"text\", text: \"My submission\" }] }\n * ]\n * };\n *\n * const hash = computeCommitmentHash(evidence);\n * // Use this hash as commitment_hash in the transaction\n * ```\n */\nexport function computeCommitmentHash(evidence: unknown): string {\n const normalized = normalizeForHashing(evidence);\n const jsonString = JSON.stringify(normalized);\n const bytes = new TextEncoder().encode(jsonString);\n return blake.blake2bHex(bytes, undefined, 32);\n}\n\n/**\n * Verifies that evidence content matches an expected hash.\n *\n * Use this to verify that database evidence matches the on-chain commitment.\n *\n * @param evidence - The evidence content to verify\n * @param expectedHash - The expected hash (from on-chain data)\n * @returns True if the evidence produces the expected hash\n */\nexport function verifyCommitmentHash(\n evidence: unknown,\n expectedHash: string\n): boolean {\n const computedHash = computeCommitmentHash(evidence);\n return computedHash.toLowerCase() === expectedHash.toLowerCase();\n}\n\n/**\n * Validates that a string is a valid commitment hash format.\n *\n * A valid hash is a 64-character hexadecimal string (Blake2b-256 output).\n *\n * @param hash - The string to validate\n * @returns True if the string is a valid hash format\n */\nexport function isValidCommitmentHash(hash: string): boolean {\n if (typeof hash !== \"string\") {\n return false;\n }\n if (hash.length !== 64) {\n return false;\n }\n return /^[0-9a-fA-F]{64}$/.test(hash);\n}\n\n/**\n * Result of comparing evidence with an on-chain hash\n */\nexport type EvidenceVerificationResult = {\n /** Whether the evidence matches the on-chain hash */\n isValid: boolean;\n /** The hash computed from the evidence */\n computedHash: string;\n /** The expected hash (from on-chain) */\n expectedHash: string;\n /** Human-readable status message */\n message: string;\n};\n\n/**\n * Performs a detailed verification of evidence against an on-chain hash.\n *\n * Returns a detailed result object with both hashes and a status message,\n * useful for debugging and user feedback.\n *\n * @param evidence - The evidence content to verify\n * @param onChainHash - The hash from on-chain data\n * @returns Detailed verification result\n */\nexport function verifyEvidenceDetailed(\n evidence: unknown,\n onChainHash: string\n): EvidenceVerificationResult {\n if (!isValidCommitmentHash(onChainHash)) {\n return {\n isValid: false,\n computedHash: \"\",\n expectedHash: onChainHash,\n message: `Invalid on-chain hash format: expected 64 hex characters, got \"${onChainHash}\"`,\n };\n }\n\n const computedHash = computeCommitmentHash(evidence);\n const isValid = computedHash.toLowerCase() === onChainHash.toLowerCase();\n\n return {\n isValid,\n computedHash,\n expectedHash: onChainHash.toLowerCase(),\n message: isValid\n ? \"Evidence matches on-chain commitment\"\n : \"Evidence does not match on-chain commitment - content may have been modified\",\n };\n}\n\n// =============================================================================\n// Backwards Compatibility Aliases (deprecated)\n// =============================================================================\n\n/**\n * @deprecated Use `computeCommitmentHash` instead.\n */\nexport const computeAssignmentInfoHash = computeCommitmentHash;\n\n/**\n * @deprecated Use `verifyCommitmentHash` instead.\n */\nexport const verifyAssignmentInfoHash = verifyCommitmentHash;\n\n/**\n * @deprecated Use `isValidCommitmentHash` instead.\n */\nexport const isValidAssignmentInfoHash = isValidCommitmentHash;\n"]}
|
package/package.json
CHANGED
|
@@ -8,34 +8,135 @@ import {
|
|
|
8
8
|
import type { TaskData, NativeAsset } from "./task-hash";
|
|
9
9
|
|
|
10
10
|
describe("computeTaskHash", () => {
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// All 7 on-chain test vectors from project 490e6da6be3dbfae3baa8431351dc148dd8bdebc62e2dd7772675e76
|
|
12
|
+
// All have expiration_time: 1782792000000n and native_assets: []
|
|
13
|
+
const ON_CHAIN_VECTORS: Array<{
|
|
14
|
+
title: string;
|
|
15
|
+
lovelace: bigint;
|
|
16
|
+
expected_hash: string;
|
|
17
|
+
}> = [
|
|
18
|
+
{
|
|
19
|
+
title: "Introduce Yourself",
|
|
20
|
+
lovelace: 5000000n,
|
|
21
|
+
expected_hash:
|
|
22
|
+
"b1e5c9234e8a4481da7cb3fb525fc54430f8df127ab9f10464ddc8a4e7560614",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
title: "Review the Docs",
|
|
26
|
+
lovelace: 8000000n,
|
|
27
|
+
expected_hash:
|
|
28
|
+
"9d113eafdbe599d624c1ae3e545083e3ec7a053e14ebb6cb730eb3fb59eb3363",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: "Find a Typo",
|
|
32
|
+
lovelace: 5000000n,
|
|
33
|
+
expected_hash:
|
|
34
|
+
"c79b778c46a26148c5a33ad669b3452ecf0263539270513003abef73c5858cb2",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: "Attend a Sync Call",
|
|
38
|
+
lovelace: 8000000n,
|
|
39
|
+
expected_hash:
|
|
40
|
+
"090391c308370ca1846e6cf39641dc975e8b2f3e370fb812f61bebcacb6902aa",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: "Test a Feature",
|
|
44
|
+
lovelace: 10000000n,
|
|
45
|
+
expected_hash:
|
|
46
|
+
"801eae4957a456034025e61f23f2a508eb8a6e15f8d55edb239712033ff06d18",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: "Write a How-To",
|
|
50
|
+
lovelace: 15000000n,
|
|
51
|
+
expected_hash:
|
|
52
|
+
"b6ac09b203c7a81d1cd819bc6064eec2f713e64a6cc5a2fac16f864fcfeee949",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
title: "Propose an Improvement",
|
|
56
|
+
lovelace: 5000000n,
|
|
57
|
+
expected_hash:
|
|
58
|
+
"eb14effb2a81bece91708a2fb2478bd36711b06804f1fa5fca049d0a9192c784",
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const DEADLINE = 1782792000000n;
|
|
63
|
+
|
|
13
64
|
describe("on-chain compatibility", () => {
|
|
14
|
-
|
|
65
|
+
for (const vector of ON_CHAIN_VECTORS) {
|
|
66
|
+
it(`matches on-chain hash for "${vector.title}"`, () => {
|
|
67
|
+
const task: TaskData = {
|
|
68
|
+
project_content: vector.title,
|
|
69
|
+
expiration_time: DEADLINE,
|
|
70
|
+
lovelace_amount: vector.lovelace,
|
|
71
|
+
native_assets: [],
|
|
72
|
+
};
|
|
73
|
+
const hash = computeTaskHash(task);
|
|
74
|
+
expect(hash).toBe(vector.expected_hash);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("CBOR encoding", () => {
|
|
80
|
+
it("uses Plutus Data Constructor 0 (tag 121)", () => {
|
|
15
81
|
const task: TaskData = {
|
|
16
|
-
project_content: "
|
|
17
|
-
expiration_time:
|
|
18
|
-
lovelace_amount:
|
|
82
|
+
project_content: "Test",
|
|
83
|
+
expiration_time: 1n,
|
|
84
|
+
lovelace_amount: 1n,
|
|
19
85
|
native_assets: [],
|
|
20
86
|
};
|
|
21
|
-
const
|
|
22
|
-
//
|
|
23
|
-
expect(
|
|
87
|
+
const bytes = debugTaskBytes(task);
|
|
88
|
+
// Should start with d8 79 (tag 121) and 9f (indefinite array)
|
|
89
|
+
expect(bytes.startsWith("d8799f")).toBe(true);
|
|
24
90
|
});
|
|
25
91
|
|
|
26
|
-
it
|
|
92
|
+
it("uses indefinite-length array encoding", () => {
|
|
27
93
|
const task: TaskData = {
|
|
28
|
-
project_content: "
|
|
29
|
-
expiration_time:
|
|
30
|
-
lovelace_amount:
|
|
31
|
-
native_assets: [
|
|
32
|
-
// TODO: Replace with real policy ID and token name from on-chain data
|
|
33
|
-
["a".repeat(56), "746f6b656e6e616d65", 1000n],
|
|
34
|
-
],
|
|
94
|
+
project_content: "Test",
|
|
95
|
+
expiration_time: 1n,
|
|
96
|
+
lovelace_amount: 1n,
|
|
97
|
+
native_assets: [],
|
|
35
98
|
};
|
|
36
|
-
const
|
|
37
|
-
//
|
|
38
|
-
expect(
|
|
99
|
+
const bytes = debugTaskBytes(task);
|
|
100
|
+
// Should start with 9f (indefinite array) after tag and end with ff (break)
|
|
101
|
+
expect(bytes.slice(4, 6)).toBe("9f"); // after d879
|
|
102
|
+
expect(bytes.endsWith("ff")).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("encodes empty tokens list as empty definite array (0x80)", () => {
|
|
106
|
+
const task: TaskData = {
|
|
107
|
+
project_content: "A",
|
|
108
|
+
expiration_time: 1n,
|
|
109
|
+
lovelace_amount: 1n,
|
|
110
|
+
native_assets: [],
|
|
111
|
+
};
|
|
112
|
+
const bytes = debugTaskBytes(task);
|
|
113
|
+
// Format: d8799f [content] [deadline] [lovelace] 80 ff
|
|
114
|
+
// The 80 (empty array) should be right before the final ff (break)
|
|
115
|
+
expect(bytes.slice(-4)).toBe("80ff");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("encodes content as CBOR byte string", () => {
|
|
119
|
+
const task: TaskData = {
|
|
120
|
+
project_content: "Hi",
|
|
121
|
+
expiration_time: 1n,
|
|
122
|
+
lovelace_amount: 1n,
|
|
123
|
+
native_assets: [],
|
|
124
|
+
};
|
|
125
|
+
const bytes = debugTaskBytes(task);
|
|
126
|
+
// "Hi" = 2 bytes, so CBOR header is 0x42 (0x40 + 2), then 4869
|
|
127
|
+
expect(bytes).toContain("424869");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("encodes integers as CBOR unsigned integers (big-endian)", () => {
|
|
131
|
+
const task: TaskData = {
|
|
132
|
+
project_content: "",
|
|
133
|
+
expiration_time: 0x12345678n,
|
|
134
|
+
lovelace_amount: 0n,
|
|
135
|
+
native_assets: [],
|
|
136
|
+
};
|
|
137
|
+
const bytes = debugTaskBytes(task);
|
|
138
|
+
// 0x12345678 as CBOR uint32: 1a 12345678 (big-endian, NOT little-endian)
|
|
139
|
+
expect(bytes).toContain("1a12345678");
|
|
39
140
|
});
|
|
40
141
|
});
|
|
41
142
|
|
|
@@ -52,7 +153,7 @@ describe("computeTaskHash", () => {
|
|
|
52
153
|
expect(hash).toMatch(/^[0-9a-f]{64}$/);
|
|
53
154
|
});
|
|
54
155
|
|
|
55
|
-
it("
|
|
156
|
+
it("handles empty content", () => {
|
|
56
157
|
const task: TaskData = {
|
|
57
158
|
project_content: "",
|
|
58
159
|
expiration_time: 0n,
|
|
@@ -60,22 +161,8 @@ describe("computeTaskHash", () => {
|
|
|
60
161
|
native_assets: [],
|
|
61
162
|
};
|
|
62
163
|
const bytes = debugTaskBytes(task);
|
|
63
|
-
// Empty
|
|
64
|
-
expect(bytes).
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("handles empty native assets as empty bytes", () => {
|
|
68
|
-
const task: TaskData = {
|
|
69
|
-
project_content: "Test",
|
|
70
|
-
expiration_time: 1n,
|
|
71
|
-
lovelace_amount: 1n,
|
|
72
|
-
native_assets: [],
|
|
73
|
-
};
|
|
74
|
-
const bytes = debugTaskBytes(task);
|
|
75
|
-
// Should be: "Test" (54657374) + 0x01 + 0x01 = "5465737401 01"
|
|
76
|
-
expect(bytes).toBe("546573740101");
|
|
77
|
-
// No CBOR empty array marker (0x80)
|
|
78
|
-
expect(bytes).not.toContain("80");
|
|
164
|
+
// Empty byte string is 0x40
|
|
165
|
+
expect(bytes).toContain("40");
|
|
79
166
|
});
|
|
80
167
|
|
|
81
168
|
it("handles empty token name", () => {
|
|
@@ -99,21 +186,8 @@ describe("computeTaskHash", () => {
|
|
|
99
186
|
expect(computeTaskHash(task)).toHaveLength(64);
|
|
100
187
|
});
|
|
101
188
|
|
|
102
|
-
it("encodes large integers in little-endian format", () => {
|
|
103
|
-
const task: TaskData = {
|
|
104
|
-
project_content: "",
|
|
105
|
-
expiration_time: 0x12345678n,
|
|
106
|
-
lovelace_amount: 0n,
|
|
107
|
-
native_assets: [],
|
|
108
|
-
};
|
|
109
|
-
const bytes = debugTaskBytes(task);
|
|
110
|
-
// 0x12345678 in little-endian is 78 56 34 12
|
|
111
|
-
// Then 0x00 for lovelace
|
|
112
|
-
expect(bytes).toBe("7856341200");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
189
|
it("normalizes Unicode strings (NFC)", () => {
|
|
116
|
-
//
|
|
190
|
+
// cafe with combining acute accent vs precomposed
|
|
117
191
|
const task1: TaskData = {
|
|
118
192
|
project_content: "cafe\u0301", // e + combining acute
|
|
119
193
|
expiration_time: 1n,
|
|
@@ -121,7 +195,7 @@ describe("computeTaskHash", () => {
|
|
|
121
195
|
native_assets: [],
|
|
122
196
|
};
|
|
123
197
|
const task2: TaskData = {
|
|
124
|
-
project_content: "caf\u00e9", // precomposed
|
|
198
|
+
project_content: "caf\u00e9", // precomposed e
|
|
125
199
|
expiration_time: 1n,
|
|
126
200
|
lovelace_amount: 1n,
|
|
127
201
|
native_assets: [],
|
|
@@ -326,6 +400,21 @@ describe("verifyTaskHash", () => {
|
|
|
326
400
|
const hash = computeTaskHash(task);
|
|
327
401
|
expect(verifyTaskHash(task, hash.toUpperCase())).toBe(true);
|
|
328
402
|
});
|
|
403
|
+
|
|
404
|
+
it("verifies on-chain test vector", () => {
|
|
405
|
+
const task: TaskData = {
|
|
406
|
+
project_content: "Introduce Yourself",
|
|
407
|
+
expiration_time: 1782792000000n,
|
|
408
|
+
lovelace_amount: 5000000n,
|
|
409
|
+
native_assets: [],
|
|
410
|
+
};
|
|
411
|
+
expect(
|
|
412
|
+
verifyTaskHash(
|
|
413
|
+
task,
|
|
414
|
+
"b1e5c9234e8a4481da7cb3fb525fc54430f8df127ab9f10464ddc8a4e7560614",
|
|
415
|
+
),
|
|
416
|
+
).toBe(true);
|
|
417
|
+
});
|
|
329
418
|
});
|
|
330
419
|
|
|
331
420
|
describe("isValidTaskHash", () => {
|
|
@@ -359,7 +448,7 @@ describe("isValidTaskHash", () => {
|
|
|
359
448
|
});
|
|
360
449
|
|
|
361
450
|
describe("debugTaskBytes", () => {
|
|
362
|
-
it("returns hex representation of encoded
|
|
451
|
+
it("returns hex representation of CBOR-encoded Plutus Data", () => {
|
|
363
452
|
const task: TaskData = {
|
|
364
453
|
project_content: "Hi",
|
|
365
454
|
expiration_time: 1n,
|
|
@@ -367,8 +456,9 @@ describe("debugTaskBytes", () => {
|
|
|
367
456
|
native_assets: [],
|
|
368
457
|
};
|
|
369
458
|
const bytes = debugTaskBytes(task);
|
|
370
|
-
//
|
|
371
|
-
|
|
459
|
+
// Should be Plutus Data: d879 9f 42 4869 01 02 80 ff
|
|
460
|
+
// tag 121, indef array, 2-byte string "Hi", int 1, int 2, empty array, break
|
|
461
|
+
expect(bytes).toBe("d8799f42486901028 0ff".replace(/ /g, ""));
|
|
372
462
|
});
|
|
373
463
|
|
|
374
464
|
it("validates input before encoding", () => {
|