wallet-kit / test / ledger.test.mjs
   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");
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/wallet-kit.git