alberta-buck-core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/chart.js ADDED
@@ -0,0 +1,70 @@
1
+ // Tiny dependency-free SVG line charts, BigInt-free numbers in, string
2
+ // out -- renders identically in node (write to a .svg file) and the
3
+ // browser (innerHTML), so the demo page and bin/eqsim.mjs share it.
4
+
5
+ const COLORS = ["#2563eb", "#dc2626", "#059669", "#d97706", "#7c3aed"];
6
+
7
+ /**
8
+ * @param opts.title chart title
9
+ * @param opts.series [{label, points: [[x, y], ...]}] -- y as Number
10
+ * @param opts.refY optional dashed reference line (e.g. 1.0 parity)
11
+ * @param opts.yFmt tick formatter
12
+ * @returns a standalone <svg> fragment string (nestable via svgDoc)
13
+ */
14
+ export function lineChart({ title, series, width = 860, height = 240,
15
+ refY = null, yFmt = (v) => v.toPrecision(4) }) {
16
+ const m = { l: 64, r: 14, t: 26, b: 22 };
17
+ const pts = series.flatMap((s) => s.points);
18
+ const xs = pts.map((p) => p[0]), ys = pts.map((p) => p[1]);
19
+ const xmin = Math.min(...xs), xmax = Math.max(...xs);
20
+ let ymin = Math.min(...ys, refY ?? Infinity);
21
+ let ymax = Math.max(...ys, refY ?? -Infinity);
22
+ const pad = (ymax - ymin || Math.abs(ymax) || 1) * 0.08;
23
+ ymin -= pad; ymax += pad;
24
+ const sx = (x) => m.l + ((x - xmin) / (xmax - xmin || 1)) * (width - m.l - m.r);
25
+ const sy = (y) => height - m.b - ((y - ymin) / (ymax - ymin)) * (height - m.t - m.b);
26
+
27
+ const yTicks = [0, 1, 2, 3, 4].map((i) => ymin + (i / 4) * (ymax - ymin));
28
+ const xTicks = [0, 1, 2, 3, 4, 5].map((i) => xmin + (i / 5) * (xmax - xmin));
29
+ const el = [];
30
+ el.push(`<rect width="${width}" height="${height}" fill="#ffffff"/>`);
31
+ el.push(`<text x="${m.l}" y="16" font-size="13" font-weight="bold" ` +
32
+ `font-family="monospace">${title}</text>`);
33
+ for (const t of yTicks) {
34
+ el.push(`<line x1="${m.l}" y1="${sy(t)}" x2="${width - m.r}" y2="${sy(t)}" ` +
35
+ `stroke="#e5e7eb"/>`);
36
+ el.push(`<text x="${m.l - 6}" y="${sy(t) + 4}" font-size="10" ` +
37
+ `font-family="monospace" text-anchor="end">${yFmt(t)}</text>`);
38
+ }
39
+ for (const t of xTicks) {
40
+ el.push(`<text x="${sx(t)}" y="${height - 6}" font-size="10" ` +
41
+ `font-family="monospace" text-anchor="middle">${Math.round(t)}</text>`);
42
+ }
43
+ if (refY !== null) {
44
+ el.push(`<line x1="${m.l}" y1="${sy(refY)}" x2="${width - m.r}" ` +
45
+ `y2="${sy(refY)}" stroke="#9ca3af" stroke-dasharray="5,4"/>`);
46
+ }
47
+ series.forEach((s, i) => {
48
+ const path = s.points.map((p) => `${sx(p[0]).toFixed(1)},${sy(p[1]).toFixed(1)}`)
49
+ .join(" ");
50
+ const c = s.color ?? COLORS[i % COLORS.length];
51
+ el.push(`<polyline points="${path}" fill="none" stroke="${c}" stroke-width="1.6"/>`);
52
+ el.push(`<text x="${width - m.r}" y="${m.t + 12 * i}" font-size="11" ` +
53
+ `font-family="monospace" text-anchor="end" fill="${c}">${s.label}</text>`);
54
+ });
55
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" ` +
56
+ `height="${height}" viewBox="0 0 ${width} ${height}">${el.join("")}</svg>`;
57
+ }
58
+
59
+ /** Stack chart fragments vertically into one SVG document. */
60
+ export function svgDoc(charts, { width = 860 } = {}) {
61
+ let y = 0;
62
+ const parts = [];
63
+ for (const c of charts) {
64
+ const h = Number(c.match(/height="(\d+)"/)[1]);
65
+ parts.push(`<svg y="${y}">${c.replace(/<\/?svg[^>]*>/g, "")}</svg>`);
66
+ y += h + 8;
67
+ }
68
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${y}">` +
69
+ parts.join("") + `</svg>`;
70
+ }
@@ -0,0 +1,248 @@
1
+ // BigInt-native wrapper over the buck-identity wasm kernel -- the
2
+ // environment-free CORE. `wrapIdentity(wasm)` takes the raw wasm module
3
+ // (0x-hex string ABI) however the host loaded it -- node require of the
4
+ // nodejs-target package (identity.js) or awaited init of the web-target
5
+ // package (identity-web.js) -- and returns the structured API:
6
+ //
7
+ // point {x, y} (BigInt; {0n, 0n} = infinity)
8
+ // G2 point {x: [c0, c1], y: [c0, c1]}
9
+ // ciphertext {R: point, C: point}
10
+ // PS sig {sigma_1: point, sigma_2: point}
11
+ // proofs objects with the Python dataclass field names
12
+ //
13
+ // Deterministic: every nonce is an argument. `randScalar()` draws from
14
+ // WebCrypto for live use; tests replay recorded nonces from
15
+ // core/vectors/identity-kernel-vectors.json.
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Pure helpers (no wasm)
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export const hex = (v) => "0x" + BigInt(v).toString(16).padStart(64, "0");
22
+ export const big = (s) => BigInt(s);
23
+
24
+ /** THE canonical JSON dialect: sorted keys, compact separators, raw
25
+ * UTF-8 -- byte-identical to Python's canonical_json(), shared by the
26
+ * identity preimage (canonical_identity_data) and the AB-RCPT/1 receipt
27
+ * core. Values must be strings and integers (floats are not canonical).
28
+ * JSON.stringify emits this natively once keys are sorted. */
29
+ export function canonicalIdentity(fields) {
30
+ const sort = (v) => {
31
+ if (Array.isArray(v)) return v.map(sort);
32
+ if (v && typeof v === "object") {
33
+ return Object.fromEntries(
34
+ Object.keys(v).sort().map((k) => [k, sort(v[k])]),
35
+ );
36
+ }
37
+ return v;
38
+ };
39
+ return JSON.stringify(sort(fields));
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // The wrapped API
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export function wrapIdentity(wasm) {
47
+ const P = (arr, i = 0) => ({ x: big(arr[i]), y: big(arr[i + 1]) });
48
+ const CT = (arr, i = 0) => ({ R: P(arr, i), C: P(arr, i + 2) });
49
+ const flatP = (p) => [hex(p.x), hex(p.y)];
50
+ const flatCT = (ct) => [...flatP(ct.R), ...flatP(ct.C)];
51
+ const flatG2 = (g) => [hex(g.x[0]), hex(g.x[1]), hex(g.y[0]), hex(g.y[1])];
52
+
53
+ const ORDER = big(wasm.order());
54
+
55
+ /** Random non-zero scalar mod ORDER via WebCrypto. */
56
+ function randScalar() {
57
+ for (;;) {
58
+ const bytes = new Uint8Array(32);
59
+ globalThis.crypto.getRandomValues(bytes);
60
+ let v = 0n;
61
+ for (const b of bytes) v = (v << 8n) | BigInt(b);
62
+ v %= ORDER;
63
+ if (v !== 0n) return v;
64
+ }
65
+ }
66
+
67
+ return {
68
+ hex, big, canonicalIdentity, randScalar,
69
+
70
+ // ---- constants --------------------------------------------------------
71
+ ORDER,
72
+ F_R: ORDER,
73
+ FIELD_MODULUS: big(wasm.field_modulus()),
74
+ G1: P(wasm.g1_generator()),
75
+ G2: (() => {
76
+ const g = wasm.g2_generator();
77
+ return { x: [big(g[0]), big(g[1])], y: [big(g[2]), big(g[3])] };
78
+ })(),
79
+ H_POINT: P(wasm.h_point()),
80
+ FLAVOR_A1: 1,
81
+ FLAVOR_A2: 2,
82
+ FLAVOR_B1: 3,
83
+
84
+ // ---- curve ops / hashes ------------------------------------------------
85
+ g1Add: (a, b) => P(wasm.g1_add(...flatP(a), ...flatP(b))),
86
+ g1Mul: (p, k) => P(wasm.g1_mul(...flatP(p), hex(k))),
87
+ g1Neg: (p) => P(wasm.g1_neg(...flatP(p))),
88
+ g2Mul: (g, k) => {
89
+ const r = wasm.g2_mul(...flatG2(g), hex(k));
90
+ return { x: [big(r[0]), big(r[1])], y: [big(r[2]), big(r[3])] };
91
+ },
92
+ /** EVM ecPairing semantics: pairs = [[g1Point, g2Point], ...]. */
93
+ pairingCheck: (pairs) =>
94
+ wasm.pairing_check(pairs.flatMap(([p, q]) => [...flatP(p), ...flatG2(q)])),
95
+ keccakScalar: (words) => big(wasm.keccak_scalar(words.map(hex))),
96
+ identityScalar: (canonical) => big(wasm.identity_scalar(canonical)),
97
+ reduceModOrder: (v) => big(wasm.reduce_mod_order(hex(v))),
98
+ poseidon: (inputs) => big(wasm.poseidon(inputs.map(hex))),
99
+
100
+ // ---- ElGamal / PS ------------------------------------------------------
101
+ elgamalEncrypt: (M, pk, r) =>
102
+ CT(wasm.elgamal_encrypt(...flatP(M), ...flatP(pk), hex(r))),
103
+ elgamalDecrypt: (E, sk) => P(wasm.elgamal_decrypt(...flatCT(E), hex(sk))),
104
+ psSign(skX, skY, m, t) {
105
+ const r = wasm.ps_sign(hex(skX), hex(skY), hex(m), hex(t));
106
+ return { sigma_1: P(r, 0), sigma_2: P(r, 2) };
107
+ },
108
+ psVerify: (pkX, pkY, sig, m) =>
109
+ wasm.ps_verify(flatG2(pkX), flatG2(pkY),
110
+ ...flatP(sig.sigma_1), ...flatP(sig.sigma_2), hex(m)),
111
+ psRerandomize(sig, t) {
112
+ const r = wasm.ps_rerandomize(...flatP(sig.sigma_1), ...flatP(sig.sigma_2), hex(t));
113
+ return { sigma_1: P(r, 0), sigma_2: P(r, 2) };
114
+ },
115
+
116
+ // ---- Schnorr batch binding ---------------------------------------------
117
+ /** Raw UNREDUCED keccak word (what the Schnorr transcript signs). */
118
+ batchCommitment: (cms) => big(wasm.batch_commitment(cms.map(hex))),
119
+ issuerSchnorrSign(skIss, hBatch, issuer, chainid, k) {
120
+ const r = wasm.issuer_schnorr_sign(
121
+ hex(skIss), hex(hBatch), hex(issuer), hex(chainid), hex(k));
122
+ return { e: big(r[0]), s: big(r[1]), R: P(r, 2) };
123
+ },
124
+ issuerSchnorrVerify: (pkIss, proof, hBatch, issuer, chainid) =>
125
+ wasm.issuer_schnorr_verify(
126
+ ...flatP(pkIss),
127
+ [hex(proof.e), hex(proof.s), ...flatP(proof.R)],
128
+ hex(hBatch), hex(issuer), hex(chainid)),
129
+
130
+ // ---- Registration NIZK -------------------------------------------------
131
+ registrationProve(sig, m, r, pk, E, registrant, mTilde, rTilde) {
132
+ const o = wasm.registration_prove(
133
+ [...flatP(sig.sigma_1), ...flatP(sig.sigma_2)],
134
+ hex(m), hex(r), ...flatP(pk), flatCT(E),
135
+ hex(registrant), hex(mTilde), hex(rTilde));
136
+ return { e: big(o[0]), s_m: big(o[1]), s_r: big(o[2]),
137
+ A_ps: P(o, 3), T_C: P(o, 5), T_R: P(o, 7) };
138
+ },
139
+ registrationVerify: (sig, E, pk, issuerX, issuerY, proof, registrant) =>
140
+ wasm.registration_verify(
141
+ [...flatP(sig.sigma_1), ...flatP(sig.sigma_2)],
142
+ flatCT(E), ...flatP(pk), flatG2(issuerX), flatG2(issuerY),
143
+ [hex(proof.e), hex(proof.s_m), hex(proof.s_r),
144
+ ...flatP(proof.A_ps), ...flatP(proof.T_C), ...flatP(proof.T_R)],
145
+ hex(registrant)),
146
+
147
+ // ---- Chaum-Pedersen approve --------------------------------------------
148
+ chaumPedersenProve(eAlice, eBob, pkA, pkB, skA, rPrime,
149
+ sender, spender, chainid, k1, k2) {
150
+ const o = wasm.chaum_pedersen_prove(
151
+ flatCT(eAlice), flatCT(eBob), ...flatP(pkA), ...flatP(pkB),
152
+ hex(skA), hex(rPrime), hex(sender), hex(spender), hex(chainid),
153
+ hex(k1), hex(k2));
154
+ return { e: big(o[0]), s1: big(o[1]), s2: big(o[2]),
155
+ T1: P(o, 3), T2: P(o, 5), T3: P(o, 7) };
156
+ },
157
+ chaumPedersenVerify: (eAlice, eBob, pkA, pkB, proof, sender, spender, chainid) =>
158
+ wasm.chaum_pedersen_verify(
159
+ flatCT(eAlice), flatCT(eBob), ...flatP(pkA), ...flatP(pkB),
160
+ [hex(proof.e), hex(proof.s1), hex(proof.s2),
161
+ ...flatP(proof.T1), ...flatP(proof.T2), ...flatP(proof.T3)],
162
+ hex(sender), hex(spender), hex(chainid)),
163
+
164
+ // ---- Verifiable decryption ---------------------------------------------
165
+ verifiableDecryptProve(E, sk, M, account, chainid, t) {
166
+ const o = wasm.verifiable_decrypt_prove(
167
+ flatCT(E), hex(sk), ...flatP(M), hex(account), hex(chainid), hex(t));
168
+ return { e: big(o[0]), s: big(o[1]), T1: P(o, 2), T2: P(o, 4) };
169
+ },
170
+ verifiableDecryptVerify: (E, pk, M, proof, account, chainid) =>
171
+ wasm.verifiable_decrypt_verify(
172
+ flatCT(E), ...flatP(pk), ...flatP(M),
173
+ [hex(proof.e), hex(proof.s), ...flatP(proof.T1), ...flatP(proof.T2)],
174
+ hex(account), hex(chainid)),
175
+
176
+ // ---- A2 issuer re-encryption binding -----------------------------------
177
+ issuerReencProve(skIss, rPrime, pkRec, eReg, eIss, issuer, chainid,
178
+ beta, gamma, kR, kB, kS, kG) {
179
+ const o = wasm.issuer_reenc_prove(
180
+ hex(skIss), hex(rPrime), ...flatP(pkRec), flatCT(eReg), flatCT(eIss),
181
+ hex(issuer), hex(chainid),
182
+ hex(beta), hex(gamma), hex(kR), hex(kB), hex(kS), hex(kG));
183
+ return {
184
+ e: big(o[0]), s_r: big(o[1]), s_b: big(o[2]), s_s: big(o[3]), s_g: big(o[4]),
185
+ A1: P(o, 5), A2: P(o, 7), A3: P(o, 9), A4: P(o, 11), A5: P(o, 13),
186
+ Q: P(o, 15), U: P(o, 17), T: P(o, 19),
187
+ };
188
+ },
189
+ issuerReencVerify: (pkIss, eReg, eIss, proof, issuer, chainid) =>
190
+ wasm.issuer_reenc_verify(
191
+ ...flatP(pkIss), flatCT(eReg), flatCT(eIss),
192
+ [hex(proof.e), hex(proof.s_r), hex(proof.s_b), hex(proof.s_s), hex(proof.s_g),
193
+ ...flatP(proof.A1), ...flatP(proof.A2), ...flatP(proof.A3),
194
+ ...flatP(proof.A4), ...flatP(proof.A5),
195
+ ...flatP(proof.Q), ...flatP(proof.U), ...flatP(proof.T)],
196
+ hex(issuer), hex(chainid)),
197
+
198
+ // ---- Deposit coupling / B1 depositor binding ----------------------------
199
+ depositCoupleProve(mRec, skDep, eDep, eIss, account, chainid, b, kM, kS, kB) {
200
+ const o = wasm.deposit_couple_prove(
201
+ hex(mRec), hex(skDep), flatCT(eDep), flatCT(eIss),
202
+ hex(account), hex(chainid), hex(b), hex(kM), hex(kS), hex(kB));
203
+ return {
204
+ e: big(o[0]), s_m: big(o[1]), s_s: big(o[2]), s_b: big(o[3]),
205
+ A2: P(o, 4), A3: P(o, 6), A4: P(o, 8), P_I: P(o, 10),
206
+ };
207
+ },
208
+ depositCoupleVerify: (pkDep, eDep, eIss, proof, account, chainid) =>
209
+ wasm.deposit_couple_verify(
210
+ ...flatP(pkDep), flatCT(eDep), flatCT(eIss),
211
+ [hex(proof.e), hex(proof.s_m), hex(proof.s_s), hex(proof.s_b),
212
+ ...flatP(proof.A2), ...flatP(proof.A3), ...flatP(proof.A4), ...flatP(proof.P_I)],
213
+ hex(account), hex(chainid)),
214
+ b1BindProve(mDep, skDep, eDep, pkIss, account, chainid, r, b, kM, kS, kR, kB) {
215
+ const o = wasm.b1_bind_prove(
216
+ hex(mDep), hex(skDep), flatCT(eDep), ...flatP(pkIss),
217
+ hex(account), hex(chainid),
218
+ hex(r), hex(b), hex(kM), hex(kS), hex(kR), hex(kB));
219
+ return {
220
+ proof: {
221
+ e: big(o[0]), s_m: big(o[1]), s_s: big(o[2]), s_r: big(o[3]), s_b: big(o[4]),
222
+ A2: P(o, 5), A4: P(o, 7), B1: P(o, 9), B2: P(o, 11),
223
+ A_p: P(o, 13), P_dep: P(o, 15),
224
+ },
225
+ eDepForIss: CT(o, 17),
226
+ };
227
+ },
228
+ b1BindVerify: (pkDep, eDep, pkIss, eDepForIss, proof, account, chainid) =>
229
+ wasm.b1_bind_verify(
230
+ ...flatP(pkDep), flatCT(eDep), ...flatP(pkIss), flatCT(eDepForIss),
231
+ [hex(proof.e), hex(proof.s_m), hex(proof.s_s), hex(proof.s_r), hex(proof.s_b),
232
+ ...flatP(proof.A2), ...flatP(proof.A4), ...flatP(proof.B1),
233
+ ...flatP(proof.B2), ...flatP(proof.A_p), ...flatP(proof.P_dep)],
234
+ hex(account), hex(chainid)),
235
+
236
+ // ---- Notes family --------------------------------------------------------
237
+ noteCommitment: (flavor, v, rho, idHash, predicate) =>
238
+ big(wasm.note_commitment(Number(flavor), hex(v), hex(rho), hex(idHash), hex(predicate))),
239
+ nullifierB: (rho, idHash) => big(wasm.nullifier_b(hex(rho), hex(idHash))),
240
+ nullifierA: (rho, idHash) => big(wasm.nullifier_a(hex(rho), hex(idHash))),
241
+ idHashB1: (mIssuer, sigmaR, sigmaS) =>
242
+ big(wasm.id_hash_b1(hex(mIssuer), ...flatP(sigmaR), hex(sigmaS))),
243
+ idHashA1: (eNote, mIssuer, sigmaR, sigmaS) =>
244
+ big(wasm.id_hash_a1(flatCT(eNote), hex(mIssuer), ...flatP(sigmaR), hex(sigmaS))),
245
+ idHashA2: (eNote, eIss) => big(wasm.id_hash_a2(flatCT(eNote), flatCT(eIss))),
246
+ identityLeaf: (M) => big(wasm.identity_leaf(...flatP(M))),
247
+ };
248
+ }
@@ -0,0 +1,19 @@
1
+ // Browser entry for the buck-identity kernel: initializes the WEB-target
2
+ // wasm package (built by `make nix-core-build-wasm-web` into
3
+ // the alberta-buck-kernel package) and wraps it in the same BigInt-native API node code
4
+ // gets from identity.js.
5
+ //
6
+ // Always pass an explicit wasm source -- a URL string the page serves
7
+ // (e.g. "./wasm-web/buck_identity_bg.wasm"), a Response/Promise thereof,
8
+ // or a BufferSource -- because bundlers break wasm-bindgen's default
9
+ // import.meta.url-relative fetch.
10
+
11
+ import { wrapIdentity } from "./identity-core.js";
12
+
13
+ export async function loadIdentity(wasmSource) {
14
+ const mod = await import("alberta-buck-kernel/web/identity");
15
+ await mod.default(
16
+ wasmSource !== undefined ? { module_or_path: wasmSource } : undefined,
17
+ );
18
+ return wrapIdentity(mod);
19
+ }
@@ -0,0 +1,32 @@
1
+ // Node entry for the buck-identity kernel: loads the nodejs-target wasm
2
+ // package (built by `make nix-core-build-wasm`) and wraps it in the
3
+ // BigInt-native API. The wrapper itself lives in identity-core.js
4
+ // (environment-free); the browser loads the web-target package through
5
+ // identity-web.js instead.
6
+
7
+ import { createRequire } from "node:module";
8
+
9
+ import { wrapIdentity } from "./identity-core.js";
10
+
11
+ const require = createRequire(import.meta.url);
12
+ const api = wrapIdentity(require("alberta-buck-kernel/identity"));
13
+
14
+ export default api;
15
+ export const {
16
+ hex, big, canonicalIdentity, randScalar,
17
+ ORDER, F_R, FIELD_MODULUS, G1, G2, H_POINT,
18
+ FLAVOR_A1, FLAVOR_A2, FLAVOR_B1,
19
+ g1Add, g1Mul, g1Neg, g2Mul, pairingCheck,
20
+ keccakScalar, identityScalar, reduceModOrder, poseidon,
21
+ elgamalEncrypt, elgamalDecrypt,
22
+ psSign, psVerify, psRerandomize,
23
+ batchCommitment, issuerSchnorrSign, issuerSchnorrVerify,
24
+ registrationProve, registrationVerify,
25
+ chaumPedersenProve, chaumPedersenVerify,
26
+ verifiableDecryptProve, verifiableDecryptVerify,
27
+ issuerReencProve, issuerReencVerify,
28
+ depositCoupleProve, depositCoupleVerify,
29
+ b1BindProve, b1BindVerify,
30
+ noteCommitment, nullifierB, nullifierA,
31
+ idHashB1, idHashA1, idHashA2, identityLeaf,
32
+ } = api;
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // alberta-buck-core -- the JS side of the Alberta Buck platform.
2
+ // See alberta-buck-platform.org and core/README.md.
3
+
4
+ export { FIELDS, JournalWriter, parseJournal, mismatches, totalGas,
5
+ summarize } from "./journal.js";
6
+ export { CALL_GAS, DEPLOY_GAS, Session } from "./session.js";
7
+ export { DEV_KEYS, devAccount, anvilSession, tevmSession } from "./backends.js";
8
+ export { MIN_SQRT_RATIO, MAX_SQRT_RATIO, HUGE, Q96, isqrt, sqrtPriceX96,
9
+ fullRangeTicks, spotFromSqrtPriceX96 } from "./v3.js";
10
+ export { runDays } from "./world.js";
11
+ export { PinWhale } from "./agents/whale.js";
12
+ export { RoundTripTrader } from "./agents/trader.js";
13
+ export { buildOnePool } from "./scenarios/onepool.js";
package/src/journal.js ADDED
@@ -0,0 +1,86 @@
1
+ // Journal: the shared JSONL operation record of a ChainSession run.
2
+ //
3
+ // Schema (alberta-buck-platform.org, Layer 1; pinned by
4
+ // core/vectors/journal-sample.jsonl, asserted identically by the Python
5
+ // suite): one JSON object per line with fields
6
+ // i, tag, op, fn, sender, expect, outcome, matched, gas, tx, block, err
7
+ // Unknown fields are ignored; extra whitespace/blank lines are tolerated.
8
+ //
9
+ // JournalWriter mirrors buck_core.session.Journal: entries in FIELDS
10
+ // order, compact separators, missing fields as "" -- so identical runs
11
+ // produce byte-identical lines across languages. It writes through a
12
+ // caller-supplied sink so this module stays environment-agnostic; use
13
+ // nodefs.js fileJournalWriter() for a file sink in Node.
14
+
15
+ const REQUIRED = ["i", "op", "fn", "expect", "outcome", "matched"];
16
+
17
+ export const FIELDS = ["i", "tag", "op", "fn", "sender", "expect", "outcome",
18
+ "matched", "gas", "tx", "block", "err"];
19
+
20
+ export class JournalWriter {
21
+ /** @param {(line: string) => void} sink called with one JSONL line per entry */
22
+ constructor(sink) {
23
+ this.sink = sink;
24
+ this.seq = 0;
25
+ }
26
+
27
+ /** Record one entry; fills schema order, returns the entry object. */
28
+ record(fields) {
29
+ this.seq += 1;
30
+ const entry = { i: this.seq };
31
+ for (const f of FIELDS) {
32
+ if (f !== "i") entry[f] = fields[f] ?? "";
33
+ }
34
+ this.sink(JSON.stringify(entry) + "\n");
35
+ return entry;
36
+ }
37
+ }
38
+
39
+ /** Parse JSONL journal text into an array of entry objects. */
40
+ export function parseJournal(text) {
41
+ const entries = [];
42
+ for (const [n, raw] of text.split("\n").entries()) {
43
+ const line = raw.trim();
44
+ if (!line) continue;
45
+ let entry;
46
+ try {
47
+ entry = JSON.parse(line);
48
+ } catch (e) {
49
+ throw new Error(`journal line ${n + 1}: invalid JSON: ${e.message}`);
50
+ }
51
+ for (const f of REQUIRED) {
52
+ if (!(f in entry)) {
53
+ throw new Error(`journal line ${n + 1}: missing field '${f}'`);
54
+ }
55
+ }
56
+ entries.push(entry);
57
+ }
58
+ return entries;
59
+ }
60
+
61
+ /** Entries whose outcome contradicted their declared expectation. */
62
+ export function mismatches(entries) {
63
+ return entries.filter((e) => e.matched === false);
64
+ }
65
+
66
+ /** Total gasUsed across entries. */
67
+ export function totalGas(entries) {
68
+ return entries.reduce((sum, e) => sum + (e.gas ?? 0), 0);
69
+ }
70
+
71
+ /** One-object rollup of a journal: op counts, reverts, mismatches, gas. */
72
+ export function summarize(entries) {
73
+ const ops = {};
74
+ let reverts = 0;
75
+ for (const e of entries) {
76
+ ops[e.op] = (ops[e.op] ?? 0) + 1;
77
+ if (e.outcome === "revert") reverts += 1;
78
+ }
79
+ return {
80
+ entries: entries.length,
81
+ ops,
82
+ reverts,
83
+ mismatches: mismatches(entries).length,
84
+ gas: totalGas(entries),
85
+ };
86
+ }
package/src/nodefs.js ADDED
@@ -0,0 +1,52 @@
1
+ // Node-only IO glue: repo/artifact discovery and file journal sinks.
2
+ // Keep environment-specific imports here so session.js/journal.js/v3.js
3
+ // stay loadable in the browser (Phase 4).
4
+
5
+ import { readFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+
8
+ import { JournalWriter } from "./journal.js";
9
+
10
+ /** The repository checkout root: ALBERTA_BUCK_REPO, or walk up from cwd
11
+ * (then from this file) looking for foundry.toml -- the same resolution
12
+ * the Python side uses (buck_core.artifacts.repo_root). */
13
+ export function repoRoot() {
14
+ const starts = [];
15
+ if (process.env.ALBERTA_BUCK_REPO) starts.push(process.env.ALBERTA_BUCK_REPO);
16
+ starts.push(process.cwd(), dirname(new URL(import.meta.url).pathname));
17
+ for (const start of starts) {
18
+ let p = resolve(start);
19
+ for (;;) {
20
+ if (existsSync(join(p, "foundry.toml"))) return p;
21
+ const up = dirname(p);
22
+ if (up === p) break;
23
+ p = up;
24
+ }
25
+ }
26
+ throw new Error(
27
+ "alberta-buck repo root not found (looked for foundry.toml); " +
28
+ "set ALBERTA_BUCK_REPO or run from inside the repo checkout");
29
+ }
30
+
31
+ /** Return {abi, bytecode} for out/<solFile or name>.sol/<name>.json. */
32
+ export function loadArtifact(name, solFile = null) {
33
+ const f = join(repoRoot(), "out", `${solFile ?? name}.sol`, `${name}.json`);
34
+ const art = JSON.parse(readFileSync(f, "utf8"));
35
+ return { abi: art.abi, bytecode: art.bytecode.object };
36
+ }
37
+
38
+ /** True when the Foundry artifacts are built (tests skip when not). */
39
+ export function artifactsAvailable() {
40
+ try {
41
+ loadArtifact("MockERC20");
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /** A JournalWriter appending JSONL lines to `path`. */
49
+ export function fileJournalWriter(path) {
50
+ mkdirSync(dirname(resolve(path)), { recursive: true });
51
+ return new JournalWriter((line) => appendFileSync(path, line));
52
+ }
package/src/prices.js ADDED
@@ -0,0 +1,38 @@
1
+ // Seeded synthetic price walk -- deterministic BigInt-only math so any
2
+ // platform (or a re-run) reproduces the same path bit-for-bit. The demo's
3
+ // stand-in for the Python sim's CSV Brownian-bridge quotes; the Stage-4
4
+ // parity mini-scenario pins its targets in core/vectors/mini-scenario.json
5
+ // instead (no cross-language RNG at all).
6
+
7
+ /** A 64-bit LCG (MMIX constants) over BigInt. */
8
+ export function lcg(seed) {
9
+ let s = BigInt(seed) & ((1n << 64n) - 1n);
10
+ return () => {
11
+ s = (s * 6364136223846793005n + 1442695040888963407n) & ((1n << 64n) - 1n);
12
+ return s;
13
+ };
14
+ }
15
+
16
+ /**
17
+ * A multiplicative random walk: each step moves the price by a draw in
18
+ * [-stepBp, +stepBp] basis points.
19
+ *
20
+ * @param opts.seed walk seed
21
+ * @param opts.start starting price (quote base units per whole token)
22
+ * @param opts.stepBp max per-step move in basis points (default 150)
23
+ * @param opts.steps path length
24
+ * @returns BigInt[] of length `steps` (walk[0] === start)
25
+ */
26
+ export function seededWalk({ seed, start, stepBp = 150, steps }) {
27
+ const next = lcg(seed);
28
+ const out = [BigInt(start)];
29
+ const span = BigInt(2 * stepBp + 1);
30
+ while (out.length < steps) {
31
+ const delta = (next() % span) - BigInt(stepBp); // [-stepBp, +stepBp]
32
+ const prev = out[out.length - 1];
33
+ let p = (prev * (10_000n + delta)) / 10_000n;
34
+ if (p < 1n) p = 1n;
35
+ out.push(p);
36
+ }
37
+ return out;
38
+ }
package/src/router.js ADDED
@@ -0,0 +1,76 @@
1
+ // REAL Uniswap periphery for JS worlds: the Universal Router deploy
2
+ // recipe the Python sim proved on anvil (deploy.py), and the
3
+ // V3_SWAP_EXACT_IN encoding (mirroring alberta_buck/sim/router.py).
4
+ //
5
+ // The pools were always real (UniswapV3Factory/Pool); SimLP only stood in
6
+ // for the periphery. Doctrine: SimLP remains for simulation god-modes
7
+ // (whale pinning, liquidity seeding); agent and USER swaps go through the
8
+ // real router -- the code path an integrator copies, and the surface real
9
+ // JS AMM tooling (@uniswap/v3-sdk et al.) can be pointed at.
10
+ //
11
+ // Wiring facts (proven by the Python sim, mirrored here):
12
+ // * permit2 is a DUMMY address: swaps use the pre-fund route
13
+ // (transfer the input to the router, execute with payerIsUser=false),
14
+ // so Permit2 is never consulted. Real Permit2 is optional follow-up
15
+ // for signature-based approvals.
16
+ // * poolInitCodeHash = keccak of OUR compiled UniswapV3Pool creation
17
+ // bytecode -- the router computes pool addresses via CREATE2, and the
18
+ // locally-built pool hash differs from mainnet's canonical one.
19
+ // * in BUCK worlds the router must be identity-bound public+carrying
20
+ // (it transiently custodies BUCK mid-route); deploy.py shows the bind.
21
+
22
+ import { encodeAbiParameters, encodePacked, keccak256 } from "viem";
23
+
24
+ export const ZERO_ADDR = "0x" + "00".repeat(20);
25
+ export const DUMMY_PERMIT2 = "0x" + "be".repeat(20); // pre-fund route: unused
26
+
27
+ /**
28
+ * Deploy the Universal Router from the vendored artifact
29
+ * (alberta_buck/sim/artifacts/UniversalRouter.json), wired exactly as the
30
+ * Python sim wires it. RouterParameters field NAMES come from the
31
+ * artifact's ABI; the VALUES follow deploy.py's order.
32
+ *
33
+ * @param session a Session (deployer pays)
34
+ * @param urArtifact the parsed UniversalRouter.json artifact
35
+ * @param opts.weth WETH9 address (unused by V3 swaps; real for fidelity)
36
+ * @param opts.v3Factory our UniswapV3Factory address
37
+ * @param opts.poolInitCode our UniswapV3Pool CREATION bytecode (0x-hex)
38
+ */
39
+ export async function deployUniversalRouter(session, urArtifact,
40
+ { weth, v3Factory, poolInitCode, gas = 15_000_000n }) {
41
+ const ctor = urArtifact.abi.find((e) => e.type === "constructor");
42
+ const comps = ctor.inputs[0].components;
43
+ const vals = [DUMMY_PERMIT2, weth, ZERO_ADDR, v3Factory,
44
+ "0x" + "00".repeat(32), keccak256(poolInitCode),
45
+ ZERO_ADDR, ZERO_ADDR, ZERO_ADDR, ZERO_ADDR];
46
+ if (comps.length !== vals.length) {
47
+ throw new Error(`UniversalRouter constructor has ${comps.length} ` +
48
+ `params; the deploy.py recipe wires ${vals.length}`);
49
+ }
50
+ const params = Object.fromEntries(comps.map((c, i) => [c.name, vals[i]]));
51
+ // Accept the raw forge-artifact shape ({bytecode: {object}}) and the
52
+ // flattened bundle shape ({bytecode: "0x.."}) alike.
53
+ const bytecode = urArtifact.bytecode.object ?? urArtifact.bytecode;
54
+ return session.deploy(
55
+ { abi: urArtifact.abi, bytecode },
56
+ [params], { name: "UniversalRouter", gas });
57
+ }
58
+
59
+ /** [tokenA, fee0, tokenB, fee1, tokenC, ...] -> packed V3 path bytes. */
60
+ export function encodePath(tokensFees) {
61
+ const types = tokensFees.map((_, i) => (i % 2 === 0 ? "address" : "uint24"));
62
+ return encodePacked(types, tokensFees);
63
+ }
64
+
65
+ /**
66
+ * (commands, inputs) for UniversalRouter.execute V3_SWAP_EXACT_IN on the
67
+ * pre-fund route (payerIsUser=false: the router pays from its own
68
+ * balance, so transfer amountIn to the router first).
69
+ */
70
+ export function urExecArgs(recipient, amountIn, path) {
71
+ const input = encodeAbiParameters(
72
+ [{ type: "address" }, { type: "uint256" }, { type: "uint256" },
73
+ { type: "bytes" }, { type: "bool" }, { type: "uint256[]" }],
74
+ [recipient, amountIn, 0n, path, false, []]);
75
+ return ["0x00", [input]]; // 0x00 = V3_SWAP_EXACT_IN
76
+ }