@andamio/core 0.1.0 → 0.2.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 +84 -115
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +81 -96
- package/dist/index.mjs.map +1 -1
- package/dist/utils/hashing/index.d.mts +39 -20
- package/dist/utils/hashing/index.d.ts +39 -20
- package/dist/utils/hashing/index.js +83 -114
- package/dist/utils/hashing/index.js.map +1 -1
- package/dist/utils/hashing/index.mjs +80 -95
- package/dist/utils/hashing/index.mjs.map +1 -1
- package/package.json +4 -2
- package/src/utils/hashing/commitment-hash.ts +1 -1
- package/src/utils/hashing/index.ts +2 -1
- package/src/utils/hashing/slt-hash.test.ts +18 -0
- package/src/utils/hashing/slt-hash.ts +1 -1
- package/src/utils/hashing/task-hash.test.ts +383 -0
- package/src/utils/hashing/task-hash.ts +148 -160
|
@@ -4,47 +4,61 @@
|
|
|
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 hash_project_data function using
|
|
8
|
+
* raw byte concatenation and little-endian integer encoding.
|
|
8
9
|
*
|
|
9
10
|
* @module @andamio/core/hashing
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import
|
|
13
|
+
import blake from "blakejs";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
* Native asset in
|
|
16
|
+
* Native asset in Cardano format: [policyId, tokenName, quantity]
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const asset: NativeAsset = [
|
|
21
|
+
* "abc123def456...".repeat(4), // 56 hex chars (28 bytes) - policy ID
|
|
22
|
+
* "746f6b656e", // hex-encoded token name
|
|
23
|
+
* 1000n // quantity as bigint
|
|
24
|
+
* ];
|
|
25
|
+
* ```
|
|
16
26
|
*/
|
|
17
|
-
type NativeAsset = [
|
|
27
|
+
export type NativeAsset = [
|
|
28
|
+
policyId: string, // 56 hex chars (28 bytes)
|
|
29
|
+
tokenName: string, // hex encoded (0-64 chars / 0-32 bytes)
|
|
30
|
+
quantity: bigint, // arbitrary precision integer
|
|
31
|
+
];
|
|
18
32
|
|
|
19
33
|
/**
|
|
20
|
-
* Task data structure matching the
|
|
34
|
+
* Task data structure matching the Aiken ProjectData type.
|
|
21
35
|
*
|
|
22
|
-
* Fields
|
|
23
|
-
* 1. project_content (
|
|
24
|
-
* 2. expiration_time (
|
|
25
|
-
* 3. lovelace_amount (
|
|
26
|
-
* 4. native_assets (
|
|
36
|
+
* Fields are concatenated in this order for hashing:
|
|
37
|
+
* 1. project_content (UTF-8 bytes, NFC normalized)
|
|
38
|
+
* 2. expiration_time (little-endian, minimal bytes)
|
|
39
|
+
* 3. lovelace_amount (little-endian, minimal bytes)
|
|
40
|
+
* 4. native_assets (raw concatenation of each asset's bytes)
|
|
27
41
|
*/
|
|
28
42
|
export interface TaskData {
|
|
43
|
+
/** Task description (max 140 characters) */
|
|
29
44
|
project_content: string;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
/** Unix timestamp in milliseconds */
|
|
46
|
+
expiration_time: bigint;
|
|
47
|
+
/** Lovelace amount (micro-ADA) */
|
|
48
|
+
lovelace_amount: bigint;
|
|
49
|
+
/** Native assets attached to task */
|
|
50
|
+
native_assets: readonly NativeAsset[];
|
|
33
51
|
}
|
|
34
52
|
|
|
35
|
-
/**
|
|
36
|
-
* Plutus chunk size for byte strings.
|
|
37
|
-
*/
|
|
38
|
-
const PLUTUS_CHUNK_SIZE = 64;
|
|
39
|
-
|
|
40
53
|
/**
|
|
41
54
|
* Compute the task hash (token name / task_hash) from task data.
|
|
42
55
|
*
|
|
43
|
-
* This produces the same hash as the on-chain
|
|
44
|
-
* clients to pre-compute or verify task hashes.
|
|
56
|
+
* This produces the same hash as the on-chain Aiken hash_project_data function,
|
|
57
|
+
* allowing clients to pre-compute or verify task hashes.
|
|
45
58
|
*
|
|
46
59
|
* @param task - Task data object
|
|
47
60
|
* @returns 64-character hex string (256-bit Blake2b hash)
|
|
61
|
+
* @throws Error if task data validation fails
|
|
48
62
|
*
|
|
49
63
|
* @example
|
|
50
64
|
* ```typescript
|
|
@@ -52,8 +66,8 @@ const PLUTUS_CHUNK_SIZE = 64;
|
|
|
52
66
|
*
|
|
53
67
|
* const task = {
|
|
54
68
|
* project_content: "Open Task #1",
|
|
55
|
-
* expiration_time:
|
|
56
|
-
* lovelace_amount:
|
|
69
|
+
* expiration_time: 1769027280000n,
|
|
70
|
+
* lovelace_amount: 15000000n,
|
|
57
71
|
* native_assets: []
|
|
58
72
|
* };
|
|
59
73
|
*
|
|
@@ -62,11 +76,14 @@ const PLUTUS_CHUNK_SIZE = 64;
|
|
|
62
76
|
* ```
|
|
63
77
|
*/
|
|
64
78
|
export function computeTaskHash(task: TaskData): string {
|
|
65
|
-
//
|
|
66
|
-
|
|
79
|
+
// Validate inputs
|
|
80
|
+
validateTaskData(task);
|
|
81
|
+
|
|
82
|
+
// Encode task as raw bytes matching Aiken format
|
|
83
|
+
const bytes = encodeTaskAsRawBytes(task);
|
|
67
84
|
|
|
68
85
|
// Hash with Blake2b-256
|
|
69
|
-
return blake.blake2bHex(
|
|
86
|
+
return blake.blake2bHex(bytes, undefined, 32);
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
/**
|
|
@@ -94,194 +111,165 @@ export function isValidTaskHash(hash: string): boolean {
|
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
/**
|
|
97
|
-
* Debug function to show the
|
|
114
|
+
* Debug function to show the raw byte encoding of a task.
|
|
98
115
|
* Useful for comparing against on-chain data.
|
|
99
116
|
*
|
|
100
117
|
* @param task - Task data object
|
|
101
|
-
* @returns Hex string of the
|
|
118
|
+
* @returns Hex string of the encoded data (before hashing)
|
|
102
119
|
*/
|
|
103
|
-
export function
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
export function debugTaskBytes(task: TaskData): string {
|
|
121
|
+
validateTaskData(task);
|
|
122
|
+
const bytes = encodeTaskAsRawBytes(task);
|
|
123
|
+
return uint8ArrayToHex(bytes);
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
// =============================================================================
|
|
109
|
-
//
|
|
127
|
+
// Raw Byte Encoding (Internal) - Matches Aiken hash_project_data
|
|
110
128
|
// =============================================================================
|
|
111
129
|
|
|
112
130
|
/**
|
|
113
|
-
* Encode task data
|
|
114
|
-
*
|
|
115
|
-
* Plutus represents this as a constructor with fields in an INDEFINITE array:
|
|
116
|
-
* Constr 0 [project_content, expiration_time, lovelace_amount, native_assets]
|
|
131
|
+
* Encode task data as raw bytes matching Aiken's hash_project_data format.
|
|
117
132
|
*
|
|
118
|
-
*
|
|
133
|
+
* Format: project_content ++ int_to_bbs(deadline) ++ int_to_bbs(lovelace) ++ combine_flat_val(tokens)
|
|
119
134
|
*
|
|
120
135
|
* @internal
|
|
121
136
|
*/
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// Field 2: expiration_time as Integer
|
|
134
|
-
chunks.push(encodePlutusInteger(task.expiration_time));
|
|
135
|
-
|
|
136
|
-
// Field 3: lovelace_amount as Integer
|
|
137
|
-
chunks.push(encodePlutusInteger(task.lovelace_amount));
|
|
138
|
-
|
|
139
|
-
// Field 4: native_assets as List of pairs
|
|
140
|
-
chunks.push(encodeNativeAssets(task.native_assets));
|
|
141
|
-
|
|
142
|
-
// End indefinite array
|
|
143
|
-
chunks.push(new Uint8Array([0xff])); // Break
|
|
144
|
-
|
|
145
|
-
return concatUint8Arrays(chunks);
|
|
137
|
+
function encodeTaskAsRawBytes(task: TaskData): Uint8Array {
|
|
138
|
+
// Normalize Unicode for consistent hashing
|
|
139
|
+
const normalizedContent = task.project_content.normalize("NFC");
|
|
140
|
+
|
|
141
|
+
return concatUint8Arrays([
|
|
142
|
+
new TextEncoder().encode(normalizedContent),
|
|
143
|
+
intToBytesLittleEndian(task.expiration_time),
|
|
144
|
+
intToBytesLittleEndian(task.lovelace_amount),
|
|
145
|
+
combineNativeAssets(task.native_assets),
|
|
146
|
+
]);
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* Each asset is a pair: (policyId.tokenName, quantity)
|
|
152
|
-
* In Plutus, this is: List [(ByteString, Integer)]
|
|
150
|
+
* Validate TaskData before hashing.
|
|
153
151
|
*
|
|
152
|
+
* @throws Error if validation fails
|
|
154
153
|
* @internal
|
|
155
154
|
*/
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
function validateTaskData(task: TaskData): void {
|
|
156
|
+
// Validate project_content
|
|
157
|
+
if (task.project_content.length > 140) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`project_content exceeds 140 characters (got ${task.project_content.length})`,
|
|
160
|
+
);
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Each asset is a 2-element array: [bytestring, integer]
|
|
169
|
-
chunks.push(new Uint8Array([0x82])); // Array of 2 elements
|
|
170
|
-
chunks.push(encodePlutusBuiltinByteString(new TextEncoder().encode(assetClass)));
|
|
171
|
-
chunks.push(encodePlutusInteger(quantity));
|
|
163
|
+
// Validate numeric fields
|
|
164
|
+
if (task.expiration_time < 0n) {
|
|
165
|
+
throw new Error("expiration_time must be non-negative");
|
|
166
|
+
}
|
|
167
|
+
if (task.lovelace_amount < 0n) {
|
|
168
|
+
throw new Error("lovelace_amount must be non-negative");
|
|
172
169
|
}
|
|
173
170
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
// Validate native assets
|
|
172
|
+
for (const [policyId, tokenName, quantity] of task.native_assets) {
|
|
173
|
+
if (policyId.length !== 56) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`policyId must be 56 hex chars (got ${policyId.length})`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (!/^[0-9a-fA-F]*$/.test(policyId)) {
|
|
179
|
+
throw new Error("policyId contains invalid hex characters");
|
|
180
|
+
}
|
|
181
|
+
if (tokenName.length > 64 || tokenName.length % 2 !== 0) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`tokenName must be 0-64 hex chars with even length (got ${tokenName.length})`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (tokenName.length > 0 && !/^[0-9a-fA-F]*$/.test(tokenName)) {
|
|
187
|
+
throw new Error("tokenName contains invalid hex characters");
|
|
188
|
+
}
|
|
189
|
+
if (quantity < 0n) {
|
|
190
|
+
throw new Error("asset quantity must be non-negative");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
178
193
|
}
|
|
179
194
|
|
|
180
195
|
/**
|
|
181
|
-
*
|
|
196
|
+
* Convert a non-negative bigint to little-endian byte representation.
|
|
197
|
+
* Matches Aiken's integer_to_bytearray(False, 0, int).
|
|
182
198
|
*
|
|
183
|
-
* -
|
|
184
|
-
* -
|
|
199
|
+
* - False = little-endian byte order
|
|
200
|
+
* - 0 = minimal byte length (no zero-padding)
|
|
185
201
|
*
|
|
202
|
+
* @param n - Non-negative bigint to convert
|
|
203
|
+
* @returns Uint8Array with little-endian byte representation
|
|
186
204
|
* @internal
|
|
187
205
|
*/
|
|
188
|
-
function
|
|
189
|
-
if (
|
|
190
|
-
|
|
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]);
|
|
191
212
|
}
|
|
192
213
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
chunks.push(new Uint8Array([0x5f])); // Start indefinite byte string
|
|
214
|
+
const bytes: number[] = [];
|
|
215
|
+
let remaining = n;
|
|
196
216
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
217
|
+
while (remaining > 0n) {
|
|
218
|
+
bytes.push(Number(remaining & 0xffn));
|
|
219
|
+
remaining = remaining >> 8n;
|
|
200
220
|
}
|
|
201
221
|
|
|
202
|
-
|
|
203
|
-
return concatUint8Arrays(chunks);
|
|
222
|
+
return new Uint8Array(bytes);
|
|
204
223
|
}
|
|
205
224
|
|
|
206
225
|
/**
|
|
207
|
-
*
|
|
226
|
+
* Combine native assets into raw bytes matching Aiken's combine_flat_val.
|
|
227
|
+
* Format: policy_id ++ token_name ++ quantity for each asset.
|
|
208
228
|
*
|
|
229
|
+
* @param assets - Array of native assets
|
|
230
|
+
* @returns Uint8Array of concatenated asset bytes
|
|
209
231
|
* @internal
|
|
210
232
|
*/
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (n <= 23) {
|
|
215
|
-
return new Uint8Array([n]);
|
|
216
|
-
} else if (n <= 0xff) {
|
|
217
|
-
return new Uint8Array([0x18, n]);
|
|
218
|
-
} else if (n <= 0xffff) {
|
|
219
|
-
return new Uint8Array([0x19, n >> 8, n & 0xff]);
|
|
220
|
-
} else if (n <= 0xffffffff) {
|
|
221
|
-
return new Uint8Array([0x1a, (n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]);
|
|
222
|
-
} else {
|
|
223
|
-
// 64-bit integer - use BigInt for precision
|
|
224
|
-
const buf = new Uint8Array(9);
|
|
225
|
-
buf[0] = 0x1b;
|
|
226
|
-
const big = BigInt(n);
|
|
227
|
-
const view = new DataView(buf.buffer);
|
|
228
|
-
view.setBigUint64(1, big, false); // false = big-endian
|
|
229
|
-
return buf;
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
// Negative integers: major type 1, encode (-1 - n)
|
|
233
|
-
const absVal = -1 - n;
|
|
234
|
-
if (absVal <= 23) {
|
|
235
|
-
return new Uint8Array([0x20 + absVal]);
|
|
236
|
-
} else if (absVal <= 0xff) {
|
|
237
|
-
return new Uint8Array([0x38, absVal]);
|
|
238
|
-
} else if (absVal <= 0xffff) {
|
|
239
|
-
return new Uint8Array([0x39, absVal >> 8, absVal & 0xff]);
|
|
240
|
-
} else if (absVal <= 0xffffffff) {
|
|
241
|
-
return new Uint8Array([0x3a, (absVal >> 24) & 0xff, (absVal >> 16) & 0xff, (absVal >> 8) & 0xff, absVal & 0xff]);
|
|
242
|
-
} else {
|
|
243
|
-
const buf = new Uint8Array(9);
|
|
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;
|
|
249
|
-
}
|
|
233
|
+
function combineNativeAssets(assets: readonly NativeAsset[]): Uint8Array {
|
|
234
|
+
if (assets.length === 0) {
|
|
235
|
+
return new Uint8Array([]);
|
|
250
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);
|
|
251
245
|
}
|
|
252
246
|
|
|
253
247
|
/**
|
|
254
|
-
*
|
|
248
|
+
* Convert hex string to Uint8Array.
|
|
255
249
|
*
|
|
250
|
+
* @param hex - Hexadecimal string (must be even length)
|
|
251
|
+
* @returns Uint8Array of bytes
|
|
256
252
|
* @internal
|
|
257
253
|
*/
|
|
258
|
-
function
|
|
259
|
-
|
|
254
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
255
|
+
if (hex.length === 0) {
|
|
256
|
+
return new Uint8Array([]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Validation already done in validateTaskData, but defensive check
|
|
260
|
+
if (hex.length % 2 !== 0) {
|
|
261
|
+
throw new Error(`Invalid hex string: odd length (${hex.length})`);
|
|
262
|
+
}
|
|
260
263
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
result.set(buffer, 1);
|
|
265
|
-
return result;
|
|
266
|
-
} else if (len <= 255) {
|
|
267
|
-
const result = new Uint8Array(2 + len);
|
|
268
|
-
result[0] = 0x58;
|
|
269
|
-
result[1] = len;
|
|
270
|
-
result.set(buffer, 2);
|
|
271
|
-
return result;
|
|
272
|
-
} else if (len <= 65535) {
|
|
273
|
-
const result = new Uint8Array(3 + len);
|
|
274
|
-
result[0] = 0x59;
|
|
275
|
-
result[1] = len >> 8;
|
|
276
|
-
result[2] = len & 0xff;
|
|
277
|
-
result.set(buffer, 3);
|
|
278
|
-
return result;
|
|
264
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
265
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
266
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
279
267
|
}
|
|
280
|
-
|
|
268
|
+
return bytes;
|
|
281
269
|
}
|
|
282
270
|
|
|
283
271
|
/**
|
|
284
|
-
* Concatenate multiple Uint8Arrays into one
|
|
272
|
+
* Concatenate multiple Uint8Arrays into one.
|
|
285
273
|
*
|
|
286
274
|
* @internal
|
|
287
275
|
*/
|
|
@@ -297,7 +285,7 @@ function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|
|
|
297
285
|
}
|
|
298
286
|
|
|
299
287
|
/**
|
|
300
|
-
* Convert Uint8Array to hex string
|
|
288
|
+
* Convert Uint8Array to hex string.
|
|
301
289
|
*
|
|
302
290
|
* @internal
|
|
303
291
|
*/
|