@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
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Computes the task token name (hash) from task data.
|
|
5
5
|
* This hash is used as the task_hash on-chain (content-addressed identifier).
|
|
6
6
|
*
|
|
7
|
-
* The algorithm matches the on-chain Aiken hash_project_data function
|
|
8
|
-
*
|
|
7
|
+
* The algorithm matches the on-chain Aiken/Haskell hash_project_data function:
|
|
8
|
+
* - Serialize task data as Plutus Data (CBOR with tag 121 for Constructor 0)
|
|
9
|
+
* - Use indefinite-length arrays for constructor fields
|
|
10
|
+
* - Hash with Blake2b-256
|
|
9
11
|
*
|
|
10
12
|
* @module @andamio/core/hashing
|
|
11
13
|
*/
|
|
@@ -33,11 +35,11 @@ export type NativeAsset = [
|
|
|
33
35
|
/**
|
|
34
36
|
* Task data structure matching the Aiken ProjectData type.
|
|
35
37
|
*
|
|
36
|
-
*
|
|
37
|
-
* 1. project_content (UTF-8
|
|
38
|
-
* 2.
|
|
39
|
-
* 3.
|
|
40
|
-
* 4.
|
|
38
|
+
* Serialized as Plutus Data Constructor 0 (CBOR tag 121) with fields:
|
|
39
|
+
* 1. project_content (ByteArray - UTF-8 encoded, NFC normalized)
|
|
40
|
+
* 2. deadline (Int - milliseconds)
|
|
41
|
+
* 3. lovelace_am (Int - micro-ADA)
|
|
42
|
+
* 4. tokens (List<FlatValue> - native assets)
|
|
41
43
|
*/
|
|
42
44
|
export interface TaskData {
|
|
43
45
|
/** Task description (max 140 characters) */
|
|
@@ -76,13 +78,8 @@ export interface TaskData {
|
|
|
76
78
|
* ```
|
|
77
79
|
*/
|
|
78
80
|
export function computeTaskHash(task: TaskData): string {
|
|
79
|
-
// Validate inputs
|
|
80
81
|
validateTaskData(task);
|
|
81
|
-
|
|
82
|
-
// Encode task as raw bytes matching Aiken format
|
|
83
|
-
const bytes = encodeTaskAsRawBytes(task);
|
|
84
|
-
|
|
85
|
-
// Hash with Blake2b-256
|
|
82
|
+
const bytes = encodeTaskAsPlutusData(task);
|
|
86
83
|
return blake.blake2bHex(bytes, undefined, 32);
|
|
87
84
|
}
|
|
88
85
|
|
|
@@ -111,41 +108,157 @@ export function isValidTaskHash(hash: string): boolean {
|
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
/**
|
|
114
|
-
* Debug function to show the
|
|
111
|
+
* Debug function to show the CBOR encoding of a task.
|
|
115
112
|
* Useful for comparing against on-chain data.
|
|
116
113
|
*
|
|
117
114
|
* @param task - Task data object
|
|
118
|
-
* @returns Hex string of the encoded
|
|
115
|
+
* @returns Hex string of the CBOR-encoded Plutus Data (before hashing)
|
|
119
116
|
*/
|
|
120
117
|
export function debugTaskBytes(task: TaskData): string {
|
|
121
118
|
validateTaskData(task);
|
|
122
|
-
const bytes =
|
|
119
|
+
const bytes = encodeTaskAsPlutusData(task);
|
|
123
120
|
return uint8ArrayToHex(bytes);
|
|
124
121
|
}
|
|
125
122
|
|
|
126
123
|
// =============================================================================
|
|
127
|
-
//
|
|
124
|
+
// Plutus Data CBOR Encoding (Internal)
|
|
128
125
|
// =============================================================================
|
|
129
126
|
|
|
130
127
|
/**
|
|
131
|
-
* Encode task data as
|
|
128
|
+
* Encode task data as Plutus Data matching Aiken/Haskell serialization.
|
|
129
|
+
*
|
|
130
|
+
* Format: tag(121) + indefinite-array + [content, deadline, lovelace, tokens] + break
|
|
132
131
|
*
|
|
133
|
-
*
|
|
132
|
+
* The key insight is that Haskell's `serialiseData . toBuiltinData` uses
|
|
133
|
+
* indefinite-length CBOR arrays (0x9f ... 0xff) for Plutus Data constructors.
|
|
134
134
|
*
|
|
135
135
|
* @internal
|
|
136
136
|
*/
|
|
137
|
-
function
|
|
138
|
-
// Normalize Unicode for consistent hashing
|
|
137
|
+
function encodeTaskAsPlutusData(task: TaskData): Uint8Array {
|
|
139
138
|
const normalizedContent = task.project_content.normalize("NFC");
|
|
139
|
+
const contentBytes = new TextEncoder().encode(normalizedContent);
|
|
140
140
|
|
|
141
141
|
return concatUint8Arrays([
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
// Tag 121 (Plutus Data Constructor 0) + indefinite array start
|
|
143
|
+
new Uint8Array([0xd8, 121, 0x9f]),
|
|
144
|
+
// Field 1: project_content (ByteArray)
|
|
145
|
+
encodeCborBytes(contentBytes),
|
|
146
|
+
// Field 2: deadline (Int)
|
|
147
|
+
encodeCborUint(task.expiration_time),
|
|
148
|
+
// Field 3: lovelace_am (Int)
|
|
149
|
+
encodeCborUint(task.lovelace_amount),
|
|
150
|
+
// Field 4: tokens (List<FlatValue>)
|
|
151
|
+
encodeTokensList(task.native_assets),
|
|
152
|
+
// Break (end of indefinite array)
|
|
153
|
+
new Uint8Array([0xff]),
|
|
146
154
|
]);
|
|
147
155
|
}
|
|
148
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Encode a list of native assets as Plutus Data.
|
|
159
|
+
*
|
|
160
|
+
* Each FlatValue is encoded as a Plutus Data constructor with:
|
|
161
|
+
* - PolicyId (ByteArray)
|
|
162
|
+
* - AssetName (ByteArray)
|
|
163
|
+
* - Quantity (Int)
|
|
164
|
+
*
|
|
165
|
+
* @internal
|
|
166
|
+
*/
|
|
167
|
+
function encodeTokensList(assets: readonly NativeAsset[]): Uint8Array {
|
|
168
|
+
if (assets.length === 0) {
|
|
169
|
+
// Empty definite-length array
|
|
170
|
+
return new Uint8Array([0x80]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Encode as indefinite-length array of FlatValue constructors
|
|
174
|
+
const parts: Uint8Array[] = [new Uint8Array([0x9f])]; // indefinite array start
|
|
175
|
+
|
|
176
|
+
for (const [policyId, tokenName, quantity] of assets) {
|
|
177
|
+
// Each FlatValue is Constructor 0 with 3 fields
|
|
178
|
+
parts.push(new Uint8Array([0xd8, 121, 0x9f])); // tag 121, indefinite array
|
|
179
|
+
parts.push(encodeCborBytes(hexToBytes(policyId)));
|
|
180
|
+
parts.push(encodeCborBytes(hexToBytes(tokenName)));
|
|
181
|
+
parts.push(encodeCborUint(quantity));
|
|
182
|
+
parts.push(new Uint8Array([0xff])); // break
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
parts.push(new Uint8Array([0xff])); // break (end of list)
|
|
186
|
+
return concatUint8Arrays(parts);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Maximum value for CBOR uint64 encoding.
|
|
191
|
+
* @internal
|
|
192
|
+
*/
|
|
193
|
+
const MAX_UINT64 = 18446744073709551615n; // 2^64 - 1
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Encode CBOR unsigned integer (major type 0).
|
|
197
|
+
*
|
|
198
|
+
* @internal
|
|
199
|
+
*/
|
|
200
|
+
function encodeCborUint(n: bigint): Uint8Array {
|
|
201
|
+
if (n < 0n) {
|
|
202
|
+
throw new Error("Negative integers not supported");
|
|
203
|
+
}
|
|
204
|
+
if (n > MAX_UINT64) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Integer exceeds maximum CBOR uint64 value (got ${n}, max ${MAX_UINT64})`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (n < 24n) {
|
|
211
|
+
return new Uint8Array([Number(n)]);
|
|
212
|
+
} else if (n < 256n) {
|
|
213
|
+
return new Uint8Array([0x18, Number(n)]);
|
|
214
|
+
} else if (n < 65536n) {
|
|
215
|
+
return new Uint8Array([0x19, Number(n >> 8n) & 0xff, Number(n) & 0xff]);
|
|
216
|
+
} else if (n < 4294967296n) {
|
|
217
|
+
return new Uint8Array([
|
|
218
|
+
0x1a,
|
|
219
|
+
Number((n >> 24n) & 0xffn),
|
|
220
|
+
Number((n >> 16n) & 0xffn),
|
|
221
|
+
Number((n >> 8n) & 0xffn),
|
|
222
|
+
Number(n & 0xffn),
|
|
223
|
+
]);
|
|
224
|
+
} else {
|
|
225
|
+
// 8-byte unsigned integer
|
|
226
|
+
return new Uint8Array([
|
|
227
|
+
0x1b,
|
|
228
|
+
Number((n >> 56n) & 0xffn),
|
|
229
|
+
Number((n >> 48n) & 0xffn),
|
|
230
|
+
Number((n >> 40n) & 0xffn),
|
|
231
|
+
Number((n >> 32n) & 0xffn),
|
|
232
|
+
Number((n >> 24n) & 0xffn),
|
|
233
|
+
Number((n >> 16n) & 0xffn),
|
|
234
|
+
Number((n >> 8n) & 0xffn),
|
|
235
|
+
Number(n & 0xffn),
|
|
236
|
+
]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Encode CBOR byte string (major type 2).
|
|
242
|
+
*
|
|
243
|
+
* @internal
|
|
244
|
+
*/
|
|
245
|
+
function encodeCborBytes(bytes: Uint8Array): Uint8Array {
|
|
246
|
+
const len = bytes.length;
|
|
247
|
+
let header: Uint8Array;
|
|
248
|
+
|
|
249
|
+
if (len < 24) {
|
|
250
|
+
header = new Uint8Array([0x40 + len]);
|
|
251
|
+
} else if (len < 256) {
|
|
252
|
+
header = new Uint8Array([0x58, len]);
|
|
253
|
+
} else if (len < 65536) {
|
|
254
|
+
header = new Uint8Array([0x59, (len >> 8) & 0xff, len & 0xff]);
|
|
255
|
+
} else {
|
|
256
|
+
throw new Error("Byte string too long for CBOR encoding");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return concatUint8Arrays([header, bytes]);
|
|
260
|
+
}
|
|
261
|
+
|
|
149
262
|
/**
|
|
150
263
|
* Validate TaskData before hashing.
|
|
151
264
|
*
|
|
@@ -153,14 +266,12 @@ function encodeTaskAsRawBytes(task: TaskData): Uint8Array {
|
|
|
153
266
|
* @internal
|
|
154
267
|
*/
|
|
155
268
|
function validateTaskData(task: TaskData): void {
|
|
156
|
-
// Validate project_content
|
|
157
269
|
if (task.project_content.length > 140) {
|
|
158
270
|
throw new Error(
|
|
159
271
|
`project_content exceeds 140 characters (got ${task.project_content.length})`,
|
|
160
272
|
);
|
|
161
273
|
}
|
|
162
274
|
|
|
163
|
-
// Validate numeric fields
|
|
164
275
|
if (task.expiration_time < 0n) {
|
|
165
276
|
throw new Error("expiration_time must be non-negative");
|
|
166
277
|
}
|
|
@@ -168,7 +279,6 @@ function validateTaskData(task: TaskData): void {
|
|
|
168
279
|
throw new Error("lovelace_amount must be non-negative");
|
|
169
280
|
}
|
|
170
281
|
|
|
171
|
-
// Validate native assets
|
|
172
282
|
for (const [policyId, tokenName, quantity] of task.native_assets) {
|
|
173
283
|
if (policyId.length !== 56) {
|
|
174
284
|
throw new Error(
|
|
@@ -192,63 +302,9 @@ function validateTaskData(task: TaskData): void {
|
|
|
192
302
|
}
|
|
193
303
|
}
|
|
194
304
|
|
|
195
|
-
/**
|
|
196
|
-
* Convert a non-negative bigint to little-endian byte representation.
|
|
197
|
-
* Matches Aiken's integer_to_bytearray(False, 0, int).
|
|
198
|
-
*
|
|
199
|
-
* - False = little-endian byte order
|
|
200
|
-
* - 0 = minimal byte length (no zero-padding)
|
|
201
|
-
*
|
|
202
|
-
* @param n - Non-negative bigint to convert
|
|
203
|
-
* @returns Uint8Array with little-endian byte representation
|
|
204
|
-
* @internal
|
|
205
|
-
*/
|
|
206
|
-
function intToBytesLittleEndian(n: bigint): Uint8Array {
|
|
207
|
-
if (n < 0n) {
|
|
208
|
-
throw new Error("Negative integers not supported");
|
|
209
|
-
}
|
|
210
|
-
if (n === 0n) {
|
|
211
|
-
return new Uint8Array([0]);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const bytes: number[] = [];
|
|
215
|
-
let remaining = n;
|
|
216
|
-
|
|
217
|
-
while (remaining > 0n) {
|
|
218
|
-
bytes.push(Number(remaining & 0xffn));
|
|
219
|
-
remaining = remaining >> 8n;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return new Uint8Array(bytes);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Combine native assets into raw bytes matching Aiken's combine_flat_val.
|
|
227
|
-
* Format: policy_id ++ token_name ++ quantity for each asset.
|
|
228
|
-
*
|
|
229
|
-
* @param assets - Array of native assets
|
|
230
|
-
* @returns Uint8Array of concatenated asset bytes
|
|
231
|
-
* @internal
|
|
232
|
-
*/
|
|
233
|
-
function combineNativeAssets(assets: readonly NativeAsset[]): Uint8Array {
|
|
234
|
-
if (assets.length === 0) {
|
|
235
|
-
return new Uint8Array([]);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const chunks: Uint8Array[] = [];
|
|
239
|
-
for (const [policyId, tokenName, quantity] of assets) {
|
|
240
|
-
chunks.push(hexToBytes(policyId));
|
|
241
|
-
chunks.push(hexToBytes(tokenName));
|
|
242
|
-
chunks.push(intToBytesLittleEndian(quantity));
|
|
243
|
-
}
|
|
244
|
-
return concatUint8Arrays(chunks);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
305
|
/**
|
|
248
306
|
* Convert hex string to Uint8Array.
|
|
249
307
|
*
|
|
250
|
-
* @param hex - Hexadecimal string (must be even length)
|
|
251
|
-
* @returns Uint8Array of bytes
|
|
252
308
|
* @internal
|
|
253
309
|
*/
|
|
254
310
|
function hexToBytes(hex: string): Uint8Array {
|
|
@@ -256,14 +312,19 @@ function hexToBytes(hex: string): Uint8Array {
|
|
|
256
312
|
return new Uint8Array([]);
|
|
257
313
|
}
|
|
258
314
|
|
|
259
|
-
// Validation already done in validateTaskData, but defensive check
|
|
260
315
|
if (hex.length % 2 !== 0) {
|
|
261
316
|
throw new Error(`Invalid hex string: odd length (${hex.length})`);
|
|
262
317
|
}
|
|
263
318
|
|
|
264
319
|
const bytes = new Uint8Array(hex.length / 2);
|
|
265
320
|
for (let i = 0; i < bytes.length; i++) {
|
|
266
|
-
|
|
321
|
+
const byte = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
322
|
+
if (Number.isNaN(byte)) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`Invalid hex character at position ${i * 2}: "${hex.slice(i * 2, i * 2 + 2)}"`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
bytes[i] = byte;
|
|
267
328
|
}
|
|
268
329
|
return bytes;
|
|
269
330
|
}
|