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 | /**
* The independent derivation path — no imports from test-dist/, no side
* effects. Everything the tests build fixtures with lives here: canonical
* JSON re-implemented from its spec sentence, the two bursa preimages written
* out as literal templates, Ed25519 via @noble/curves. If the testlib and
* this module agree on a byte, two implementations agree on it.
*
* Pure module: importing it computes nothing and writes nothing.
*/
import { createHash } from "node:crypto";
import { ed25519 } from "@noble/curves/ed25519";
// ---- fixed test material ----------------------------------------------------
export const seedBytes = (fill) => {
const b = new Uint8Array(32);
if (typeof fill === "number") b.fill(fill);
else for (let i = 0; i < 32; i++) b[i] = fill(i);
return b;
};
export const SEED_MAKER = seedBytes((i) => i); // 00..1f
export const SEED_TAKER = seedBytes((i) => 32 + i); // 20..3f
export const SEED_BANK_B = seedBytes(0x11); // issues the BASE currency
export const SEED_BANK_Q = seedBytes(0x33); // issues the QUOTE currency
/** 2025-01-01T00:00:00Z — fixed and in the past, so no verdict moves. */
export const TS_FIXED = 1735689600000;
export const b64url = (bytes) => Buffer.from(bytes).toString("base64url");
export const utf8 = (s) => new Uint8Array(Buffer.from(s, "utf8"));
export const sha256b64url = (s) =>
createHash("sha256").update(Buffer.from(s, "utf8")).digest("base64url");
export const pubOf = (seed) => b64url(ed25519.getPublicKey(seed));
export const signOver = (seed, preimage) => b64url(ed25519.sign(utf8(preimage), seed));
export const ACTORS = {
makerPub: pubOf(SEED_MAKER),
takerPub: pubOf(SEED_TAKER),
bankBPub: pubOf(SEED_BANK_B),
bankQPub: pubOf(SEED_BANK_Q),
};
export const CUR_BASE = `${ACTORS.bankBPub}.DUH`;
export const CUR_QUOTE = `${ACTORS.bankQPub}.MOR`;
export const PAIR = `${CUR_BASE}|${CUR_QUOTE}`;
/** 16 deterministic bytes -> a canonical 22-char base64url order id. */
export const orderIdBytes = (n) => new Uint8Array(16).map((_, i) => (n + i) & 0xff);
// ---- canonical JSON, re-implemented from the spec ----------------------------
export function canonIndependent(v) {
if (v === null) return "null";
const t = typeof v;
if (t === "number" || t === "boolean" || t === "string") return JSON.stringify(v);
if (Array.isArray(v)) return "[" + v.map(canonIndependent).join(",") + "]";
if (t === "object") {
const keys = Object.keys(v)
.filter((k) => v[k] !== undefined)
.sort();
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonIndependent(v[k])).join(",") + "}";
}
throw new Error("canonIndependent: unsupported " + t);
}
// ---- the two preimages, written out by hand ----------------------------------
//
// The templates are the point: a reader can hold one beside the artifact and
// check, character by character, that the keys are in ASCII order, that px is
// a nested object canon() sorts too, and that the domain prefix is bursa's.
export function quotePreimageByHand(a) {
const s = JSON.stringify;
return (
`bursa-ord|v1|{"ctx":${s(a.ctx)},"exp":${a.exp},"from":${s(a.from)},"id":${s(a.id)},` +
`"pair":${s(a.pair)},"px":{"den":${a.px.den},"num":${a.px.num}},"qty":${a.qty},` +
`"side":${s(a.side)},"t":"bxo","ts":${a.ts},"v":1}`
);
}
export function cancelPreimageByHand(c) {
const s = JSON.stringify;
return `bursa-cxl|v1|{"from":${s(c.from)},"h":${s(c.h)},"t":"bxc","ts":${c.ts},"v":1}`;
}
/** A signed quote. TWELVE keys in wire order — sig appended, never signed. */
export function makeQuote({ seed, idN, pair = PAIR, side, px, qty, ctx = "", ts = TS_FIXED, ttl = 86400000 }) {
const unsigned = {
v: 1,
t: "bxo",
id: b64url(orderIdBytes(idN)),
pair,
side,
px: { num: px.num, den: px.den },
qty,
from: pubOf(seed),
ctx,
ts,
exp: ts + ttl,
};
const preimage = quotePreimageByHand(unsigned);
if (preimage !== `bursa-ord|v1|${canonIndependent(unsigned)}`) {
throw new Error("independent.mjs: the by-hand quote template and canonIndependent disagree");
}
return { ...unsigned, sig: signOver(seed, preimage) };
}
/** A signed cancel. SIX keys. */
export function makeCancel({ seed, h, ts = TS_FIXED }) {
const unsigned = { v: 1, t: "bxc", h, from: pubOf(seed), ts };
const preimage = cancelPreimageByHand(unsigned);
if (preimage !== `bursa-cxl|v1|${canonIndependent(unsigned)}`) {
throw new Error("independent.mjs: the by-hand cancel template and canonIndependent disagree");
}
return { ...unsigned, sig: signOver(seed, preimage) };
}
// ---- synthetic log entries ----------------------------------------------------
//
// A rooms-kit LogEntryMeta is {hash, from, clock, op}. The fold treats a hash
// as an OPAQUE COMPARABLE STRING, so fixtures use short sortable names a
// reader can order by eye.
export const entry = (hash, clock, from, op) => ({ hash, from, clock, op });
export const opBank = (ts, s) => ({ t: "bank", ts, s });
export const opQuote = (ts, o) => ({ t: "quote", ts, o });
export const opCancel = (ts, c) => ({ t: "cancel", ts, c });
export const opTake = (ts, h, qty) => ({ t: "take", ts, h, qty });
export const opLeg = (ts, m, lh) => ({ t: "leg", ts, m, lh });
// ---- permutations -------------------------------------------------------------
export function permutations(xs) {
return [
xs,
[...xs].reverse(),
[...xs.slice(Math.floor(xs.length / 2)), ...xs.slice(0, Math.floor(xs.length / 2))],
];
}
// ---- minimal bank snapshots -----------------------------------------------------
//
// The trade engine reads banca fold SNAPSHOTS, not folds — so its tests build
// exactly the fields it documents reading: ok, cur, accounts, escrows. A field
// the engine grows a dependency on must be added HERE, which is the point: the
// test then states the whole read surface.
export const bankSnap = (cur, accounts, escrows) => ({
ok: true,
cur,
accounts: accounts.map(([id, state]) => ({ id, state })),
escrows,
});
export const escrowRow = ({ h, from, to, amt, hash = "H", ctx, exp = TS_FIXED + 600000, pre = null, status }) => ({
h, from, to, amt, hash, ctx, exp, pre, status,
});
|