banca / client / test / escrow.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
// BANK/2's escrow fold: locks, claims, returns.
//
// Every artifact here is signed by @noble/curves in test/vectors/independent.mjs
// against preimage templates written out by hand, so nothing under test produced
// its own inputs. Every expectation is a number a reader can add up.
//
// Three claims are under test, and the middle one is the design:
//
//   · CONSERVATION — money in escrow has left a balance without reaching
//     anyone, so `supply` must equal Σ bal + Σ locked at every point. A fold
//     that reported only balances would look like the money had evaporated.
//   · A CLAIM BEATS A RETURN, UNCONDITIONALLY — whatever the order, whatever
//     the clocks. This is what removes the timeout-ordering problem that a
//     cross-bank hashlock swap otherwise has, and it is why this fold needs no
//     clock at all.
//   · THE v1 GATE — the same entry set folded as a BANK/1 bank must be
//     completely unaffected by the escrow entries. If it is not, a v2 client
//     writing into a v1 bank partitions it, and the partition arrives from the
//     side that knew better.
//
// Order independence is asserted for every scenario, as in fold.test.mjs: the
// same entries in several insertion orders must give a byte-identical snapshot.
//
// Runs against test-dist/testlib.js — the built output — never src/.

import test from "node:test";
import assert from "node:assert/strict";
import * as L from "../test-dist/testlib.js";
import {
  ACTORS,
  CUR,
  CUR2,
  HASHLOCK_HASH,
  HASHLOCK_PRE,
  SEED_PAYEE,
  SEED_PAYER,
  TS_FIXED,
  entry,
  makeLock,
  makeRelease,
  opAcct,
  opCharter,
  opClaim,
  opLock,
  opMint,
  opOpen,
  opUnlock,
  permutations,
} from "./vectors/independent.mjs";

const { bankPub: BANK, payerPub: PAYER, payeePub: PAYEE } = ACTORS;
const T = TS_FIXED;

/** A v2 fold over `entries`, with every signature and preimage verdict landed. */
async function fold(entries, { version = 2, banker = BANK } = {}) {
  const f = new L.BankFold(banker, null, version);
  for (const e of structuredClone(entries)) f.ingest(e);
  await f.awaitVerified();
  // a preimage verdict is kicked by the FOLD, not by ingest — it needs the lock
  // in hand — so the first fold starts the digests and the second sees them
  f.snapshot();
  await f.awaitVerified();
  return f;
}

const snap = async (entries, opts) => (await fold(entries, opts)).snapshot();

const balOf = (s, id) => s.accounts.find((a) => a.id === id)?.balExact ?? "0";
const lockedOf = (s, id) => s.accounts.find((a) => a.id === id)?.lockedExact ?? "0";
const recOf = (s, h) => s.payments.find((p) => p.h === h);
const escOf = (s, h) => s.escrows.find((e) => e.h === h);

/** supply === Σ bal + Σ locked. The invariant escrow could break and must not. */
function assertConserved(s, why) {
  const total = s.accounts.reduce(
    (acc, a) => acc + BigInt(a.balExact) + BigInt(a.lockedExact),
    0n,
  );
  assert.equal(total.toString(), s.supplyExact, `conservation: ${why}`);
}

/** The bank, its two customers, and 1000 minted to the payer. h01..h05. */
const BASE = [
  entry("h01", 1, BANK, opCharter(T, "Banca", "LEI", "L", 2, "open")),
  entry("h02", 2, PAYER, opOpen(T, "payer")),
  entry("h03", 3, PAYEE, opOpen(T, "payee")),
  entry("h04", 4, BANK, opAcct(T, PAYER, "approve")),
  entry("h05", 5, BANK, opMint(T, 1, PAYER, 1000)),
];

const LK = makeLock({ seed: SEED_PAYER, idN: 1, cur: CUR, amt: 300, seq: 1, to: PAYEE, ctx: "match:demo" });

// ---- the happy path ---------------------------------------------------------

test("a lock holds money: it leaves the balance and reaches nobody", async () => {
  const s = await snap([...BASE, entry("h06", 6, PAYER, opLock(T, LK))]);
  assert.equal(balOf(s, PAYER), "700", "1000 - 300");
  assert.equal(lockedOf(s, PAYER), "300");
  assert.equal(balOf(s, PAYEE), "0", "the beneficiary has NOT been paid");
  assert.equal(recOf(s, "h06").status, "held");
  assert.equal(recOf(s, "h06").kind, "lock");
  assert.equal(escOf(s, "h06").status, "held");
  assert.equal(s.supplyExact, "1000", "a lock mints and burns nothing");
  assertConserved(s, "held");
});

test("a claim pays the beneficiary, and the held bucket empties", async () => {
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)),
  ]);
  assert.equal(balOf(s, PAYER), "700");
  assert.equal(lockedOf(s, PAYER), "0");
  assert.equal(balOf(s, PAYEE), "300");
  assert.equal(recOf(s, "h06").status, "settled", "provisional until acked");
  assert.equal(escOf(s, "h06").status, "claimed");
  assertConserved(s, "claimed");
});

test("a wrong preimage claims nothing, and is not an error either", async () => {
  const wrong = HASHLOCK_HASH; // well-shaped, and not the preimage
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYEE, opClaim(T, "h06", wrong)),
  ]);
  assert.equal(balOf(s, PAYEE), "0");
  assert.equal(lockedOf(s, PAYER), "300", "the money is still held");
  assert.equal(escOf(s, "h06").status, "held");
  assertConserved(s, "bad preimage");
});

test("the banker returns a lock, and the money goes back to its payer", async () => {
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, BANK, opUnlock(T, "h06")),
  ]);
  assert.equal(balOf(s, PAYER), "1000", "whole again");
  assert.equal(lockedOf(s, PAYER), "0");
  assert.equal(balOf(s, PAYEE), "0");
  assert.equal(recOf(s, "h06").status, "returned");
  assert.equal(escOf(s, "h06").status, "returned");
  assertConserved(s, "returned");
});

test("a stranger cannot return a lock", async () => {
  // Only the banker's authorship or the beneficiary's signature authorises a
  // return. A member with the link may append anything; it must fold to nothing.
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYER, opUnlock(T, "h06")),
  ]);
  assert.equal(lockedOf(s, PAYER), "300", "still held — the payer cannot take it back");
  assert.equal(escOf(s, "h06").status, "held");
});

test("the beneficiary can hand it back with no bank involved", async () => {
  // This is what keeps a lost banker seed from stranding a lock for ever.
  const rel = makeRelease({ seed: SEED_PAYEE, cur: CUR, lh: "h06" });
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYER, opUnlock(T, "h06", rel)),
  ]);
  assert.equal(balOf(s, PAYER), "1000");
  assert.equal(escOf(s, "h06").status, "returned");
  assertConserved(s, "released");
});

test("a release for a DIFFERENT lock does not open this one", async () => {
  // wallet-kit cannot check this — it never sees the lock — so the fold must.
  const rel = makeRelease({ seed: SEED_PAYEE, cur: CUR, lh: "h99" });
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYER, opUnlock(T, "h06", rel)),
  ]);
  assert.equal(lockedOf(s, PAYER), "300", "still held");
});

test("a release signed by someone who is not the beneficiary does nothing", async () => {
  const rel = makeRelease({ seed: SEED_PAYER, cur: CUR, lh: "h06" });
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYER, opUnlock(T, "h06", rel)),
  ]);
  assert.equal(lockedOf(s, PAYER), "300", "still held");
});

// ---- THE RULE ---------------------------------------------------------------

test("A CLAIM BEATS A RETURN — even when the return folded first", async () => {
  // The design's keystone. A textbook hashlock swap needs the second leg's
  // timeout strictly before the first's, so nobody can still be claiming after
  // their own lock expired. Across two INDEPENDENT bankers that ordering cannot
  // be enforced — and with this rule it is not needed: once the preimage is
  // public the counterparty's claim wins whenever it lands, against a return
  // that folded weeks earlier.
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, BANK, opUnlock(T, "h06")), // the banker refunds FIRST
    entry("h08", 8, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)), // and is overruled
  ]);
  assert.equal(balOf(s, PAYEE), "300", "the counterparty is still paid");
  assert.equal(balOf(s, PAYER), "700");
  assert.equal(escOf(s, "h06").status, "claimed");
  assertConserved(s, "claim beats return");
});

test("…and when the return comes long after, with a higher clock", async () => {
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)),
    entry("h08", 99, BANK, opUnlock(T, "h06")),
  ]);
  assert.equal(balOf(s, PAYEE), "300");
  assert.equal(escOf(s, "h06").status, "claimed");
});

test("a claim that sorts BEFORE its own lock is still applied", async () => {
  // Causally impossible in an honest log and trivially produced by a crafted
  // clock. Applying a resolution only where the fold meets it would lose this
  // one for ever; applying it only at the lock would move money before the lock
  // existed. It is applied at whichever of the two comes LATER.
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYEE, opClaim(T, "h07", HASHLOCK_PRE)), // clock 6
    entry("h07", 7, PAYER, opLock(T, LK)), // …its lock, at clock 7
  ]);
  assert.equal(balOf(s, PAYEE), "300", "not lost");
  assert.equal(lockedOf(s, PAYER), "0");
  assertConserved(s, "claim before lock");
});

test("two claims on one lock pay once, and the same one every time", async () => {
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)),
    entry("h08", 8, BANK, opClaim(T, "h06", HASHLOCK_PRE)),
  ]);
  assert.equal(balOf(s, PAYEE), "300", "paid once, not twice");
  assertConserved(s, "double claim");
});

// ---- the slot, shared with payments -----------------------------------------

test("a lock and a payment cannot share a sequence slot", async () => {
  // Both are this account moving its own money, and the bank consumes one slot
  // per act. Whichever sorts first wins; the loser dies `seq`, permanently.
  const { makeOrder } = await import("./vectors/independent.mjs");
  const po = makeOrder({ seed: SEED_PAYER, idN: 2, cur: CUR, amt: 50, seq: 1, to: PAYEE });
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYER, { t: "pay", ts: T, po }),
  ]);
  assert.equal(recOf(s, "h06").status, "held", "the lock took slot 1");
  assert.equal(recOf(s, "h07").status, "failed");
  assert.equal(recOf(s, "h07").why, "seq");
  assert.equal(s.accounts.find((a) => a.id === PAYER).seq, 1);
  assertConserved(s, "slot contention");
});

test("a lock the payer cannot afford fails, and spends its slot anyway", async () => {
  const big = makeLock({ seed: SEED_PAYER, idN: 3, cur: CUR, amt: 5000, seq: 1, to: PAYEE });
  const s = await snap([...BASE, entry("h06", 6, PAYER, opLock(T, big))]);
  assert.equal(recOf(s, "h06").status, "failed");
  assert.equal(recOf(s, "h06").why, "insufficient");
  assert.equal(lockedOf(s, PAYER), "0");
  assert.equal(balOf(s, PAYER), "1000");
  assert.equal(s.accounts.find((a) => a.id === PAYER).seq, 1, "spent anyway — rule 1");
});

test("another bank's paper cannot be locked here, and consumes no slot", async () => {
  const foreign = makeLock({ seed: SEED_PAYER, idN: 4, cur: CUR2, amt: 10, seq: 1, to: PAYEE });
  const s = await snap([...BASE, entry("h06", 6, PAYER, opLock(T, foreign))]);
  assert.equal(recOf(s, "h06").status, "failed");
  assert.equal(recOf(s, "h06").why, "cur");
  assert.equal(s.accounts.find((a) => a.id === PAYER).seq, 0, "no slot consumed");
});

// ---- THE COMPATIBILITY GATE -------------------------------------------------

test("a BANK/1 fold is COMPLETELY unaffected by escrow entries", async () => {
  // The gate of the whole design. A v1 client drops an escrow op silently, so if
  // a v2 client ever wrote one into a v1 bank the two would fold different
  // balances — a partition, arriving from the side that knew better. Refusing
  // escrow under v1 is what makes that unrepresentable.
  const withEscrow = [
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)),
    entry("h08", 8, BANK, opUnlock(T, "h06")),
  ];
  const v1 = await snap(withEscrow, { version: 1 });
  const plain = await snap(BASE, { version: 1 });
  assert.equal(JSON.stringify(v1), JSON.stringify(plain),
    "the escrow entries influenced NOTHING under v1");
  assert.equal(balOf(v1, PAYER), "1000");
  assert.equal(v1.payments.length, 1, "only the mint");
  assert.equal(v1.escrows.length, 0);
});

test("an absent version fails CLOSED, at v1", async () => {
  const withEscrow = [...BASE, entry("h06", 6, PAYER, opLock(T, LK))];
  const noVersion = new L.BankFold(BANK, null);
  for (const e of structuredClone(withEscrow)) noVersion.ingest(e);
  await noVersion.awaitVerified();
  assert.equal(balOf(noVersion.snapshot(), PAYER), "1000", "the lock was refused");
});

// ---- order independence -----------------------------------------------------

const SCENARIOS = {
  "lock only": [...BASE, entry("h06", 6, PAYER, opLock(T, LK))],
  "lock + claim": [
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)),
  ],
  "return then claim": [
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, BANK, opUnlock(T, "h06")),
    entry("h08", 8, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)),
  ],
  "claim before its lock": [
    ...BASE,
    entry("h06", 6, PAYEE, opClaim(T, "h07", HASHLOCK_PRE)),
    entry("h07", 7, PAYER, opLock(T, LK)),
  ],
};

for (const [name, entries] of Object.entries(SCENARIOS)) {
  test(`order independence — ${name}`, async () => {
    // The property that keeps two replicas from ever partitioning. Asserted as
    // a property, never pinned as a value.
    const want = JSON.stringify(await snap(entries));
    const orders = [
      [...entries].reverse(),
      [...entries.slice(3), ...entries.slice(0, 3)],
      permutations([...entries.slice(-3)]).at(-1).concat(entries.slice(0, -3)),
    ];
    for (const [i, o] of orders.entries()) {
      assert.equal(JSON.stringify(await snap(o)), want, `insertion order ${i}`);
    }
  });
}

// ---- the vocabulary, and the iff that keeps it honest -----------------------

test("a lock's state reads as a word the fold never uses", async () => {
  // §12: the fold's `settled` must not reach a screen, and BANK/2 adds two more
  // words that must not either — `held` and `returned` are fold states, and
  // what a person reads is `locked` and `returned`.
  const held = await snap([...BASE, entry("h06", 6, PAYER, opLock(T, LK))]);
  assert.equal(L.paymentState(recOf(held, "h06")), "locked");
  assert.equal(L.stateKey(recOf(held, "h06")), "pay.state.locked");
  assert.equal(L.stateNoteKey("locked", "lock"), "pay.state.locked.note");

  const done = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)),
  ]);
  // a CLAIMED lock is an ordinary provisional payment, and says so: a member's
  // money reached another member and the banker has not pinned it yet
  assert.equal(L.paymentState(recOf(done, "h06")), "provisional");

  const back = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, BANK, opUnlock(T, "h06")),
  ]);
  assert.equal(L.paymentState(recOf(back, "h06")), "returned");
  assert.equal(L.stateNoteKey("returned", "lock"), "pay.state.returned.note");
});

test("ackable and payment-state agree, as an iff, over every escrow state", async () => {
  // The pair banca got wrong once, in the other direction: a wallet reading
  // "the banker has not acknowledged it yet" beside a banker's tab reading
  // "nothing is waiting", on the same record, for ever. Asserted as an iff
  // rather than as two rules that happen to line up.
  const scenarios = [
    ["held", [...BASE, entry("h06", 6, PAYER, opLock(T, LK))]],
    ["claimed", [...BASE, entry("h06", 6, PAYER, opLock(T, LK)),
                 entry("h07", 7, PAYEE, opClaim(T, "h06", HASHLOCK_PRE))]],
    ["returned", [...BASE, entry("h06", 6, PAYER, opLock(T, LK)),
                  entry("h07", 7, BANK, opUnlock(T, "h06"))]],
    ["failed", [...BASE, entry("h06", 6, PAYER,
      opLock(T, makeLock({ seed: SEED_PAYER, idN: 9, cur: CUR, amt: 5000, seq: 1, to: PAYEE })))]],
  ];
  for (const [name, entries] of scenarios) {
    const s = await snap(entries);
    const offered = new Set(L.ackable(s));
    for (const rec of s.payments) {
      const word = L.paymentState(rec);
      const promises = word === "provisional" || (word === "unpinned" && rec.kind === "mint");
      assert.equal(offered.has(rec.h), promises,
        `${name}: ${rec.kind} reading "${word}" ${promises ? "must" : "must NOT"} be offered`);
    }
  }
});

test("a held lock is never offered for acknowledgement", async () => {
  // Nothing has happened yet that a bank could sign a receipt for, and offering
  // one would promise a finality the protocol cannot deliver.
  const s = await snap([...BASE, entry("h06", 6, PAYER, opLock(T, LK))]);
  // h05 is BASE's mint and is legitimately waiting — the lock is what must not be
  assert.deepEqual(L.ackable(s), ["h05"]);
  assert.equal(L.isReceiptable(recOf(s, "h06")), false);
});

test("a claimed lock acks through its own arm, not the payment one", async () => {
  // The pay arm needs the original wpo; a lock carries a wlk instead, and its
  // ack signs a wallet-kit `wlr`.
  const s = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)),
  ]);
  assert.deepEqual(L.ackable(s), ["h05", "h06"],
    "oldest first: BASE's mint, then the LOCK entry — the lock, not the claim");
  assert.equal(L.ackDispatch(recOf(s, "h06")), "lock");
});

test('"all of it is final" needs all THREE counts at zero', async () => {
  // The rule grew from one number to two to three. Each time, a screen that had
  // assembled its own condition would have kept saying the old sentence over
  // the new money — so it has one owner.
  const held = await snap([...BASE, entry("h06", 6, PAYER, opLock(T, LK))]);
  assert.equal(L.lockedCount(held, PAYER), 1);
  assert.equal(L.lockedNet(held, PAYER), "-300", "money out, from the payer's side");
  assert.equal(L.allFinal(held, PAYER), false, "300 of it is in escrow");

  // BOTH SIDES count it, like every other total here: the payer cannot spend
  // it and the beneficiary does not have it, so neither may be told "all of it
  // is final". The sign says which side of it you are on.
  assert.equal(L.lockedCount(held, PAYEE), 1);
  assert.equal(L.lockedNet(held, PAYEE), "300", "it could become theirs");
  assert.equal(L.allFinal(held, PAYEE), false);

  const back = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, BANK, opUnlock(T, "h06")),
  ]);
  assert.equal(L.lockedCount(back, PAYER), 0, "a returned lock is no longer held");
  // …and `allFinal` is STILL false, for a different reason: BASE's mint is
  // unacked, so the payer has an unpinned issuance. That the three categories
  // are independent is the point of having three of them — clearing one must
  // not clear the sentence.
  assert.equal(L.allFinal(back, PAYER), false);
  assert.equal(L.lockedCount(back, PAYER) + L.provisionalCount(back, PAYER), 0);
  assert.ok(L.unpinnedCount(back, PAYER) > 0, "the mint is what is left");
});

// ---- the receipt a third party can check ------------------------------------

test("a banker's ack on a claimed lock rebuilds a receipt that VERIFIES", async () => {
  // The end of the story, and the only part a third party ever sees. Every
  // signature here came from @noble/curves, so a receipt this fold rebuilds and
  // wallet-kit's own verifier accepts is two independent implementations
  // agreeing on the bytes.
  const { SEED_BANK, signLockReceipt } = await import("./vectors/independent.mjs");
  const ackTs = T + 5000;
  const bsig = signLockReceipt({ bankSeed: SEED_BANK, lk: LK, logRef: "h06", ts: ackTs });

  const f = await fold([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)),
    // the ack names the LOCK entry — the same number the receipt's log ref is
    entry("h08", 8, BANK, { t: "ack", ts: ackTs, h: "h06", bsig }),
  ]);
  const s = f.snapshot();
  assert.equal(recOf(s, "h06").status, "final", "acked, and irreversible");
  assert.equal(recOf(s, "h06").acked, true);
  assert.equal(L.isReceiptable(recOf(s, "h06")), true);

  const wlr = f.receiptFor("h06");
  assert.ok(wlr, "the fold rebuilds it from the lock entry and the ack");
  assert.equal(wlr.t, "wlr");
  assert.equal(wlr.seq, "h06", "the LOCK entry's hash, as ack.h names");
  assert.equal(wlr.ts, ackTs, "the ack's own ts — the one the bank signed over");
  assert.deepEqual(Object.keys(wlr), ["v", "t", "lk", "seq", "ts", "bank", "bsig"]);

  assert.ok(await L.verifyLockReceipt(wlr, BANK), "it verifies against its bank");
  assert.equal(await L.verifyLockReceipt(wlr, PAYEE), null, "and not against another");

  // a held lock has no receipt to show, and pretending otherwise is the exact
  // confusion this protocol exists to avoid
  const held = await fold([...BASE, entry("h06", 6, PAYER, opLock(T, LK))]);
  assert.equal(held.receiptFor("h06"), null);
});

test("the escrow record carries what the OTHER party of a trade needs", async () => {
  // `ctx` binds a lock to its trade (the field wallet-kit reserved for exactly
  // this), and the revealed preimage rides the claimed record because the
  // counterparty of an atomic swap claims the other leg with it, out of the
  // log they were already replicating.
  const held = await snap([...BASE, entry("h06", 6, PAYER, opLock(T, LK))]);
  assert.equal(escOf(held, "h06").ctx, "match:demo");
  assert.equal(escOf(held, "h06").exp, LK.exp);
  assert.equal(escOf(held, "h06").pre, null, "no claim, no preimage");

  const done = await snap([
    ...BASE,
    entry("h06", 6, PAYER, opLock(T, LK)),
    entry("h07", 7, PAYEE, opClaim(T, "h06", HASHLOCK_PRE)),
  ]);
  assert.equal(escOf(done, "h06").pre, HASHLOCK_PRE, "public now — publishing it is what a claim does");
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/banca.git