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 +66 -0
- package/package.json +49 -0
- package/src/agents/arb.js +61 -0
- package/src/agents/saver.js +72 -0
- package/src/agents/trader.js +70 -0
- package/src/agents/whale.js +45 -0
- package/src/backends.js +54 -0
- package/src/buckworld.js +198 -0
- package/src/chart.js +70 -0
- package/src/identity-core.js +248 -0
- package/src/identity-web.js +19 -0
- package/src/identity.js +32 -0
- package/src/index.js +13 -0
- package/src/journal.js +86 -0
- package/src/nodefs.js +52 -0
- package/src/prices.js +38 -0
- package/src/router.js +76 -0
- package/src/scenarios/eqworld.js +517 -0
- package/src/scenarios/mini.js +35 -0
- package/src/scenarios/onepool.js +59 -0
- package/src/session.js +122 -0
- package/src/v3.js +48 -0
- package/src/wallet-core.js +75 -0
- package/src/wallet.js +23 -0
- package/src/world.js +21 -0
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
|
+
}
|
package/src/backends.js
ADDED
|
@@ -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
|
+
}
|
package/src/buckworld.js
ADDED
|
@@ -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
|
+
}
|