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 | // THE VIEW MODEL — what the interface decides, tested without an interface.
//
// Every screen in banca reads its numbers, its words and its refusals out of
// lib/view.cljs, so the part of the app that could lie to a user is a pure
// function of a fold snapshot. That is deliberate: a browser is the wrong place
// to find out that "provisional" was rendered as "done", and this file runs
// every one of those decisions over the SAME hand-computed scenarios the
// settlement tests use (test/vectors/fold.json).
//
// The four claims under test, in order of how much they matter:
//
// 1. PENDING IS NOT FINAL, ANYWHERE. The fold's `settled` means money moved
// and NOTHING HAS PINNED IT; every other financial interface ever built
// reads that word as "done". `paymentState` is the one place it is
// translated, and no settled record in any scenario may read as `final` or
// as the word itself.
//
// 2. THE WALLET AND THE BANKER'S DESK MUST AGREE. A word (or a sentence)
// that promises an acknowledgement must name something the banker's own
// screen is offering. Since the wri, that set is settled PAYMENTS and
// settled MINTS: a record's hash is in `ackable` EXACTLY when it reads
// `provisional` (a pay) or reads `unpinned` and is a mint — and a settled
// BURN reads `unpinned` and is never offered, because a burn has no
// signable preimage. Held as an iff, not as rules that happen to line up.
//
// 3. `ackable` NEVER CONTAINS A FAILED PAYMENT. An ack is not a comment: the
// slot pre-pass ranks an acked entry above an unacked one, so "acknowledge
// everything" over a list that included failures would silently REVERSE
// settlements. The fixture has the pair that proves it.
//
// 4. NOTHING IS SIGNED THAT CANNOT SETTLE — `nextSeq` returning null
// included, which is the one refusal no browser click will ever surface.
//
// Runs against test-dist/testlib.js — the built output — never src/.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { ed25519 } from "@noble/curves/ed25519";
import * as L from "../test-dist/testlib.js";
import { makeOrder, CUR, SEED_PAYER, SEED_BANK, signOver } from "./vectors/independent.mjs";
const V = JSON.parse(readFileSync(new URL("./vectors/fold.json", import.meta.url), "utf8"));
const A = V.actors;
const BANK = A.bank;
async function foldOf(entries, bankerPub = BANK) {
const f = new L.BankFold(bankerPub, null);
for (const e of structuredClone(entries)) f.ingest(e);
await f.awaitVerified();
return f;
}
const snapOf = async (entries, bankerPub) => (await foldOf(entries, bankerPub)).snapshot();
const scenario = (name) => V.scenarios.find((s) => s.name === name);
const snapOfScenario = (name) => snapOf(scenario(name).entries);
// ---- 1. provisional is not final ---------------------------------------------
test("no `settled` record reads as final, or as the fold's own word, in any scenario", async () => {
// THE FOUR WORDS, AND THE MAP FROM THE FOLD'S THREE. `settled` splits by
// KIND, and only by kind: a payment is `provisional` (an ack will pin it), an
// issuance is `unpinned` (nothing will). `final` and `failed` do not split —
// the fold's ack pre-pass is blind to kind, so an acked mint really is final.
const seen = { provisional: 0, unpinned: 0, final: 0, failed: 0 };
for (const sc of V.scenarios) {
const s = await snapOf(sc.entries);
for (const rec of s.payments) {
const state = L.paymentState(rec);
const want =
rec.status === "final" ? "final"
: rec.status === "failed" ? "failed"
: L.isIssuance(rec) ? "unpinned" : "provisional";
assert.equal(state, want, `${sc.name}/${rec.h} (${rec.kind}/${rec.status})`);
if (rec.status === "settled") {
assert.notEqual(state, "final", `${sc.name}/${rec.h}: settled must never read as final`);
}
assert.equal(L.stateKey(rec), `pay.state.${want}`);
// the four words, and only the four
assert.ok(["provisional", "unpinned", "final", "failed"].includes(state));
seen[state]++;
}
}
// non-vacuity: the corpus really exercises all four
for (const [word, n] of Object.entries(seen)) assert.ok(n > 0, `${word} never occurs: ${JSON.stringify(seen)}`);
});
test("a word that promises is kept by the banker's desk — the iff, reworked for the wri", async () => {
// THE INVARIANT, DELIBERATELY REWORKED rather than loosened. It used to be
// "provisional iff in ackable", and the wri broke that on purpose: a settled
// MINT still reads `unpinned` (a member asking "is my payment waiting on the
// banker?" and a banker asking "what did I issue?" are different questions)
// but its ack now signs a wri, so the banker's queue offers it. The new iff:
//
// a record's hash is in `ackable`
// ⟺ it reads `provisional` (necessarily a settled pay)
// OR it reads `unpinned` AND is a MINT (a settled mint)
//
// and the word depends only on kind: every provisional record is a pay,
// every unpinned record is an issuance, and a settled BURN reads `unpinned`
// while NEVER being offered — a burn has no signable preimage (§8).
const seen = { promisedPays: 0, offeredMints: 0, excludedBurns: 0 };
for (const sc of V.scenarios) {
const s = await snapOf(sc.entries);
const offered = new Set(L.ackable(s));
for (const rec of s.payments) {
const state = L.paymentState(rec);
const shouldOffer = state === "provisional" || (state === "unpinned" && rec.kind === "mint");
assert.equal(shouldOffer, offered.has(rec.h),
`${sc.name}/${rec.h} (${rec.kind}/${rec.status}): the wallet reads ${state}, ` +
`the banker's queue ${offered.has(rec.h) ? "does" : "does not"} offer it`);
if (state === "provisional") {
assert.equal(rec.kind, "pay", `${sc.name}/${rec.h}: provisional is the payment word`);
seen.promisedPays++;
}
if (state === "unpinned") {
assert.ok(rec.kind === "mint" || rec.kind === "burn",
`${sc.name}/${rec.h}: unpinned is the issuance word`);
if (rec.kind === "mint") seen.offeredMints++;
else seen.excludedBurns++;
}
}
}
// non-vacuity in all three directions the iff can fail
assert.ok(seen.promisedPays > 0, "the corpus never renders a provisional payment");
assert.ok(seen.offeredMints > 0, "the corpus never offers a settled mint");
assert.ok(seen.excludedBurns > 0, "the corpus never excludes a settled burn");
});
test("the unpinned chip's sentence splits by kind — a mint may promise, a burn must not", async () => {
// One word, two futures. A settled MINT sits in the banker's queue now (its
// ack signs a wri), so its note may say "yet"; a settled BURN has no
// signable preimage, nothing will ever pin it, and its note must promise
// nothing. `state-note-key` is the one place that routing lives.
const s = await snapOfScenario("burn-debits-the-banker-only");
const mint = s.payments.find((p) => p.h === "h09");
const burn = s.payments.find((p) => p.h === "h10");
assert.equal(mint.kind, "mint");
assert.equal(burn.kind, "burn");
assert.equal(mint.status, "settled");
assert.equal(burn.status, "settled");
assert.equal(L.isIssuance(mint), true);
assert.equal(L.isIssuance(burn), true);
assert.equal(L.paymentState(mint), "unpinned");
assert.equal(L.paymentState(burn), "unpinned", "one word for both kinds of issuance…");
assert.equal(L.stateNoteKey("unpinned", "mint"), "pay.state.unpinned.note.mint");
assert.equal(L.stateNoteKey("unpinned", "burn"), "pay.state.unpinned.note.burn");
assert.equal(L.stateNoteKey("provisional", "pay"), "pay.state.provisional.note");
assert.equal(L.stateNoteKey("final", "mint"), "pay.state.final.note");
assert.equal(L.stateNoteKey("failed", "pay"), null, "a failure's sentence is the why");
assert.equal(L.ackable(s).includes("h09"), true, "the queue is offering to pin the mint");
assert.equal(L.ackable(s).includes("h10"), false, "…and can never offer the burn");
for (const lang of ["en", "ro", "hu"]) {
const mintNote = L.catalogValue(lang, "pay.state.unpinned.note.mint");
const burnNote = L.catalogValue(lang, "pay.state.unpinned.note.burn");
assert.ok(mintNote && mintNote.length > 25, `${lang}: ${mintNote}`);
assert.ok(burnNote && burnNote.length > 25, `${lang}: ${burnNote}`);
assert.notEqual(mintNote, burnNote, `${lang}: the two futures collapsed into one sentence`);
assert.notEqual(L.catalogValue(lang, "pay.state.unpinned"),
L.catalogValue(lang, "pay.state.provisional"),
`${lang}: the two states collapsed into one word`);
assert.equal(L.catalogValue(lang, "pay.state.unpinned.note"), null,
`${lang}: the old kind-blind sentence must be gone`);
}
assert.equal(L.catalogValue("en", "pay.state.provisional.note").includes("yet"), true,
"the payment sentence promises");
assert.equal(L.catalogValue("en", "pay.state.unpinned.note.mint").includes("yet"), true,
"the mint sentence may promise now — the queue keeps it");
assert.equal(L.catalogValue("en", "pay.state.unpinned.note.burn").includes("yet"), false,
"…and the burn sentence must promise nothing");
});
test("the confiscation window: open without the ack, closed by the wri-carrying ack", async () => {
// THE MONEY PAIR OF THE WHOLE CHANGE, taken from the fold rather than from
// any wording. An unpinned mint really is displaceable: a banker who
// withheld a second mint for the same issuance slot can release it later,
// back-dated, and the mint everyone acted on becomes `failed` — the one way
// a banker could empty a customer's account. Since the wri, the banker's
// own screen offers the ack that closes it.
const before = await snapOfScenario("prelude");
const open = await snapOfScenario("withheld-backdated-mint-displaces-an-issuance");
const was = before.payments.find((p) => p.h === "h08");
const now = open.payments.find((p) => p.h === "h08");
assert.equal(L.paymentState(was), "unpinned");
assert.equal(L.paymentState(now), "failed", "with no ack, the issuance was displaced");
assert.equal(L.whyKey(now), "pay.why.seq");
assert.equal(before.accounts.find((a) => a.id === A.payer).balExact, "1000");
assert.equal(open.accounts.find((a) => a.id === A.payer).balExact, "0",
"…and money the payer had been shown as theirs is gone");
// the same entry set plus ONE ack — whose bsig signs the wri — and the
// window is shut: the original mint is final, the withheld one dies `seq`,
// and the payer's 1000 stays theirs
const closed = await snapOfScenario("a-wri-ack-closes-the-confiscation-window");
assert.equal(closed.payments.find((p) => p.h === "h08").status, "final");
assert.equal(closed.payments.find((p) => p.h === "h07a").status, "failed");
assert.equal(L.whyKey(closed.payments.find((p) => p.h === "h07a")), "pay.why.seq");
assert.equal(closed.accounts.find((a) => a.id === A.payer).balExact, "1000",
"the window is closed — the customer's money survived the back-dated mint");
// and the queue is what delivers it: the unacked mint IS offered, before
// anything went wrong
assert.equal(L.ackable(before).includes("h08"), true,
"the banker's screen offers to pin the mint — this ack is producible now");
});
test("a receipt is offered exactly where one can be rebuilt", async () => {
// `receiptable?` is `final` and not a burn — exactly the set `receiptFor`
// answers for: a `wrc` for a final pay, a `wri` for a final mint, nothing
// for a burn ever (§8). A button guarded on `final` alone would appear on a
// foreign log's final burn and answer "the banker's signature does not
// verify" — naming the one thing that did not happen.
let pays = 0;
let mints = 0;
for (const sc of V.scenarios) {
const f = await foldOf(sc.entries);
const s = f.snapshot();
for (const rec of s.payments) {
const can = L.isReceiptable(rec);
assert.equal(can, f.receiptFor(rec.h) !== null,
`${sc.name}/${rec.h} (${rec.kind}/${rec.status})`);
if (can && rec.kind === "pay") pays++;
if (can && rec.kind === "mint") mints++;
}
}
assert.ok(pays > 0, "the corpus never reaches a payment receipt at all");
assert.ok(mints > 0, "the corpus never reaches an issuance receipt at all");
// the case a kind-blind guard would get wrong now is the BURN: force one
// final (a log some other implementation wrote can) and the button must not
// appear — receiptFor's null beside it is asserted in receipts.test.mjs
assert.equal(L.isReceiptable({ kind: "burn", status: "final", acked: true }), false);
assert.equal(L.isReceiptable({ kind: "mint", status: "final", acked: true }), true,
"…while the same guard offers a final mint its wri");
assert.equal(L.isReceiptable({ kind: "mint", status: "settled", acked: false }), false,
"an unpinned mint has no receipt YET — the queue is what gets it one");
});
test("rows precompute `receiptable` — the record's own answer, at row-build time", async () => {
// The defect this pins: `receiptable?` reads a fold RECORD's `status`, and a
// ROW from `view/row` carries the translated `state` word instead — so a UI
// that asked the record rule of a row got false on EVERY row, and the
// `pay.receipt` button (the wri's v1 delivery mechanism, §8, and the
// payment-receipt export with it) never rendered at all. The row now carries
// the decision, precomputed from the record by the one true rule; this
// drives the VALUE over the whole corpus, and source-hygiene pins the UI's
// gate to the field, so the pair can only break red.
const seen = { finalPays: 0, finalMints: 0, burns: 0, notFinal: 0 };
for (const sc of V.scenarios) {
const s = await snapOf(sc.entries);
const byHash = Object.fromEntries(s.payments.map((p) => [p.h, p]));
for (const viewer of [BANK, A.payer, A.payee]) {
for (const r of [...L.historyOf(s, viewer), ...L.ledgerOf(s, viewer)]) {
assert.equal(r.receiptable, L.isReceiptable(byHash[r.h]),
`${sc.name}/${r.h} (${viewer.slice(0, 8)}): the row must carry the record's answer`);
if (r.state === "final" && r.kind === "pay") { seen.finalPays++; assert.equal(r.receiptable, true); }
if (r.state === "final" && r.kind === "mint") { seen.finalMints++; assert.equal(r.receiptable, true); }
if (r.kind === "burn") { seen.burns++; assert.equal(r.receiptable, false); }
if (r.state !== "final") { seen.notFinal++; assert.equal(r.receiptable, false); }
}
}
}
// non-vacuity in every direction the button can be wrong: a final pay and a
// final mint really get one; a burn and everything provisional/unpinned/
// failed really do not
for (const [k, n] of Object.entries(seen)) assert.ok(n > 0, `${k} never occurs: ${JSON.stringify(seen)}`);
// the case a kind-blind row would get wrong: a FINAL burn. The ack pre-pass
// is blind to kind, so a log some other implementation wrote can hold one —
// force it with an ack naming the settled burn (shape-valid bsig; no burn
// preimage exists for it to be right about) — and the row still says NO.
const sc = scenario("burn-debits-the-banker-only");
const forced = [...sc.entries,
{ hash: "h20", from: BANK, clock: 20,
op: { t: "ack", ts: 1735689660000, h: "h10",
bsig: signOver(SEED_BANK, "no-burn-preimage-exists") } }];
const s = await snapOf(forced);
assert.equal(s.payments.find((p) => p.h === "h10").status, "final",
"the fixture really reaches a final burn");
const row = L.ledgerOf(s, null).find((r) => r.h === "h10");
assert.equal(row.state, "final");
assert.equal(row.receiptable, false, "a final burn has no receipt to rebuild — no button");
});
test("the fold's own word never reaches a screen", async () => {
// `settled` is the fold's vocabulary and belongs to the fold. If it ever
// becomes a catalog key, the interface starts saying "settled" to a user who
// will read it as "done" — which is the exact confusion docs/PROTOCOL.md §11
// says an interface must not create.
const s = await snapOfScenario("settled-provisionally");
const rec = s.payments.find((p) => p.h === "h09");
assert.equal(rec.status, "settled", "the fixture really is the provisional one");
assert.equal(L.stateKey(rec), "pay.state.provisional");
for (const lang of ["en", "ro", "hu"]) {
assert.equal(L.catalogValue(lang, "pay.state.settled"), null, `${lang} must have no such key`);
assert.ok(L.catalogValue(lang, "pay.state.provisional"), `${lang} must have the real one`);
}
// …and the English word for it does not read as "done"
assert.equal(L.catalogValue("en", "pay.state.provisional"), "provisional");
assert.ok(L.catalogValue("en", "pay.state.provisional.note").includes("not final"));
});
test("a failure carries its reason, and only a failure does", async () => {
const over = await snapOfScenario("overdraft-consumes-the-slot");
const short = over.payments.find((p) => p.h === "h09");
const lost = over.payments.find((p) => p.h === "h11");
assert.equal(L.whyKey(short), "pay.why.insufficient");
assert.equal(L.whyKey(lost), "pay.why.seq");
assert.equal(L.whyKey(over.payments.find((p) => p.h === "h08")), null, "a settled record has no reason");
// every reason wallet-kit can produce has a key, and an unknown one still does
for (const why of L.DECLINE_REASONS) {
assert.equal(L.whyKey({ status: "failed", why }), `pay.why.${why}`);
}
assert.equal(L.whyKey({ status: "failed", why: "something-from-the-future" }), "pay.why.unknown");
assert.equal(L.whyKey({ status: "failed", why: null }), "pay.why.unknown");
});
test("failed AND acked is expressible — the banker did answer that one", async () => {
// A banker who acks both contenders for one slot leaves the loser failed with
// a valid ack naming it. A member asking "did the banker answer my payment?"
// has to be told yes, and `acked` is carried separately for exactly that.
const s = await snapOfScenario("an-ack-on-both-contenders");
const loser = s.payments.find((p) => p.h === "h10");
assert.equal(L.paymentState(loser), "failed");
assert.equal(L.isAcked(loser), true);
assert.ok(L.catalogValue("en", "pay.failed-but-acked"));
// and the winner of the same slot is final
assert.equal(L.paymentState(s.payments.find((p) => p.h === "h09")), "final");
});
// ---- 2. the banker's queue ---------------------------------------------------
test("`ackable` is settled PAYS and MINTS — never a failure, never a burn, never twice", async () => {
for (const sc of V.scenarios) {
const s = await snapOf(sc.entries);
const byHash = Object.fromEntries(s.payments.map((p) => [p.h, p]));
for (const h of L.ackable(s)) {
const rec = byHash[h];
assert.ok(rec, `${sc.name}: ${h} must be a real record`);
assert.ok(rec.kind === "pay" || rec.kind === "mint",
`${sc.name}/${h}: only a pay (settlement) or a mint (wri) has a preimage to sign`);
assert.equal(rec.status, "settled", `${sc.name}/${h}`);
// acked entries are off the list BY CONSTRUCTION, and for a mint that is
// also the equivocation guard: the banker's screen never builds a second
// wri for a hash the bank already answered
assert.equal(rec.acked, false, `${sc.name}/${h}`);
}
}
// non-vacuity, with the exact lists: the settled MINT is on the list now —
// its ack signs a wri — beside the settled payment
const prov = await snapOfScenario("settled-provisionally");
assert.equal(prov.payments.find((p) => p.h === "h08").status, "settled", "…the mint really is settled");
assert.equal(prov.payments.find((p) => p.h === "h08").kind, "mint");
assert.deepEqual(L.ackable(prov), ["h08", "h09"]);
// a settled BURN beside a settled mint: only the mint is offered
const burns = await snapOfScenario("burn-debits-the-banker-only");
assert.equal(burns.payments.find((p) => p.h === "h10").status, "settled");
assert.deepEqual(L.ackable(burns), ["h08", "h09"], "the settled burn h10 must not be here");
// an already-final entry drops off the list (h08 stays: the ack named h09)
assert.deepEqual(L.ackable(await snapOfScenario("acked-is-final")), ["h08"]);
// …and once the mint's own wri-ack folds, the queue is empty
assert.deepEqual(L.ackable(await snapOfScenario("an-acked-mint-is-final-with-a-wri")), []);
});
test("acking a FAILED contender reverses a settlement — which is why it is not on the list", async () => {
// The sharpest pair in the fixture. `double-spend-unacked` and
// `ack-pins-the-clock-loser` are the same entry set apart from ONE ack, and
// that ack names the payment that FAILED. Adding it does not annotate the
// failure — it wins the slot, and the payment everyone had already acted on
// becomes the failure instead.
const un = scenario("double-spend-unacked");
const pin = scenario("ack-pins-the-clock-loser");
const before = await snapOf(un.entries);
const added = pin.entries.filter((e) => !un.entries.some((u) => u.hash === e.hash));
assert.equal(added.length, 1);
assert.equal(added[0].op.t, "ack");
assert.equal(added[0].op.h, "h10", "the fixture's ack really does name the failed one");
assert.equal(before.payments.find((p) => p.h === "h10").status, "failed");
const after = await snapOf(pin.entries);
assert.equal(after.payments.find((p) => p.h === "h09").status, "failed", "the settlement was displaced");
assert.equal(after.payments.find((p) => p.h === "h10").status, "final");
assert.notEqual(
after.accounts.find((a) => a.id === A.payee).balExact,
before.accounts.find((a) => a.id === A.payee).balExact,
"…and someone's money moved to someone else",
);
// So the list a one-tap "acknowledge everything" walks must contain h09 (and
// the prelude's settled mint) and must NOT contain h10. A rule written as
// "status !== final" would contain h10 too, and the button would reverse a
// settlement on every tap.
assert.deepEqual(L.ackable(before), ["h08", "h09"]);
assert.equal(L.ackable(before).includes("h10"), false);
});
// ---- the banker's ack: the three-way dispatch, and the ts hand-off -----------
//
// `store/ack!` runs on two seams the store cannot smuggle a second copy of:
// `ackDispatch` decides WHICH preimage an ack signs (or refuses), and
// `ackOpFor` builds the ack op out of the artifact wallet-kit just signed —
// carrying the ARTIFACT'S stamped ts, never a second reading of the clock.
// Driven here black-box, with the artifacts built by the kit's own builders
// under a fixture-seed signer, so the burn refusal is REACHED and the
// wri.ts → ack.ts equality is asserted over a wri somebody actually signed.
const bankSigner = () =>
({ publicKeyB64: BANK, signRaw: async (bytes) => ed25519.sign(bytes, SEED_BANK) });
test("ack-dispatch is three-way, and the burn refusal is reached, not declared", async () => {
const s = await snapOfScenario("burn-debits-the-banker-only");
const mint = s.payments.find((p) => p.h === "h09");
const burn = s.payments.find((p) => p.h === "h10");
assert.equal(mint.kind, "mint");
assert.equal(burn.kind, "burn");
assert.equal(burn.status, "settled", "the refused burn really is the settled one");
assert.equal(L.ackDispatch(mint), "mint", "a mint's ack signs the issuance preimage");
assert.equal(L.ackDispatch(burn), "err.ack.burn", "a burn is refused — no signable preimage (§8)");
const pay = (await snapOfScenario("settled-provisionally")).payments.find((p) => p.h === "h09");
assert.equal(pay.kind, "pay");
assert.equal(L.ackDispatch(pay), "pay", "a pay's ack signs the settlement preimage");
// a record the store could not find dispatches "pay" BY DESIGN: the missing
// original order is the store's knowledge (err.ack.unknown), not this rule's
assert.equal(L.ackDispatch(null), "pay");
assert.equal(L.ackDispatch(undefined), "pay");
// and the queue agrees with the dispatch: nothing `ackable` offers is ever
// dispatched to the refusal — the two halves of the mint-only rule hold
// together, over every scenario
for (const sc of V.scenarios) {
const snap2 = await snapOf(sc.entries);
const byHash = Object.fromEntries(snap2.payments.map((p) => [p.h, p]));
for (const h of L.ackable(snap2)) {
assert.notEqual(L.ackDispatch(byHash[h]), "err.ack.burn",
`${sc.name}/${h}: the queue offered what the dispatch refuses`);
}
}
});
test("a mint's ack carries the BUILT wri's own ts — the wri.ts → ack.ts hand-off", async () => {
const before = Date.now();
const wri = await L.buildIssuance(bankSigner(), CUR, 2, A.payer, 500, "h08");
const op = L.ackOpFor("h08", wri);
// four keys in the wire order, and it validates as a BANK/1 op
assert.deepEqual(Object.keys(op), ["t", "ts", "h", "bsig"]);
assert.equal(op.t, "ack");
assert.equal(op.h, "h08");
assert.equal(L.validAck(op), true);
// THE EQUALITY THE RECEIPT DEPENDS ON: the ack's ts is the ts the kit
// stamped INTO the wri and signed over — never the moment the op was built
assert.equal(op.ts, wri.ts, "the ack must carry the wri's signed ts");
assert.ok(wri.ts >= before, "…a ts the kit stamped, not one the fixture chose");
assert.equal(op.bsig, wri.bsig);
// the round trip: the receipt rebuilt from the mint entry plus THIS ack is
// the built wri, field for field, and verifies for a third party
const rebuilt = L.issuanceOf(CUR, 2, A.payer, 500, "h08", op.ts, BANK, op.bsig);
assert.deepEqual(rebuilt, wri);
assert.ok(await L.verifyIssuance(rebuilt, BANK), "the rebuilt wri must verify against this bank");
// and an ack whose ts drifted by ONE millisecond rebuilds a receipt that
// verifies for nobody — the defect the hand-off exists to prevent
const skewed = L.issuanceOf(CUR, 2, A.payer, 500, "h08", wri.ts + 1, BANK, op.bsig);
assert.equal(await L.verifyIssuance(skewed, BANK), null);
});
test("a pay's ack carries the built settlement's own ts, and the wrc round-trips", async () => {
const po = makeOrder({ seed: SEED_PAYER, idN: 0xd7, cur: CUR, amt: 250, seq: 1,
to: A.payee, ts: Date.now() });
const st = await L.buildSettlement(bankSigner(), po, "h09");
const op = L.ackOpFor("h09", st);
assert.equal(L.validAck(op), true);
assert.equal(op.ts, st.ts, "the ack must carry the settlement's signed ts");
assert.equal(op.bsig, st.bsig);
const rebuilt = L.receiptOf(po, "h09", op.ts, BANK, op.bsig);
assert.deepEqual(rebuilt, st);
assert.ok(await L.verifySettlement(rebuilt, BANK), "the rebuilt wrc must verify against this bank");
});
test("knocks are the accounts waiting for a word", async () => {
const s = await snapOfScenario("knocks-wait-for-the-banker");
const waiting = L.knocks(s);
assert.equal(waiting.length, 1);
assert.equal(waiting[0].id, A.payee);
assert.equal(L.accountState(s, A.payee), "pending");
assert.equal(L.isMember(s, A.payee), false, "a knock is not an account yet");
assert.deepEqual(L.knocks(await snapOfScenario("settled-provisionally")), []);
});
// ---- 3. nothing is signed that cannot settle ---------------------------------
test("a spent sequence space is refused BEFORE anything is signed", async () => {
// `nextSeq` answers null when the payer's slots are used up (§5). The refusal
// has to happen here, in words: `buildOrder` with a null `seq` would either
// reject deep inside wallet-kit or produce an order carrying a number no fold
// can ever shape — signed, appended, and dead.
const MAXS = 2 ** 50;
const po = makeOrder({ seed: SEED_PAYER, idN: 0xf0, cur: CUR, amt: 1, seq: MAXS, to: A.payee });
const entries = [
{ hash: "h01", from: BANK, clock: 1, op: { t: "charter", ts: 0, name: "B", code: "LEI", sym: "L", dec: 2, join: "open" } },
{ hash: "h02", from: A.payer, clock: 2, op: { t: "open", ts: 0, name: "P" } },
{ hash: "h03", from: A.payee, clock: 3, op: { t: "open", ts: 0, name: "Q" } },
{ hash: "h04", from: BANK, clock: 4, op: { t: "mint", ts: 0, seq: MAXS, to: A.payer, amt: 1000 } },
{ hash: "h05", from: A.payer, clock: 5, op: { t: "pay", ts: 0, po } },
];
const f = await foldOf(entries);
const s = f.snapshot();
assert.equal(f.nextSeq(A.payer), null, "the precondition of this test");
assert.equal(L.payError(s, A.payer, A.payee, 10, f.nextSeq(A.payer)), "err.pay.seq-spent");
// …and it is not the balance talking: the payer is rich
assert.equal(s.accounts.find((a) => a.id === A.payer).balExact, "999");
assert.equal(L.payError(s, A.payer, A.payee, 10, 7), null, "with a real slot, the same payment is fine");
// the issuance space is a different space with the same ceiling and the same
// answer — and `nextIssueSeq` null must stop a mint AND a burn
assert.equal(f.nextIssueSeq(), null);
assert.equal(L.issueError(s, A.payer, 10, f.nextIssueSeq()), "err.issue.seq-spent");
assert.equal(L.burnError(s, 1, f.nextIssueSeq()), "err.issue.seq-spent");
assert.equal(L.issueError(s, A.payer, 10, 3), null);
// every null-ish thing a caller could pass instead is refused too — the guard
// is on the VALUE, not on one spelling of absence
for (const bad of [null, undefined, 0, -1, "3", 1.5, NaN, MAXS + 1, {}]) {
assert.equal(L.payError(s, A.payer, A.payee, 10, bad), "err.pay.seq-spent", String(bad));
}
});
test("pay-error refuses everything a payment can be wrong about, in a fixed order", async () => {
const s = await snapOfScenario("settled-provisionally");
const nobody = A.outsider;
assert.equal(L.payError(s, A.payer, A.payee, 100, 2), null, "the honest case");
assert.equal(L.payError({ ok: false, accounts: [], payments: [] }, A.payer, A.payee, 100, 2), "err.pay.no-bank");
assert.equal(L.payError(s, nobody, A.payee, 100, 2), "err.pay.no-account");
assert.equal(L.payError(s, A.payer, "", 100, 2), "err.pay.payee-missing");
assert.equal(L.payError(s, A.payer, A.payer, 100, 2), "err.pay.payee-self");
assert.equal(L.payError(s, A.payer, nobody, 100, 2), "err.pay.payee-closed");
for (const amt of [0, -1, 1.5, "100", null, 2 ** 50 + 1]) {
assert.equal(L.payError(s, A.payer, A.payee, amt, 2), "err.pay.amount", String(amt));
}
assert.equal(L.payError(s, A.payer, A.payee, 751, 2), "err.pay.balance", "the payer holds 750");
assert.equal(L.payError(s, A.payer, A.payee, 750, 2), null, "…and exactly 750 is fine");
});
test("a closed account is refused as a payee, and as a payer", async () => {
// The fold folds a payment to a closed account as failed/unknown-payer and
// spends the slot anyway. Refusing it here is the difference between "you
// cannot pay them" and a permanently dead slot the user never asked to spend.
const sc = scenario("closed-accounts");
const s = await snapOf(sc.entries);
const closed = sc.entries.find((e) => e.op.t === "acct" && e.op.op === "close");
assert.ok(closed, "the fixture really closes someone");
const dead = await snapOf(sc.entries.filter((e) => e.clock <= closed.clock));
assert.equal(L.accountState(dead, closed.op.to), "closed");
assert.equal(L.payError(dead, A.payer, closed.op.to, 10, 1), "err.pay.payee-closed");
assert.equal(L.payError(dead, closed.op.to, A.payer, 10, 1), "err.pay.no-account");
// and the fixture's own failed row says the same thing in the past tense
assert.equal(L.whyKey(s.payments.find((p) => p.h === "h10")), "pay.why.unknown-payer");
});
test("issue-error and burn-error refuse what the fold would fail", async () => {
const s = await snapOfScenario("knocks-wait-for-the-banker");
// §5: a mint to a PENDING knock fails and spends the issuance slot anyway
assert.equal(L.accountState(s, A.payee), "pending");
assert.equal(L.issueError(s, A.payee, 100, 1), "err.issue.payee-closed");
assert.equal(L.issueError(s, A.payer, 100, 1), null);
assert.equal(L.issueError(s, A.payer, 0, 1), "err.pay.amount");
// a burn debits the banker's OWN account and names no other
assert.equal(L.viewBalanceOf(s, BANK), "0");
assert.equal(L.burnError(s, 1, 1), "err.burn.balance");
const rich = await snapOfScenario("the-banker-pays-out-of-their-own-bank");
assert.equal(L.viewBalanceOf(rich, BANK), "400");
assert.equal(L.burnError(rich, 400, 5), null);
assert.equal(L.burnError(rich, 401, 5), "err.burn.balance");
});
test("charter-error refuses the reserved code, and agrees with the validator", async () => {
const ok = ["Banca Vecinilor", "LEI", "L", 2, "approve"];
assert.equal(L.charterError(...ok), null);
assert.equal(L.charterError("", "LEI", "L", 2, "approve"), "err.charter.name-empty");
assert.equal(L.charterError(" ", "LEI", "L", 2, "approve"), "err.charter.name-empty");
assert.equal(L.charterError("x".repeat(41), "LEI", "L", 2, "approve"), "err.charter.name");
assert.equal(L.charterError("B", "lei", "L", 2, "approve"), "err.charter.code");
assert.equal(L.charterError("B", "AB", "L", 2, "approve"), "err.charter.code");
assert.equal(L.charterError("B", "ABCDEFGHI", "L", 2, "approve"), "err.charter.code");
assert.equal(L.charterError("B", "GAZ", "L", 2, "approve"), "err.charter.code-reserved");
assert.equal(L.charterError("B", "LEI", "x".repeat(9), 2, "approve"), "err.charter.sym");
assert.equal(L.charterError("B", "LEI", "L", 9, "approve"), "err.charter.dec");
assert.equal(L.charterError("B", "LEI", "L", 1.5, "approve"), "err.charter.dec");
assert.equal(L.charterError("B", "LEI", "L", 2, "whenever"), "err.charter.join");
// THE POINT OF SAYING IT BEFORE THE BUTTON: a `GAZ` charter is not refused by
// the banker's client and accepted by everyone else — it is DROPPED at ingest,
// on every replica. A founder who was not told would be left with a link, a
// log, and a bank that silently does not exist.
assert.equal(L.validCharter(L.mkCharter("B", "GAZ", "G", 2, "open")), false);
assert.equal(L.validCharter(L.mkCharter("B", "LEI", "L", 2, "open")), true);
// and everything charter-error accepts really does validate as an op
for (const [nm, code, sym, dec, join] of [
["B", "LEI", "L", 0, "open"],
["Banca Vecinilor", "RON", "", 8, "approve"],
["ăî", "ABCDEFGH", "€", 4, "open"],
]) {
assert.equal(L.charterError(nm, code, sym, dec, join), null, code);
assert.equal(L.validCharter(L.mkCharter(nm, code, sym, dec, join)), true, code);
}
// …and one thing the UI refuses that the protocol would allow: an empty name.
// That is a deliberate stricter-than-the-wire rule, not a disagreement.
assert.equal(L.validCharter(L.mkCharter("", "LEI", "L", 2, "open")), true);
assert.equal(L.charterError("", "LEI", "L", 2, "open"), "err.charter.name-empty");
});
test("req-error lets only an open account ask to be paid", async () => {
const s = await snapOfScenario("settled-provisionally");
assert.equal(L.reqError(s, A.payer, A.payee, 100), null);
assert.equal(L.reqError(s, A.outsider, A.payee, 100), "err.pay.no-account");
assert.equal(L.reqError(s, A.payer, A.payer, 100), "err.pay.payee-self");
assert.equal(L.reqError(s, A.payer, A.payee, 0), "err.pay.amount");
// …but it may name someone with no account: a `req` moves nothing, and the
// fold does not gate the payee at all
assert.equal(L.reqError(s, A.payer, A.outsider, 100), null);
});
test("rate-error accepts a ratio and nothing else", () => {
assert.equal(L.rateError(1, 1), null);
assert.equal(L.rateError(3, 250), null);
for (const [n, d] of [[0, 1], [1, 0], [-1, 1], [1.5, 2], ["1", 1], [1, null], [2 ** 50 + 1, 1]]) {
assert.equal(L.rateError(n, d), "err.rate.shape", `${n}/${d}`);
}
});
// ---- the wallet's own numbers ------------------------------------------------
test("history is the viewer's money, newest first — and a mint is the PAYEE's row", async () => {
// The regression this pins: a `mint` NAMES THE BANKER as its actor while
// debiting nobody. Read as an outgoing payment, a banker's own history would
// drift down by the entire supply they had ever issued.
const s = await snapOfScenario("the-banker-pays-out-of-their-own-bank");
const mine = L.historyOf(s, BANK);
assert.deepEqual(mine.map((r) => r.h), ["h10", "h09"], "newest first");
assert.equal(mine.find((r) => r.h === "h09").dir, "in", "a mint TO the banker is money in");
assert.equal(mine.find((r) => r.h === "h10").dir, "out", "a payment BY the banker is money out");
assert.equal(mine.some((r) => r.h === "h08"), false, "the mint to someone else is not the banker's row");
// …and the arithmetic that follows from it: +500 − 100 = 400, the balance
assert.equal(L.viewBalanceOf(s, BANK), "400");
// TWO TOTALS, NOT ONE. Nothing is acked here, so none of the 400 is pinned —
// but the two halves are pinned by different things and one of them by
// nothing at all, so they are counted apart. The mint the banker made to
// itself is `unpinned`: no ack for it is coming from anywhere.
assert.equal(L.provisionalCount(s, BANK), 1, "the payment out");
assert.equal(L.provisionalNet(s, BANK), "-100");
assert.equal(L.unpinnedCount(s, BANK), 1, "the mint in");
assert.equal(L.unpinnedNet(s, BANK), "500");
// …and together they still account for every unpinned unit of the balance
assert.equal(
(BigInt(L.provisionalNet(s, BANK)) + BigInt(L.unpinnedNet(s, BANK))).toString(), "400");
// the payer's slice of the same bank
const payer = L.historyOf(s, A.payer);
assert.deepEqual(payer.map((r) => r.h), ["h08"]);
assert.equal(payer[0].dir, "in");
assert.equal(L.provisionalNet(s, A.payer), "0", "the payer has made no payment");
assert.equal(L.provisionalCount(s, A.payer), 0);
assert.equal(L.unpinnedNet(s, A.payer), "1000", "…all of it was issued to them");
assert.equal(L.unpinnedCount(s, A.payer), 1);
});
test("the ledger is every payment, including the ones that are nobody's business", async () => {
const s = await snapOfScenario("settled-provisionally");
const all = L.ledgerOf(s, BANK);
assert.deepEqual(all.map((r) => r.h), ["h09", "h08"]);
// the banker is not party to the customer-to-customer payment, and the row
// says so rather than claiming a direction
assert.equal(all.find((r) => r.h === "h09").dir, "none");
assert.equal(all.find((r) => r.h === "h09").actor, A.payer);
assert.equal(all.find((r) => r.h === "h09").to, A.payee);
// …and a `none` row moves none of the viewer's provisional money
assert.equal(L.provisionalNet(s, BANK), "0");
assert.equal(L.provisionalCount(s, BANK), 0);
assert.equal(L.unpinnedNet(s, BANK), "0", "the mint went to someone else");
// while both parties see it
assert.equal(L.provisionalNet(s, A.payee), "250");
assert.equal(L.unpinnedNet(s, A.payee), "0");
// the payer's 1000 came from a mint and their 250 went out as a payment, and
// the two are not one number: only the 250 is waiting on the banker
assert.equal(L.provisionalNet(s, A.payer), "-250", "the payment out");
assert.equal(L.unpinnedNet(s, A.payer), "1000", "the issuance in");
});
test("an ack takes a payment out of the provisional total and moves no money", async () => {
const un = await snapOfScenario("settled-provisionally");
const ac = await snapOfScenario("acked-is-final");
assert.equal(L.viewBalanceOf(un, A.payee), L.viewBalanceOf(ac, A.payee));
assert.equal(L.provisionalCount(un, A.payee), 1);
assert.equal(L.provisionalCount(ac, A.payee), 0);
assert.equal(L.provisionalNet(un, A.payee), "250");
assert.equal(L.provisionalNet(ac, A.payee), "0");
});
test("a failed payment is in the history and in no total", async () => {
const s = await snapOfScenario("overdraft-consumes-the-slot");
const rows = L.historyOf(s, A.payer);
assert.equal(rows.filter((r) => r.state === "failed").length, 2);
assert.equal(L.provisionalCount(s, A.payer), 0, "neither failure, and neither mint");
assert.equal(L.provisionalNet(s, A.payer), "0");
assert.equal(L.unpinnedCount(s, A.payer), 2, "the two mints");
assert.equal(L.unpinnedNet(s, A.payer), "10000");
assert.equal(L.viewBalanceOf(s, A.payer), "10000");
});
test("unpinned totals are exact past 2^53", async () => {
// The same reason the fold refuses to add in doubles: one amount may be 2^50,
// so nine of them is past the exact-double ceiling. A net built on Numbers
// would report a clean-looking total that is one unit wrong. Ten mints, so it
// is `unpinnedNet` that does the adding — the two totals share one BigInt
// accumulator and this is the one that can be driven past the ceiling without
// signing ten orders.
const MAXA = 2 ** 50;
const entries = [
{ hash: "h01", from: BANK, clock: 1, op: { t: "charter", ts: 0, name: "B", code: "LEI", sym: "L", dec: 2, join: "open" } },
{ hash: "h02", from: A.payer, clock: 2, op: { t: "open", ts: 0, name: "P" } },
];
for (let i = 0; i < 9; i++) {
entries.push({ hash: `h1${i}`, from: BANK, clock: 10 + i, op: { t: "mint", ts: 0, seq: i + 1, to: A.payer, amt: MAXA } });
}
entries.push({ hash: "h20", from: BANK, clock: 30, op: { t: "mint", ts: 0, seq: 10, to: A.payer, amt: 1 } });
const s = await snapOf(entries);
const want = (BigInt(MAXA) * 9n + 1n).toString();
assert.equal(L.unpinnedNet(s, A.payer), want);
assert.equal(L.provisionalNet(s, A.payer), "0", "not one of them is a payment");
assert.equal(BigInt(want) > 2n ** 53n, true);
assert.notEqual(String(Number(want)), want, "…and a Number fold would have been wrong");
});
test("requests are shown to the person they were asked of, and nobody else", async () => {
const s = await snapOfScenario("rate-and-req");
assert.equal(s.reqs.length, 1);
assert.equal(L.requestsFor(s, A.payee).length, 1);
assert.equal(L.requestsFor(s, A.payer).length, 0, "the asker is not the asked");
assert.equal(L.requestsFor(s, A.third).length, 0);
assert.equal(L.requestsFor(s, null).length, 0);
});
// ---- accounts ----------------------------------------------------------------
test("account-state tells an empty account from no account at all", async () => {
const s = await snapOfScenario("settled-provisionally");
assert.equal(L.accountState(s, A.payee), "open");
assert.equal(L.accountState(s, A.third), "open");
assert.equal(L.accountState(s, A.outsider), "none");
assert.equal(L.viewBalanceOf(s, A.third), "0");
assert.equal(L.viewBalanceOf(s, A.outsider), "0", "the same zero, and NOT the same thing");
assert.equal(L.isMember(s, A.outsider), false);
assert.equal(L.isMember(s, null), false);
assert.equal(L.accountState(s, null), "none");
});
test("open-accounts is who you can pay, and never yourself", async () => {
const s = await snapOfScenario("settled-provisionally");
const ids = L.openAccounts(s, A.payer).map((a) => a.id);
assert.deepEqual(ids.sort(), [BANK, A.payee, A.third].sort());
assert.equal(ids.includes(A.payer), false);
assert.equal(L.openAccounts(s, null).length, 4, "with no viewer, everyone open is listed");
// a pending knock is not payable
const knocks = await snapOfScenario("knocks-wait-for-the-banker");
assert.equal(L.openAccounts(knocks, null).map((a) => a.id).includes(A.payee), false);
});
test("label-of never leaks a raw key or an empty string", async () => {
const s = await snapOfScenario("settled-provisionally");
assert.equal(L.labelOf(s, A.payer), "Payer", "the name the account opened with");
// an UNNAMED account falls back to the suite's four-emoji fingerprint — the
// identity language chat verifies with — never to a slice of the key. The
// fingerprint is an async digest, so the first read is a placeholder and the
// cache warms behind it.
const cold = L.labelOf(s, BANK);
assert.ok(!cold.includes(BANK.slice(0, 6)), "no key bytes, even cold");
let warm = cold;
for (let i = 0; i < 40 && warm === "▢▢"; i++) {
await new Promise((r) => setTimeout(r, 25));
warm = L.labelOf(s, BANK);
}
assert.notEqual(warm, "▢▢", "the fingerprint landed");
assert.ok(!warm.includes(BANK.slice(0, 6)), "and it is emoji, not key bytes");
assert.equal(L.labelOf(s, BANK), warm, "…and stable once warm");
assert.equal(L.labelOf(s, null), "?");
assert.equal(L.labelOf(s, ""), "?");
});
// ---- money, as strings -------------------------------------------------------
test("amounts are formatted by moving a decimal point, never by dividing", async () => {
assert.equal(L.fmtUnits("123456", 2), "1234.56");
assert.equal(L.fmtUnits("5", 2), "0.05");
assert.equal(L.fmtUnits("0", 2), "0.00");
assert.equal(L.fmtUnits("7", 0), "7");
assert.equal(L.fmtUnits("1", 8), "0.00000001");
assert.equal(L.fmtUnits("-250", 2), "-2.50");
assert.equal(L.fmtUnits(1234, 2), "12.34", "a Number amount formats the same way");
// past 2^53, where a division would start lying
const huge = (2n ** 60n).toString();
assert.equal(L.fmtUnits(huge, 2), `${huge.slice(0, -2)}.${huge.slice(-2)}`);
assert.equal(L.fmtUnits(huge, 0), huge);
// an out-of-range exponent reads as 0 — no decimal point is safer than one in
// the wrong place
assert.equal(L.fmtUnits("1234", null), "1234");
assert.equal(L.fmtUnits("1234", -1), "1234");
});
test("the symbol and the sign come from the charter, not from a guess", async () => {
const s = await snapOfScenario("settled-provisionally");
const c = s.charter;
assert.equal(L.decOf(c), 2);
assert.equal(L.symOf(c), "L");
assert.equal(L.nameOf(c), "Banca Test");
assert.equal(L.fmtMoney("750", c), "L 7.50");
assert.equal(L.fmtSigned("750", c), "+L 7.50");
assert.equal(L.fmtSigned("-750", c), "-L 7.50");
assert.equal(L.fmtSigned("0", c), "L 0.00", "zero carries no sign");
// an unchartered bank still has to render something
assert.equal(L.decOf(null), 0);
assert.equal(L.symOf(null), "");
assert.equal(L.nameOf(null), "");
assert.equal(L.fmtMoney("5", null), "5");
// a bank with no symbol falls back to its code rather than to nothing
assert.equal(L.symOf({ code: "LEI", sym: "", dec: 2 }), "LEI");
// …and an out-of-range `dec` from a fold this build does not know reads as 0
assert.equal(L.decOf({ dec: 99 }), 0);
assert.equal(L.decOf({ dec: 1.5 }), 0);
});
test("a typed amount becomes integer minor units, or nothing", () => {
assert.equal(L.parseUnits("12.34", 2), 1234);
assert.equal(L.parseUnits("12,34", 2), 1234, "a comma is a decimal mark on half this suite's keyboards");
assert.equal(L.parseUnits("12", 2), 1200);
assert.equal(L.parseUnits("12.", 2), 1200);
assert.equal(L.parseUnits("0.01", 2), 1);
assert.equal(L.parseUnits(" 7 ", 0), 7);
assert.equal(L.parseUnits("12.345", 2), null, "more decimals than the currency has");
assert.equal(L.parseUnits("0", 2), null, "AMT_MIN is 1 minor unit");
assert.equal(L.parseUnits("0.00", 2), null);
for (const bad of ["", "x", "1.2.3", "-1", "1e3", "١٢", null, undefined, 12, {}]) {
assert.equal(L.parseUnits(bad, 2), null, JSON.stringify(bad));
}
// the ceiling is wallet-kit's, and it is checked as a BigInt before anything
// becomes a Number
const max = (2 ** 50).toString();
assert.equal(L.parseUnits(max, 0), 2 ** 50);
assert.equal(L.parseUnits((2 ** 50 + 1).toString(), 0), null);
assert.equal(L.parseUnits("99999999999999999999", 0), null);
});
test("amount-error says WHICH way an amount is wrong", () => {
assert.equal(L.amountError("1.00", 2), null);
assert.equal(L.amountError("", 2), "err.amt.empty");
assert.equal(L.amountError(" ", 2), "err.amt.empty");
assert.equal(L.amountError(null, 2), "err.amt.empty");
assert.equal(L.amountError("abc", 2), "err.amt.shape");
assert.equal(L.amountError("1.2.3", 2), "err.amt.shape");
assert.equal(L.amountError("1.234", 2), "err.amt.dec", "not 'not a number' — it IS a number");
assert.equal(L.amountError("0", 2), "err.amt.range");
assert.equal(L.amountError("99999999999999999999", 0), "err.amt.range");
// the two agree: an amount with no error parses, and one with an error does not
for (const s of ["1", "0.01", "1234.56", "", "x", "1.234", "0"]) {
assert.equal(L.parseUnits(s, 2) === null, L.amountError(s, 2) !== null, s);
}
});
// ---- every key these functions can name must exist ---------------------------
test("every catalog key the view layer can return is defined, in all three languages", async () => {
// The gate a screen cannot have: a refusal that renders as its own key,
// because `t` falls back to the key itself (visible, greppable — and still a
// sentence nobody can read). Every key below is one a real refusal path can
// produce, collected by DRIVING those paths rather than by listing them.
const keys = new Set();
const base = await snapOfScenario("settled-provisionally");
for (const rec of base.payments) {
keys.add(L.stateKey(rec));
const note = L.stateNoteKey(L.paymentState(rec), rec.kind);
if (note !== null) keys.add(note);
}
// every route state-note-key can take, driven through the function itself so
// a new route cannot ship without its sentence
for (const state of ["final", "provisional", "unpinned", "failed"]) {
keys.add(`pay.state.${state}`);
for (const kind of ["pay", "mint", "burn"]) {
const note = L.stateNoteKey(state, kind);
if (note !== null) keys.add(note);
}
}
// the balance card's two totals and the sentence under each
for (const k of ["wallet.provisional", "wallet.provisional.none", "wallet.provisional.warn",
"wallet.unpinned", "wallet.unpinned.warn"]) keys.add(k);
for (const why of [...L.DECLINE_REASONS, "from-the-future"]) keys.add(L.whyKey({ status: "failed", why }));
for (const st of ["open", "pending", "closed"]) keys.add(`acct.state.${st}`);
for (const st of ["pending", "closed"]) {
keys.add(`acct.${st}.title`);
keys.add(`acct.${st}.body`);
}
const collect = (k) => { if (k !== null && k !== undefined) keys.add(k); };
collect(L.charterError("", "LEI", "L", 2, "open"));
collect(L.charterError("x".repeat(41), "LEI", "L", 2, "open"));
collect(L.charterError("B", "lei", "L", 2, "open"));
collect(L.charterError("B", "GAZ", "L", 2, "open"));
collect(L.charterError("B", "LEI", "x".repeat(9), 2, "open"));
collect(L.charterError("B", "LEI", "L", 99, "open"));
collect(L.charterError("B", "LEI", "L", 2, "nope"));
for (const amt of ["", "x", "1.234", "0"]) collect(L.amountError(amt, 2));
collect(L.payError({ ok: false, accounts: [], payments: [] }, A.payer, A.payee, 1, 1));
collect(L.payError(base, A.outsider, A.payee, 1, 1));
collect(L.payError(base, A.payer, "", 1, 1));
collect(L.payError(base, A.payer, A.payer, 1, 1));
collect(L.payError(base, A.payer, A.outsider, 1, 1));
collect(L.payError(base, A.payer, A.payee, 0, 1));
collect(L.payError(base, A.payer, A.payee, 1, null));
collect(L.payError(base, A.payer, A.payee, 100000, 1));
collect(L.issueError(base, A.outsider, 1, 1));
collect(L.issueError(base, A.payer, 1, null));
collect(L.burnError(base, 1, 1));
collect(L.rateError(0, 1));
collect(L.reqError(base, A.outsider, A.payee, 1));
// the burn refusal is DRIVEN through the dispatch the store runs on, not
// declared: a settled burn's ack arm IS the key
const settledBurn = (await snapOfScenario("burn-debits-the-banker-only"))
.payments.find((p) => p.kind === "burn");
collect(L.ackDispatch(settledBurn));
assert.ok(keys.has("err.ack.burn"), "the dispatch really reached the burn refusal");
// the keys the store's own refusals produce, which no pure function returns
for (const k of ["err.pay.sign", "err.ack.unknown", "err.ack.sign",
"err.append", "err.no-identity", "err.not-banker"]) keys.add(k);
assert.ok(keys.size >= 30, `only ${keys.size} keys collected — the drive-it loop stopped driving`);
for (const lang of ["en", "ro", "hu"]) {
const have = new Set(L.catalogKeys(lang));
const missing = [...keys].filter((k) => !have.has(k)).sort();
assert.deepEqual(missing, [], `${lang} is missing keys the interface can ask for`);
}
});
// ---- the lobby label adopts the charter name ----------------------------------
test("adopt-label: the lobby entry takes the bank's name exactly once, and never over a user's", async () => {
// The defect this pins: room boot asked ONCE, right after the local replay —
// before a founder had chartered and before a first joiner's log had
// replicated — so the charter was nil at the only moment anyone asked and
// the lobby said "bank · <date>" forever, under a room header that knew
// better. The decision is pure now and the caller re-asks on every fold
// change; these are the answers that make re-asking safe.
const charter = (await snapOfScenario("prelude")).charter;
assert.equal(charter.name, "Banca Test", "the fixture charter carries a name");
const fresh = { s: "tok", label: "bank · Aug 30", ts: 1 };
assert.equal(L.adoptLabel(charter, fresh), "Banca Test", "a generated label adopts");
// no charter yet — the founder has not chartered / the log has not arrived
assert.equal(L.adoptLabel(null, fresh), null);
assert.equal(L.adoptLabel(undefined, fresh), null);
assert.equal(L.adoptLabel({ name: "" }, fresh), null, "an empty name is no name");
// a label the user typed (lts is set only by an explicit rename) is theirs
assert.equal(L.adoptLabel(charter, { s: "tok", label: "our co-op", ts: 1, lts: 5 }), null);
// idempotent: once adopted, every later fold change writes nothing
assert.equal(L.adoptLabel(charter, { s: "tok", label: "Banca Test", ts: 1 }), null);
// and a missing entry answers nothing rather than something
assert.equal(L.adoptLabel(charter, null), null);
});
// ---- the knock field's default is the suite profile's name --------------------
test("default-knock-name: the profile name prefills the knock field, and no junk does", () => {
// The defect this pins: a user whose suite identity said "Ardelean" opened a
// bank link and the "your name" field sat EMPTY — the one input in the suite
// that ignored a profile every other app honors (chat joins with no prompt,
// the games prefill the callsign). The decision is pure: the store's name —
// the clamped profile value the hellos carry — becomes the field's default,
// and "" keeps the placeholder.
assert.equal(L.defaultKnockName("Ardelean"), "Ardelean");
// a default, sanitized the way the open op will validate it: trimmed…
assert.equal(L.defaultKnockName(" Ardelean "), "Ardelean");
// …a whitespace-only name is no name (the placeholder beats three spaces)…
assert.equal(L.defaultKnockName(" "), "");
// …and an empty or absent profile keeps the field empty
assert.equal(L.defaultKnockName(""), "");
assert.equal(L.defaultKnockName(null), "");
assert.equal(L.defaultKnockName(undefined), "");
// not a string at all (a hostile mirror) is no name either
assert.equal(L.defaultKnockName(42), "");
// never longer than what open-account! accepts: MAX_NAME code points, cut
// the way id-kit's clamp cuts (Array.from, so surrogate pairs stay whole)
assert.equal(L.defaultKnockName("x".repeat(80)), "x".repeat(L.MAX_NAME));
const glyphs = "🐢".repeat(L.MAX_NAME + 3);
const cut = L.defaultKnockName(glyphs);
assert.equal([...cut].length, L.MAX_NAME, "code points, not UTF-16 units");
assert.ok([...cut].every((c) => c === "🐢"), "no split surrogate at the cut");
});
|