@andamio/core 0.1.1 → 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.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +132 -92
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +132 -92
- package/dist/index.mjs.map +1 -1
- package/dist/utils/hashing/index.d.mts +40 -19
- package/dist/utils/hashing/index.d.ts +40 -19
- package/dist/utils/hashing/index.js +130 -90
- package/dist/utils/hashing/index.js.map +1 -1
- package/dist/utils/hashing/index.mjs +130 -90
- package/dist/utils/hashing/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/utils/hashing/index.ts +2 -1
- package/src/utils/hashing/task-hash.test.ts +473 -0
- package/src/utils/hashing/task-hash.ts +203 -154
|
@@ -4,7 +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
|
|
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
|
|
8
11
|
*
|
|
9
12
|
* @module @andamio/core/hashing
|
|
10
13
|
*/
|
|
@@ -12,39 +15,52 @@
|
|
|
12
15
|
import blake from "blakejs";
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
|
-
* Native asset in
|
|
18
|
+
* Native asset in Cardano format: [policyId, tokenName, quantity]
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const asset: NativeAsset = [
|
|
23
|
+
* "abc123def456...".repeat(4), // 56 hex chars (28 bytes) - policy ID
|
|
24
|
+
* "746f6b656e", // hex-encoded token name
|
|
25
|
+
* 1000n // quantity as bigint
|
|
26
|
+
* ];
|
|
27
|
+
* ```
|
|
16
28
|
*/
|
|
17
|
-
type NativeAsset = [
|
|
29
|
+
export type NativeAsset = [
|
|
30
|
+
policyId: string, // 56 hex chars (28 bytes)
|
|
31
|
+
tokenName: string, // hex encoded (0-64 chars / 0-32 bytes)
|
|
32
|
+
quantity: bigint, // arbitrary precision integer
|
|
33
|
+
];
|
|
18
34
|
|
|
19
35
|
/**
|
|
20
|
-
* Task data structure matching the
|
|
36
|
+
* Task data structure matching the Aiken ProjectData type.
|
|
21
37
|
*
|
|
22
|
-
*
|
|
23
|
-
* 1. project_content (
|
|
24
|
-
* 2.
|
|
25
|
-
* 3.
|
|
26
|
-
* 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)
|
|
27
43
|
*/
|
|
28
44
|
export interface TaskData {
|
|
45
|
+
/** Task description (max 140 characters) */
|
|
29
46
|
project_content: string;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
/** Unix timestamp in milliseconds */
|
|
48
|
+
expiration_time: bigint;
|
|
49
|
+
/** Lovelace amount (micro-ADA) */
|
|
50
|
+
lovelace_amount: bigint;
|
|
51
|
+
/** Native assets attached to task */
|
|
52
|
+
native_assets: readonly NativeAsset[];
|
|
33
53
|
}
|
|
34
54
|
|
|
35
|
-
/**
|
|
36
|
-
* Plutus chunk size for byte strings.
|
|
37
|
-
*/
|
|
38
|
-
const PLUTUS_CHUNK_SIZE = 64;
|
|
39
|
-
|
|
40
55
|
/**
|
|
41
56
|
* Compute the task hash (token name / task_hash) from task data.
|
|
42
57
|
*
|
|
43
|
-
* This produces the same hash as the on-chain
|
|
44
|
-
* clients to pre-compute or verify task hashes.
|
|
58
|
+
* This produces the same hash as the on-chain Aiken hash_project_data function,
|
|
59
|
+
* allowing clients to pre-compute or verify task hashes.
|
|
45
60
|
*
|
|
46
61
|
* @param task - Task data object
|
|
47
62
|
* @returns 64-character hex string (256-bit Blake2b hash)
|
|
63
|
+
* @throws Error if task data validation fails
|
|
48
64
|
*
|
|
49
65
|
* @example
|
|
50
66
|
* ```typescript
|
|
@@ -52,8 +68,8 @@ const PLUTUS_CHUNK_SIZE = 64;
|
|
|
52
68
|
*
|
|
53
69
|
* const task = {
|
|
54
70
|
* project_content: "Open Task #1",
|
|
55
|
-
* expiration_time:
|
|
56
|
-
* lovelace_amount:
|
|
71
|
+
* expiration_time: 1769027280000n,
|
|
72
|
+
* lovelace_amount: 15000000n,
|
|
57
73
|
* native_assets: []
|
|
58
74
|
* };
|
|
59
75
|
*
|
|
@@ -62,11 +78,9 @@ const PLUTUS_CHUNK_SIZE = 64;
|
|
|
62
78
|
* ```
|
|
63
79
|
*/
|
|
64
80
|
export function computeTaskHash(task: TaskData): string {
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
// Hash with Blake2b-256
|
|
69
|
-
return blake.blake2bHex(cborData, undefined, 32);
|
|
81
|
+
validateTaskData(task);
|
|
82
|
+
const bytes = encodeTaskAsPlutusData(task);
|
|
83
|
+
return blake.blake2bHex(bytes, undefined, 32);
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
/**
|
|
@@ -98,190 +112,225 @@ export function isValidTaskHash(hash: string): boolean {
|
|
|
98
112
|
* Useful for comparing against on-chain data.
|
|
99
113
|
*
|
|
100
114
|
* @param task - Task data object
|
|
101
|
-
* @returns Hex string of the CBOR-encoded
|
|
115
|
+
* @returns Hex string of the CBOR-encoded Plutus Data (before hashing)
|
|
102
116
|
*/
|
|
103
|
-
export function
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
export function debugTaskBytes(task: TaskData): string {
|
|
118
|
+
validateTaskData(task);
|
|
119
|
+
const bytes = encodeTaskAsPlutusData(task);
|
|
120
|
+
return uint8ArrayToHex(bytes);
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
// =============================================================================
|
|
109
|
-
// Plutus Data Encoding (Internal)
|
|
124
|
+
// Plutus Data CBOR Encoding (Internal)
|
|
110
125
|
// =============================================================================
|
|
111
126
|
|
|
112
127
|
/**
|
|
113
|
-
* Encode task data
|
|
128
|
+
* Encode task data as Plutus Data matching Aiken/Haskell serialization.
|
|
114
129
|
*
|
|
115
|
-
*
|
|
116
|
-
* Constr 0 [project_content, expiration_time, lovelace_amount, native_assets]
|
|
130
|
+
* Format: tag(121) + indefinite-array + [content, deadline, lovelace, tokens] + break
|
|
117
131
|
*
|
|
118
|
-
*
|
|
132
|
+
* The key insight is that Haskell's `serialiseData . toBuiltinData` uses
|
|
133
|
+
* indefinite-length CBOR arrays (0x9f ... 0xff) for Plutus Data constructors.
|
|
119
134
|
*
|
|
120
135
|
* @internal
|
|
121
136
|
*/
|
|
122
137
|
function encodeTaskAsPlutusData(task: TaskData): Uint8Array {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
chunks.push(encodeNativeAssets(task.native_assets));
|
|
141
|
-
|
|
142
|
-
// End indefinite array
|
|
143
|
-
chunks.push(new Uint8Array([0xff])); // Break
|
|
144
|
-
|
|
145
|
-
return concatUint8Arrays(chunks);
|
|
138
|
+
const normalizedContent = task.project_content.normalize("NFC");
|
|
139
|
+
const contentBytes = new TextEncoder().encode(normalizedContent);
|
|
140
|
+
|
|
141
|
+
return concatUint8Arrays([
|
|
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]),
|
|
154
|
+
]);
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
/**
|
|
149
|
-
* Encode native assets as Plutus
|
|
158
|
+
* Encode a list of native assets as Plutus Data.
|
|
150
159
|
*
|
|
151
|
-
* Each
|
|
152
|
-
*
|
|
160
|
+
* Each FlatValue is encoded as a Plutus Data constructor with:
|
|
161
|
+
* - PolicyId (ByteArray)
|
|
162
|
+
* - AssetName (ByteArray)
|
|
163
|
+
* - Quantity (Int)
|
|
153
164
|
*
|
|
154
165
|
* @internal
|
|
155
166
|
*/
|
|
156
|
-
function
|
|
167
|
+
function encodeTokensList(assets: readonly NativeAsset[]): Uint8Array {
|
|
157
168
|
if (assets.length === 0) {
|
|
158
|
-
// Empty
|
|
159
|
-
return new Uint8Array([0x80]);
|
|
169
|
+
// Empty definite-length array
|
|
170
|
+
return new Uint8Array([0x80]);
|
|
160
171
|
}
|
|
161
172
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// Start indefinite array
|
|
165
|
-
chunks.push(new Uint8Array([0x9f]));
|
|
173
|
+
// Encode as indefinite-length array of FlatValue constructors
|
|
174
|
+
const parts: Uint8Array[] = [new Uint8Array([0x9f])]; // indefinite array start
|
|
166
175
|
|
|
167
|
-
for (const [
|
|
168
|
-
// Each
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return concatUint8Arrays(chunks);
|
|
185
|
+
parts.push(new Uint8Array([0xff])); // break (end of list)
|
|
186
|
+
return concatUint8Arrays(parts);
|
|
178
187
|
}
|
|
179
188
|
|
|
180
189
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
|
|
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).
|
|
185
197
|
*
|
|
186
198
|
* @internal
|
|
187
199
|
*/
|
|
188
|
-
function
|
|
189
|
-
if (
|
|
190
|
-
|
|
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
|
+
);
|
|
191
208
|
}
|
|
192
209
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
+
}
|
|
196
239
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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");
|
|
200
257
|
}
|
|
201
258
|
|
|
202
|
-
|
|
203
|
-
return concatUint8Arrays(chunks);
|
|
259
|
+
return concatUint8Arrays([header, bytes]);
|
|
204
260
|
}
|
|
205
261
|
|
|
206
262
|
/**
|
|
207
|
-
*
|
|
263
|
+
* Validate TaskData before hashing.
|
|
208
264
|
*
|
|
265
|
+
* @throws Error if validation fails
|
|
209
266
|
* @internal
|
|
210
267
|
*/
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
268
|
+
function validateTaskData(task: TaskData): void {
|
|
269
|
+
if (task.project_content.length > 140) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`project_content exceeds 140 characters (got ${task.project_content.length})`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (task.expiration_time < 0n) {
|
|
276
|
+
throw new Error("expiration_time must be non-negative");
|
|
277
|
+
}
|
|
278
|
+
if (task.lovelace_amount < 0n) {
|
|
279
|
+
throw new Error("lovelace_amount must be non-negative");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const [policyId, tokenName, quantity] of task.native_assets) {
|
|
283
|
+
if (policyId.length !== 56) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`policyId must be 56 hex chars (got ${policyId.length})`,
|
|
286
|
+
);
|
|
230
287
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
buf[0] = 0x3b;
|
|
245
|
-
const big = BigInt(absVal);
|
|
246
|
-
const view = new DataView(buf.buffer);
|
|
247
|
-
view.setBigUint64(1, big, false);
|
|
248
|
-
return buf;
|
|
288
|
+
if (!/^[0-9a-fA-F]*$/.test(policyId)) {
|
|
289
|
+
throw new Error("policyId contains invalid hex characters");
|
|
290
|
+
}
|
|
291
|
+
if (tokenName.length > 64 || tokenName.length % 2 !== 0) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`tokenName must be 0-64 hex chars with even length (got ${tokenName.length})`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (tokenName.length > 0 && !/^[0-9a-fA-F]*$/.test(tokenName)) {
|
|
297
|
+
throw new Error("tokenName contains invalid hex characters");
|
|
298
|
+
}
|
|
299
|
+
if (quantity < 0n) {
|
|
300
|
+
throw new Error("asset quantity must be non-negative");
|
|
249
301
|
}
|
|
250
302
|
}
|
|
251
303
|
}
|
|
252
304
|
|
|
253
305
|
/**
|
|
254
|
-
*
|
|
306
|
+
* Convert hex string to Uint8Array.
|
|
255
307
|
*
|
|
256
308
|
* @internal
|
|
257
309
|
*/
|
|
258
|
-
function
|
|
259
|
-
|
|
310
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
311
|
+
if (hex.length === 0) {
|
|
312
|
+
return new Uint8Array([]);
|
|
313
|
+
}
|
|
260
314
|
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
result[0] = 0x59;
|
|
275
|
-
result[1] = len >> 8;
|
|
276
|
-
result[2] = len & 0xff;
|
|
277
|
-
result.set(buffer, 3);
|
|
278
|
-
return result;
|
|
315
|
+
if (hex.length % 2 !== 0) {
|
|
316
|
+
throw new Error(`Invalid hex string: odd length (${hex.length})`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
320
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
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;
|
|
279
328
|
}
|
|
280
|
-
|
|
329
|
+
return bytes;
|
|
281
330
|
}
|
|
282
331
|
|
|
283
332
|
/**
|
|
284
|
-
* Concatenate multiple Uint8Arrays into one
|
|
333
|
+
* Concatenate multiple Uint8Arrays into one.
|
|
285
334
|
*
|
|
286
335
|
* @internal
|
|
287
336
|
*/
|
|
@@ -297,7 +346,7 @@ function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|
|
|
297
346
|
}
|
|
298
347
|
|
|
299
348
|
/**
|
|
300
|
-
* Convert Uint8Array to hex string
|
|
349
|
+
* Convert Uint8Array to hex string.
|
|
301
350
|
*
|
|
302
351
|
* @internal
|
|
303
352
|
*/
|