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 | /**
* Hand-authored declarations for the "." export of ardegazu-wallet-kit — the
* compiled dist/index.js (ClojureScript).
*
* The types are the wire, written down. Where a name is load-bearing for a
* second implementation the comment says so; where a signature departs from
* what a reader would guess (the promise-returning ledger mutators, `seq`
* meaning two different things) it says why.
*/
/** Bytes always backed by a plain ArrayBuffer — id-kit's convention. */
export type Bytes = Uint8Array<ArrayBuffer>;
/** Just enough of id-kit's Identity to sign with. */
export interface SigningIdentity {
readonly publicKeyB64: string;
signRaw(data: Bytes): Promise<Bytes>;
}
// ---- constants --------------------------------------------------------------
//
// There is no WALLET_SALT. This layer derives nothing — every value it hashes is
// already domain-separated by the "<domain>|v1|" prefix — so a published salt
// would be a constant no golden vector pins and nothing has agreed to.
/** Signature domains. wrc and wrj deliberately share one — `t` is inside canon(). */
export declare const DOM_PAY_REQ: string;
export declare const DOM_PAY_ORD: string;
export declare const DOM_PAY_RCP: string;
/** The escrow lock's own domain — a different act, a different preimage. */
export declare const DOM_PAY_LOCK: "wpay-lock";
/** A beneficiary's release — the only artifact signed by the party paid TO. */
export declare const DOM_PAY_REL: "wpay-rel";
/** The issuance receipt's own domain — a different act, a different preimage. */
export declare const DOM_PAY_ISS: string;
/** Integer minor units, 1 .. 2^50. The ceiling is precision, not policy. */
export declare const AMT_MIN: number;
export declare const AMT_MAX: number;
/** Code-point bounds on the two free-text fields. */
export declare const CTX_MAX: number;
export declare const MEMO_MAX: number;
/** Canonical, unpadded base64url shapes — the trailing padding bits are pinned. */
export declare const PUB_RE: RegExp;
export declare const SIG_RE: RegExp;
export declare const ORDER_ID_RE: RegExp;
export declare const DIGEST_RE: RegExp;
export declare const CODE_RE: RegExp;
export declare const CUR_RE: RegExp;
/** The reserved neutral unit of account: parseable, priced in, never payable. */
export declare const NEUTRAL_BANKER: string;
export declare const NEUTRAL_CODE: string;
export declare const NEUTRAL_UNIT: string;
export declare const SKEW_MS: number;
export declare const REQ_TTL_MS: number;
export declare const ORDER_TTL_MS: number;
export declare const MAX_TTL_MS: number;
export declare const MAX_ARTIFACT_BYTES: number;
export declare const MAX_SETTLEMENT_BYTES: number;
export type DeclineReason = "insufficient" | "unknown-payer" | "expired" | "cur" | "seq";
/** The closed set, in wire order. */
export declare const DECLINE_REASONS: readonly DeclineReason[];
/** localStorage prefix — namespaced by identity, never by app. */
export declare const LEDGER_KEY_PREFIX: string;
export declare const PAY_BASE_URL: string;
export declare const PAY_FRAGMENT_KEY: string;
// ---- the artifacts ----------------------------------------------------------
/**
* A payment request, signed by the PAYEE. Key order is the wire.
* `exp` is an absolute epoch-ms expiry.
*/
export interface PayRequest {
v: 1;
t: "wpr";
cur: string;
amt: number;
to: string;
tox: string;
ctx: string;
memo: string;
exp: number;
sig: string;
}
/**
* A payment order, signed by the PAYER. Thirteen keys, and the order of them is
* the wire.
*
* `seq` is the payer's sequence FOR THAT BANK: the bank's fold binds an order to
* exactly one slot, which is its double-spend defense. Not to be confused with
* `Settlement["seq"]`, which is a string and belongs to the bank.
*/
export interface PayOrder {
v: 1;
t: "wpo";
id: string;
cur: string;
amt: number;
seq: number;
from: string;
to: string;
ctx: string;
memo: string;
ts: number;
exp: number;
sig: string;
}
/** A settlement receipt, signed by the BANK. `seq` is the bank's log ref. */
export interface Receipt {
v: 1;
t: "wrc";
po: PayOrder;
seq: string;
ts: number;
bank: string;
bsig: string;
}
/** A decline, signed by the BANK. Same signature domain as a receipt. */
export interface Decline {
v: 1;
t: "wrj";
po: PayOrder;
why: DeclineReason;
ts: number;
bank: string;
bsig: string;
}
/**
* An issuance receipt, signed by the BANK: the artifact that gives a MINT
* finality. Ten keys, and the order of them is the wire.
*
* The bank's fold pins an issuance to a slot the moment the bank acks it, but
* a wrc's bsig covers a payment preimage and a mint has no payment inside it —
* so before this artifact an acked mint had nothing signed for the recipient
* to hold, and a banker could withhold a back-dated second mint on the same
* issuance slot and displace the first: the one way a banker could empty a
* customer's account. A wri is the signable preimage that closes that window
* for acked issuance.
*
* `seq` is the BANK's own issuance slot number — a third meaning of the word:
* `PayOrder["seq"]` is the payer's number, `Receipt["seq"]` is the bank's
* log-ref string, and this one is the bank's issuance counter. `h` is the log
* hash of the mint entry being made final — opaque to this kit: a non-empty,
* control-free string of at most CTX_MAX code points, compared only for
* equality (banca's are multibase strings; the grammar belongs to the log).
* `bank` must equal the banker half of `cur`, the same binding a settlement
* enforces — without it any bank could "finalize" another bank's issuance.
*
* MINT ONLY. A wri never covers a burn: a burn debits the banker's own
* account and names no recipient, so displacing one moves only the money of
* the party doing the displacing, and a burn-wri would be signed content with
* no natural meaning (a `to` that is really a `from`). Burns are unpinnable
* by design; a future artifact that pins them gets its own `t` and domain.
*/
export interface IssuanceReceipt {
v: 1;
t: "wri";
cur: string;
/** The bank's own issuance slot number. */
seq: number;
/** The recipient of the mint. The ONLY identity a wri can ever credit. */
to: string;
amt: number;
/** The mint entry's log hash — opaque, non-empty, ≤ CTX_MAX code points. */
h: string;
ts: number;
bank: string;
bsig: string;
}
/**
* An escrow lock order, signed by the PAYER: money a bank HOLDS against a
* hashlock instead of moving. It is a PayOrder with a condition — the same
* twelve fields, plus `hash`.
*
* WHY IT EXISTS. A trade of one bank's paper for another's is two payments in
* two independent logs with no shared ordering, so it cannot be atomic. Lock
* both legs to ONE hashlock and it can be: the secret-holder claims first and
* thereby publishes the secret, and the counterparty claims with it. Either
* both legs complete or neither does.
*
* `seq` draws from THE SAME per-payer-per-bank space a PayOrder does, not a
* second one — a lock and a payment are both this account moving its own
* money, and a bank consumes one slot per act.
*
* `exp` is ADVISORY. A bank's fold never reads it, because a verdict that
* depended on the reader's clock would let two replicas disagree; it is what a
* banker consults before attesting a return, and what a UI counts down.
*/
export interface LockOrder {
v: 1;
t: "wlk";
id: string;
cur: string;
amt: number;
/** The payer's sequence slot — the SAME space PayOrder.seq draws from. */
seq: number;
from: string;
/** The beneficiary: who is paid if the lock is claimed. */
to: string;
/** The hashlock digest. sha256B64url of the preimage's base64url TEXT. */
hash: string;
ctx: string;
memo: string;
ts: number;
/** Advisory only — never read by a bank's fold. */
exp: number;
sig: string;
}
/**
* A lock receipt, signed by the BANK: the artifact a CLAIMED lock gets, so the
* ack that pins it has a preimage to cover and the beneficiary holds a receipt.
*
* `seq` is the bank's log entry reference for THE CLAIM, not the lock — the
* lock is the promise and the claim is the money moving.
*
* RECEIPT ONLY, no decline twin. A Decline exists because a payer waiting on an
* order needs to know why it will not settle; a lock that does not settle is
* RETURNED instead, and the return is itself the answer.
*/
export interface LockReceipt {
v: 1;
t: "wlr";
lk: LockOrder;
/** The bank's log entry reference for the claim (a STRING). */
seq: string;
ts: number;
bank: string;
bsig: string;
}
/**
* A release, signed by the BENEFICIARY: a lock handed back to its payer without
* a preimage. This is what keeps a dead banker from being a lost-funds event —
* a return needs either the bank attesting the clock or this, and this needs no
* bank at all.
*
* `lh` is the LOCK ENTRY's hash in the bank's log, not the lock's `id`: one
* append has one entry hash forever, so a release names exactly one lock and
* cannot be replayed against a re-appended copy of the same paper.
*
* VERIFYING ONE PROVES ONLY THAT SOMEBODY SIGNED AWAY SOME LOCK. That it is the
* right somebody and the right lock (`to` === lk.to, `cur` === lk.cur, `lh` ===
* that lock's entry hash) is a comparison only the holder of the lock can make,
* and a bank MUST make it.
*/
export interface Release {
v: 1;
t: "wrl";
cur: string;
/** The lock entry's log hash — opaque, non-empty, ≤ CTX_MAX code points. */
lh: string;
/** The beneficiary — the signer, and the party giving the money back. */
to: string;
ts: number;
sig: string;
}
/** A hashlock pair. `pre` is a SECRET until claimed with, and public after. */
export interface Hashlock {
/** 32 random bytes, canonical base64url. Never reuse one across two locks. */
pre: string;
/** sha256B64url of `pre`'s base64url TEXT. */
hash: string;
}
export type Settlement = Receipt | Decline;
export type WalletArtifact =
| PayRequest
| PayOrder
| Settlement
| IssuanceReceipt
| LockOrder
| LockReceipt
| Release;
// ---- currency ids -----------------------------------------------------------
export interface ParsedCurrency {
/** The issuing identity's public key, or "~" for the neutral unit. */
banker: string;
code: string;
}
/** Parses "~.GAZ" too — a pricing layer needs it. Null when malformed. */
export declare function parseCurrency(cur: unknown): ParsedCurrency | null;
/** False for the neutral unit: nobody can mint it, so nobody can settle it. */
export declare function isPayableCurrency(cur: unknown): boolean;
export declare function isNeutralUnit(cur: unknown): boolean;
export declare function bankerOf(cur: unknown): string | null;
// ---- text clamps (build side) -----------------------------------------------
/** Strip control characters, trim, clamp to CTX_MAX / MEMO_MAX code points. */
export declare function sanitizeCtx(s: unknown): string;
export declare function sanitizeMemo(s: unknown): string;
// ---- signature preimages ----------------------------------------------------
//
// The interop contract. A second implementation that reproduces these strings
// byte-for-byte interoperates; one that does not cannot be made to.
export declare function unsignedPayRequest(r: PayRequest | Omit<PayRequest, "sig">): object;
export declare function unsignedOrder(o: PayOrder | Omit<PayOrder, "sig">): object;
export declare function unsignedSettlement(s: Settlement | Omit<Settlement, "bsig">): object;
/** `"<domain>|v1|" + canon(<artifact minus its signature>)`. */
export declare function payRequestPreimage(r: PayRequest | Omit<PayRequest, "sig">): string;
export declare function orderPreimage(o: PayOrder | Omit<PayOrder, "sig">): string;
export declare function settlementPreimage(s: Settlement | Omit<Settlement, "bsig">): string;
export declare function unsignedIssuance(i: IssuanceReceipt | Omit<IssuanceReceipt, "bsig">): object;
export declare function issuancePreimage(i: IssuanceReceipt | Omit<IssuanceReceipt, "bsig">): string;
export declare function unsignedLock(l: LockOrder | Omit<LockOrder, "sig">): object;
export declare function lockPreimage(l: LockOrder | Omit<LockOrder, "sig">): string;
export declare function unsignedLockReceipt(r: LockReceipt | Omit<LockReceipt, "bsig">): object;
export declare function lockReceiptPreimage(r: LockReceipt | Omit<LockReceipt, "bsig">): string;
export declare function unsignedRelease(r: Release | Omit<Release, "sig">): object;
export declare function releasePreimage(r: Release | Omit<Release, "sig">): string;
// ---- dedup keys + liveness ---------------------------------------------------
/** sha256B64url(canon(order minus sig)) — a function of what the order SAYS. */
export declare function orderId(po: PayOrder): Promise<string>;
/**
* sha256B64url(canon({v, t, bank, oid})). Deliberately blind to `ts` and to the
* bank's log ref: one bank answering one order once, however often it says so.
*/
export declare function receiptKey(rc: Settlement): Promise<string>;
/**
* sha256B64url(canon({v, t: "wri", bank, h})) — receiptKey's shape with the
* mint entry's log hash where the order id sits. (bank, h) IS the fact being
* made final — this bank pinned this mint entry — so a receipt re-issued with
* a fresh `ts` folds once, and a bank that signs two receipts with one `h`
* and two amounts has signed a contradiction the ledger folds first-wins
* rather than credits twice.
*/
export declare function issuanceKey(wri: IssuanceReceipt): Promise<string>;
/** orderId for escrow: sha256B64url(canon(lock minus sig)). */
export declare function lockId(lk: LockOrder): Promise<string>;
/**
* sha256B64url(canon({v, t: "wlr", bank, lkid})) — receiptKey's shape with a
* lock id where the order id sits, and blind to `ts` and the log ref for the
* same reason: a bank re-issuing one answer with a fresh stamp has answered
* once, and folding it twice would double a balance.
*/
export declare function lockReceiptKey(wlr: LockReceipt): Promise<string>;
/**
* A fresh hashlock. ONE PER LOCK, always: the first claim publishes the
* preimage, so any other lock sharing the digest becomes claimable by anyone
* watching. A quote that can be partially filled needs one per FILL.
*/
export declare function newHashlock(): Promise<Hashlock>;
/** Does `pre` open `hash`? Total — false on anything malformed, never throws. */
export declare function hashlockMatches(pre: unknown, hash: unknown): Promise<boolean>;
/**
* Liveness, which verification deliberately does NOT check — a bank has to be
* able to read an expired order in order to decline it. A settlement has no
* expiry of its own; its order's governs.
*
* Total: `false` on anything it cannot read, hostile getters and `po` chains of
* any depth included. Pass `nowMs` to make the verdict reproducible — see the
* wall-clock note under LedgerStore.balances.
*
* NO CLOCK-FREE FORM, unlike the shape checks below: liveness IS the clock
* question, so `null` here means the wall clock rather than "skip it". A fold
* that needs determinism pins `nowMs` to a number.
*/
export declare function expired(a: unknown, nowMs?: number): boolean;
// ---- the clock a verdict is taken against ------------------------------------
//
// Every admission check below ends in an optional `nowMs`:
//
// omitted / undefined this reader's wall clock — the historical behaviour,
// and still the default;
// a finite number that instant, so a verdict can be reproduced;
// null NO CLOCK. Every wall-clock bound is skipped; every
// bound a timestamp carries relative to itself stands.
//
// PASS `null` FROM A REPLICATED FOLD. A bank ledger that folds signed orders out
// of a log must reach the same state on every replica, and a wall clock inside
// the shape check makes "is this entry a valid order?" a question two replicas
// three minutes apart answer differently — two balances for one set of entries,
// which is a partition bug in a money ledger. `orderShape(e, null)` and
// `verifyOrder(e, null)` are the deterministic forms such a fold must use, and
// they exist so no consumer has to transcribe a clock-free copy of these rules
// into its own repo.
//
// The clock-free form is DETERMINISTIC, NOT WEAKER. On a `wpo` it drops exactly
// one bound — `ts` no further ahead than SKEW_MS — and still enforces `exp >
// ts`, `exp <= ts + MAX_TTL_MS`, `ts` a non-negative integer, and every
// structural rule. On a settlement it additionally keeps `ts >= po.ts -
// SKEW_MS`. A `wpr` has no `ts`, so clock-free drops its only clock-relative
// bound (`exp <= now + MAX_TTL_MS + SKEW_MS`) and keeps `exp` a positive
// integer; that is the one place `null` is genuinely more permissive, so use it
// there only for determinism, not for convenience.
//
// Anything that is neither a finite number nor `null` — a string, NaN,
// Infinity, an object — reads as OMITTED. An admission check must not have a
// most-permissive setting a typo can select.
export type AdmissionClock = number | null;
// ---- structural checks (synchronous, signature-free) -------------------------
//
// Everything verify* does except the signature, rebuilt in wire key order.
// LedgerStore uses these on its own storage; app code should prefer verify*.
export declare function payRequestShape(raw: unknown, nowMs?: AdmissionClock): PayRequest | null;
export declare function orderShape(raw: unknown, nowMs?: AdmissionClock): PayOrder | null;
export declare function settlementShape(raw: unknown, nowMs?: AdmissionClock): Settlement | null;
/**
* A wri carries no `exp`, so its clock-free form drops exactly one bound —
* `ts` no further ahead than SKEW_MS — and keeps every other rule.
*/
export declare function issuanceShape(raw: unknown, nowMs?: AdmissionClock): IssuanceReceipt | null;
export declare function lockShape(raw: unknown, nowMs?: AdmissionClock): LockOrder | null;
export declare function lockReceiptShape(raw: unknown, nowMs?: AdmissionClock): LockReceipt | null;
export declare function releaseShape(raw: unknown, nowMs?: AdmissionClock): Release | null;
// ---- build + verify ----------------------------------------------------------
//
// build* REJECTS on bad input; verify* resolves null on any failure and never
// throws.
export interface PayRequestOpts {
cur: string;
amt: number;
/** The payee's suite X25519 public key. */
tox: string;
ctx?: string;
memo?: string;
/** ABSOLUTE epoch-ms expiry. Default: now + one hour. */
expMs?: number;
}
/** The signer is the payee, so `to` is not an argument. */
export declare function buildPayRequest(identity: SigningIdentity, opts: PayRequestOpts): Promise<PayRequest>;
export declare function verifyPayRequest(raw: unknown, nowMs?: AdmissionClock): Promise<PayRequest | null>;
export interface OrderOpts {
cur: string;
amt: number;
to: string;
/** The payer's next sequence for that bank — LedgerStore.nextSeq derives it. */
seq: number;
ctx?: string;
memo?: string;
/** RELATIVE, unlike PayRequestOpts.expMs. Default: ten minutes. */
ttlMs?: number;
}
/** The signer is the payer. */
export declare function buildOrder(identity: SigningIdentity, opts: OrderOpts): Promise<PayOrder>;
/**
* `verifyOrder(e, null)` is the form a REPLICATED FOLD must call: the signature
* never depends on a clock, so with the clock-free shape check the whole verdict
* is a pure function of the entry. See AdmissionClock.
*/
export declare function verifyOrder(raw: unknown, nowMs?: AdmissionClock): Promise<PayOrder | null>;
export interface SettlementOpts {
/** The BANK's log entry reference — a string. Required for a receipt. */
seq?: string;
/** Present => a decline; absent => a receipt. */
decline?: DeclineReason;
}
/**
* The banker side. Re-verifies the order and embeds the rebuilt one, so a bank
* can never co-sign a shape it did not itself accept, and refuses to settle a
* currency it does not issue.
*/
export declare function buildSettlement(
identity: SigningIdentity,
po: PayOrder,
opts: SettlementOpts,
): Promise<Settlement>;
/**
* Verifies the embedded order's signature too, and that banker(po.cur) is the
* signing bank. Pass `expectedBankPub`: without it a valid settlement by some
* OTHER bank is indistinguishable from the one you were waiting for.
*/
export declare function verifySettlement(
raw: unknown,
expectedBankPub?: string | null,
nowMs?: AdmissionClock,
): Promise<Settlement | null>;
export interface IssuanceOpts {
cur: string;
/** The bank's OWN issuance slot number for this mint. */
seq: number;
/** The recipient's identity public key. */
to: string;
amt: number;
/** The mint entry's log hash — non-empty, ≤ CTX_MAX code points, no controls. */
h: string;
/** Epoch ms; default now. Pass the mint's own time when replaying a log. */
ts?: number;
}
/**
* The banker side: co-sign one MINT of this bank's own currency, so the ack
* that pins its issuance slot has a preimage to cover and the recipient holds
* a receipt no back-dated log entry can displace. Rejects a currency this
* identity does not issue, and the neutral unit always. MINT ONLY — a burn
* has no wri; see IssuanceReceipt.
*/
export declare function buildIssuanceReceipt(
identity: SigningIdentity,
opts: IssuanceOpts,
): Promise<IssuanceReceipt>;
/**
* `expectedBankPub` has verifySettlement's exact semantics: the bank whose
* mint this is supposed to be. Pass it — without it a valid issuance by some
* OTHER bank is indistinguishable from the one you were told about, which is
* the replay a wallet must not fold. The internal banker(cur) === bank
* binding always holds either way. `nowMs` is the shape check's clock; `null`
* is the deterministic form a replicated fold needs.
*/
export declare function verifyIssuanceReceipt(
raw: unknown,
expectedBankPub?: string | null,
nowMs?: AdmissionClock,
): Promise<IssuanceReceipt | null>;
// ---- escrow (BANK/2) ---------------------------------------------------------
export interface LockOpts {
cur: string;
amt: number;
/** The beneficiary's identity public key. */
to: string;
/** The payer's next slot for that bank — THE SAME space an order draws from. */
seq: number;
/** The hashlock digest. `newHashlock()` produces a matching pair. */
hash: string;
ctx?: string;
memo?: string;
/** Relative; default ORDER_TTL_MS. Advisory — no fold reads it. */
ttlMs?: number;
}
/**
* Sign an instruction to LOCK. The signer IS the payer. Rejects the neutral
* unit, a malformed hashlock, and a currency id that does not parse.
*/
export declare function buildLock(
identity: SigningIdentity,
opts: LockOpts,
): Promise<LockOrder>;
/**
* Does NOT check liveness and does NOT check that you hold a preimage — a bank
* must be able to read a lock in order to hold it. `verifyLock(e, null)` is the
* form a replicated fold must use.
*/
export declare function verifyLock(
raw: unknown,
nowMs?: AdmissionClock,
): Promise<LockOrder | null>;
export interface LockReceiptOpts {
/** The bank's log entry reference for THE CLAIM (a STRING). */
seq: string;
}
/**
* The banker side: co-sign one CLAIMED lock. The lock is re-verified here,
* signature included, and the receipt embeds the REBUILT one — a bank can never
* co-sign a shape it did not itself accept.
*/
export declare function buildLockReceipt(
identity: SigningIdentity,
lk: unknown,
opts: LockReceiptOpts,
): Promise<LockReceipt>;
/**
* Verifies the embedded lock's signature too, and that banker(lk.cur) is the
* signing bank. `expectedBankPub` has verifySettlement's exact semantics.
*/
export declare function verifyLockReceipt(
raw: unknown,
expectedBankPub?: string | null,
nowMs?: AdmissionClock,
): Promise<LockReceipt | null>;
export interface ReleaseOpts {
cur: string;
/** The LOCK ENTRY's hash in the bank's log. */
lh: string;
/** Epoch ms; default now. */
ts?: number;
}
/**
* The beneficiary side: hand a lock back to its payer without a preimage. The
* signer IS the beneficiary, so `to` is not an argument — a release naming
* someone else would be giving away money never payable to you.
*/
export declare function buildRelease(
identity: SigningIdentity,
opts: ReleaseOpts,
): Promise<Release>;
/**
* `expectedTo` is the beneficiary of the lock being released, and a bank MUST
* pass it: this kit never sees the lock, so a release that verifies proves only
* that SOMEBODY signed away SOME lock.
*/
export declare function verifyRelease(
raw: unknown,
expectedTo?: string | null,
nowMs?: AdmissionClock,
): Promise<Release | null>;
// ---- the ledger --------------------------------------------------------------
/**
* One currency's row.
*
* The fold is done in BigInt and the `*Exact` fields carry it. The three Numbers
* are for the UI and are `Number(<the BigInt>)` — which is the same value right
* up to 2^53 and a ROUNDING of it above, and `exact` is how you find out which
* you got. Nothing here ever returns a rounded total dressed as an exact one.
*
* Why it is not simply doubles: one amount may be 2^50, doubles are exact on
* integers only to 2^53, so eight maximal receipts is the entire headroom — and
* a double fold past it is not merely approximate, it is ORDER-DEPENDENT. Eight
* receipts of 2^50 and two of 1 sum to 2^53+2 folded small-first and 2^53 folded
* big-first, so two devices holding identical evidence would disagree about the
* balance. BigInt is associative, so the total is a function of the set.
*/
export interface CurrencyBalance {
/**
* The fold over settled evidence: settlement credits where po.to is me,
* settlement debits where po.from is me, and issuance credits where wri.to
* is me. Issuance never debits — money is created at the bank.
*/
settled: number;
/** What LIVE outgoing pending orders have committed. Not spent, not available. */
held: number;
/** settled - held. */
available: number;
/** The exact fold. Always right; `settled` may be a rounding of it. */
settledExact: bigint;
heldExact: bigint;
availableExact: bigint;
/** Are all three Numbers equal to their BigInt, rather than a rounding? */
exact: boolean;
}
export interface StorageFailure {
/**
* `QuotaExceededError` and friends from a failed write, plus two of this
* kit's own: `UnreadableStorageError` (bytes are stored that this build
* cannot read — see `unreadable`) and `StorageUnavailableError`.
*/
name: string;
message: string;
ts: number;
}
/** What `prune` is told about each record it is offered. */
export type LedgerKind = "wrc" | "wrj" | "pending" | "wri";
/**
* What a ledger mutator did. NOT a boolean, and the reason is that "recorded"
* and "recorded but nothing reached disk" used to be the same `true`, so a
* caller could not tell a REJECTION from a BLOCKED write at all.
*
* 0 REJECTED. Malformed, a duplicate, an order the bank has already
* answered, or a (payer, bank, seq) slot already taken. Nothing changed.
* 1 STORED. Recorded, and on disk.
* 2 IN MEMORY ONLY. Recorded, and the write did not happen: quota, a
* disabled store, unacknowledged `unreadable` bytes, or a closed store.
* `storageError` names which. The session is correct; durability is not.
*
* Integers rather than a string union or a result object so that FALSINESS
* still means exactly "was it recorded": 0 is the only falsy member, so
* `if (await store.addOrder(o))` keeps working unchanged. `=== true` stops
* matching, which fails loudly on a success path — an object would have made
* every rejection truthy, which fails silently on a money one.
*/
export type LedgerWrite = 0 | 1 | 2;
/**
* A wallet's own ledger, under `localStorage["wal:" + idPub]` — namespaced by
* IDENTITY, not by app: money follows the person across the suite.
*
* NO RETENTION CAPS. Nothing is evicted, aged out or dropped to make room; a
* receipt is the only evidence money moved. A failed write sets `storageError`
* and calls `onStorageError`, and keeps every record in memory.
*
* MULTI-TAB BY DESIGN. Because the key is the identity, two apps open at once
* are two stores over one blob, and that is the normal case. Every write is a
* read-modify-write that UNIONS what is on disk into memory before writing —
* sound because every record is keyed by a digest of its own content, so two
* copies of a record are one record.
*
* A read-modify-write is NOT atomic by itself: a sibling that completes its own
* write between our read and our write is simply overwritten. What serializes it
* is a COMPARE-AND-SWAP, not a lock — the stored bytes are re-read immediately
* before the write and compared with what was merged from (different means
* discard the attempt and start again from what the other writer left), then
* read back afterwards so a write that landed on top of ours is re-merged rather
* than believed. Bounded retries. `navigator.locks` is deliberately not used:
* it is async-only, `prune` is synchronous by contract, and a lock one writer
* skips guarantees nothing, so the CAS would have to carry the guarantee anyway.
*
* What that guarantees exactly: no writer's records are lost to a merge taken
* from a stale read, and a clobbered write is detected and repaired. What it
* does not: true mutual exclusion. The residue is the gap between the verifying
* read and the write — two adjacent statements with no yield point, so no agent
* in this document can run there, and only a genuinely concurrent one can.
*
* A `storage` listener converges this store when ANOTHER TAB writes. It can
* never fire for a store in the SAME document as the writer — the DOM does not
* fire it on the window that wrote — so stores over one key also find each other
* in process and notify their siblings directly. `close()` releases both, which
* is why calling it matters. Nothing else a consumer has to call, except:
*
* - `reserveSeq` instead of `nextSeq` before signing an order;
* - `close()` when the store outlives its page context (a SPA view teardown);
* - `acknowledgeUnreadable()` if `unreadable` is ever non-null.
*
* `prune` is a LOCAL deletion: a store elsewhere still holding the record
* restores it on its next write unless it converges first — which the listener
* and the registry are what arrange.
*
* The mutators return promises because their dedup keys are SHA-256 digests, and
* every one of them is total — a hostile artifact resolves `0`, it never
* rejects. Every reader is synchronous. `reserveSeq` is the one deliberate
* exception and DOES reject; see there.
*
* The store re-checks STRUCTURE on everything, including its own storage, but
* does not verify signatures — call verifyOrder / verifySettlement first.
* `importJson` is the exception: it verifies every signature it is given, and
* refuses a blob that is not this identity's own.
*
* The storage read path uses the CLOCK-FREE shape checks (`orderShape(raw,
* null)`): those artifacts were admitted by a verify* when they entered, so
* re-reading them is a question about structure, and answering it against the
* wall clock would make a fold depend on when it was loaded — a device whose
* clock jumped backwards would silently drop its own history. The mutators use
* the wall clock, since admitting new evidence is an act in the present.
*/
export declare class LedgerStore {
constructor(idPub: string);
readonly idPub: string;
/** Fires after every mutation, and after a sibling tab's write is absorbed. */
onChange: () => void;
/** Fires when a write fails, and when storage cannot be read at all. */
onStorageError: (e: StorageFailure) => void;
/** The last storage failure, or null once storage works again. */
readonly storageError: StorageFailure | null;
/**
* The stored bytes this build could not read (bad JSON, or a `v` it does not
* know), held so you can save them. While this is non-null NOTHING IS
* WRITTEN: unreadable bytes are not an empty ledger, and overwriting them
* would destroy whatever wrote them — including sections a newer version
* owns. The session still works in memory. Call `acknowledgeUnreadable()` to
* accept the loss and resume writing.
*/
readonly unreadable: string | null;
/** Re-read from storage (and fire onChange). */
load(): void;
/**
* Balances per currency. `nowMs` is the clock used to decide which pending
* orders are still live enough to be held; it defaults to `Date.now()`, and
* passing it makes the result reproducible.
*
* THE RESULT CANNOT BE `JSON.stringify`d. The `*Exact` fields are `bigint`
* and `JSON.stringify` throws `TypeError` on one, by specification and with
* no way round it: those fields exist precisely to hold values no double can,
* and a serializer whose number type is a double has nothing to write.
* Convert at the boundary — `String(b.settledExact)` for anything durable,
* `b.settled` for a UI that has already checked `b.exact`. `exportJson()` and
* the persisted blob are unaffected; both carry receipts, not a fold.
*/
balances(nowMs?: number): Record<string, CurrencyBalance>;
/**
* Record an order this wallet signed. 0 if already known, already answered,
* or if its (payer, bank, seq) slot is already taken — two signed orders on
* one slot is the double-spend `seq` exists to prevent, and at most one of
* them can ever settle. 1 recorded and on disk, 2 recorded in memory only —
* see LedgerWrite.
*/
addOrder(po: unknown): Promise<LedgerWrite>;
/** Idempotent by receiptKey; removes the matching pending order. */
applyReceipt(rc: unknown): Promise<LedgerWrite>;
applyDecline(rj: unknown): Promise<LedgerWrite>;
/**
* Fold one issuance receipt, so minted money is visible OUTSIDE banca — the
* hub and the games see a balance the bank's own log gave finality to.
*
* 0 unless `to` is THIS identity: a wri has exactly one beneficiary (unlike
* a settlement, which can touch a wallet from either side), so a receipt
* naming someone else can never fold here and is rejected rather than
* stored as dead weight. Issuance CREDITS `to` and never debits anyone in
* this ledger — money is created at the bank; a burn has no wri at all.
* Idempotent by issuanceKey, which is blind to `ts`, so a re-issued receipt
* folds once. Verify with verifyIssuanceReceipt first — like the other
* mutators this re-checks structure, not signatures.
*/
applyIssuance(wri: unknown): Promise<LedgerWrite>;
pendingOrders(): PayOrder[];
/**
* The outgoing pending orders that have stopped being held. `exp` is inside
* what the payer signed, so holding against them for ever would depress
* `available` with no exit but a prune — but the release happens at
* `exp + SKEW_MS`, not at `exp`: `ts` and settlement admission both tolerate a
* bank ±SKEW_MS, so a bank a millisecond behind this wallet's clock still
* settles an order `exp` alone would have released, and `available` would have
* overstated for that window. Same predicate `held` uses, so an order is never
* in both lists and never in neither. Show these; offer to `prune` them.
*/
expiredOrders(nowMs?: number): PayOrder[];
receipts(): Receipt[];
declines(): Decline[];
issuances(): IssuanceReceipt[];
/**
* The payer's next sequence for that currency's BANK. ADVISORY — it reads a
* mark, it does not take one, so two calls in one tick return the same
* number. Display with it; allocate with `reserveSeq`.
*/
nextSeq(cur: string): number;
/**
* ESCROW IS NOT TRACKED BY THIS STORE, AND A LOCK SHARES THE SEQUENCE SPACE.
* A LockOrder claims the same (payer, bank, seq) slot a PayOrder does, but
* nothing here holds locks, so a slot handed out by `nextSeq`/`reserveSeq`
* has not been checked against one.
*
* No consumer in the suite is exposed: everything that locks replicates the
* bank's log and takes its slot from that fold, which sees both. A wallet
* that does NOT replicate and signs both must own one counter across the two
* and not use this one, until this store learns about locks.
*/
readonly __escrowNote?: never;
/**
* Take the next sequence slot for that currency's bank, PERSIST the
* allocation, and only then resolve it. Serialized against a second tab, a
* reload and a prune: a slot handed out here is never handed out again. Use
* this, not `nextSeq`, for the `seq` of an order you are about to sign.
*
* IT REJECTS WHEN THE ALLOCATION DID NOT REACH DISK — a quota failure, a
* disabled store, unacknowledged `unreadable` bytes, a closed store. This is
* the whole point of the call and the one place in the ledger where a failed
* write is an error rather than a status: a slot nothing recorded is a slot
* the next store over this key will hand out again, and two signed orders on
* one (payer, bank, seq) is exactly the double-spend `seq` exists to prevent.
* There is no honest number to resolve, so it does not resolve one. Handle the
* rejection by NOT SIGNING; the in-memory mark still moves, so a later
* successful reserve returns a higher slot rather than reusing this one.
*
* Also rejects on a currency id it cannot parse or that nothing can settle,
* and on a bank whose sequence space is spent (the next slot would exceed
* AMT_MAX, which no `wpo.seq` may carry).
*/
reserveSeq(cur: string): Promise<number>;
/**
* The user's own data as JSON: the four record arrays, the reserved `stakes`
* section — and `seqs`, the per-bank sequence high-water marks, which are the
* one thing here not derivable from the records. They have to travel: after a
* `prune` the mark is deliberately higher than the records justify, and an
* export without it hands the other device a ledger that offers slot 1 for a
* slot this one already spent.
*/
exportJson(): string;
/**
* Verifies every signature. Only ever adds; returns how many RECORDS were new.
* Refuses a blob whose `idPub` is not this store's, and drops any record that
* names neither this identity as payer nor as payee. The `seqs` it carries are
* merged by MAX and re-validated on the way in, so an import can raise a
* sequence mark and can never lower one; a raised mark is not counted in the
* return, which counts records.
*/
importJson(raw: string | object): Promise<number>;
/**
* The only deletion path in the kit. Nothing calls it on the user's behalf.
* All-or-nothing: if `keep` throws, nothing is removed and the error reaches
* you.
*
* Returns how many RECORDS were removed, which always happens; whether the
* removal reached disk is `storageError`, checked after the call. A count has
* no room to say both, and the count is what the caller asked for.
*/
prune(keep: (artifact: PayOrder | Settlement | IssuanceReceipt, kind: LedgerKind) => boolean): number;
/** Accept the loss of `unreadable` and resume writing. False if there is none. */
acknowledgeUnreadable(): boolean;
/**
* Retire this store: detach the `storage` listener, leave the same-document
* registry, and STOP WRITING. The last clause matters — a store that no longer
* hears about disk and still writes over it is a permanently stale writer that
* resurrects whatever it held when it was closed. Reading a closed store still
* works and its in-memory ledger stays correct; mutators return 2 and
* `storageError.name` is `StoreClosedError`. There is no reopen; construct a
* new store.
*
* Call it when a store outlives its page view. Until you do, the listener and
* the registry both retain it.
*/
close(): void;
}
// ---- pay links ---------------------------------------------------------------
//
// Also available on their own as "ardegazu-wallet-kit/paylink".
/**
* `https://banca.ardegazu.ro/#pay=<b64url(JSON.stringify(wpr))>`
*
* Build-side, so it THROWS on a request that is not structurally valid rather
* than handing back a link to nonsense. The rebuilt request is what gets
* encoded, so a link always carries the documented key order.
*/
export declare function payRequestUrl(wpr: PayRequest): string;
/**
* Decode + fully verify. Accepts a whole URL, a fragment with or without its
* `#`, and a fragment carrying other parameters. Null on any failure.
*
* CANONICAL base64url only. Standard base64 (`+` `/` `=`) and percent-encoded
* fragments are rejected even though a lenient decoder would recover the same
* request: the kit is strict about encoding malleability everywhere else, and
* one request with three link spellings breaks any cache or "have I seen this?"
* check a wallet builds on the string. Nothing honest emits them.
*/
export declare function parsePayFragment(fragment: unknown): Promise<PayRequest | null>;
|