@andamio/core 0.1.1 → 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.
@@ -0,0 +1,383 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ computeTaskHash,
4
+ verifyTaskHash,
5
+ isValidTaskHash,
6
+ debugTaskBytes,
7
+ } from "./task-hash";
8
+ import type { TaskData, NativeAsset } from "./task-hash";
9
+
10
+ describe("computeTaskHash", () => {
11
+ // Golden tests - must match on-chain hashes
12
+ // TODO: Replace placeholder hashes with actual on-chain test vectors from Andamioscan
13
+ describe("on-chain compatibility", () => {
14
+ it.skip("matches known on-chain hash for simple task", () => {
15
+ const task: TaskData = {
16
+ project_content: "Open Task #1",
17
+ expiration_time: 1769027280000n,
18
+ lovelace_amount: 15000000n,
19
+ native_assets: [],
20
+ };
21
+ const hash = computeTaskHash(task);
22
+ // TODO: Replace with actual on-chain hash from Andamioscan
23
+ expect(hash).toBe("EXPECTED_HASH_FROM_ANDAMIOSCAN");
24
+ });
25
+
26
+ it.skip("matches known on-chain hash for task with native assets", () => {
27
+ const task: TaskData = {
28
+ project_content: "Task with tokens",
29
+ expiration_time: 1700000000000n,
30
+ lovelace_amount: 1000000n,
31
+ native_assets: [
32
+ // TODO: Replace with real policy ID and token name from on-chain data
33
+ ["a".repeat(56), "746f6b656e6e616d65", 1000n],
34
+ ],
35
+ };
36
+ const hash = computeTaskHash(task);
37
+ // TODO: Replace with actual on-chain hash from Andamioscan
38
+ expect(hash).toBe("EXPECTED_HASH_FROM_ANDAMIOSCAN");
39
+ });
40
+ });
41
+
42
+ describe("edge cases", () => {
43
+ it("handles zero lovelace amount", () => {
44
+ const task: TaskData = {
45
+ project_content: "Test",
46
+ expiration_time: 1700000000000n,
47
+ lovelace_amount: 0n,
48
+ native_assets: [],
49
+ };
50
+ const hash = computeTaskHash(task);
51
+ expect(hash).toHaveLength(64);
52
+ expect(hash).toMatch(/^[0-9a-f]{64}$/);
53
+ });
54
+
55
+ it("encodes zero as single byte [0x00]", () => {
56
+ const task: TaskData = {
57
+ project_content: "",
58
+ expiration_time: 0n,
59
+ lovelace_amount: 0n,
60
+ native_assets: [],
61
+ };
62
+ const bytes = debugTaskBytes(task);
63
+ // Empty content + 0x00 for expiration + 0x00 for lovelace = "0000"
64
+ expect(bytes).toBe("0000");
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");
79
+ });
80
+
81
+ it("handles empty token name", () => {
82
+ const task: TaskData = {
83
+ project_content: "Test",
84
+ expiration_time: 1n,
85
+ lovelace_amount: 1n,
86
+ native_assets: [["a".repeat(56), "", 100n]],
87
+ };
88
+ expect(() => computeTaskHash(task)).not.toThrow();
89
+ });
90
+
91
+ it("handles large bigint values beyond Number.MAX_SAFE_INTEGER", () => {
92
+ const task: TaskData = {
93
+ project_content: "Test",
94
+ expiration_time: BigInt(Number.MAX_SAFE_INTEGER) + 1n,
95
+ lovelace_amount: 9999999999999999999n,
96
+ native_assets: [],
97
+ };
98
+ expect(() => computeTaskHash(task)).not.toThrow();
99
+ expect(computeTaskHash(task)).toHaveLength(64);
100
+ });
101
+
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
+ it("normalizes Unicode strings (NFC)", () => {
116
+ // café with combining acute accent vs precomposed
117
+ const task1: TaskData = {
118
+ project_content: "cafe\u0301", // e + combining acute
119
+ expiration_time: 1n,
120
+ lovelace_amount: 1n,
121
+ native_assets: [],
122
+ };
123
+ const task2: TaskData = {
124
+ project_content: "caf\u00e9", // precomposed é
125
+ expiration_time: 1n,
126
+ lovelace_amount: 1n,
127
+ native_assets: [],
128
+ };
129
+ expect(computeTaskHash(task1)).toBe(computeTaskHash(task2));
130
+ });
131
+
132
+ it("handles multiple native assets in order", () => {
133
+ const policyId1 = "a".repeat(56);
134
+ const policyId2 = "b".repeat(56);
135
+ const task: TaskData = {
136
+ project_content: "",
137
+ expiration_time: 0n,
138
+ lovelace_amount: 0n,
139
+ native_assets: [
140
+ [policyId1, "01", 1n],
141
+ [policyId2, "02", 2n],
142
+ ],
143
+ };
144
+ const bytes = debugTaskBytes(task);
145
+ // Should contain policy IDs in order provided
146
+ const policyId1Hex = "aa".repeat(28);
147
+ const policyId2Hex = "bb".repeat(28);
148
+ expect(bytes).toContain(policyId1Hex);
149
+ expect(bytes).toContain(policyId2Hex);
150
+ expect(bytes.indexOf(policyId1Hex)).toBeLessThan(
151
+ bytes.indexOf(policyId2Hex),
152
+ );
153
+ });
154
+ });
155
+
156
+ describe("determinism", () => {
157
+ it("produces identical output for identical input", () => {
158
+ const task: TaskData = {
159
+ project_content: "Test",
160
+ expiration_time: 12345n,
161
+ lovelace_amount: 67890n,
162
+ native_assets: [],
163
+ };
164
+ expect(computeTaskHash(task)).toBe(computeTaskHash(task));
165
+ });
166
+
167
+ it("produces different hashes for different inputs", () => {
168
+ const task1: TaskData = {
169
+ project_content: "Test1",
170
+ expiration_time: 1n,
171
+ lovelace_amount: 1n,
172
+ native_assets: [],
173
+ };
174
+ const task2: TaskData = {
175
+ project_content: "Test2",
176
+ expiration_time: 1n,
177
+ lovelace_amount: 1n,
178
+ native_assets: [],
179
+ };
180
+ expect(computeTaskHash(task1)).not.toBe(computeTaskHash(task2));
181
+ });
182
+ });
183
+ });
184
+
185
+ describe("input validation", () => {
186
+ it("rejects project_content over 140 characters", () => {
187
+ const task: TaskData = {
188
+ project_content: "x".repeat(141),
189
+ expiration_time: 1n,
190
+ lovelace_amount: 1n,
191
+ native_assets: [],
192
+ };
193
+ expect(() => computeTaskHash(task)).toThrow(/exceeds 140 characters/);
194
+ });
195
+
196
+ it("accepts project_content at exactly 140 characters", () => {
197
+ const task: TaskData = {
198
+ project_content: "x".repeat(140),
199
+ expiration_time: 1n,
200
+ lovelace_amount: 1n,
201
+ native_assets: [],
202
+ };
203
+ expect(() => computeTaskHash(task)).not.toThrow();
204
+ });
205
+
206
+ it("rejects negative expiration_time", () => {
207
+ const task: TaskData = {
208
+ project_content: "Test",
209
+ expiration_time: -1n,
210
+ lovelace_amount: 1n,
211
+ native_assets: [],
212
+ };
213
+ expect(() => computeTaskHash(task)).toThrow(/non-negative/);
214
+ });
215
+
216
+ it("rejects negative lovelace_amount", () => {
217
+ const task: TaskData = {
218
+ project_content: "Test",
219
+ expiration_time: 1n,
220
+ lovelace_amount: -1n,
221
+ native_assets: [],
222
+ };
223
+ expect(() => computeTaskHash(task)).toThrow(/non-negative/);
224
+ });
225
+
226
+ it("rejects invalid policyId length (too short)", () => {
227
+ const task: TaskData = {
228
+ project_content: "Test",
229
+ expiration_time: 1n,
230
+ lovelace_amount: 1n,
231
+ native_assets: [["abc", "def0", 1n]],
232
+ };
233
+ expect(() => computeTaskHash(task)).toThrow(/policyId must be 56 hex chars/);
234
+ });
235
+
236
+ it("rejects invalid policyId length (too long)", () => {
237
+ const task: TaskData = {
238
+ project_content: "Test",
239
+ expiration_time: 1n,
240
+ lovelace_amount: 1n,
241
+ native_assets: [["a".repeat(58), "", 1n]],
242
+ };
243
+ expect(() => computeTaskHash(task)).toThrow(/policyId must be 56 hex chars/);
244
+ });
245
+
246
+ it("rejects invalid hex characters in policyId", () => {
247
+ const task: TaskData = {
248
+ project_content: "Test",
249
+ expiration_time: 1n,
250
+ lovelace_amount: 1n,
251
+ native_assets: [["g".repeat(56), "", 1n]],
252
+ };
253
+ expect(() => computeTaskHash(task)).toThrow(/invalid hex characters/);
254
+ });
255
+
256
+ it("rejects tokenName with odd length", () => {
257
+ const task: TaskData = {
258
+ project_content: "Test",
259
+ expiration_time: 1n,
260
+ lovelace_amount: 1n,
261
+ native_assets: [["a".repeat(56), "abc", 1n]], // odd length
262
+ };
263
+ expect(() => computeTaskHash(task)).toThrow(/even length/);
264
+ });
265
+
266
+ it("rejects tokenName over 64 characters", () => {
267
+ const task: TaskData = {
268
+ project_content: "Test",
269
+ expiration_time: 1n,
270
+ lovelace_amount: 1n,
271
+ native_assets: [["a".repeat(56), "a".repeat(66), 1n]],
272
+ };
273
+ expect(() => computeTaskHash(task)).toThrow(/0-64 hex chars/);
274
+ });
275
+
276
+ it("rejects invalid hex characters in tokenName", () => {
277
+ const task: TaskData = {
278
+ project_content: "Test",
279
+ expiration_time: 1n,
280
+ lovelace_amount: 1n,
281
+ native_assets: [["a".repeat(56), "gg", 1n]],
282
+ };
283
+ expect(() => computeTaskHash(task)).toThrow(/invalid hex characters/);
284
+ });
285
+
286
+ it("rejects negative asset quantity", () => {
287
+ const task: TaskData = {
288
+ project_content: "Test",
289
+ expiration_time: 1n,
290
+ lovelace_amount: 1n,
291
+ native_assets: [["a".repeat(56), "", -1n]],
292
+ };
293
+ expect(() => computeTaskHash(task)).toThrow(/non-negative/);
294
+ });
295
+ });
296
+
297
+ describe("verifyTaskHash", () => {
298
+ it("returns true for matching hash", () => {
299
+ const task: TaskData = {
300
+ project_content: "Test",
301
+ expiration_time: 1n,
302
+ lovelace_amount: 1n,
303
+ native_assets: [],
304
+ };
305
+ const hash = computeTaskHash(task);
306
+ expect(verifyTaskHash(task, hash)).toBe(true);
307
+ });
308
+
309
+ it("returns false for non-matching hash", () => {
310
+ const task: TaskData = {
311
+ project_content: "Test",
312
+ expiration_time: 1n,
313
+ lovelace_amount: 1n,
314
+ native_assets: [],
315
+ };
316
+ expect(verifyTaskHash(task, "0".repeat(64))).toBe(false);
317
+ });
318
+
319
+ it("handles case-insensitive comparison", () => {
320
+ const task: TaskData = {
321
+ project_content: "Test",
322
+ expiration_time: 1n,
323
+ lovelace_amount: 1n,
324
+ native_assets: [],
325
+ };
326
+ const hash = computeTaskHash(task);
327
+ expect(verifyTaskHash(task, hash.toUpperCase())).toBe(true);
328
+ });
329
+ });
330
+
331
+ describe("isValidTaskHash", () => {
332
+ it("validates correct lowercase hash", () => {
333
+ expect(isValidTaskHash("a".repeat(64))).toBe(true);
334
+ });
335
+
336
+ it("validates correct uppercase hash", () => {
337
+ expect(isValidTaskHash("A".repeat(64))).toBe(true);
338
+ });
339
+
340
+ it("validates correct mixed case hash", () => {
341
+ expect(isValidTaskHash("0123456789abcdefABCDEF".repeat(3).slice(0, 64))).toBe(true);
342
+ });
343
+
344
+ it("rejects hash that is too short", () => {
345
+ expect(isValidTaskHash("a".repeat(63))).toBe(false);
346
+ });
347
+
348
+ it("rejects hash that is too long", () => {
349
+ expect(isValidTaskHash("a".repeat(65))).toBe(false);
350
+ });
351
+
352
+ it("rejects hash with invalid characters", () => {
353
+ expect(isValidTaskHash("g".repeat(64))).toBe(false);
354
+ });
355
+
356
+ it("rejects empty string", () => {
357
+ expect(isValidTaskHash("")).toBe(false);
358
+ });
359
+ });
360
+
361
+ describe("debugTaskBytes", () => {
362
+ it("returns hex representation of encoded bytes", () => {
363
+ const task: TaskData = {
364
+ project_content: "Hi",
365
+ expiration_time: 1n,
366
+ lovelace_amount: 2n,
367
+ native_assets: [],
368
+ };
369
+ const bytes = debugTaskBytes(task);
370
+ // "Hi" = 0x48 0x69, then 0x01 for expiration, 0x02 for lovelace
371
+ expect(bytes).toBe("48690102");
372
+ });
373
+
374
+ it("validates input before encoding", () => {
375
+ const task: TaskData = {
376
+ project_content: "x".repeat(141),
377
+ expiration_time: 1n,
378
+ lovelace_amount: 1n,
379
+ native_assets: [],
380
+ };
381
+ expect(() => debugTaskBytes(task)).toThrow(/exceeds 140 characters/);
382
+ });
383
+ });