1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549 | /**
* LedgerStore against the committed dist/.
*
* The fold table in test/vectors/ledger.json is arithmetic written out by hand
* — every expected number is derivable from the step before it with one
* addition — and this file drives it. Everything else here is about the
* properties the ledger exists to keep:
*
* pending is not settled,
* nothing is ever silently dropped — by a cap, by a sibling tab, or by a
* rounding error,
* a sequence slot is handed out once, and
* bytes this build cannot read are not an empty ledger.
*
* THE MUTATORS RETURN 0 / 1 / 2, not a boolean: 0 rejected, 1 recorded and on
* disk, 2 recorded in memory only (`storageError` says why). The constants
* below name them so an assertion says which of the three it means — "durable"
* and "in memory only" used to be the same `true`, which is the whole reason
* this round exists.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { installFakeStorage } from "./helpers/fake-storage.mjs";
const storage = installFakeStorage(); // before dist/ constructs anything
const { AMT_MAX, LedgerStore, LEDGER_KEY_PREFIX, SKEW_MS, expired, issuanceKey, orderId, receiptKey } =
await import("../dist/index.js");
/** The three write outcomes. */
const REJECTED = 0;
const STORED = 1;
const MEMORY_ONLY = 2;
const {
SEED_BANK,
SEED_PAYEE,
SEED_PAYER,
TS_FIXED,
b64url,
pubOf,
signArtifact,
} = await import("./vectors/independent.mjs");
const fold = JSON.parse(await readFile(new URL("./vectors/ledger.json", import.meta.url), "utf8"));
const payerPub = pubOf(SEED_PAYER);
const payeePub = pubOf(SEED_PAYEE);
const bankPub = pubOf(SEED_BANK);
const CUR = `${bankPub}.LEI`;
/**
* The clock every assertion about `held` is made against.
*
* `held` now excludes orders that can no longer settle, so it is a function of
* a clock, and the fixture orders expired in January 2025. Pinning it is not a
* workaround: `balances(nowMs)` exists precisely so a fold can be reproducible,
* and a test that let the wall clock in would assert a different thing every
* time it ran. NOW sits inside every fixture order's live window.
*/
const NOW = TS_FIXED + 1000;
/** The three numbers, for comparing against the hand-written fold table. */
const bal = (store, cur = CUR, now = NOW) => {
const b = store.balances(now)[cur];
return b === undefined ? undefined : { settled: b.settled, held: b.held, available: b.available };
};
const oid = (n) => b64url(new Uint8Array(16).fill(n));
const mkOrder = ({ id, amt, seq, from, to, seed, ctx = "", memo = "", ts = TS_FIXED, ttl = 600000 }) =>
signArtifact(
"wpo",
{ v: 1, t: "wpo", id, cur: CUR, amt, seq, from, to, ctx, memo, ts, exp: ts + ttl },
seed,
);
const out = (o) => mkOrder({ from: payerPub, to: payeePub, seed: SEED_PAYER, ...o });
const inbound = (o) => mkOrder({ from: payeePub, to: payerPub, seed: SEED_PAYEE, ...o });
const mkReceipt = (po, ref = "L-1") =>
signArtifact("wrc", { v: 1, t: "wrc", po, seq: ref, ts: TS_FIXED + 1000, bank: bankPub }, SEED_BANK);
const mkDecline = (po, why = "insufficient") =>
signArtifact("wrj", { v: 1, t: "wrj", po, why, ts: TS_FIXED + 1000, bank: bankPub }, SEED_BANK);
/** A bank-signed issuance receipt: {seq, amt, h, to} plus an optional ts. */
const mkIssuance = ({ seq, amt, h, to }, ts = TS_FIXED + 1000) =>
signArtifact("wri", { v: 1, t: "wri", cur: CUR, seq, to, amt, h, ts, bank: bankPub }, SEED_BANK);
const ORDERS = {
out_a: out({ id: oid(1), amt: 100, seq: 1 }),
out_b: out({ id: oid(2), amt: 250, seq: 2 }),
in_c: inbound({ id: oid(3), amt: 500, seq: 1 }),
};
// ---- the fold table ---------------------------------------------------------
test("the hand-computed fold table holds, step by step", async () => {
storage.reset();
let store = new LedgerStore(fold.me);
assert.equal(fold.me, payerPub);
assert.equal(fold.cur, CUR);
for (const step of fold.steps) {
let result;
if (step.op === "addOrder") result = await store.addOrder(ORDERS[step.order]);
else if (step.op === "applyReceipt") result = await store.applyReceipt(mkReceipt(ORDERS[step.order]));
else if (step.op === "applyDecline") result = await store.applyDecline(mkDecline(ORDERS[step.order]));
else if (step.op === "reload") store = new LedgerStore(fold.me);
// the fixture says ACCEPTED or not; storage works throughout this table, so
// "accepted" pins the durable code exactly rather than merely "not rejected"
if (step.expectResult !== undefined) {
assert.equal(result, step.expectResult ? STORED : REJECTED, step.why);
}
const b = bal(store, fold.cur);
if (step.expect === null) assert.equal(b, undefined, step.why);
else assert.deepEqual(b, step.expect, `${step.op}: ${step.why}`);
assert.equal(store.nextSeq(fold.cur), step.nextSeq, `${step.op} nextSeq: ${step.why}`);
}
});
// ---- pending is not settled -------------------------------------------------
test("an order alone moves nothing; the receipt does", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
assert.deepEqual(bal(store), { settled: 0, held: 100, available: -100 });
assert.equal(store.pendingOrders().length, 1);
assert.equal(store.receipts().length, 0);
await store.applyReceipt(mkReceipt(ORDERS.out_a));
assert.deepEqual(bal(store), { settled: -100, held: 0, available: -100 });
assert.equal(store.pendingOrders().length, 0);
assert.equal(store.receipts().length, 1);
});
test("an incoming pending order is not held against me", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.in_c); // someone else's order, paid TO me
assert.deepEqual(bal(store), { settled: 0, held: 0, available: 0 });
});
test("a settled order cannot be put back in flight", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
await store.applyReceipt(mkReceipt(ORDERS.out_a));
assert.equal(await store.addOrder(ORDERS.out_a), REJECTED, "re-adding would hold money that is already spent");
assert.deepEqual(bal(store), { settled: -100, held: 0, available: -100 });
});
test("the answered-order-id guard is reached, and is not the slot guard wearing a hat", async () => {
// The assertion above passes for the WRONG REASON and mutation testing said
// so: addOrder checks `_oids` (this order has been answered) before `_slots`
// (this (payer, bank, seq) is taken), and for an honest store the two can
// never disagree — same order id means same order means same slot — so
// deleting the `_oids` branch entirely changed no verdict anywhere.
//
// They CAN disagree in exactly one situation, and it is one the kit documents
// taking on trust: the `o` stored beside a settlement is checked as a digest
// and not recomputed (read-settlements!), so hand-edited storage can name an
// order id that is not the id of the order beside it. Here the answered id is
// seq 9's while the slot marked is seq 1's, so `_slots` cannot catch it and
// only `_oids` can. Killing that branch makes this addOrder succeed.
storage.reset();
const store = new LedgerStore(payerPub);
const settled = out({ id: oid(1), amt: 100, seq: 1 });
const other = out({ id: oid(9), amt: 250, seq: 9 }); // a different slot entirely
await store.applyReceipt(mkReceipt(settled));
const key = `wal:${payerPub}`;
const blob = JSON.parse(storage.map.get(key));
blob.receipts[0].o = await orderId(other); // a well-formed digest, of the wrong order
storage.map.set(key, JSON.stringify(blob));
const reloaded = new LedgerStore(payerPub);
assert.equal(reloaded.receipts().length, 1, "the receipt still loads — only its `o` was edited");
assert.equal(
await reloaded.addOrder(other),
REJECTED,
"an order id recorded as answered is refused even when its slot is free",
);
assert.equal(reloaded.pendingOrders().length, 0);
// and the control: the same order against a store whose `o` was left alone
storage.reset();
const clean = new LedgerStore(payerPub);
await clean.applyReceipt(mkReceipt(settled));
assert.equal(await clean.addOrder(other), STORED, "nothing else about `other` is objectionable");
});
test("a re-issued receipt with a fresh ts and log ref folds once", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
assert.equal(await store.applyReceipt(mkReceipt(ORDERS.out_a, "L-1")), STORED);
const reissued = signArtifact(
"wrc",
{ v: 1, t: "wrc", po: ORDERS.out_a, seq: "L-77", ts: TS_FIXED + 9000, bank: bankPub },
SEED_BANK,
);
assert.equal(await store.applyReceipt(reissued), REJECTED, "same bank, same order, one settlement");
assert.deepEqual(bal(store), { settled: -100, held: 0, available: -100 });
});
// ---- the mutators are total -------------------------------------------------
test("the store refuses malformed artifacts without throwing", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
for (const junk of [null, undefined, {}, [], 0, "wpo", { ...ORDERS.out_a, amt: -1 }]) {
assert.equal(await store.addOrder(junk), REJECTED);
assert.equal(await store.applyReceipt(junk), REJECTED);
assert.equal(await store.applyDecline(junk), REJECTED);
}
assert.equal(await store.applyReceipt(mkDecline(ORDERS.out_a)), REJECTED, "a decline is not a receipt");
assert.equal(await store.applyDecline(mkReceipt(ORDERS.out_a)), REJECTED, "a receipt is not a decline");
});
test("a hostile artifact off the network resolves false, it does not reject", async () => {
// the fixed junk list above cannot reach this: every one of those values is
// read without incident. These THROW when read, and a throw one rung up is
// `await store.applyReceipt(x)` becoming an unhandled rejection in a caller
// that a .d.ts told to expect a boolean.
storage.reset();
const store = new LedgerStore(payerPub);
const revoked = Proxy.revocable({}, {});
revoked.revoke();
const hostile = [
new Proxy({}, { get() { throw new Error("boom"); } }),
new Proxy({}, { ownKeys() { throw new Error("boom"); } }),
revoked.proxy,
{ get v() { throw new Error("boom"); } },
{ ...ORDERS.out_a, get memo() { throw new Error("boom"); } },
{ ...mkReceipt(ORDERS.out_a), get po() { throw new Error("boom"); } },
];
for (const h of hostile) {
assert.equal(await store.addOrder(h), REJECTED);
assert.equal(await store.applyReceipt(h), REJECTED);
assert.equal(await store.applyDecline(h), REJECTED);
assert.equal(await store.importJson(h), 0);
}
assert.equal(store.receipts().length, 0, "and nothing was recorded on the way past");
});
// ---- sequences: the payer's half of the double-spend defense ----------------
test("nextSeq is per bank, per payer, and monotone over every outcome", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
assert.equal(store.nextSeq(CUR), 1);
await store.addOrder(ORDERS.out_a); // seq 1
assert.equal(store.nextSeq(CUR), 2);
await store.applyDecline(mkDecline(ORDERS.out_a));
assert.equal(store.nextSeq(CUR), 2, "a declined slot is not reused");
const otherBank = pubOf(new Uint8Array(32).fill(0x33));
assert.equal(store.nextSeq(`${otherBank}.LEI`), 1, "another bank keeps its own sequence");
assert.equal(store.nextSeq(`${bankPub}.RON`), 2, "one sequence per BANK, not per code");
assert.equal(store.nextSeq("nonsense"), 1, "an unparseable currency starts over rather than throwing");
});
test("two orders on one (payer, bank, seq) slot: the second is refused", async () => {
// `seq` is the bank's double-spend defense: its fold binds an order to exactly
// one slot, so two signed orders on one slot means at most one can ever settle
// and the payer cannot tell which. A wallet that held both would report a hold
// for money only one of them can spend, and would have signed a second
// instruction it can neither cancel nor predict.
storage.reset();
const store = new LedgerStore(payerPub);
const first = out({ id: oid(1), amt: 100, seq: 4 });
const second = out({ id: oid(2), amt: 250, seq: 4 }); // same slot, different order
assert.equal(await store.addOrder(first), STORED);
assert.equal(await store.addOrder(second), REJECTED, "one slot, one order");
assert.deepEqual(bal(store), { settled: 0, held: 100, available: -100 }, "and no double hold");
assert.equal(store.pendingOrders().length, 1);
// a settled slot stays taken, and so does a declined one
await store.applyReceipt(mkReceipt(first));
assert.equal(await store.addOrder(second), REJECTED, "the slot is spent, not free");
const declined = out({ id: oid(3), amt: 7, seq: 5 });
await store.applyDecline(mkDecline(declined));
assert.equal(await store.addOrder(out({ id: oid(4), amt: 8, seq: 5 })), REJECTED, "a declined slot is not free");
// another payer's slots are their own
assert.equal(await store.addOrder(inbound({ id: oid(5), amt: 9, seq: 4 })), STORED);
});
test("nextSeq alone hands the same slot out twice; reserveSeq does not", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
assert.equal(store.nextSeq(CUR), store.nextSeq(CUR), "nextSeq is advisory and says so");
const a = await store.reserveSeq(CUR);
const b = await store.reserveSeq(CUR);
assert.equal(a, 1);
assert.equal(b, 2, "an allocation is taken, not read");
assert.equal(store.nextSeq(CUR), 3, "and the advisory reading follows it");
});
test("two tabs reserving from one storage never collide", async () => {
storage.reset();
const a = new LedgerStore(payerPub);
const b = new LedgerStore(payerPub);
const taken = [];
for (let i = 0; i < 6; i++) taken.push(await (i % 2 ? b : a).reserveSeq(CUR));
assert.deepEqual(taken, [1, 2, 3, 4, 5, 6], "one sequence, two holders");
assert.equal(new LedgerStore(payerPub).nextSeq(CUR), 7, "and a third tab picks up after them");
});
test("a reserved slot survives a reload and a prune", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.reserveSeq(CUR);
await store.reserveSeq(CUR);
assert.equal(new LedgerStore(payerPub).nextSeq(CUR), 3, "a reload does not re-offer a taken slot");
await store.addOrder(out({ id: oid(1), amt: 100, seq: 3 }));
assert.equal(store.nextSeq(CUR), 4);
store.prune(() => false);
assert.equal(store.receipts().length + store.pendingOrders().length, 0, "the record went");
assert.equal(store.nextSeq(CUR), 4, "the slot did not come back — its docstring promises monotone");
assert.equal(new LedgerStore(payerPub).nextSeq(CUR), 4, "on disk too");
});
test("reserveSeq rejects a currency it cannot parse", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await assert.rejects(() => store.reserveSeq("nonsense"), /wallet-kit/);
await assert.rejects(() => store.reserveSeq("~.GAZ"), /wallet-kit/);
});
// ---- one ledger, many tabs --------------------------------------------------
test("two tabs on one storage keep BOTH their receipts", async () => {
// the key is the identity, so this is the ordinary case: the hub shows the
// balance while the game takes the stake. A store that wrote its own memory
// over the blob would make the second write erase the first tab's receipt —
// money that silently ceases to exist, with storageError still null.
storage.reset();
const a = new LedgerStore(payerPub);
const b = new LedgerStore(payerPub);
assert.equal(await a.applyReceipt(mkReceipt(ORDERS.out_a)), STORED);
assert.equal(await b.applyReceipt(mkReceipt(ORDERS.in_c)), STORED);
const fresh = new LedgerStore(payerPub);
assert.equal(fresh.receipts().length, 2, "both receipts are on disk");
assert.deepEqual(bal(fresh), { settled: 400, held: 0, available: 400 }, "-100 + 500");
assert.equal(a.storageError, null, "and nothing failed");
assert.equal(b.storageError, null);
});
test("after a merge, memory is exactly what a fresh load would produce", async () => {
storage.reset();
const a = new LedgerStore(payerPub);
const b = new LedgerStore(payerPub);
await a.addOrder(ORDERS.out_a);
await b.addOrder(ORDERS.out_b);
await b.applyDecline(mkDecline(ORDERS.out_a)); // b answers a's order
await a.addOrder(inbound({ id: oid(9), amt: 5, seq: 3 })); // a writes again, absorbing b
const fresh = new LedgerStore(payerPub);
const snapshot = (s) => ({
balances: bal(s),
receipts: s.receipts().map((r) => r.po.id).sort(),
declines: s.declines().map((r) => r.po.id).sort(),
pending: s.pendingOrders().map((o) => o.id).sort(),
nextSeq: s.nextSeq(CUR),
});
assert.deepEqual(snapshot(a), snapshot(fresh), "the merging tab converged");
assert.equal(
snapshot(a).pending.includes(ORDERS.out_a.id),
false,
"and the order the other tab got answered is out of flight here too",
);
});
test("a storage event from another tab reloads this one", async () => {
// the fake storage does not dispatch; a real browser does. What is under test
// is the listener the store attaches, and that close() takes it off again.
//
// The other writer here is a raw blob written straight to storage, NOT a
// second LedgerStore: a sibling store in this same process now notifies its
// peers directly (a real browser's `storage` event never reaches the document
// that wrote), so using one would test the registry instead of the listener.
const listeners = [];
const origAdd = globalThis.addEventListener;
const origRemove = globalThis.removeEventListener;
globalThis.addEventListener = (type, h) => void listeners.push({ type, h });
globalThis.removeEventListener = (type, h) => {
const i = listeners.findIndex((l) => l.type === type && l.h === h);
if (i >= 0) listeners.splice(i, 1);
};
try {
storage.reset();
const watcher = new LedgerStore(payerPub);
assert.equal(listeners.length, 1, "the store listens for storage");
assert.equal(listeners[0].type, "storage");
let changes = 0;
watcher.onChange = () => void changes++;
const rc = mkReceipt(ORDERS.out_a);
storage.map.set(
`wal:${payerPub}`,
JSON.stringify({
v: 1,
receipts: [{ k: await receiptKey(rc), o: await orderId(ORDERS.out_a), a: rc }],
declines: [],
pending: [],
seqs: {},
stakes: [],
}),
);
assert.equal(watcher.receipts().length, 0, "no event yet, no news yet");
for (const l of [...listeners]) l.h({ key: `wal:${payerPub}` });
assert.equal(watcher.receipts().length, 1, "the event reloaded it");
assert.ok(changes >= 1, "and told the UI");
const before = watcher.receipts().length;
for (const l of [...listeners]) l.h({ key: "something:else" });
assert.equal(watcher.receipts().length, before, "another key is not our business");
watcher.close();
assert.equal(listeners.length, 0, "close detaches");
} finally {
globalThis.addEventListener = origAdd;
globalThis.removeEventListener = origRemove;
}
});
test("a store holding an unpersisted receipt does not lose it to a convergence", async () => {
// a reload replaces memory with disk, and for a store whose write failed
// memory is the only copy of that receipt. Converging must union, not replace
// — otherwise the retention rule is broken from the inside, by the very
// mechanism that exists to stop a sibling breaking it.
const listeners = [];
const origAdd = globalThis.addEventListener;
const origRemove = globalThis.removeEventListener;
globalThis.addEventListener = (type, h) => void listeners.push({ type, h });
globalThis.removeEventListener = (type, h) => {
const i = listeners.findIndex((l) => l.type === type && l.h === h);
if (i >= 0) listeners.splice(i, 1);
};
try {
storage.reset();
const store = new LedgerStore(payerPub);
storage.failWrites(true);
assert.equal(await store.applyReceipt(mkReceipt(ORDERS.out_a)), MEMORY_ONLY);
storage.failWrites(false);
// meanwhile another tab writes a ledger that has never heard of it
const rc = mkReceipt(ORDERS.in_c);
storage.map.set(
`wal:${payerPub}`,
JSON.stringify({
v: 1,
receipts: [{ k: await receiptKey(rc), o: await orderId(ORDERS.in_c), a: rc }],
declines: [],
pending: [],
seqs: {},
stakes: [],
}),
);
for (const l of [...listeners]) l.h({ key: `wal:${payerPub}` });
assert.equal(store.receipts().length, 2, "the unpersisted receipt survived the event");
assert.deepEqual(bal(store), { settled: 400, held: 0, available: 400 }, "-100 + 500");
store.close();
} finally {
globalThis.addEventListener = origAdd;
globalThis.removeEventListener = origRemove;
}
});
test("a prune is not undone by the merge that writes it", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
await store.applyReceipt(mkReceipt(ORDERS.out_a));
assert.equal(store.prune((_a, kind) => kind !== "wrc"), 1);
assert.equal(store.receipts().length, 0);
assert.equal(new LedgerStore(payerPub).receipts().length, 0, "and it stayed gone on disk");
});
// ---- exact arithmetic -------------------------------------------------------
test("the fold is exact and order-independent at the top of the amount range", async () => {
// One amount may be 2^50 and doubles are exact on integers only to 2^53, so
// EIGHT maximal receipts is the entire headroom — the comment that claimed a
// thousand was wrong by 128x. Past it a double fold is not merely approximate,
// it is order-dependent: these ten receipts total 2^53+2, which is itself a
// perfectly representable double, and a double fold returns it or 2^53
// depending only on the sequence they arrived in.
const EXACT = 8n * BigInt(AMT_MAX) + 2n;
assert.equal(Number(EXACT), 9007199254740994, "the true total is representable; the fold just missed it");
const credits = [];
for (let i = 0; i < 8; i++) credits.push(inbound({ id: oid(i), amt: AMT_MAX, seq: i + 1 }));
for (let i = 0; i < 2; i++) credits.push(inbound({ id: oid(100 + i), amt: 1, seq: 100 + i }));
const foldIn = async (orders) => {
storage.reset();
const s = new LedgerStore(payerPub);
for (const po of orders) assert.equal(await s.applyReceipt(mkReceipt(po, `L-${po.id}`)), STORED);
return s.balances(NOW)[CUR];
};
const bigFirst = await foldIn(credits);
const smallFirst = await foldIn([...credits].reverse());
assert.equal(bigFirst.settledExact, EXACT, "big-first");
assert.equal(smallFirst.settledExact, EXACT, "small-first");
assert.equal(bigFirst.settledExact, smallFirst.settledExact, "the total is a function of the SET");
assert.equal(bigFirst.settled, Number(EXACT), "and the Number agrees, because this one fits");
assert.equal(bigFirst.exact, true);
// the double fold, done here, to show what is being avoided
const naive = (xs) => xs.reduce((n, po) => n + po.amt, 0);
assert.notEqual(naive(credits), naive([...credits].reverse()), "a float fold is order-dependent here");
});
test("a total past 2^53 is reported exactly and says it is not an exact Number", async () => {
// nine maximal receipts and one of 1: the total is odd and above 2^53, where
// the gap between doubles is 2, so no double is this number
storage.reset();
const s = new LedgerStore(payerPub);
for (let i = 0; i < 9; i++) {
await s.applyReceipt(mkReceipt(inbound({ id: oid(i), amt: AMT_MAX, seq: i + 1 }), `L-${i}`));
}
await s.applyReceipt(mkReceipt(inbound({ id: oid(50), amt: 1, seq: 50 }), "L-50"));
const EXACT = 9n * BigInt(AMT_MAX) + 1n;
assert.ok(EXACT > BigInt(Number.MAX_SAFE_INTEGER), "past the exact-integer range");
assert.notEqual(BigInt(Number(EXACT)), EXACT, "and genuinely not representable as a double");
const b = s.balances(NOW)[CUR];
assert.equal(b.settledExact, EXACT, "the exact fold is exact");
assert.equal(b.exact, false, "and it says the Number is a rounding, rather than pretending");
assert.equal(b.settled, Number(b.settledExact), "the Number is the honest conversion of it");
});
test("held and available are exact too, and the three agree", async () => {
storage.reset();
const s = new LedgerStore(payerPub);
await s.applyReceipt(mkReceipt(inbound({ id: oid(1), amt: AMT_MAX, seq: 1 }), "L-1"));
await s.addOrder(out({ id: oid(2), amt: 3, seq: 1 }));
const b = s.balances(NOW)[CUR];
assert.equal(b.heldExact, 3n);
assert.equal(b.availableExact, b.settledExact - b.heldExact);
assert.equal(b.available, Number(b.availableExact));
assert.equal(b.exact, true);
});
// ---- expiry releases a hold -------------------------------------------------
test("an expired order stops being held, and can be seen and pruned", async () => {
// `exp` is inside what the payer signed, so no honest bank can still settle
// it. Holding against it depressed `available` for ever with no exit but a
// user-driven prune the user had no reason to suspect they needed.
storage.reset();
const store = new LedgerStore(payerPub);
const po = ORDERS.out_a;
await store.addOrder(po);
assert.deepEqual(bal(store, CUR, NOW), { settled: 0, held: 100, available: -100 }, "live: held");
assert.equal(store.expiredOrders(NOW).length, 0);
const after = po.exp + SKEW_MS + 1;
assert.equal(expired(po, after), true);
assert.deepEqual(bal(store, CUR, after), { settled: 0, held: 0, available: 0 }, "expired: released");
assert.equal(store.expiredOrders(after).length, 1, "and visible");
assert.equal(store.expiredOrders(after)[0].id, po.id);
assert.equal(store.pendingOrders().length, 1, "the paper is still there — nothing was dropped");
assert.equal(store.prune((a, kind) => !(kind === "pending" && expired(a, after))), 1);
assert.equal(store.pendingOrders().length, 0);
});
test("the hold survives the skew window a bank one millisecond behind still settles in", async () => {
// `ts?` accepts a bank's timestamp SKEW_MS into this reader's future and
// `settlement-shape` accepts a settlement SKEW_MS before its order, so a bank
// whose clock is a hair behind ours can and does settle an order we have just
// passed `exp` on. Releasing the hold at `exp` exactly made `available`
// overstate for those two minutes — it counted money that was still
// committed. `expired` itself is unchanged; this is the LEDGER's margin.
storage.reset();
const store = new LedgerStore(payerPub);
const po = ORDERS.out_a;
await store.addOrder(po);
for (const [when, why] of [
[po.exp + 1, "one millisecond past exp"],
[po.exp + SKEW_MS, "the last millisecond of the skew window"],
]) {
assert.equal(expired(po, when), true, `${why}: expired() says so, exactly`);
assert.deepEqual(bal(store, CUR, when), { settled: 0, held: 100, available: -100 }, why);
assert.equal(store.expiredOrders(when).length, 0, `${why}: and it is not offered for pruning yet`);
}
assert.deepEqual(bal(store, CUR, po.exp + SKEW_MS + 1), { settled: 0, held: 0, available: 0 },
"past the margin the hold goes");
assert.equal(store.expiredOrders(po.exp + SKEW_MS + 1).length, 1,
"and held and expiredOrders never disagree about one order");
});
test("a receipt for an expired order still folds — the bank is the judge of that", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
const after = ORDERS.out_a.exp + SKEW_MS + 1;
assert.deepEqual(bal(store, CUR, after), { settled: 0, held: 0, available: 0 });
assert.equal(await store.applyReceipt(mkReceipt(ORDERS.out_a)), STORED);
assert.deepEqual(bal(store, CUR, after), { settled: -100, held: 0, available: -100 });
});
// ---- portability ------------------------------------------------------------
test("export/import round trips, verifies signatures, and only ever adds", async () => {
storage.reset();
const a = new LedgerStore(payerPub);
await a.addOrder(ORDERS.out_a);
await a.applyReceipt(mkReceipt(ORDERS.out_a));
await a.addOrder(ORDERS.out_b);
const blob = a.exportJson();
storage.reset(); // the same identity on another device
const b = new LedgerStore(payerPub);
assert.equal(await b.importJson(blob), 2, "one receipt and one pending order");
assert.equal(await b.importJson(blob), 0, "importing twice adds nothing");
assert.deepEqual(bal(b), bal(a), "and the fold came with it");
const forged = JSON.parse(blob);
forged.receipts[0].po.amt = 999999;
storage.reset();
const c = new LedgerStore(payerPub);
assert.equal(await c.importJson(JSON.stringify(forged)), 1, "the tampered receipt is dropped, the order is not");
assert.equal(c.receipts().length, 0);
});
test("importJson checks the signature on a PENDING order too, not just on receipts", async () => {
storage.reset();
const a = new LedgerStore(payerPub);
await a.addOrder(ORDERS.out_a);
const blob = JSON.parse(a.exportJson());
assert.equal(blob.pending.length, 1);
// a valid-shaped, canonical, wrong signature: the shape check cannot see it
const flipped = blob.pending[0].sig.startsWith("A") ? "B" : "A";
blob.pending[0].sig = flipped + blob.pending[0].sig.slice(1);
storage.reset();
const b = new LedgerStore(payerPub);
assert.equal(await b.importJson(JSON.stringify(blob)), 0, "an unsigned order is not a promise");
assert.equal(b.pendingOrders().length, 0);
assert.deepEqual(bal(b), undefined, "and it certainly does not hold money");
});
test("another identity's exported ledger is refused wholesale", async () => {
// the kit's only documented untrusted-input path. Without this, a stranger's
// export merges as YOUR ledger: their currencies in your balances, their
// pending orders in your holds, their receipts in your history.
storage.reset();
const mine = new LedgerStore(payerPub);
await mine.addOrder(ORDERS.out_a);
await mine.applyReceipt(mkReceipt(ORDERS.out_a));
const blob = mine.exportJson();
assert.equal(JSON.parse(blob).idPub, payerPub);
storage.reset();
const stranger = new LedgerStore(payeePub);
assert.equal(await stranger.importJson(blob), 0, "not my ledger");
assert.equal(stranger.receipts().length, 0);
assert.deepEqual(stranger.balances(NOW), {}, "and no phantom currency row either");
});
test("a blob that CLAIMS my idPub still cannot smuggle in records about strangers", async () => {
// the second half of the gate, and the half that does not rely on the blob
// being honest about whose ledger it is
const third = pubOf(new Uint8Array(32).fill(0x55));
const between = signArtifact(
"wpo",
{ v: 1, t: "wpo", id: oid(7), cur: CUR, amt: 42, seq: 1, from: payeePub, to: third,
ctx: "", memo: "", ts: TS_FIXED, exp: TS_FIXED + 600000 },
SEED_PAYEE,
);
const mineToo = ORDERS.out_a;
const blob = JSON.stringify({
v: 1,
idPub: payerPub, // the claim
receipts: [mkReceipt(between, "L-x"), mkReceipt(mineToo, "L-y")],
declines: [],
pending: [between],
stakes: [],
});
storage.reset();
const store = new LedgerStore(payerPub);
assert.equal(await store.importJson(blob), 1, "only the record I am a party to");
assert.equal(store.receipts().length, 1);
assert.equal(store.receipts()[0].po.id, mineToo.id);
assert.equal(store.pendingOrders().length, 0, "a stranger's order is not my pending order");
assert.deepEqual(bal(store), { settled: -100, held: 0, available: -100 }, "and not my money either");
});
test("importJson survives garbage without throwing", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
for (const junk of ["", "{", "null", "[]", JSON.stringify({ v: 2 }),
JSON.stringify({ v: 1, receipts: 3 }),
JSON.stringify({ v: 1, idPub: payerPub, receipts: 3 })]) {
assert.equal(await store.importJson(junk), 0, junk);
}
});
// ---- retention: the rule this kit is built around ---------------------------
test("nothing is capped, evicted or aged out", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
const N = 400; // more than any cap in the suite (social-kit's receipt cap is 300)
for (let i = 0; i < N; i++) {
const po = out({ id: oid(i % 200) + "", amt: 1, seq: i + 1, ctx: `n:${i}` });
await store.applyReceipt(mkReceipt(po, `L-${i}`));
}
assert.equal(store.receipts().length, N, "every receipt is still there");
assert.equal(bal(store).settled, -N, "and every one of them is still in the balance");
});
test("a failed write costs durability, never history", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
const seen = [];
store.onStorageError = (e) => seen.push(e);
storage.failWrites(true);
assert.equal(await store.applyReceipt(mkReceipt(ORDERS.out_a)), MEMORY_ONLY,
"the fold still happens, and the return says it never reached disk");
assert.deepEqual(bal(store), { settled: -100, held: 0, available: -100 });
assert.equal(seen.length, 1, "and it is reported");
assert.equal(seen[0].name, "QuotaExceededError");
assert.equal(store.storageError.name, "QuotaExceededError");
assert.equal(store.receipts().length, 1, "no record was discarded to make room");
storage.failWrites(false);
await store.addOrder(ORDERS.out_b);
assert.equal(store.storageError, null, "and the flag clears when writing works again");
});
test("prune is the only deletion, and only the user drives it", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
await store.applyReceipt(mkReceipt(ORDERS.out_a));
await store.addOrder(ORDERS.out_b);
const kinds = [];
assert.equal(store.prune((a, kind) => { kinds.push(kind); return true; }), 0, "keeping everything removes nothing");
assert.deepEqual([...new Set(kinds)].sort(), ["pending", "wrc"]);
assert.equal(store.prune((a, kind) => kind !== "wrc"), 1);
assert.equal(store.receipts().length, 0);
assert.equal(store.pendingOrders().length, 1, "pruning receipts left the pending order alone");
});
test("a prune whose predicate throws removes nothing at all", async () => {
// it used to remove whatever it had got through first and then propagate,
// leaving memory permanently ahead of disk with nothing to say so
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
await store.addOrder(ORDERS.out_b);
await store.applyReceipt(mkReceipt(ORDERS.in_c));
const before = JSON.parse(storage.map.get(`wal:${payerPub}`));
let calls = 0;
assert.throws(() => store.prune(() => { if (++calls === 2) throw new Error("nope"); return false; }), /nope/);
assert.ok(calls >= 2, "it really got part way");
assert.equal(store.receipts().length, 1, "memory is untouched");
assert.equal(store.pendingOrders().length, 2);
assert.deepEqual(JSON.parse(storage.map.get(`wal:${payerPub}`)), before, "and so is disk");
assert.deepEqual(bal(store), { settled: 500, held: 350, available: 150 });
});
// ---- storage ----------------------------------------------------------------
test("the ledger key is namespaced by identity, not by app", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
assert.equal(LEDGER_KEY_PREFIX, "wal:");
assert.deepEqual([...storage.map.keys()], [`wal:${payerPub}`]);
});
test("a corrupted store degrades to fewer records, never to a wrong balance", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
await store.applyReceipt(mkReceipt(ORDERS.out_a));
const key = `wal:${payerPub}`;
const blob = JSON.parse(storage.map.get(key));
blob.receipts[0].a.po.amt = 12345; // the record no longer shapes/agrees with its key
blob.pending.push({ k: "not-a-digest", a: ORDERS.out_b });
blob.receipts.push({ k: "x", o: "y", a: { junk: true } });
storage.map.set(key, JSON.stringify(blob));
const reloaded = new LedgerStore(payerPub);
assert.equal(reloaded.receipts().length, 1, "the shape check keeps the structurally valid record");
assert.equal(reloaded.receipts()[0].po.amt, 12345, "and does not invent a signature check it did not do");
assert.equal(reloaded.pendingOrders().length, 0, "records with unusable keys are dropped");
assert.equal(reloaded.storageError, null, "readable bytes with bad records are not a storage failure");
});
test("stored records are RE-SHAPED on load, not believed because they have a key", async () => {
// the shape check on the way in is the only thing between hand-edited
// localStorage and a balance. A record with a well-formed digest key and an
// artifact that no verify* would ever have produced must not fold.
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
await store.applyReceipt(mkReceipt(ORDERS.out_a));
const key = `wal:${payerPub}`;
const good = JSON.parse(storage.map.get(key));
const realKey = good.receipts[0].k;
const realOid = good.receipts[0].o;
for (const [name, patch] of [
["an impossible amount", (a) => { a.po.amt = -5; }],
["a non-integer amount", (a) => { a.po.amt = 1.5; }],
["an amount past the ceiling", (a) => { a.po.amt = AMT_MAX + 1; }],
["exp at or before ts", (a) => { a.po.exp = a.po.ts; }],
["a stray fourteenth key on the order", (a) => { a.po.extra = 1; }],
["a control character in ctx", (a) => { a.po.ctx = `a${String.fromCharCode(7)}b`; }],
["a line separator in memo", (a) => { a.po.memo = `a${String.fromCodePoint(0x2028)}b`; }],
["a bank that is not the currency's banker", (a) => { a.bank = payeePub; }],
["a decline filed as a receipt", (a) => { a.t = "wrj"; a.why = "cur"; }],
]) {
const blob = JSON.parse(JSON.stringify(good));
patch(blob.receipts[0].a);
blob.receipts[0].k = realKey; // the key stays perfectly well-formed
blob.receipts[0].o = realOid;
storage.map.set(key, JSON.stringify(blob));
const reloaded = new LedgerStore(payerPub);
assert.equal(reloaded.receipts().length, 0, name);
assert.deepEqual(reloaded.balances(NOW), {}, `${name}: and no balance came from it`);
}
});
test("the reserved stakes section survives a write by this version", async () => {
storage.reset();
const key = `wal:${payerPub}`;
storage.map.set(key, JSON.stringify({ v: 1, receipts: [], declines: [], pending: [], stakes: [{ future: 1 }] }));
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
assert.deepEqual(JSON.parse(storage.map.get(key)).stakes, [{ future: 1 }]);
});
test("a section this version has never heard of survives a write too", async () => {
storage.reset();
const key = `wal:${payerPub}`;
storage.map.set(key, JSON.stringify({ v: 1, receipts: [], declines: [], pending: [],
stakes: [], escrow: [{ v: 2 }], trust: { x: 1 } }));
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
const after = JSON.parse(storage.map.get(key));
assert.deepEqual(after.escrow, [{ v: 2 }], "the stakes promise, generalized");
assert.deepEqual(after.trust, { x: 1 });
assert.equal(after.pending.length, 1, "and this version's own write still happened");
});
test("the dedup keys the store persists are the exported ones", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
const rc = mkReceipt(ORDERS.out_a);
await store.addOrder(ORDERS.out_a);
await store.applyReceipt(rc);
const blob = JSON.parse(storage.map.get(`wal:${payerPub}`));
assert.equal(blob.receipts[0].k, await receiptKey(rc));
assert.equal(blob.receipts[0].o, await orderId(ORDERS.out_a));
});
// ---- unreadable bytes are not an empty ledger -------------------------------
for (const [name, stored] of [
["unparseable JSON", "{not json"],
["JSON that is not an object", "[1,2,3]"],
["a version this build does not know", JSON.stringify({ v: 2, receipts: [], stakes: [{ future: 1 }] })],
]) {
test(`${name} is reported, and is never overwritten`, async () => {
storage.reset();
const key = `wal:${payerPub}`;
storage.map.set(key, stored);
const seen = [];
const store = new LedgerStore(payerPub);
store.onStorageError = (e) => seen.push(e);
assert.equal(store.unreadable, stored, "the bytes are handed to the consumer");
assert.equal(store.storageError.name, "UnreadableStorageError", "and it is an error, not a fresh ledger");
assert.equal(await store.addOrder(ORDERS.out_a), MEMORY_ONLY,
"the session still works in memory, and says the write was blocked");
assert.deepEqual(bal(store), { settled: 0, held: 100, available: -100 });
assert.equal(storage.map.get(key), stored, "but nothing was written over the bytes");
assert.equal(seen.length, 1, "and the refusal was reported");
assert.equal(store.storageError.name, "UnreadableStorageError");
assert.equal(store.acknowledgeUnreadable(), true, "until the consumer says so");
assert.equal(store.unreadable, null);
assert.equal(store.storageError, null);
assert.equal(JSON.parse(storage.map.get(key)).pending.length, 1, "and then the write lands");
assert.equal(store.acknowledgeUnreadable(), false, "there is nothing left to acknowledge");
});
}
test("an absent key is a fresh ledger and not an error", () => {
storage.reset();
const store = new LedgerStore(payerPub);
assert.equal(store.unreadable, null);
assert.equal(store.storageError, null);
assert.deepEqual(store.balances(NOW), {});
});
// ---- a write that did not happen says so ------------------------------------
test("the three write outcomes are three different answers", async () => {
// they used to be two: `true` meant "recorded", durable or not, so a caller
// could not tell a REJECTION from a BLOCKED write and `storageError` was the
// only clue — checked after the fact, if at all.
storage.reset();
const store = new LedgerStore(payerPub);
const rc = mkReceipt(ORDERS.out_a);
assert.equal(await store.applyReceipt(rc), STORED, "recorded and on disk");
assert.equal(await store.applyReceipt(rc), REJECTED, "a duplicate is a rejection");
assert.equal(await store.applyReceipt({ junk: true }), REJECTED, "and so is nonsense");
storage.failWrites(true);
const blocked = await store.applyReceipt(mkReceipt(ORDERS.in_c));
storage.failWrites(false);
assert.equal(blocked, MEMORY_ONLY, "recorded, not durable — and distinguishable from both");
assert.notEqual(blocked, REJECTED, "a blocked write did not reject the record");
assert.equal(store.receipts().length, 2, "the record really is there");
assert.equal(store.storageError.name, "QuotaExceededError", "and storageError says why it is not on disk");
// falsiness still means exactly "was it recorded"
assert.equal(Boolean(REJECTED), false);
assert.equal(Boolean(STORED) && Boolean(MEMORY_ONLY), true);
});
test("balances() cannot be JSON.stringify'd, and that is a property of being exact", () => {
// a consumer WILL try this. The *Exact fields are BigInt and JSON.stringify
// throws TypeError on one by specification; there is no replacer-free way
// round it, because a serializer whose number type is a double has nothing to
// write for a value no double can hold. Convert at the boundary.
storage.reset();
const store = new LedgerStore(payerPub);
assert.deepEqual(store.balances(NOW), {}, "an empty ledger has no rows and stringifies fine");
return store.applyReceipt(mkReceipt(ORDERS.in_c)).then(() => {
const b = store.balances(NOW);
assert.throws(() => JSON.stringify(b), TypeError);
assert.equal(String(b[CUR].settledExact), "500", "String() is the conversion to reach for");
assert.doesNotThrow(() => JSON.parse(store.exportJson()), "the export carries receipts, not a fold");
assert.doesNotThrow(() => JSON.parse(storage.map.get(`wal:${payerPub}`)), "and so does the blob");
});
});
// ---- the read-modify-write is serialized ------------------------------------
test("a sibling landing inside the read-modify-write window is not overwritten", async () => {
// The merge alone does NOT make a read-modify-write atomic, and the header
// used to claim it did. Between the getItem and the setItem sit a parse, a
// merge, a reindex and a stringify; a second tab that completes its own write
// in there is simply erased by ours — a receipt lost, `storageError` null,
// which is the exact failure the retention rule exists to forbid.
//
// What closes it is the compare-and-swap: re-read immediately before writing,
// and if the bytes moved, throw the merge away and start again from what the
// other writer actually left. Without that re-read this test finds one
// receipt on disk and a balance of -300 instead of -700.
storage.reset();
const mine = mkReceipt(out({ id: oid(3), amt: 300, seq: 3 }), "L-300");
const theirs = mkReceipt(out({ id: oid(4), amt: 400, seq: 4 }), "L-400");
const theirKey = await receiptKey(theirs);
const theirOid = await orderId(theirs.po);
const store = new LedgerStore(payerPub);
storage.raceOnce(() => {
const key = `wal:${payerPub}`;
const raw = storage.map.get(key);
const blob = raw
? JSON.parse(raw)
: { v: 1, receipts: [], declines: [], pending: [], seqs: {}, stakes: [] };
blob.receipts.push({ k: theirKey, o: theirOid, a: theirs });
storage.map.set(key, JSON.stringify(blob));
});
assert.equal(await store.applyReceipt(mine), STORED);
const onDisk = JSON.parse(storage.map.get(`wal:${payerPub}`));
assert.equal(onDisk.receipts.length, 2, "both receipts are on disk, not just the last writer's");
const fresh = new LedgerStore(payerPub);
assert.deepEqual(bal(fresh), { settled: -700, held: 0, available: -700 }, "-300 + -400");
assert.equal(store.storageError, null, "and nothing failed, because nothing was lost");
});
test("a sequence slot is not collided by a sibling landing in the same window", async () => {
// the same race, aimed at what it costs most: two stores handing out one
// (payer, bank, seq) slot is two signed orders at most one of which can ever
// settle, and the payer cannot tell which
storage.reset();
const store = new LedgerStore(payerPub);
await store.reserveSeq(CUR); // 1
storage.raceOnce(() => {
const key = `wal:${payerPub}`;
const blob = JSON.parse(storage.map.get(key));
blob.seqs[bankPub] = 5; // a sibling took 2..5 while we were merging
storage.map.set(key, JSON.stringify(blob));
});
assert.equal(await store.reserveSeq(CUR), 6, "the allocation is taken after the sibling's, not over it");
assert.equal(new LedgerStore(payerPub).nextSeq(CUR), 7, "and that is what disk says too");
});
test("a write clobbered after it landed is detected and re-merged", async () => {
// the other half of the compare-and-swap: a writer that is not doing a
// read-modify-write of its own can still drop our bytes after we wrote them.
// Reading back is what notices, and the retry restores what it took.
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
let clobbered = false;
const realSet = globalThis.localStorage.setItem;
globalThis.localStorage.setItem = (k, v) => {
realSet(k, v);
if (!clobbered && k === `wal:${payerPub}`) {
clobbered = true;
// a last-writer-wins tab stamps its own blob over ours
storage.map.set(k, JSON.stringify({ v: 1, receipts: [], declines: [], pending: [], seqs: {}, stakes: [] }));
}
};
try {
assert.equal(await store.applyReceipt(mkReceipt(ORDERS.out_a)), STORED);
} finally {
globalThis.localStorage.setItem = realSet;
}
assert.ok(clobbered, "the clobber really happened");
const fresh = new LedgerStore(payerPub);
assert.equal(fresh.receipts().length, 1, "the receipt is on disk after the retry");
assert.deepEqual(bal(fresh), { settled: -100, held: 0, available: -100 });
});
// ---- reserveSeq hands out nothing it has not persisted ----------------------
test("reserveSeq rejects rather than hand out a slot it could not persist", async () => {
// it used to throw persist!'s answer away and resolve the number anyway, so
// under a blocked or failing write it handed out a slot nothing had recorded
// — and the next store over the same key handed out the same one. The .d.ts
// promised "a slot handed out here is never handed out again"; this is that
// promise made true rather than merely written down.
storage.reset();
const store = new LedgerStore(payerPub);
assert.equal(await store.reserveSeq(CUR), 1);
storage.failWrites(true);
await assert.rejects(() => store.reserveSeq(CUR), /could not persist slot 2/,
"a quota failure is not an allocation");
storage.failWrites(false);
assert.equal(new LedgerStore(payerPub).nextSeq(CUR), 2, "and disk never heard of slot 2");
});
test("reserveSeq rejects while storage is unreadable, and slot 1 is still free after", async () => {
storage.reset();
const key = `wal:${payerPub}`;
storage.map.set(key, "{not json");
const store = new LedgerStore(payerPub);
await assert.rejects(() => store.reserveSeq(CUR), /could not persist slot 1/);
assert.equal(storage.map.get(key), "{not json", "and the bytes are still untouched");
storage.reset();
const other = new LedgerStore(payerPub);
assert.equal(await other.reserveSeq(CUR), 1, "nothing was spent, so slot 1 is genuinely free");
});
test("reserveSeq refuses a bank whose sequence space is spent", async () => {
storage.reset();
const key = `wal:${payerPub}`;
storage.map.set(key, JSON.stringify({
v: 1, receipts: [], declines: [], pending: [], seqs: { [bankPub]: AMT_MAX }, stakes: [],
}));
const store = new LedgerStore(payerPub);
await assert.rejects(() => store.reserveSeq(CUR), /sequence space is exhausted/,
"handing back 2^50 + 1 would only fail again inside buildOrder");
});
// ---- the sequence marks travel, and only ever upward ------------------------
test("exportJson carries the sequence marks, and an import raises them", async () => {
// device A prunes, correctly keeping its mark; device B imports. Without the
// marks in the export B offers slot 1 for a slot A has already spent — the
// prune regression, reintroduced over the portability path.
storage.reset();
const a = new LedgerStore(payerPub);
await a.reserveSeq(CUR);
await a.reserveSeq(CUR);
await a.addOrder(out({ id: oid(1), amt: 100, seq: 3 }));
a.prune(() => false);
assert.equal(a.nextSeq(CUR), 4, "A kept its mark across the prune");
const blob = a.exportJson();
assert.equal(JSON.parse(blob).seqs[bankPub], 3, "and the export says so");
storage.reset();
const b = new LedgerStore(payerPub);
assert.equal(await b.importJson(blob), 0, "no records survived A's prune to import");
assert.equal(b.nextSeq(CUR), 4, "but the mark came across anyway");
assert.equal(new LedgerStore(payerPub).nextSeq(CUR), 4, "and it was persisted, not just held");
});
test("a mark is merged by max, never by last-wins — in memory, from disk and on import", async () => {
// in order, 1 2 3, max and last-wins agree, which is all the suite used to
// exercise. Out of order is the real case: an import, a sibling's merge and a
// receipt arriving before the order it answers all deliver marks unsorted.
storage.reset();
const store = new LedgerStore(payerPub);
await store.addOrder(out({ id: oid(9), amt: 1, seq: 9 }));
assert.equal(store.nextSeq(CUR), 10);
await store.addOrder(out({ id: oid(2), amt: 1, seq: 2 }));
assert.equal(store.nextSeq(CUR), 10, "a lower slot arriving later does not lower the mark");
// from disk: a sibling's blob carrying a lower mark
const key = `wal:${payerPub}`;
const blob = JSON.parse(storage.map.get(key));
blob.seqs[bankPub] = 4;
storage.map.set(key, JSON.stringify(blob));
await store.addOrder(out({ id: oid(5), amt: 1, seq: 5 }));
assert.equal(store.nextSeq(CUR), 10, "and neither does a lower mark merged from disk");
assert.equal(JSON.parse(storage.map.get(key)).seqs[bankPub], 9, "what we wrote back is the max");
});
test("an import cannot lower a mark that no record justifies", async () => {
// Two things would otherwise mask a last-wins import and make the assertion
// pass for the wrong reason: `reindex!` re-raises the mark from the RECORDS
// on every persist, and the disk merge re-raises it from disk. So the mark
// here is one no record accounts for — taken by `reserveSeq` and never
// spent — and disk is put BEHIND memory before the import. That is also the
// case that matters in the field: a device that pruned.
storage.reset();
const key = `wal:${payerPub}`;
const store = new LedgerStore(payerPub);
for (let i = 0; i < 9; i++) await store.reserveSeq(CUR);
assert.equal(store.nextSeq(CUR), 10);
assert.equal(store.receipts().length + store.pendingOrders().length, 0, "and nothing justifies it");
const behind = JSON.parse(storage.map.get(key));
behind.seqs[bankPub] = 1;
storage.map.set(key, JSON.stringify(behind));
const stale = JSON.stringify({
v: 1, idPub: payerPub, receipts: [], declines: [], pending: [],
seqs: { [bankPub]: 3 }, stakes: [],
});
assert.equal(await store.importJson(stale), 0, "no records to import");
assert.equal(store.nextSeq(CUR), 10, "and the mark did not follow the blob down");
assert.equal(await store.reserveSeq(CUR), 10, "so the next slot is still the unspent one");
});
test("an unreadable sequence mark is bad bytes, not a fresh sequence", async () => {
// dropping a bad RECORD is conservative — it loses evidence and the user can
// see the hole. Dropping a bad MARK re-offers a spent slot: nextSeq quietly
// returns to 1 and the wallet signs a second order on a slot the bank has
// already bound. So it is the one section whose corruption is fatal.
storage.reset();
const key = `wal:${payerPub}`;
const store = new LedgerStore(payerPub);
await store.reserveSeq(CUR);
await store.reserveSeq(CUR);
await store.reserveSeq(CUR);
for (const [name, mark] of [
["a mark that is a string", "3"],
["a fractional mark", 3.5],
["a negative mark", -1],
["a mark past the ceiling a wpo.seq may carry", AMT_MAX + 1],
["a mark under a key that is not a banker", null],
]) {
const blob = JSON.parse(storage.map.get(key));
if (mark === null) blob.seqs["not-a-banker"] = 2;
else blob.seqs[bankPub] = mark;
const bytes = JSON.stringify(blob);
storage.map.set(key, bytes);
const reloaded = new LedgerStore(payerPub);
assert.equal(reloaded.storageError?.name, "UnreadableStorageError", name);
assert.equal(reloaded.unreadable, bytes, `${name}: and the bytes are handed over`);
await assert.rejects(() => reloaded.reserveSeq(CUR), /could not persist/,
`${name}: so no slot is handed out over them`);
assert.equal(storage.map.get(key), bytes, `${name}: and nothing was written`);
}
// a seqs section that is not a map at all, and an absent one (a v1.0.0 blob)
storage.map.set(key, JSON.stringify({ v: 1, receipts: [], declines: [], pending: [], seqs: 3, stakes: [] }));
assert.equal(new LedgerStore(payerPub).storageError?.name, "UnreadableStorageError", "seqs is not a map");
storage.map.set(key, JSON.stringify({ v: 1, receipts: [], declines: [], pending: [], stakes: [] }));
assert.equal(new LedgerStore(payerPub).storageError, null, "an absent seqs section is not corruption");
});
// ---- two stores in one document --------------------------------------------
test("two stores in one document converge without a storage event", async () => {
// the DOM fires `storage` on every window of the origin EXCEPT the one that
// wrote, so it can never carry news between two stores in one document — and
// one document holding two is the ordinary case, since the key is the
// identity. Without the in-process registry S1's prune is undone by S2's very
// next write, for ever and with nothing to say so.
storage.reset();
const s1 = new LedgerStore(payerPub);
const s2 = new LedgerStore(payerPub);
await s1.addOrder(ORDERS.out_a);
assert.equal(s2.pendingOrders().length, 1, "S2 heard about S1's order without being asked");
assert.equal(s1.prune(() => false), 1);
assert.equal(s2.pendingOrders().length, 0, "and about the prune");
await s2.addOrder(ORDERS.out_b);
assert.equal(new LedgerStore(payerPub).pendingOrders().length, 1, "so S2 could not resurrect it");
s1.close();
s2.close();
});
test("a closed store stops writing, and stops being told", async () => {
// close() used to detach the listener and nothing else, which produced the
// worst object available: a store that no longer hears about disk and still
// writes over it — a permanently stale writer resurrecting whatever it held
// when it was closed.
storage.reset();
const live = new LedgerStore(payerPub);
const closed = new LedgerStore(payerPub);
await live.addOrder(ORDERS.out_a);
closed.close();
const before = storage.map.get(`wal:${payerPub}`);
assert.equal(await closed.addOrder(ORDERS.out_b), MEMORY_ONLY, "recorded in memory, and it says so");
assert.equal(storage.map.get(`wal:${payerPub}`), before, "nothing reached disk");
assert.equal(closed.storageError?.name, "StoreClosedError");
assert.equal(closed.pendingOrders().length, 2, "reading a closed store still works");
await live.applyReceipt(mkReceipt(ORDERS.out_a));
assert.equal(closed.receipts().length, 0, "and it is no longer notified");
live.close();
});
// ---- forward compatibility is a courtesy with a budget ----------------------
test("a section deleted on disk is not restored by our next write", async () => {
storage.reset();
const key = `wal:${payerPub}`;
storage.map.set(key, JSON.stringify({
v: 1, receipts: [], declines: [], pending: [], seqs: {}, stakes: [], escrow: [{ v: 2 }],
}));
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
assert.deepEqual(JSON.parse(storage.map.get(key)).escrow, [{ v: 2 }], "carried while disk has it");
// whoever owns `escrow` removes it
const without = JSON.parse(storage.map.get(key));
delete without.escrow;
storage.map.set(key, JSON.stringify(without));
await store.addOrder(ORDERS.out_b);
const after = JSON.parse(storage.map.get(key));
assert.equal("escrow" in after, false, "an accumulating carry made a junk section unremovable");
assert.equal(after.pending.length, 2, "and this version's own write still happened");
});
test("an oversize unknown section is not carried for ever", async () => {
// a 2 MB opaque blob re-serialized on every write is a permanent quota denial
// of service on the money layer, and a receipt is worth more than a section
// this build has never heard of.
storage.reset();
const key = `wal:${payerPub}`;
storage.map.set(key, JSON.stringify({
v: 1, receipts: [], declines: [], pending: [], seqs: {}, stakes: [],
small: { a: 1 },
junk: "x".repeat(200000),
}));
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
const after = JSON.parse(storage.map.get(key));
assert.equal("junk" in after, false, "the oversize section is shed");
assert.deepEqual(after.small, { a: 1 }, "a section inside the budget is still carried");
assert.ok(storage.map.get(key).length < 100000, "and the blob is a ledger again, not a payload");
assert.equal(after.pending.length, 1);
});
test("a sibling tab writing garbage stops this one writing over it", async () => {
storage.reset();
const key = `wal:${payerPub}`;
const store = new LedgerStore(payerPub);
await store.addOrder(ORDERS.out_a);
assert.equal(store.storageError, null);
storage.map.set(key, "half a blob {");
assert.equal(await store.applyReceipt(mkReceipt(ORDERS.out_a)), MEMORY_ONLY, "the fold still happens");
assert.equal(storage.map.get(key), "half a blob {", "the bytes are left alone");
assert.equal(store.storageError.name, "UnreadableStorageError");
assert.equal(store.unreadable, "half a blob {");
assert.equal(store.receipts().length, 1, "and memory kept the receipt");
});
// ---- issuance: minted money visible outside banca ---------------------------
//
// A wri credits `to` and NEVER debits anyone in this ledger: issuance creates
// money at the bank. Idempotent by issuanceKey — (bank, h), blind to ts — so a
// re-issued receipt folds once; rejected outright when `to` is not this
// identity, because a wri has exactly one beneficiary (a settlement can touch
// a wallet from either side, which is why applyReceipt has no such gate).
test("the hand-computed issuance fold table holds, step by step", async () => {
storage.reset();
const t = fold.issuance_steps;
const who = { me: payerPub, payee: payeePub };
let store = new LedgerStore(payerPub);
assert.equal(fold.me, payerPub);
for (const step of t.steps) {
let result;
if (step.op === "applyIssuance") {
const m = t.mints[step.mint];
const wri = mkIssuance({ ...m, to: who[m.to] }, TS_FIXED + 1000 + (step.reissueTsDelta ?? 0));
result = await store.applyIssuance(wri);
} else if (step.op === "addOrder") result = await store.addOrder(ORDERS[step.order]);
else if (step.op === "applyReceipt") result = await store.applyReceipt(mkReceipt(ORDERS[step.order]));
else if (step.op === "reload") store = new LedgerStore(payerPub);
if (step.expectResult !== undefined) {
assert.equal(result, step.expectResult ? STORED : REJECTED, step.why);
}
assert.deepEqual(bal(store), step.expect, `${step.op}: ${step.why}`);
assert.equal(store.nextSeq(CUR), step.nextSeq, `${step.op} nextSeq: ${step.why}`);
}
});
test("a wri to someone else is rejected, not stored", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
assert.equal(
await store.applyIssuance(mkIssuance({ seq: 1, amt: 999, h: "mint-else", to: payeePub })),
REJECTED,
"a wri has exactly one beneficiary and it is not me",
);
assert.equal(store.issuances().length, 0, "and it is not kept as dead weight either");
assert.deepEqual(bal(store), undefined, "no phantom currency row");
});
test("a wri smuggled into storage naming someone else still does not credit me", async () => {
// applyIssuance gates on `to`, but storage is re-shaped on load rather than
// re-gated, so the FOLD must also refuse to credit — hand-edited bytes must
// not turn someone else's mint into my money. This is the test that reaches
// the fold's own `to` check, which the mutator's gate otherwise shadows.
storage.reset();
const store = new LedgerStore(payerPub);
await store.applyIssuance(mkIssuance({ seq: 1, amt: 500, h: "mint-mine", to: payerPub }));
const key = `wal:${payerPub}`;
const blob = JSON.parse(storage.map.get(key));
const foreign = mkIssuance({ seq: 2, amt: 999, h: "mint-foreign", to: payeePub });
blob.issuance.push({ k: await issuanceKey(foreign), a: foreign });
storage.map.set(key, JSON.stringify(blob));
const reloaded = new LedgerStore(payerPub);
assert.equal(reloaded.issuances().length, 2, "the record itself is structurally valid and loads");
assert.deepEqual(bal(reloaded), { settled: 500, held: 0, available: 500 }, "but the fold credits only me");
});
test("a re-issued wri with a fresh ts folds once — issuanceKey is blind to ts", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
assert.equal(await store.applyIssuance(mkIssuance({ seq: 1, amt: 500, h: "mint-r", to: payerPub })), STORED);
assert.equal(
await store.applyIssuance(mkIssuance({ seq: 1, amt: 500, h: "mint-r", to: payerPub }, TS_FIXED + 9000)),
REJECTED,
"same bank, same mint entry, one credit",
);
assert.deepEqual(bal(store), { settled: 500, held: 0, available: 500 });
assert.equal(store.issuances().length, 1);
});
test("a bank equivocating two amounts on one mint entry folds first-wins, never both", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
assert.equal(await store.applyIssuance(mkIssuance({ seq: 1, amt: 500, h: "mint-e", to: payerPub })), STORED);
assert.equal(
await store.applyIssuance(mkIssuance({ seq: 1, amt: 100000, h: "mint-e", to: payerPub })),
REJECTED,
"two signed amounts for one (bank, h) is the bank's contradiction, not two credits",
);
assert.deepEqual(bal(store), { settled: 500, held: 0, available: 500 });
});
test("applyIssuance is total and refuses junk without throwing", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
const good = mkIssuance({ seq: 1, amt: 5, h: "mint-t", to: payerPub });
for (const junk of [null, undefined, {}, [], 0, "wri",
{ ...good, amt: -1 }, { ...good, evil: 1 },
new Proxy({}, { get() { throw new Error("boo"); } })]) {
assert.equal(await store.applyIssuance(junk), REJECTED);
}
assert.equal(await store.applyIssuance(mkReceipt(ORDERS.out_a)), REJECTED, "a settlement is not an issuance");
assert.equal(await store.applyReceipt(good), REJECTED, "and an issuance is not a settlement");
assert.deepEqual(bal(store), undefined, "nothing above folded");
});
test("the issuance fold is order-independent and BigInt-exact", async () => {
const artifacts = [
mkIssuance({ seq: 1, amt: 500, h: "mint-o1", to: payerPub }),
mkIssuance({ seq: 2, amt: 250, h: "mint-o2", to: payerPub }),
mkReceipt(ORDERS.out_a), // a 100 debit, so the shuffle crosses record types
];
const run = async (order) => {
storage.reset();
const s = new LedgerStore(payerPub);
for (const i of order) {
const a = artifacts[i];
assert.equal(a.t === "wri" ? await s.applyIssuance(a) : await s.applyReceipt(a), STORED);
}
const b = s.balances(NOW)[CUR];
return { settled: b.settled, exact: b.settledExact };
};
const first = await run([0, 1, 2]);
assert.equal(first.settled, 650, "500 + 250 - 100, by hand");
assert.equal(first.exact, 650n);
for (const order of [[2, 1, 0], [1, 2, 0], [2, 0, 1]]) {
const again = await run(order);
assert.equal(again.settled, first.settled, `order ${order}`);
assert.equal(again.exact, first.exact, `order ${order}, exact`);
}
});
test("issuance credits stay exact at the top of the amount range", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
for (let i = 0; i < 9; i++) {
assert.equal(
await store.applyIssuance(mkIssuance({ seq: i + 1, amt: AMT_MAX, h: `mint-max-${i}`, to: payerPub })),
STORED,
);
}
const b = store.balances(NOW)[CUR];
assert.equal(b.settledExact, BigInt(AMT_MAX) * 9n, "nine maximal mints, exactly");
assert.equal(b.settled, Number(BigInt(AMT_MAX) * 9n), "the Number is Number() of the BigInt");
});
test("issuance rides export/import: signature checked, party-gated, added once", async () => {
storage.reset();
const a = new LedgerStore(payerPub);
const mine = mkIssuance({ seq: 1, amt: 500, h: "mint-x", to: payerPub });
assert.equal(await a.applyIssuance(mine), STORED);
const blob = a.exportJson();
assert.equal(JSON.parse(blob).issuance.length, 1, "the export carries the artifact itself");
storage.reset();
const b = new LedgerStore(payerPub);
assert.equal(await b.importJson(blob), 1, "one issuance record");
assert.equal(await b.importJson(blob), 0, "importing twice adds nothing");
assert.deepEqual(bal(b), { settled: 500, held: 0, available: 500 });
// a canonical, valid-shaped, WRONG bank signature: the shape check cannot
// see it, so only importJson's signature verification can refuse it
const forged = JSON.parse(blob);
forged.issuance[0].bsig =
(forged.issuance[0].bsig.startsWith("A") ? "B" : "A") + forged.issuance[0].bsig.slice(1);
storage.reset();
const c = new LedgerStore(payerPub);
assert.equal(await c.importJson(JSON.stringify(forged)), 0, "an unsigned mint is not money");
assert.equal(c.issuances().length, 0);
assert.deepEqual(bal(c), undefined);
// a blob that CLAIMS my idPub still cannot smuggle in someone else's mint
const smuggle = JSON.stringify({
v: 1, idPub: payerPub, receipts: [], declines: [], pending: [],
issuance: [mkIssuance({ seq: 2, amt: 999, h: "mint-y", to: payeePub }), mine],
seqs: {}, stakes: [],
});
storage.reset();
const d = new LedgerStore(payerPub);
assert.equal(await d.importJson(smuggle), 1, "only the mint that is mine");
assert.deepEqual(bal(d), { settled: 500, held: 0, available: 500 });
});
test("prune offers issuance records as kind wri, and the deletion sticks", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.applyIssuance(mkIssuance({ seq: 1, amt: 500, h: "mint-p", to: payerPub }));
const seen = [];
const dropped = store.prune((_a, kind) => { seen.push(kind); return kind !== "wri"; });
assert.deepEqual(seen, ["wri"]);
assert.equal(dropped, 1);
assert.equal(store.issuances().length, 0);
assert.deepEqual(bal(store), undefined, "a pruned issuance stops folding");
const reloaded = new LedgerStore(payerPub);
assert.equal(reloaded.issuances().length, 0, "and the merge that wrote it did not resurrect it");
});
test("the persisted issuance key is issuanceKey, beside the artifact itself", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
const w = mkIssuance({ seq: 1, amt: 5, h: "mint-k", to: payerPub });
await store.applyIssuance(w);
const blob = JSON.parse(storage.map.get(`wal:${payerPub}`));
assert.equal(blob.issuance.length, 1);
assert.equal(blob.issuance[0].k, await issuanceKey(w), "the stored key is the exported derivation");
assert.equal(JSON.stringify(blob.issuance[0].a), JSON.stringify(w), "the artifact is stored verbatim");
});
test("a blob written before issuance existed reads as zero issuances, not an error", async () => {
storage.reset();
const store = new LedgerStore(payerPub);
await store.applyReceipt(mkReceipt(ORDERS.in_c));
const key = `wal:${payerPub}`;
const blob = JSON.parse(storage.map.get(key));
delete blob.issuance; // what a pre-wri build wrote
storage.map.set(key, JSON.stringify(blob));
const reloaded = new LedgerStore(payerPub);
assert.equal(reloaded.storageError, null);
assert.equal(reloaded.issuances().length, 0);
assert.equal(reloaded.receipts().length, 1, "everything else reads as before");
});
|