bursa / client / test / trade.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
// The trade engine: one match, two hand-built bank snapshots, and the walk
// through every phase — for both parties, on both sides of the book.
//
// The snapshots are built by vectors/independent.mjs's `bankSnap`, which
// states the engine's WHOLE read surface: ok, cur, accounts, escrows. A field
// the engine grows a dependency on must be added there, visibly.

import test from "node:test";
import assert from "node:assert/strict";
import * as L from "../test-dist/testlib.js";
import {
  ACTORS, CUR_BASE, CUR_QUOTE, PAIR, bankSnap, escrowRow,
} from "./vectors/independent.mjs";

const { makerPub: MAKER, takerPub: TAKER } = ACTORS;

/** One ask-side match: the maker sells 300 base at 3/2 → quote leg 450. */
const M_ASK = {
  m: "take1", qh: "q1", pair: PAIR, side: "ask", px: { num: 3, den: 2 },
  maker: MAKER, taker: TAKER, fill: 300, quoteAmt: 450, legs: [], clock: 2,
};
/** The same trade from a bid: the maker BUYS 300 base, paying the quote leg. */
const M_BID = { ...M_ASK, m: "take2", side: "bid" };

const OPEN_BOTH = [[MAKER, "open"], [TAKER, "open"]];
const emptyBase = () => bankSnap(CUR_BASE, OPEN_BOTH, []);
const emptyQuote = () => bankSnap(CUR_QUOTE, OPEN_BOTH, []);

// ---- the legs -------------------------------------------------------------------

test("an ask's maker pays the base leg; a bid's maker pays the quote leg", () => {
  const a = L.tradeLegs(M_ASK);
  assert.equal(a.makerLeg, "base");
  assert.deepEqual(a.base, { cur: CUR_BASE, from: MAKER, to: TAKER, amt: 300 });
  assert.deepEqual(a.quote, { cur: CUR_QUOTE, from: TAKER, to: MAKER, amt: 450 });

  const b = L.tradeLegs(M_BID);
  assert.equal(b.makerLeg, "quote");
  assert.deepEqual(b.base, { cur: CUR_BASE, from: TAKER, to: MAKER, amt: 300 });
  assert.deepEqual(b.quote, { cur: CUR_QUOTE, from: MAKER, to: TAKER, amt: 450 });
});

// ---- the walk, ask side ------------------------------------------------------------

const makerLockRow = (status, extra = {}) =>
  escrowRow({ h: "lockM", from: MAKER, to: TAKER, amt: 300, hash: "HL", ctx: "take1", status, ...extra });
const takerLockRow = (status, extra = {}) =>
  escrowRow({ h: "lockT", from: TAKER, to: MAKER, amt: 450, hash: "HL", ctx: "take1", status, ...extra });

test("phase 1: nothing is locked — the maker moves first, or nothing happens", () => {
  const st = L.tradeStatus(M_ASK, emptyBase(), emptyQuote());
  assert.equal(st.phase, "maker-lock");
  const act = L.nextAction(M_ASK, st, MAKER, null);
  assert.equal(act.act, "lock");
  assert.equal(act.leg, "base");
  assert.equal(act.cur, CUR_BASE);
  assert.equal(act.to, TAKER);
  assert.equal(act.amt, 300);
  assert.equal(act.ctx, "take1");
  assert.equal(act.hash, undefined, "the maker MINTS the hashlock; nothing to copy");
  assert.equal(L.nextAction(M_ASK, st, TAKER, null).act, "wait", "the taker commits nothing yet");
});

test("phase 2: the maker's leg is held — the taker copies the hashlock and answers", () => {
  const base = bankSnap(CUR_BASE, OPEN_BOTH, [makerLockRow("held")]);
  const st = L.tradeStatus(M_ASK, base, emptyQuote());
  assert.equal(st.phase, "taker-lock");
  const act = L.nextAction(M_ASK, st, TAKER, null);
  assert.equal(act.act, "lock");
  assert.equal(act.leg, "quote");
  assert.equal(act.cur, CUR_QUOTE);
  assert.equal(act.to, MAKER);
  assert.equal(act.amt, 450);
  assert.equal(act.hash, "HL", "ONE hashlock for both legs — copied, never minted");
});

test("phase 3: both legs held — the maker claims, publishing the secret", () => {
  const base = bankSnap(CUR_BASE, OPEN_BOTH, [makerLockRow("held")]);
  const quote = bankSnap(CUR_QUOTE, OPEN_BOTH, [takerLockRow("held")]);
  const st = L.tradeStatus(M_ASK, base, quote);
  assert.equal(st.phase, "maker-claim");
  const act = L.nextAction(M_ASK, st, MAKER, "THE-SECRET");
  assert.equal(act.act, "claim");
  assert.equal(act.leg, "quote", "the maker claims the TAKER's leg");
  assert.equal(act.lockH, "lockT");
  assert.equal(act.pre, "THE-SECRET");
});

test("a maker who lost the secret is told so — the engine does not spin", () => {
  const base = bankSnap(CUR_BASE, OPEN_BOTH, [makerLockRow("held")]);
  const quote = bankSnap(CUR_QUOTE, OPEN_BOTH, [takerLockRow("held")]);
  const st = L.tradeStatus(M_ASK, base, quote);
  const act = L.nextAction(M_ASK, st, MAKER, null);
  assert.equal(act.act, "wait");
  assert.equal(act.why, "no-secret");
});

test("phase 4: the taker reads the published preimage off its OWN leg and collects", () => {
  const base = bankSnap(CUR_BASE, OPEN_BOTH, [makerLockRow("held")]);
  const quote = bankSnap(CUR_QUOTE, OPEN_BOTH, [takerLockRow("claimed", { pre: "THE-SECRET" })]);
  const st = L.tradeStatus(M_ASK, base, quote);
  assert.equal(st.phase, "taker-claim");
  const act = L.nextAction(M_ASK, st, TAKER, null);
  assert.equal(act.act, "claim");
  assert.equal(act.leg, "base", "the taker claims the MAKER's leg");
  assert.equal(act.lockH, "lockM");
  assert.equal(act.pre, "THE-SECRET", "publishing it is what the maker's claim did");
});

test("phase 5: both claimed — done, and nobody has anything to do", () => {
  const base = bankSnap(CUR_BASE, OPEN_BOTH, [makerLockRow("claimed", { pre: "THE-SECRET" })]);
  const quote = bankSnap(CUR_QUOTE, OPEN_BOTH, [takerLockRow("claimed", { pre: "THE-SECRET" })]);
  const st = L.tradeStatus(M_ASK, base, quote);
  assert.equal(st.phase, "done");
  assert.equal(L.nextAction(M_ASK, st, MAKER, null).act, "none");
  assert.equal(L.nextAction(M_ASK, st, TAKER, null).act, "none");
});

test("a returned leg voids the trade — and the other leg's status stays visible", () => {
  const base = bankSnap(CUR_BASE, OPEN_BOTH, [makerLockRow("returned")]);
  const st = L.tradeStatus(M_ASK, base, emptyQuote());
  assert.equal(st.phase, "void");
  assert.equal(st.makerLock.status, "returned");
  assert.equal(L.nextAction(M_ASK, st, MAKER, null).act, "none");
});

test("the claim-beats-return rule reads through: a claimed maker leg is DONE, whatever else happened", () => {
  // The banker refunded after the preimage went public; banca's fold already
  // ruled the claim wins, so the engine sees `claimed` and answers done.
  const base = bankSnap(CUR_BASE, OPEN_BOTH, [makerLockRow("claimed", { pre: "P" })]);
  const quote = bankSnap(CUR_QUOTE, OPEN_BOTH, [takerLockRow("held")]);
  assert.equal(L.tradeStatus(M_ASK, base, quote).phase, "done");
});

// ---- the lookup is the verification ---------------------------------------------------

test("a lock that differs anywhere is simply not found", () => {
  const rows = [
    ["wrong amount", makerLockRow("held", { amt: 299 })],
    ["wrong beneficiary", makerLockRow("held", { to: MAKER })],
    ["wrong match", makerLockRow("held", { ctx: "other-take" })],
    ["wrong payer", makerLockRow("held", { from: TAKER })],
  ];
  for (const [name, row] of rows) {
    const st = L.tradeStatus(M_ASK, bankSnap(CUR_BASE, OPEN_BOTH, [row]), emptyQuote());
    assert.equal(st.phase, "maker-lock", name);
  }
  // …and the right lock in the WRONG BANK is not found either
  const st = L.tradeStatus(M_ASK, bankSnap(CUR_QUOTE, OPEN_BOTH, [makerLockRow("held")]), emptyQuote());
  assert.equal(st.phase, "maker-lock", "the snapshot's own cur gates the lookup");
});

test("a stranger to the trade has no action, ever", () => {
  const base = bankSnap(CUR_BASE, OPEN_BOTH, [makerLockRow("held")]);
  const st = L.tradeStatus(M_ASK, base, emptyQuote());
  assert.equal(L.nextAction(M_ASK, st, ACTORS.bankBPub, null).act, "none");
});

// ---- the bid side, briefly — the mirror must actually mirror ---------------------------

test("on a bid the maker still locks first, in the QUOTE bank", () => {
  const st = L.tradeStatus(M_BID, emptyBase(), emptyQuote());
  const act = L.nextAction(M_BID, st, MAKER, null);
  assert.equal(act.leg, "quote");
  assert.equal(act.cur, CUR_QUOTE);
  assert.equal(act.amt, 450, "the maker pays the quote leg on a bid");
});

// ---- preconditions ------------------------------------------------------------------------

test("four account relationships, and the missing ones are named before the button", () => {
  const base = bankSnap(CUR_BASE, [[MAKER, "open"], [TAKER, "pending"]], []);
  const quote = bankSnap(CUR_QUOTE, [[MAKER, "open"]], []);
  assert.deepEqual([...L.missingAccounts(PAIR, base, quote, TAKER)], [CUR_BASE, CUR_QUOTE]);
  assert.deepEqual([...L.missingAccounts(PAIR, base, quote, MAKER)], []);
});

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