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/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # alberta-buck-core
2
+
3
+ The JavaScript side of the Alberta Buck platform: one testing API over every
4
+ EVM backend, an agent simulation platform that runs in a browser, and
5
+ BigInt-native wrappers around the WebAssembly kernels.
6
+
7
+ - **`Session`** -- the ChainSession API. Send, call and deploy against Tevm
8
+ in-process, anvil, or a public testnet, with the same code. Every
9
+ state-changing operation declares whether it is expected to succeed or
10
+ revert, so expectation mismatches are first-class results rather than
11
+ surprises.
12
+ - **Journal** -- an append-only JSONL record of every operation: tag, op,
13
+ sender, expectation, outcome, gas, tx hash, block, error. Journals make
14
+ runs auditable, diffable across backends *and across languages* (the
15
+ Python platform writes the same schema), and replayable for demos.
16
+ - **Agents and worlds** -- `runDays`, whales, round-trip traders, and the
17
+ scenario builders that stand up a pool world.
18
+ - **Kernel wrappers** -- `identity.js` and `wallet.js` wrap
19
+ [`alberta-buck-kernel`](https://www.npmjs.com/package/alberta-buck-kernel)
20
+ in a BigInt-native API; `identity-web.js` does the same for the browser,
21
+ taking an explicit wasm source because bundlers break wasm-bindgen's
22
+ default relative fetch.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ npm install alberta-buck-core
28
+ ```
29
+
30
+ ## Use
31
+
32
+ ```js
33
+ import { tevmSession, runDays } from "alberta-buck-core";
34
+
35
+ const session = await tevmSession(); // or anvilSession(...)
36
+ // ... deploy contracts, then let agents run simulated days
37
+ ```
38
+
39
+ ```js
40
+ import identity from "alberta-buck-core/identity";
41
+
42
+ const scalar = identity.reduceModOrder(12345n);
43
+ ```
44
+
45
+ Subpaths map onto the modules directly: `alberta-buck-core/session`,
46
+ `/journal`, `/identity`, `/wallet`, `/world`, `/v3`, and so on.
47
+
48
+ Contract artifacts are not bundled here. A world needs the compiled
49
+ contracts, which ship separately so that Uniswap's own artifacts come from
50
+ Uniswap rather than from us.
51
+
52
+ ## Status
53
+
54
+ 0.1.0, prototype. Unaudited software for a monetary system, published so the
55
+ simulation and identity work can be reproduced and built on -- not for
56
+ custody of anything real.
57
+
58
+ ## Licence
59
+
60
+ CAL-1.0 (Cryptographic Autonomy License v1.0). Beyond the usual copyleft,
61
+ CAL requires that anyone you provide this software's functionality to
62
+ receives their own data and is not locked out of it by cryptographic or
63
+ technical means. See
64
+ [LICENSING.md](https://github.com/alberta-buck/alberta-buck/blob/master/LICENSING.md).
65
+
66
+ Repository: <https://github.com/alberta-buck/alberta-buck>
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "alberta-buck-core",
3
+ "version": "0.1.1",
4
+ "description": "Alberta Buck core platform (JS): the ChainSession API over Tevm/anvil/testnets with expectation journaling, the agent simulation platform, and the BigInt-native wrappers around the WebAssembly kernels",
5
+ "type": "module",
6
+ "license": "CAL-1.0",
7
+ "author": "Perry Kundert <perry@dominionrnd.com>",
8
+ "homepage": "https://github.com/alberta-buck/alberta-buck",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/alberta-buck/alberta-buck.git",
12
+ "directory": "core/js"
13
+ },
14
+ "bugs": "https://github.com/alberta-buck/alberta-buck/issues",
15
+ "keywords": [
16
+ "ethereum",
17
+ "stablecoin",
18
+ "simulation",
19
+ "viem",
20
+ "tevm"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "files": [
26
+ "src",
27
+ "README.md"
28
+ ],
29
+ "workspaces": [
30
+ "kernel"
31
+ ],
32
+ "scripts": {
33
+ "test": "node --test test/"
34
+ },
35
+ "exports": {
36
+ ".": "./src/index.js",
37
+ "./*": "./src/*.js"
38
+ },
39
+ "dependencies": {
40
+ "alberta-buck-kernel": "0.1.1",
41
+ "tevm": "^1.0.0-next.149",
42
+ "viem": "^2.54.2"
43
+ },
44
+ "devDependencies": {
45
+ "buffer": "^6.0.3",
46
+ "esbuild": "^0.28.1",
47
+ "wasm-pack": "^0.15.0"
48
+ }
49
+ }
@@ -0,0 +1,61 @@
1
+ // ParityArb: keeps the pool TRIANGLE consistent -- TOKEN/BUCK vs the
2
+ // price implied by TOKEN/USDC x BUCK/USDC. When the basket pool drifts
3
+ // from the implied price by more than thresholdBp, it routes the
4
+ // profitable triangle through the REAL Universal Router (both legs
5
+ // start and end in USDC, so the arb needs no identity: BUCK only
6
+ // passes through the bound router mid-hop), sized small and iterated
7
+ // until the gap closes or maxIters runs out.
8
+ //
9
+ // This is the agent that makes basketValueInBuck MEAN something: the
10
+ // controller reads the TOKEN/BUCK pools, the whale pins TOKEN/USDC to
11
+ // the reference feeds, and the arb ties the two together -- so BUCK
12
+ // supply/demand pressure in the floating BUCK/USDC pool propagates
13
+ // into the observable the PID defends.
14
+
15
+ export class ParityArb {
16
+ /**
17
+ * @param opts.size USDC base units per triangle leg
18
+ * @param opts.thresholdBp act when |divergence| exceeds this
19
+ * @param opts.maxIters triangles per token per tick
20
+ */
21
+ constructor({ size = 2_000n * 10n ** 6n, thresholdBp = 100n,
22
+ maxIters = 3 } = {}) {
23
+ Object.assign(this, { size, thresholdBp, maxIters });
24
+ this.triangles = 0; // ops counter, for the curious
25
+ }
26
+
27
+ async setup(world) {
28
+ this.me = world.session.account; // the deployer account arbs
29
+ await world.fiatIn(this.me, 10_000_000n * 10n ** 6n,
30
+ { tag: "arb:endow" });
31
+ }
32
+
33
+ async act(world, day, tick) {
34
+ if (tick !== 0) return;
35
+ const { usdc, buck, fees } = world;
36
+ for (let i = 0; i < world.tokens.length; i++) {
37
+ const t = world.tokens[i];
38
+ for (let iter = 0; iter < this.maxIters; iter++) {
39
+ const [pTU, pUB, pTB] = await Promise.all(
40
+ [world.spotUsd(i), world.spotUB(), world.spotBuck(i)]);
41
+ const implied = (pTU * 1_000_000n) / pUB;
42
+ const div = ((pTB - implied) * 10_000n) / implied;
43
+ if (div > -this.thresholdBp && div < this.thresholdBp) break;
44
+
45
+ // TOKEN rich in BUCK: buy it direct, sell it via the BUCK legs
46
+ // (and vice versa). Both directions push pTB toward implied.
47
+ const inPath = div > 0n
48
+ ? [usdc.address, fees.usdc, t.erc20.address]
49
+ : [usdc.address, fees.ub, buck.address, fees.buck, t.erc20.address];
50
+ const outPath = div > 0n
51
+ ? [t.erc20.address, fees.buck, buck.address, fees.ub, usdc.address]
52
+ : [t.erc20.address, fees.usdc, usdc.address];
53
+ const got = await world.route(inPath, this.size, this.me,
54
+ { tag: `arb:${t.sym}:in:d${day}` });
55
+ await world.route(outPath, got, this.me,
56
+ { tag: `arb:${t.sym}:out:d${day}` });
57
+ this.triangles++;
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,72 @@
1
+ // BasketSaver: the live evolution of prototypes/basket-investor.js --
2
+ // USDC wealth deployed into the BuckBasket for a term, acquiring the
3
+ // constituent TOKEN via whichever route quotes the better after-swap
4
+ // valuation (direct, or through BUCK -- world.buyTokenBest). A single
5
+ // registered EOA: deposits need no identity, but redemption pays BUCK,
6
+ // so setup runs the real onboarding ceremony.
7
+ //
8
+ // ROI accounting follows the Python DM agents: USD committed vs USD
9
+ // realized, with dollar-days so returns annualize
10
+ // (profitUsd * 365n / dollarDays).
11
+
12
+ import { fundAccount, onboard } from "../buckworld.js";
13
+
14
+ export class BasketSaver {
15
+ /**
16
+ * @param opts.account fresh viem account (funded + onboarded in setup)
17
+ * @param opts.budget USDC base units to deploy per cycle
18
+ * @param opts.holdDays days from deposit to redemption
19
+ * @param opts.tokenIdx which basket constituent to acquire
20
+ * @param opts.fields identity KYC fields (defaults provided)
21
+ */
22
+ constructor({ account, budget = 25_000n * 10n ** 6n, holdDays = 60,
23
+ tokenIdx = 0, fields = null }) {
24
+ Object.assign(this, { account, budget, holdDays, tokenIdx, fields });
25
+ this.position = null;
26
+ this.ledger = []; // {day, event, usd} -- the ROI chart
27
+ this.profitUsd = 0n;
28
+ this.dollarDays = 0n;
29
+ }
30
+
31
+ async setup(world) {
32
+ await fundAccount(world, this.account.address);
33
+ await onboard(world, this.account, this.fields ?? {
34
+ given_name: "Sana", family_name: "Saver",
35
+ id_type: "Alberta Identity Card", id_number: "AIC-2026-0055501",
36
+ jurisdiction: "Alberta, Canada", date_of_birth: "1991-05-05",
37
+ issued_at: "2026-07-04T00:00:00Z", epoch: 42,
38
+ });
39
+ await world.fiatIn(this.account, this.budget, { tag: "saver:endow" });
40
+ this.usdc = this.budget;
41
+ }
42
+
43
+ async act(world, day, tick) {
44
+ if (tick !== 0) return;
45
+
46
+ if (!this.position) {
47
+ if (this.usdc < 1_000n * 10n ** 6n) return; // spent: hold
48
+ const spend = this.usdc;
49
+ const { out, viaBuck } = await world.buyTokenBest(
50
+ this.tokenIdx, spend, this.account, { tag: `saver:acquire:d${day}` });
51
+ const { receiptId } = await world.basketDeposit(
52
+ this.tokenIdx, out, this.account, { tag: `saver:deposit:d${day}` });
53
+ this.usdc = 0n;
54
+ this.position = { receiptId, day, costUsd: spend };
55
+ this.ledger.push({ day, event: "deposit", usd: -spend, viaBuck });
56
+ return;
57
+ }
58
+
59
+ const mark = await world.receiptValueUsd(this.position.receiptId, day);
60
+ this.ledger.push({ day, event: "mark", usd: mark });
61
+
62
+ if (day - this.position.day >= this.holdDays) {
63
+ const gotUsd = await world.basketRedeem(
64
+ this.position.receiptId, this.account, { tag: `saver:redeem:d${day}` });
65
+ this.profitUsd += gotUsd - this.position.costUsd;
66
+ this.dollarDays += this.position.costUsd
67
+ * BigInt(day - this.position.day);
68
+ this.ledger.push({ day, event: "redeem", usd: gotUsd });
69
+ this.position = null; // holds the redeemed assets; one cycle v1
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,70 @@
1
+ // RoundTripTrader: the walking-skeleton "arb-shaped" agent. Each day it
2
+ // sells a fixed TOKEN amount into the pool and buys back with the quote
3
+ // proceeds -- two real V3 swaps through SimLP, both journaled. On day 0
4
+ // it also demonstrates a DECLARED failure: a swap whose price limit is on
5
+ // the wrong side, sent with expect:"revert" (Uniswap's SPL check), which
6
+ // the journal records as matched rather than an error.
7
+ //
8
+ // Swap mechanics (SimLP pays the pool from its own balance): fund SimLP
9
+ // with the input token, then SimLP.swap(pool, recipient=self, ...) sends
10
+ // the output to the trader.
11
+
12
+ import { MIN_SQRT_RATIO, MAX_SQRT_RATIO } from "../v3.js";
13
+
14
+ export class RoundTripTrader {
15
+ /**
16
+ * @param opts.simlp SimLP handle @param opts.pool pool handle
17
+ * @param opts.token TOKEN handle (ERC20 abi) -- the trader's base asset
18
+ * @param opts.quote quote handle (ERC20 abi)
19
+ * @param opts.amount TOKEN base units to round-trip each day
20
+ * @param opts.account viem Account for this agent (defaults to session's)
21
+ */
22
+ constructor({ simlp, pool, token, quote, amount, account = undefined }) {
23
+ Object.assign(this, { simlp, pool, token, quote, amount, account });
24
+ }
25
+
26
+ async setup(world) {
27
+ this.tokenIs0 =
28
+ this.token.address.toLowerCase() < this.quote.address.toLowerCase();
29
+ this.me = (this.account ?? world.session.account).address;
30
+ this.opts = { account: this.account };
31
+ }
32
+
33
+ #swapArgs(zeroForOne, amountIn) {
34
+ // exact-in (positive amount), price limit wide open on the swap side
35
+ const limit = zeroForOne ? MIN_SQRT_RATIO + 1n : MAX_SQRT_RATIO - 1n;
36
+ const [t0, t1] = this.tokenIs0
37
+ ? [this.token.address, this.quote.address]
38
+ : [this.quote.address, this.token.address];
39
+ return [this.pool.address, this.me, zeroForOne, amountIn, limit, t0, t1];
40
+ }
41
+
42
+ async act(world, day, tick) {
43
+ if (tick !== 0) return;
44
+ const s = world.session;
45
+
46
+ if (day === 0) {
47
+ // Declared failure: zeroForOne with a limit ABOVE spot always
48
+ // violates Uniswap's sqrt-price-limit require ("SPL").
49
+ const bad = this.#swapArgs(true, this.amount);
50
+ bad[4] = MAX_SQRT_RATIO - 1n;
51
+ await s.send(this.simlp, "swap", bad,
52
+ { ...this.opts, expect: "revert", tag: `trader:spl-demo:d${day}` });
53
+ }
54
+
55
+ // Sell TOKEN -> quote: fund SimLP, swap with self as recipient.
56
+ const quoteBefore = await s.call(this.quote, "balanceOf", [this.me]);
57
+ await s.send(this.token, "transfer", [this.simlp.address, this.amount],
58
+ { ...this.opts, tag: `trader:fund:d${day}` });
59
+ await s.send(this.simlp, "swap", this.#swapArgs(this.tokenIs0, this.amount),
60
+ { ...this.opts, tag: `trader:sell:d${day}` });
61
+ const quoteAfter = await s.call(this.quote, "balanceOf", [this.me]);
62
+ const proceeds = quoteAfter - quoteBefore;
63
+
64
+ // Buy back TOKEN with the proceeds.
65
+ await s.send(this.quote, "transfer", [this.simlp.address, proceeds],
66
+ { ...this.opts, tag: `trader:refund:d${day}` });
67
+ await s.send(this.simlp, "swap", this.#swapArgs(!this.tokenIs0, proceeds),
68
+ { ...this.opts, tag: `trader:buyback:d${day}` });
69
+ }
70
+ }
@@ -0,0 +1,45 @@
1
+ // PinWhale: snaps a TOKEN/quote V3 pool to a reference price path --
2
+ // the JS peer of the Python sim's MarketMakerWhale.snap().
3
+ //
4
+ // The V3 trick (identical to agents.py): swap an effectively unlimited
5
+ // amount with sqrtPriceLimitX96 = the target -- the pool executes exactly
6
+ // enough volume to land on the limit. Paid from the SimLP helper's own
7
+ // token custody, so the whale only needs SimLP to be stocked (the Python
8
+ // deploy leaves it holding deep TOKEN+quote reserves).
9
+
10
+ import { HUGE, sqrtPriceX96 } from "../v3.js";
11
+
12
+ export class PinWhale {
13
+ /**
14
+ * @param opts.simlp SimLP contract handle
15
+ * @param opts.pool UniswapV3Pool handle (TOKEN/quote)
16
+ * @param opts.token TOKEN address @param opts.tokenDec its decimals
17
+ * @param opts.quote quote address (e.g. USDC)
18
+ * @param opts.refPrice (day) => quote base units per whole TOKEN
19
+ */
20
+ constructor({ simlp, pool, token, tokenDec, quote, refPrice }) {
21
+ Object.assign(this, { simlp, pool, token, tokenDec, quote, refPrice });
22
+ }
23
+
24
+ async setup(world) {
25
+ const [t0, t1] = await Promise.all([
26
+ world.session.call(this.pool, "token0"),
27
+ world.session.call(this.pool, "token1"),
28
+ ]);
29
+ this.t0 = t0;
30
+ this.t1 = t1;
31
+ }
32
+
33
+ async act(world, day, tick) {
34
+ if (tick !== 0) return; // one snap per day
35
+ const slot0 = await world.session.call(this.pool, "slot0");
36
+ const cur = slot0[0]; // sqrtPriceX96
37
+ const targ = sqrtPriceX96(this.token, 10n ** BigInt(this.tokenDec),
38
+ this.quote, this.refPrice(day));
39
+ if (cur === targ) return;
40
+ await world.session.send(this.simlp, "swap",
41
+ [this.pool.address, this.simlp.address, targ < cur, HUGE, targ,
42
+ this.t0, this.t1],
43
+ { tag: `whale:snap:d${day}` });
44
+ }
45
+ }
@@ -0,0 +1,54 @@
1
+ // Session constructors for the two Phase 1 backends.
2
+ //
3
+ // anvilSession(url) -- join a running anvil (e.g. one a Python sim
4
+ // deployed into): HTTP transport, zero-fee legacy
5
+ // txs matching the sim's anvil profile.
6
+ // tevmSession() -- standalone in-process EVM (Tevm MemoryClient);
7
+ // the same session/agents run wholly in JS.
8
+ //
9
+ // Both sign with well-known anvil/tevm dev accounts (mnemonic "test test
10
+ // ... junk"), so one code path -- local-account raw txs -- serves both.
11
+
12
+ import { createPublicClient, createWalletClient, http, publicActions } from "viem";
13
+ import { privateKeyToAccount } from "viem/accounts";
14
+
15
+ import { Session } from "./session.js";
16
+
17
+ // The standard prefunded dev accounts (anvil and tevm share them).
18
+ export const DEV_KEYS = [
19
+ "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
20
+ "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
21
+ "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
22
+ "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6",
23
+ "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
24
+ "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba",
25
+ "0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e",
26
+ "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356",
27
+ "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97",
28
+ "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6",
29
+ ];
30
+
31
+ export function devAccount(index = 0) {
32
+ return privateKeyToAccount(DEV_KEYS[index]);
33
+ }
34
+
35
+ /** Join a running anvil node (the Python sim's zero-fee profile). */
36
+ export function anvilSession(url, { accountIndex = 9, journal = null } = {}) {
37
+ const client = createWalletClient({ transport: http(url) })
38
+ .extend(publicActions);
39
+ return new Session(client, {
40
+ account: devAccount(accountIndex),
41
+ journal,
42
+ txOverrides: { gasPrice: 0n }, // anvil runs --base-fee 0 --gas-price 0
43
+ });
44
+ }
45
+
46
+ /** Standalone in-process EVM. Import lazily: tevm is a heavy dependency. */
47
+ export async function tevmSession({ accountIndex = 0, journal = null } = {}) {
48
+ const { createMemoryClient } = await import("tevm");
49
+ const client = createMemoryClient();
50
+ return new Session(client, {
51
+ account: devAccount(accountIndex),
52
+ journal,
53
+ });
54
+ }
@@ -0,0 +1,198 @@
1
+ // buildBuckWorld: the REAL BUCK stack on any session (Tevm in-process or
2
+ // anvil), mirroring the Python sim's deploy.py order -- IdentityRegistry
3
+ // (+ a kernel-keyed trusted issuer), BuckCredit, BuckKControllerDirect,
4
+ // Buck, and their wiring. The V3 pool world composes alongside via
5
+ // buildOnePool (scenarios/onepool.js); the basket lands with the
6
+ // background-rebalancing stage.
7
+ //
8
+ // Identity onboarding runs the FULL wallet ceremony on the wasm
9
+ // buck-identity kernel (canonical identity -> m -> PS sign/rerandomize ->
10
+ // ElGamal -> registration NIZK) and the real Solidity verifier accepts it
11
+ // on-chain -- the capability the in-browser demo rests on.
12
+ //
13
+ // Environment-free by injection (the artifacts(name) pattern): pass the
14
+ // identity kernel API as opts.identity -- node code loads it from
15
+ // ./identity.js, the browser from ./identity-web.js -- and optionally
16
+ // opts.rng (a () => bigint scalar drawer) to reproduce a world; the
17
+ // default draws WebCrypto.
18
+
19
+ // BN254.sol struct components are UPPERCASE X/Y at the ABI.
20
+ const g = (p) => ({ X: p.x, Y: p.y });
21
+ const ct = (E) => ({ R: g(E.R), C: g(E.C) });
22
+ const g2 = (p) => ({ X: [p.x[0], p.x[1]], Y: [p.y[0], p.y[1]] });
23
+
24
+ // Controller deploy defaults -- the SAME values experiment.deploy_params
25
+ // derives for the Python sim (gains real*1e12: Kp = kp_frac*dk_rail/e_max
26
+ // = 0.1 -> 1e11; Ki = dk_rail/(e_max*tau_I) = 0.5/777600s -> 643004;
27
+ // dt 1800s; K rails 0..0.95, K0 0.75).
28
+ export const DEPLOY_DEFAULTS = {
29
+ kp: 100_000_000_000n,
30
+ ki: 643_004n,
31
+ kd: 0n,
32
+ dt: 1800n,
33
+ kmin: 0n,
34
+ kmax: 950_000_000_000_000_000n,
35
+ k0: 750_000_000_000_000_000n,
36
+ };
37
+
38
+ export const DAY = 86_400;
39
+
40
+ /**
41
+ * Deploy the BUCK identity + monetary stack.
42
+ *
43
+ * @param session a Session (deployer = session.account)
44
+ * @param artifacts (name) => {abi, bytecode}
45
+ * @param opts.identity the buck-identity kernel API (REQUIRED: import
46
+ * from ./identity.js in node, loadIdentity() in the browser)
47
+ * @param opts.gov governance address (default: deployer)
48
+ * @param opts.poolAcct funding-pool address (default: deployer)
49
+ * @param opts.params controller overrides over DEPLOY_DEFAULTS
50
+ * @param opts.rng scalar drawer for the issuer PS keypair
51
+ * @returns world {session, artifacts, id, reg, credit, kctrl, buck, gov,
52
+ * poolAcct, issuer:{addr, skX, skY, pkX, pkY}}
53
+ */
54
+ export async function buildBuckWorld(session, artifacts, opts = {}) {
55
+ const id = opts.identity;
56
+ if (!id) {
57
+ throw new Error(
58
+ "buildBuckWorld needs opts.identity (the buck-identity kernel API: " +
59
+ "import from src/identity.js in node, loadIdentity() in the browser)");
60
+ }
61
+ const gas = 15_000_000n;
62
+ const gov = opts.gov ?? session.account.address;
63
+ const poolAcct = opts.poolAcct ?? session.account.address;
64
+ const p = { ...DEPLOY_DEFAULTS, ...(opts.params ?? {}) };
65
+ const rng = opts.rng ?? id.randScalar;
66
+
67
+ // --- identity layer (deploy.py order) ---------------------------------
68
+ const reg = await session.deploy(artifacts("IdentityRegistry"), [gov],
69
+ { name: "IdentityRegistry", gas });
70
+ const [skX, skY] = [rng(), rng()];
71
+ const issuer = {
72
+ addr: "0x" + "15".repeat(20), // the trust-anchor address
73
+ skX, skY,
74
+ pkX: id.g2Mul(id.G2, skX),
75
+ pkY: id.g2Mul(id.G2, skY),
76
+ };
77
+ await session.send(reg, "trustIssuer",
78
+ [issuer.addr, { X: g2(issuer.pkX), Y: g2(issuer.pkY) }],
79
+ { tag: "world:trustIssuer" });
80
+
81
+ // --- Direct BUCK stack -------------------------------------------------
82
+ const credit = await session.deploy(artifacts("BuckCredit"), [],
83
+ { name: "BuckCredit", gas });
84
+ const kctrl = await session.deploy(artifacts("BuckKControllerDirect"),
85
+ [p.kp, p.ki, p.kd, p.dt, p.kmin, p.kmax, p.k0, gov],
86
+ { name: "BuckKControllerDirect", gas });
87
+ const buck = await session.deploy(artifacts("Buck"),
88
+ [credit.address, kctrl.address, reg.address, poolAcct],
89
+ { name: "Buck", gas });
90
+ await session.send(reg, "setBuck", [buck.address], { tag: "world:reg.setBuck" });
91
+ await session.send(credit, "setBuck", [buck.address], { tag: "world:credit.setBuck" });
92
+
93
+ return { session, artifacts, id, reg, credit, kctrl, buck, gov, poolAcct, issuer };
94
+ }
95
+
96
+ /**
97
+ * Register `account` with a REAL cryptographic identity: the full wallet
98
+ * ceremony on the wasm kernel, verified on-chain by IdentityRegistry.
99
+ *
100
+ * @param world from buildBuckWorld
101
+ * @param account a viem account (signs the register tx)
102
+ * @param fields identity KYC fields (issuer_id is stamped)
103
+ * @param opts.rng scalar drawer (default WebCrypto)
104
+ * @returns handle {account, fields, canonical, m, M, kp:{sk, pk}, E}
105
+ */
106
+ export async function onboard(world, account, fields, opts = {}) {
107
+ const id = world.id;
108
+ const rng = opts.rng ?? id.randScalar;
109
+ const full = { ...fields, issuer_id: fields.issuer_id ?? "atb-financial-ca" };
110
+ const canonical = id.canonicalIdentity(full);
111
+ const m = id.identityScalar(canonical);
112
+
113
+ const sigma = id.psSign(world.issuer.skX, world.issuer.skY, m, rng());
114
+ const sigmaP = id.psRerandomize(sigma, rng());
115
+ const sk = rng();
116
+ const pk = id.g1Mul(id.G1, sk);
117
+ const M = id.g1Mul(id.G1, m);
118
+ const r = rng();
119
+ const E = id.elgamalEncrypt(M, pk, r);
120
+ const proof = id.registrationProve(
121
+ sigmaP, m, r, pk, E, BigInt(account.address), rng(), rng());
122
+
123
+ await world.session.send(world.reg, "register", [
124
+ world.issuer.addr, g(pk), ct(E),
125
+ { sigma_1: g(sigmaP.sigma_1), sigma_2: g(sigmaP.sigma_2) },
126
+ { e: proof.e, s_m: proof.s_m, s_r: proof.s_r,
127
+ A_ps: g(proof.A_ps), T_C: g(proof.T_C), T_R: g(proof.T_R) },
128
+ ], { tag: `onboard:${full.given_name ?? account.address}`, gas: 3_000_000n,
129
+ account });
130
+
131
+ return { account, fields: full, canonical, m, M, kp: { sk, pk }, E };
132
+ }
133
+
134
+ /**
135
+ * The identity-approve handshake: `from` re-encrypts their REGISTERED
136
+ * identity under `to`'s key with a Chaum-Pedersen equality proof
137
+ * (IdentityRegistry.verifyApprove), storing the receipt fragment that
138
+ * gates Buck transfers. Private<->private payments need BOTH directions
139
+ * (each party approves the other); a public counterparty is exempt.
140
+ *
141
+ * @returns the re-encryption {R, C} handed to `to` (their decryption key
142
+ * recovers `from.M` -- the bilateral-disclosure half).
143
+ */
144
+ export async function identityApprove(world, from, to, opts = {}) {
145
+ const id = world.id;
146
+ const rng = opts.rng ?? id.randScalar;
147
+ const chainid = BigInt(await world.session.client.getChainId());
148
+ const rPrime = rng();
149
+ const eForTo = id.elgamalEncrypt(from.M, to.kp.pk, rPrime);
150
+ const proof = id.chaumPedersenProve(
151
+ from.E, eForTo, from.kp.pk, to.kp.pk, from.kp.sk, rPrime,
152
+ BigInt(from.account.address), BigInt(to.account.address), chainid,
153
+ rng(), rng());
154
+ await world.session.send(world.buck, "approve", [
155
+ to.account.address, opts.allowance ?? 0n, ct(eForTo),
156
+ { e: proof.e, s1: proof.s1, s2: proof.s2,
157
+ T1: g(proof.T1), T2: g(proof.T2), T3: g(proof.T3) },
158
+ ], { tag: `approve:${from.fields.given_name}->${to.fields.given_name}`,
159
+ gas: 1_000_000n, account: from.account });
160
+ return eForTo;
161
+ }
162
+
163
+ /**
164
+ * Create a BuckCredit NFT for `holder` and activate its face through
165
+ * Buck.mint (zero premium: activates credit headroom; BUCK circulates
166
+ * when the holder draws by transferring).
167
+ */
168
+ export async function createCredit(world, holder, face, opts = {}) {
169
+ const now = (await world.session.client.getBlock()).timestamp;
170
+ await world.session.send(world.credit, "createCredit",
171
+ [holder.account.address, opts.assetClass ?? 0, face,
172
+ opts.floor ?? 0n, opts.depType ?? 0, opts.depRate ?? 0,
173
+ now, opts.premiumRate ?? 0],
174
+ { tag: "world:createCredit" });
175
+ await world.session.send(world.buck, "mint", [face],
176
+ { tag: "world:activate", account: holder.account, gas: 3_000_000n });
177
+ }
178
+
179
+ /** Fund a fresh account with ETH from the deployer (plain value transfer:
180
+ * works on tevm and anvil alike; new demo citizens need gas money). */
181
+ export async function fundAccount(world, to, wei = 10n ** 19n) {
182
+ const hash = await world.session.client.sendTransaction({
183
+ account: world.session.account, to, value: wei,
184
+ gas: 21_000n, chain: null, ...world.session.txOverrides,
185
+ });
186
+ await world.session.client.waitForTransactionReceipt({ hash });
187
+ }
188
+
189
+ /** Jump the chain clock forward and mine one block. */
190
+ export async function advanceTime(world, seconds) {
191
+ const now = (await world.session.client.getBlock()).timestamp;
192
+ await world.session.client.request({
193
+ method: "evm_setNextBlockTimestamp",
194
+ params: [Number(now) + Number(seconds)],
195
+ });
196
+ await world.session.client.request({ method: "evm_mine", params: [] });
197
+ return (await world.session.client.getBlock()).timestamp;
198
+ }