diff --git a/Cargo.lock b/Cargo.lock index 551a4564..c9d3ca73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2227,7 +2227,9 @@ dependencies = [ "stablecoin-methods", "stablecoin_core", "token-methods", + "token-mint-authority-methods", "token_core", + "token_mint_authority_core", "twap-oracle-methods", "twap_oracle_core", ] @@ -4289,6 +4291,15 @@ dependencies = [ "token_core", ] +[[package]] +name = "token-mint-authority-methods" +version = "0.1.0" +dependencies = [ + "risc0-build", + "risc0-zkvm", + "token_mint_authority_core", +] + [[package]] name = "token_core" version = "0.1.0" @@ -4313,6 +4324,31 @@ dependencies = [ "token_core", ] +[[package]] +name = "token_mint_authority_core" +version = "0.1.0" +dependencies = [ + "borsh", + "lee_core", + "risc0-zkvm", + "serde", + "serde_json", + "spel-framework-macros", +] + +[[package]] +name = "token_mint_authority_program" +version = "0.1.0" +dependencies = [ + "clock_core", + "hex", + "lee_core", + "risc0-binfmt", + "risc0-zkvm", + "token_core", + "token_mint_authority_core", +] + [[package]] name = "token_program" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index baf0e02e..5a4be9a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,9 @@ members = [ "programs/stablecoin/core", "programs/stablecoin", "programs/stablecoin/methods", + "programs/token_mint_authority/core", + "programs/token_mint_authority", + "programs/token_mint_authority/methods", "programs/integration_tests", "tools/idl-gen", "tools/risc0-packager", @@ -27,6 +30,7 @@ exclude = [ "programs/amm/methods/guest", "programs/ata/methods/guest", "programs/stablecoin/methods/guest", + "programs/token_mint_authority/methods/guest", "programs/twap_oracle/methods/guest", # Cycle benchmarks: standalone crate, kept out of --workspace builds/tests/CI. Run on demand # with `cargo test --manifest-path programs/benchmark/Cargo.toml -- --ignored --nocapture`. @@ -47,6 +51,8 @@ twap_oracle_core = { path = "programs/twap_oracle/core" } twap_oracle_program = { path = "programs/twap_oracle" } stablecoin_core = { path = "programs/stablecoin/core" } stablecoin_program = { path = "programs/stablecoin" } +token_mint_authority_core = { path = "programs/token_mint_authority/core" } +token_mint_authority_program = { path = "programs/token_mint_authority" } serde = { version = "1.0", features = ["derive"] } borsh = { version = "1.5", features = ["derive"] } risc0-zkvm = { version = "=3.0.5" } diff --git a/README.md b/README.md index 8e6b6640..e2c1864d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Essential programs for the **Logos Execution Zone (LEZ)** — a zkVM-based execu | **ata** | Associated Token Account program — derives and initializes deterministic token holding accounts for a given owner and token definition | | **stablecoin** | Collateral-backed position program — open collateral positions as a foundation for stablecoin debt issuance | | **twap_oracle** | TWAP oracle — provides canonical on-chain price accounts consumed by other programs (e.g. stablecoin) | +| **token-mint-authority** | Testnet faucet mint authority — holds the mint authority for faucet token definitions and lets any account self-mint a fixed amount, rate-limited to once per day per account and token, via a chained call to the token program | ## Apps @@ -76,7 +77,7 @@ make clippy make fmt # Run unit tests for all programs (no zkVM, no ZK proof generation) -RISC0_DEV_MODE=1 cargo test -p token_program -p amm_program -p ata_program -p stablecoin_program -p twap_oracle_program +RISC0_DEV_MODE=1 cargo test -p token_program -p amm_program -p ata_program -p stablecoin_program -p twap_oracle_program -p token_mint_authority_program # Run integration tests (dev mode skips ZK proof generation) RISC0_DEV_MODE=1 cargo test -p integration_tests @@ -85,13 +86,15 @@ RISC0_DEV_MODE=1 cargo test -p integration_tests make test ``` -Integration tests live in `programs/integration_tests/tests/` and cover `token`, `amm`, and `ata` programs end-to-end through the zkVM using `RISC0_DEV_MODE=1` to skip proof generation. Each test file corresponds to a program: +Integration tests live in `programs/integration_tests/tests/` and cover `token`, `amm`, `ata`, `stablecoin`, and `token-mint-authority` programs end-to-end through the zkVM using `RISC0_DEV_MODE=1` to skip proof generation. Each test file corresponds to a program: - `programs/integration_tests/tests/token.rs` - `programs/integration_tests/tests/amm.rs` - `programs/integration_tests/tests/ata.rs` +- `programs/integration_tests/tests/stablecoin.rs` +- `programs/integration_tests/tests/token_mint_authority.rs` -`stablecoin` and `twap_oracle` are tested via their own unit tests (`cargo test -p stablecoin_program -p twap_oracle_program`). +`twap_oracle` is tested via its own unit tests (`cargo test -p twap_oracle_program`). ## Compile Guest Binaries @@ -119,6 +122,7 @@ wallet deploy-program programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf wallet deploy-program programs/ata/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/ata.bin wallet deploy-program programs/stablecoin/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/stablecoin.bin wallet deploy-program programs/twap_oracle/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/twap_oracle.bin +wallet deploy-program programs/token_mint_authority/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/token_mint_authority.bin ``` To inspect the `ProgramId` of a built binary: @@ -147,6 +151,7 @@ spel generate-idl programs/amm/methods/guest/src/bin/amm.rs > artifacts/amm-idl. spel generate-idl programs/ata/methods/guest/src/bin/ata.rs > artifacts/ata-idl.json spel generate-idl programs/stablecoin/methods/guest/src/bin/stablecoin.rs > artifacts/stablecoin-idl.json spel generate-idl programs/twap_oracle/methods/guest/src/bin/twap_oracle.rs > artifacts/twap_oracle-idl.json +spel generate-idl programs/token_mint_authority/methods/guest/src/bin/token_mint_authority.rs > artifacts/token_mint_authority-idl.json ``` Generated IDL files are committed under `artifacts/`. CI will fail if a program's IDL is missing or out of date. @@ -161,4 +166,5 @@ spel --idl artifacts/amm-idl.json [args...] spel --idl artifacts/ata-idl.json [args...] spel --idl artifacts/stablecoin-idl.json [args...] spel --idl artifacts/twap_oracle-idl.json [args...] +spel --idl artifacts/token_mint_authority-idl.json [args...] ``` diff --git a/apps/amm/.gitignore b/apps/amm/.gitignore index 8aeb9df7..f3d7a099 100644 --- a/apps/amm/.gitignore +++ b/apps/amm/.gitignore @@ -27,3 +27,7 @@ tests/testnet/amm-registry.json # Isolated custom-token store (CUSTOM_TOKEN_CONFIG) — initialized by the setup script # and written by the app during tests/custom-token.mjs tests/testnet/custom-tokens.json + +# Faucet manifest written by the setup script for tests/faucet-swap.mjs (account ids +# + PDAs + bin/IDL paths for the FaucetMint). Regenerated on every setup run. +tests/testnet/faucet.json diff --git a/apps/amm/qml/Main.qml b/apps/amm/qml/Main.qml index b7f590a1..4524f366 100644 --- a/apps/amm/qml/Main.qml +++ b/apps/amm/qml/Main.qml @@ -119,6 +119,9 @@ Item { SwapPage { id: swapPage + // Lets UI tests reach refreshHoldings() to re-pull the account list after + // an out-of-band balance change (e.g. faucet-swap.mjs mints then refreshes). + objectName: "swapPage" anchors.fill: parent visible: navbar.currentIndex === 0 diff --git a/apps/amm/tests/README.md b/apps/amm/tests/README.md index ca55a7fe..13e3eff7 100644 --- a/apps/amm/tests/README.md +++ b/apps/amm/tests/README.md @@ -20,6 +20,15 @@ inspector (framework from deliberately **absent** from the token config) into a Liquidity token slot and verifies the app resolves it, selects it, and **persists** it to the custom-token store — the "add an unlisted token by id" path. No pool / submit involved. +- `faucet-swap.mjs` funds a **brand-new** account ("Token A Holder 2") that starts + with no token A: it `initialize_account`s the account's token A holding, then + submits a **FaucetMint** through the token-mint-authority program (both via `spel`, + out of band from the UI) to mint token A into it — the faucet only mints into an + **existing** holding, so the init must come first — then **refreshes** the UI so + the newly-funded holding appears in the sell picker, selects it, and swaps **A→B**, + verifying the pool reserves changed **on-chain**. Reads `testnet/faucet.json` + (written by the setup) and needs `spel` on `PATH` plus `LEE_WALLET_HOME_DIR` + pointing at the isolated test wallet. ## Isolation @@ -66,6 +75,10 @@ node apps/amm/tests/create-pool.mjs # create the (unseeded) A/C pool node apps/amm/tests/add-liquidity.mjs # add liquidity to the seeded A/B pool node apps/amm/tests/remove-liquidity.mjs # remove 50% from the seeded A/B pool node apps/amm/tests/custom-token.mjs # add token D (unlisted) by id + +# faucet-swap needs spel on PATH + the isolated wallet home (so it can sign the mint) +LEE_WALLET_HOME_DIR=$(pwd)/apps/amm/tests/testnet/.wallet \ + node apps/amm/tests/faucet-swap.mjs # faucet-mint TKA to holder2, refresh, then swap ``` Headless CI variant (no window, launches the app itself, pass/fail only): @@ -99,6 +112,8 @@ nix build .#integration-test -L - `create-pool.mjs` — the end-to-end create-pool UI test (creates the A/C pool). - `add-liquidity.mjs` — the end-to-end add-liquidity UI test (adds to the A/B pool). - `remove-liquidity.mjs` — the end-to-end remove-liquidity UI test (removes 50% from the A/B pool). +- `faucet-swap.mjs` — faucet-mint token A to a fresh account, refresh the UI, then swap A→B. - `testnet/setup-amm-testnet.sh` — isolated testnet + wallet bootstrap (TKA/TKB/TKC, - seeds the A/B pool only). + seeds the A/B pool only; also deploys the faucet, sets every token's mint authority + to the faucet PDA, and writes `testnet/faucet.json` for faucet-swap.mjs). - `qml/`, `cpp/` — the module's own QML/C++ unit tests. diff --git a/apps/amm/tests/faucet-swap.mjs b/apps/amm/tests/faucet-swap.mjs new file mode 100644 index 00000000..d8e7be9b --- /dev/null +++ b/apps/amm/tests/faucet-swap.mjs @@ -0,0 +1,506 @@ +// --------------------------------------------------------------------------- +// AMM UI test — a BRAND-NEW account funds itself via the token-mint-authority +// faucet, then swaps. +// +// The story this proves end-to-end: +// 1. "Token A Holder 2" (the setup's holder2 / holder2-a-holding pair) starts +// with NO token A, so it is absent from the swap's sell-account picker. +// 2. We submit a FaucetMint through the token-mint-authority program (via spel, +// out of band from the UI — the UI has no faucet feature) which mints token A +// to holder2's holding. This is the whole point of setting every test token's +// mint_authority to the faucet PDA in setup-amm-testnet.sh. +// 3. Because the app already loaded its holdings snapshot BEFORE the mint, the new +// holding does not appear on its own — the UI must be REFRESHED. We call +// backend.refreshBalances() (re-scan the wallet from the sequencer) followed by +// swapPage.refreshHoldings() (re-pull tokenHoldings() into the card), after +// which holder2's holding shows up in the sell picker. +// 4. We select holder2's freshly-funded holding and swap token A -> token B, +// verifying the A/B pool reserves changed ON-CHAIN. +// +// Prereqs (same isolated setup as swap.mjs — see apps/amm/README.md and +// apps/amm/tests/testnet/setup-amm-testnet.sh): +// * setup-amm-testnet.sh has run: programs deployed, A/B pool seeded, the faucet +// deployed, and apps/amm/tests/testnet/faucet.json written. +// * the app is running against the isolated test wallet + test token config. +// * `spel` is on PATH and LEE_WALLET_HOME_DIR points at the SAME isolated wallet +// the app uses (defaults to apps/amm/tests/testnet/.wallet if unset), so the +// faucet mint is signed by the same keys. +// --------------------------------------------------------------------------- + +import { resolve } from "node:path"; +import { writeFile } from "node:fs/promises"; +import { readFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; + +// Locate the test framework (see swap.mjs for the rationale / override env var). +const fwRoot = + process.env.LOGOS_QT_MCP || + new URL("../result-mcp", import.meta.url).pathname; +const { test, run } = await import(resolve(fwRoot, "test-framework/framework.mjs")); + +const SELL_AMOUNT = "100"; // same token A sell size swap.mjs uses against this pool + +// Repo root is three levels up from apps/amm/tests/. The manifest stores bin/IDL +// paths repo-relative, so spel is invoked with cwd = repoRoot. +const REPO_ROOT = new URL("../../..", import.meta.url).pathname; +const MANIFEST_PATH = new URL("./testnet/faucet.json", import.meta.url).pathname; + +// The isolated test wallet (matches setup-amm-testnet.sh's TEST_WALLET_HOME). spel +// must sign the FaucetMint with the same keys the app uses. +const WALLET_HOME = + process.env.LEE_WALLET_HOME_DIR || + new URL("./testnet/.wallet", import.meta.url).pathname; +// The isolated wallet stores its keys in the clear (see setup), so spel signs +// non-interactively; feed the test password on stdin anyway in case a build of +// spel prompts, so the test never hangs. +const WALLET_PASSWORD = process.env.TEST_WALLET_PASSWORD ?? "test"; + +// The swap's account model exposes each holding's id as hex (the FFI does +// hex::encode(account_id), and the backend takes *Hex holding ids), while the +// manifest — and the wallet — speak base58. Decode base58 -> 64-char lowercase hex +// so we can match/select holder2's holding in the selector. Both encode the SAME +// 32 bytes big-endian; padStart(64) preserves leading zero bytes (holder2's id has +// one, which is why its base58 starts with '1'). +const B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; +function base58ToHex(s) { + let num = 0n; + for (const ch of s) { + const idx = B58_ALPHABET.indexOf(ch); + if (idx < 0) throw new Error(`invalid base58 character '${ch}' in "${s}"`); + num = num * 58n + BigInt(idx); + } + const hex = num.toString(16).padStart(64, "0"); + if (hex.length !== 64) throw new Error(`"${s}" did not decode to 32 bytes`); + return hex; +} + +// --- manifest --------------------------------------------------------------- + +function loadManifest() { + let raw; + try { + raw = readFileSync(MANIFEST_PATH, "utf8"); + } catch { + throw new Error( + `faucet manifest not found at ${MANIFEST_PATH} — run apps/amm/tests/testnet/setup-amm-testnet.sh first`, + ); + } + const m = JSON.parse(raw); + for (const key of [ + "tokenBin", "tokenIdl", "faucetBin", "faucetIdl", "recipient", "mintAllowance", + "userHolding", "tokenDefinition", "mintAuthority", "clock", + ]) { + if (!m[key]) throw new Error(`faucet manifest is missing "${key}"`); + } + return m; +} + +// --- small helpers over the raw inspector commands (mirrors swap.mjs) -------- + +const ignore = async (fn) => { try { return await fn(); } catch { /* best effort */ } }; + +async function idByObjectName(app, name) { + const res = await app.findByProperty("objectName", name); + if (res.error || !res.matches || res.matches.length === 0) + throw new Error(`no object with objectName="${name}"`); + return res.matches[0].id; +} + +async function prop(app, id, name) { + const props = (await app.getProperties(id)).properties || []; + const p = props.find((x) => x.name === name); + return p ? p.value : undefined; +} + +async function pickerOpen(app) { + const id = await idByObjectName(app, "tokenSelectorModal"); + return (await prop(app, id, "visible")) === true; +} + +async function openPicker(app, buttonObjectName) { + const btnId = await idByObjectName(app, buttonObjectName); + await app.inspector.send("click", { objectId: btnId }); + await app.waitFor( + async () => { if (!(await pickerOpen(app))) throw new Error("picker not open"); }, + { timeout: 5000, interval: 200, description: `open ${buttonObjectName}` }, + ); +} + +async function tokenItems(app) { + const res = await app.findByProperty("objectName", "tokenListItem"); + return (res && res.matches) || []; +} + +async function pickToken(app, index) { + const items = await tokenItems(app); + if (items.length <= index) + throw new Error(`token #${index + 1} not found — only ${items.length} token(s) in the list`); + const id = items[index].id; + const symbol = await prop(app, id, "tokenSymbol"); + await app.inspector.send("click", { objectId: id }); + await app.waitFor( + async () => { if (await pickerOpen(app)) throw new Error("picker still open"); }, + { timeout: 5000, interval: 200, description: `select token #${index + 1}` }, + ); + return symbol; +} + +// Pick the funding account by highest balance (swap.mjs's selectAccount, used here +// for the BUY side only — the SELL side is pinned to holder2 explicitly below). +async function selectAccountByBalance(app, selectorObjectName) { + const id = await idByObjectName(app, selectorObjectName); + await app.waitFor( + async () => { if ((await prop(app, id, "hasFunds")) !== true) throw new Error("no matching holdings yet"); }, + { timeout: 10000, interval: 300, description: `${selectorObjectName} holdings to load` }, + ); + await app.inspector.send("evaluate", { + expression: + "(function(){var r=matchingAccounts,b=r[0],bb=String(valueFor(b, 'balanceRaw')||'0');" + + "for(var i=1;ibb.length||(v.length===bb.length&&v>bb)){b=r[i];bb=v;}}" + + "setSelection(accountIdFor(b),false);})()", + objectId: id, + }); + await app.waitFor( + async () => { if (!(await prop(app, id, "selectedAccountId"))) throw new Error("holding not selected yet"); }, + { timeout: 5000, interval: 200, description: `${selectorObjectName} holding selected` }, + ); +} + +// Probe whether a specific account id is currently selectable in a selector, WITHOUT +// leaving a valid selection behind for the caller to reason about: set the selection +// to accountId, read whether the selector considers it valid (i.e. present in +// matchingAccounts). Used to show the before/after of the refresh. +async function probeSelectable(app, selectorObjectName, accountId) { + const id = await idByObjectName(app, selectorObjectName); + await app.inspector.send("evaluate", { + expression: `setSelection(${JSON.stringify(accountId)}, false)`, + objectId: id, + }); + const valid = await prop(app, id, "selectionValid"); + const selected = String((await prop(app, id, "selectedAccountId")) || ""); + return valid === true && selected === accountId; +} + +// Select a specific holding by account id and wait until the selector accepts it +// (present in matchingAccounts). Re-issues setSelection each poll because the account +// list can still be settling after a refresh. +async function selectHoldingById(app, selectorObjectName, accountId) { + const id = await idByObjectName(app, selectorObjectName); + await app.waitFor( + async () => { + await app.inspector.send("evaluate", { + expression: `setSelection(${JSON.stringify(accountId)}, false)`, + objectId: id, + }); + const valid = await prop(app, id, "selectionValid"); + const selected = String((await prop(app, id, "selectedAccountId")) || ""); + if (valid !== true || selected !== accountId) + throw new Error("holding not selectable yet"); + }, + { timeout: 20000, interval: 600, description: `select holding ${accountId} in ${selectorObjectName}` }, + ); +} + +// Re-pull the wallet's holdings into the running UI: (1) refreshBalances() re-scans +// the wallet from the sequencer so tokenHoldings() will see the new on-chain state, +// (2) refreshHoldings() re-invokes tokenHoldings() and feeds the swap card's account +// selectors. Both are needed: refreshBalances alone does not re-fetch the card's +// holdings snapshot. `logos` and refreshHoldings() are both in scope on swapPage. +async function refreshUiHoldings(app) { + const pageId = await idByObjectName(app, "swapPage"); + await ignore(() => app.inspector.send("evaluate", { + expression: "logos.module('amm_ui').refreshBalances()", objectId: pageId, + })); + await ignore(() => app.inspector.send("evaluate", { + expression: "refreshHoldings()", objectId: pageId, + })); +} + +async function setSellAmount(app, amount) { + const cardId = await idByObjectName(app, "swapCard"); + await app.inspector.send("setProperty", { objectId: cardId, property: "editingSide", value: "sell" }); + await app.inspector.send("setProperty", { objectId: cardId, property: "sellInput", value: String(amount) }); + await app.inspector.send("evaluate", { expression: "requestQuoteIn()", objectId: cardId }); +} + +async function cardState(app) { + const id = await idByObjectName(app, "swapCard"); + const props = (await app.getProperties(id)).properties || []; + const get = (n) => { const p = props.find((x) => x.name === n); return p ? p.value : undefined; }; + return { + poolResolved: get("poolResolved"), + poolExists: get("poolExists"), + poolError: get("poolError"), + swapError: get("swapError"), + canSubmit: get("canSubmit"), + submitButtonText: get("submitButtonText"), + sellHolding: get("sellHolding"), + buyHolding: get("buyHolding"), + }; +} + +async function poolReserves(app) { + const id = await idByObjectName(app, "swapCard"); + return { a: await prop(app, id, "poolReserveA"), b: await prop(app, id, "poolReserveB") }; +} + +async function saveShot(app, name) { + const shot = await ignore(() => app.screenshot()); + if (shot && shot.image) { + const path = new URL(`./${name}.png`, import.meta.url).pathname; + await writeFile(path, Buffer.from(shot.image, "base64")); + console.log(` screenshot -> ${path}`); + } +} + +// --- spel transactions (out of band from the UI) ---------------------------- + +// Run a spel program instruction against the isolated test wallet. Returns the +// combined stdout+stderr. On a non-zero exit, throws UNLESS the output matches one +// of `tolerate` (a list of regexes for expected, benign failures on re-runs), in +// which case it returns the output with `.tolerated = ` attached. +function runSpel(args, { tolerate = [] } = {}) { + console.log(` $ spel ${args.join(" ")}`); + const env = { ...process.env, LEE_WALLET_HOME_DIR: WALLET_HOME }; + try { + return execFileSync("spel", args, { + cwd: REPO_ROOT, env, input: `${WALLET_PASSWORD}\n`, encoding: "utf8", + }); + } catch (err) { + const out = `${err.stdout || ""}${err.stderr || ""}`; + const idx = tolerate.findIndex((re) => re.test(out)); + if (idx >= 0) { const r = new String(out); r.tolerated = idx; return r; } + throw new Error(`spel ${args[args.indexOf("--") + 1]} failed:\n${out}`); + } +} + +const confirmed = (out) => + /Transaction confirmed/i.test(out) && /included in a block/i.test(out); + +// Is holder2's token A holding already an initialized TokenHolding on-chain? Read-only +// `spel inspect`: it exits non-zero when the account is absent/uninitialized (empty +// data can't decode as a TokenHolding). To avoid mistaking a default/empty decode for +// a real holding, also require the output to reference token A's definition id (in +// either encoding) — a decoded token A holding carries it. +function holdingInitialized(m) { + let out; + try { + out = execFileSync( + "spel", + ["--idl", m.tokenIdl, "inspect", m.userHolding, "--type", "TokenHolding"], + { cwd: REPO_ROOT, env: { ...process.env, LEE_WALLET_HOME_DIR: WALLET_HOME }, encoding: "utf8" }, + ); + } catch { + return false; // non-zero exit -> not initialized (or unreachable; init will surface it) + } + return out.includes(m.tokenDefinition) || new RegExp(base58ToHex(m.tokenDefinition), "i").test(out); +} + +// Create holder2's token A holding (a zeroized TokenHolding), unless it already exists. +// The faucet only mints into an EXISTING holding — it doesn't sign user_holding, so it +// cannot create a fresh one — so this must run first. `initialize_account` DOES sign +// account_to_initialize, so the fresh-account Claim::Authorized succeeds here. On a +// re-run we skip it (already initialized); the tolerate list still guards a race. +function initializeHolding(m) { + if (holdingInitialized(m)) { + console.log(" holder2 token A holding already initialized — skipping init"); + return "exists"; + } + const out = runSpel( + [ + "--idl", m.tokenIdl, "--program", m.tokenBin, "--", "initialize-account", + "--definition-account", m.tokenDefinition, + "--account-to-initialize", m.userHolding, + ], + { tolerate: [/Uninitialized accounts can be initialized/i, /already/i] }, + ); + if (out.tolerated !== undefined) { + console.log(" holder2 token A holding already initialized (race) — continuing"); + return "exists"; + } + console.log(confirmed(out) ? " ✅ initialized holder2 token A holding" : " initialize_account exited 0"); + return "created"; +} + +// Has the faucet already minted to holder2? The faucet claims a MintAllowance PDA on +// the FIRST mint and rewrites it thereafter, so the account existing == a mint has +// happened (and, within 24h, the cooldown is active). Read-only `spel inspect`: exits +// non-zero when the allowance is absent; when present, confirm it's holder2's record +// (it carries recipient_id) before treating it as "already minted". +function mintAlreadyDone(m) { + let out; + try { + out = execFileSync( + "spel", + ["--idl", m.faucetIdl, "inspect", m.mintAllowance, "--type", "MintAllowance"], + { cwd: REPO_ROOT, env: { ...process.env, LEE_WALLET_HOME_DIR: WALLET_HOME }, encoding: "utf8" }, + ); + } catch { + return false; // no allowance account -> no prior mint + } + return out.includes(m.recipient) || new RegExp(base58ToHex(m.recipient), "i").test(out); +} + +// Submit the FaucetMint into holder2's (now existing) token A holding, UNLESS a mint +// was already done (the allowance PDA exists → the 24h per-(recipient, token) cooldown +// is/was active). Returns: +// "confirmed" — the mint landed on-chain (the normal, clean-run case), +// "cooldown" — skipped (or blocked) because holder2 was already funded by a prior run. +function faucetMint(m) { + if (mintAlreadyDone(m)) { + console.log(" faucet already minted to holder2 (allowance exists) — skipping mint (cooldown)"); + return "cooldown"; + } + const out = runSpel( + [ + "--idl", m.faucetIdl, "--program", m.faucetBin, "--", "faucet-mint", + "--recipient", m.recipient, + "--mint-allowance", m.mintAllowance, + "--user-holding", m.userHolding, + "--token-definition", m.tokenDefinition, + "--mint-authority", m.mintAuthority, + "--clock", m.clock, + ], + { tolerate: [/cooldown has not elapsed/i] }, + ); + if (out.tolerated !== undefined) { + console.log(" faucet cooldown active — holder2 was funded by a previous run; continuing"); + return "cooldown"; + } + console.log(confirmed(out) ? " ✅ faucet mint confirmed on-chain" : " faucet mint exited 0 (no marker)"); + return "confirmed"; +} + +// --- the test --------------------------------------------------------------- + +test("amm faucet-swap: mint token A to a fresh account, refresh, then swap", async (app) => { + const m = loadManifest(); + // The selector matches holdings by hex id (see base58ToHex); the manifest is base58. + const userHoldingHex = base58ToHex(m.userHolding); + console.log(` recipient=${m.recipient}`); + console.log(` holding=${m.userHolding} (hex ${userHoldingHex}) token A def ${m.tokenDefinition}`); + + // 1. Wait for the swap card, make the Trade tab explicit. + await app.waitFor( + async () => { await app.expectTexts(["Sell", "Buy"]); }, + { timeout: 20000, interval: 500, description: "swap card to load" }, + ); + await ignore(() => app.click("Trade")); + + // 2. Sell = token A (index 0), Buy = token B (index 1) — the TOKENS_CONFIG order. + await openPicker(app, "swapSellTokenButton"); + const sell = await pickToken(app, 0); + await openPicker(app, "swapBuyTokenButton"); + const buy = await pickToken(app, 1); + console.log(` sell ${sell} -> buy ${buy}`); + + // 3. BEFORE the mint: is holder2's holding already selectable? On a clean run it is + // NOT (no token A yet) — which is exactly why a refresh is needed after minting. + const before = await probeSelectable(app, "swapSellAccountSelector", userHoldingHex); + console.log(` holder2 holding selectable before mint: ${before}`); + + // 4. Create holder2's token A holding, then mint into it via the faucet — both + // out of band from the UI. The initialize step is required because the faucet + // only mints into an EXISTING holding (see initializeHolding()). + initializeHolding(m); + const mintResult = faucetMint(m); + if (mintResult === "cooldown" && !before) { + // Cooldown but the account was NOT funded — inconsistent; fail loudly. + throw new Error("faucet reports cooldown but holder2 had no token A — check on-chain state"); + } + + // 5. Refresh the UI so the newly-minted holding shows up, then wait for it to + // become selectable. This is the crux: without the refresh the card keeps its + // stale pre-mint snapshot and holder2 never appears. + await app.waitFor( + async () => { + await refreshUiHoldings(app); + if (!(await probeSelectable(app, "swapSellAccountSelector", userHoldingHex))) + throw new Error("holder2 holding not selectable yet"); + }, + { timeout: 40000, interval: 1500, description: "holder2 holding to appear after refresh" }, + ); + if (!before) + console.log(" ✓ refresh surfaced holder2's newly-minted holding (absent before the mint)"); + + // 6. Pin the SELL side to holder2's holding; pick the BUY account by balance. + await selectHoldingById(app, "swapSellAccountSelector", userHoldingHex); + await selectAccountByBalance(app, "swapBuyAccountSelector"); + const sellHolding = await prop(app, await idByObjectName(app, "swapCard"), "sellHolding"); + if (String(sellHolding) !== userHoldingHex) + throw new Error(`sell holding is ${sellHolding}, expected holder2's ${userHoldingHex}`); + console.log(` swap will sell from holder2 holding ${sellHolding}`); + + // 7. Enter the sell amount and wait for the CTA to become a live "Swap". + await setSellAmount(app, SELL_AMOUNT); + await app.expectTexts([SELL_AMOUNT]); + try { + await app.waitFor( + async () => { if (!(await cardState(app)).canSubmit) throw new Error("not submittable yet"); }, + { timeout: 15000, interval: 500, description: "pool resolve / CTA ready" }, + ); + } catch { + await saveShot(app, "faucet-swap-cta-not-ready"); + throw new Error(`CTA never became submittable. Card state: ${JSON.stringify(await cardState(app))}`); + } + await saveShot(app, "faucet-swap-filled"); + + const reservesBefore = await poolReserves(app); + console.log(` pool reserves before: A=${reservesBefore.a} B=${reservesBefore.b}`); + + // 8. Submit -> confirm dialog -> confirm (by objectId, with the same confirm() + // fallback swap.mjs uses for QtQuick Controls buttons). + const submitId = await idByObjectName(app, "swapSubmitButton"); + await app.inspector.send("click", { objectId: submitId }); + const dialogId = await idByObjectName(app, "swapConfirmDialog"); + await app.waitFor( + async () => { if ((await prop(app, dialogId, "visible")) !== true) throw new Error("dialog not open"); }, + { timeout: 8000, interval: 300, description: "confirm dialog open" }, + ); + const confirmId = await idByObjectName(app, "transactionConfirmButton"); + await app.inspector.send("click", { objectId: confirmId }); + try { + await app.waitFor( + async () => { if ((await prop(app, dialogId, "visible")) === true) throw new Error("still open"); }, + { timeout: 3000, interval: 300, description: "confirm click registered" }, + ); + } catch { + console.log(" confirm button click didn't take — invoking confirm() via evaluate"); + await ignore(() => app.inspector.send("evaluate", { expression: "confirm()", objectId: dialogId })); + } + + // 9. Verify the swap hit the chain: the card re-resolves the pool, so reserves move. + const cardId = await idByObjectName(app, "swapCard"); + let after = reservesBefore; + try { + await app.waitFor( + async () => { + await ignore(() => app.inspector.send("evaluate", { expression: "doResolvePool()", objectId: cardId })); + await new Promise((r) => setTimeout(r, 800)); + after = await poolReserves(app); + if (after.a === reservesBefore.a && after.b === reservesBefore.b) + throw new Error("reserves unchanged"); + }, + { timeout: 40000, interval: 1200, description: "pool reserves to change on-chain" }, + ); + } catch { + await saveShot(app, "faucet-swap-result"); + const s = await cardState(app); + const inProgress = await prop(app, cardId, "swapInProgress"); + throw new Error( + `pool reserves did not change after the swap.\n` + + ` before: A=${reservesBefore.a} B=${reservesBefore.b}\n` + + ` after: A=${after.a} B=${after.b}\n` + + ` swapInProgress=${inProgress} swapError=${JSON.stringify(s.swapError)}`, + ); + } + console.log(` pool reserves after: A=${after.a} B=${after.b} ✓ changed on-chain`); + await saveShot(app, "faucet-swap-result"); +}); + +run(); + +// How to run: bring up the isolated setup + app exactly as for swap.mjs (see +// apps/amm/README.md), ensure `spel` is on PATH and LEE_WALLET_HOME_DIR points at +// apps/amm/tests/testnet/.wallet, then: node apps/amm/tests/faucet-swap.mjs diff --git a/apps/amm/tests/testnet/setup-amm-testnet.sh b/apps/amm/tests/testnet/setup-amm-testnet.sh index 9c42d877..be412b53 100755 --- a/apps/amm/tests/testnet/setup-amm-testnet.sh +++ b/apps/amm/tests/testnet/setup-amm-testnet.sh @@ -2,12 +2,22 @@ # # setup-amm-testnet.sh # -------------------- -# Deploy the token/amm/twap programs, mint four fungible tokens, initialize the -# AMM, and create the A/B pool — from scratch — against whatever sequencer your -# `wallet` / `spel` config points at. This is the prerequisite state the AMM UI -# tests exercise: swap.mjs swaps against the seeded A/B pool, create-pool.mjs -# creates the (deliberately unseeded) A/C pool, and custom-token.mjs adds token D -# by id. Run it once, then launch the UI / run the tests. +# Deploy the token/amm/twap/token-mint-authority programs, mint four fungible +# tokens, initialize the AMM, and create the A/B pool — from scratch — against +# whatever sequencer your `wallet` / `spel` config points at. This is the +# prerequisite state the AMM UI tests exercise: swap.mjs swaps against the seeded +# A/B pool, create-pool.mjs creates the (deliberately unseeded) A/C pool, and +# custom-token.mjs adds token D by id. Run it once, then launch the UI / run the +# tests. +# +# FAUCET MINT AUTHORITY: every test token's `mint_authority` is set — at +# NewFungibleDefinition time — to the token-mint-authority (faucet) program's +# singleton mint-authority PDA, derived from the deployed faucet binary's ImageID. +# The initial supply is still minted to the holding accounts at creation (that's +# independent of mint_authority), so the pool still seeds normally; but AFTER that +# the only way to mint more of these tokens is through the faucet. That's what +# lets a brand-new account fund itself before swapping — the follow-up faucet e2e +# test relies on this wiring. # # Token D is created ON-CHAIN but deliberately LEFT OUT of the written token config # (amm-tokens.json) — it is the "custom" token the custom-token.mjs test pastes by @@ -78,12 +88,18 @@ TEST_SEQ_POLL_TIMEOUT="${TEST_SEQ_POLL_TIMEOUT:-3s}" # Deterministic accounts, created in THIS fixed order after a fresh restore so # their ids are reproducible. Resolved to ids at runtime via `wallet account id`. -# token-c-*/token-d-* are APPENDED (not inserted) so the pre-existing a/b/lp ids don't shift. +# token-c-*/token-d-*/holder2-* are APPENDED (not inserted) so the pre-existing a/b/lp ids don't shift. # Token C has no seeded pool — the create-pool UI test (apps/amm/tests/create-pool.mjs) # creates the A/C pool itself, minting its own LP holding via the app. # Token D is created but LEFT OUT of the token config — the custom-token UI test # (apps/amm/tests/custom-token.mjs) adds it by id. -ACCOUNT_LABELS=(token-a-def token-a-holding token-b-def token-b-holding lp-holding token-c-def token-c-holding token-d-def token-d-holding) +# holder2 / holder2-a-holding are the "Token A Holder 2" pair for the faucet-swap UI +# test (apps/amm/tests/faucet-swap.mjs): holder2 is the faucet recipient/signer and +# rate-limit subject; holder2-a-holding is its (initially empty) token A holding that +# the faucet mints into. They deliberately start with NO token A so the test can prove +# the account only appears in the swap picker after a faucet mint + UI refresh. The +# faucet requires recipient != user_holding, hence two accounts. +ACCOUNT_LABELS=(token-a-def token-a-holding token-b-def token-b-holding lp-holding token-c-def token-c-holding token-d-def token-d-holding holder2 holder2-a-holding) ############################################################################### # CONFIG — non-account parameters (edit freely) @@ -93,10 +109,15 @@ ACCOUNT_LABELS=(token-a-def token-a-holding token-b-def token-b-holding lp-holdi TOKEN_BIN="programs/token/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/token.bin" AMM_BIN="programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin" TWAP_BIN="programs/twap_oracle/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/twap_oracle.bin" +# The faucet (token-mint-authority) binary. Its ImageID determines the mint-authority +# PDA every test token is minted against, so it MUST be the exact bin deployed below. +MINT_AUTHORITY_BIN="programs/token_mint_authority/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/token_mint_authority.bin" # --- IDLs --- TOKEN_IDL="artifacts/token-idl.json" AMM_IDL="artifacts/amm-idl.json" +# The faucet IDL — not used by this setup, but the follow-up faucet e2e test reads it. +MINT_AUTHORITY_IDL="artifacts/token_mint_authority-idl.json" # --- Token metadata --- TOKEN_A_NAME="TOKEN A"; TOKEN_A_SYMBOL="TKA"; TOKEN_A_SUPPLY="1000000000000000000000"; TOKEN_A_DECIMALS=18 @@ -134,6 +155,13 @@ REGISTRY_CONFIG_OUT="apps/amm/tests/testnet/amm-registry.json" # the app's default per-user store. Initialized empty so a test run starts clean. CUSTOM_TOKEN_CONFIG_OUT="apps/amm/tests/testnet/custom-tokens.json" +# Faucet manifest for the faucet-swap UI test (git-ignored, tests only). Everything +# apps/amm/tests/faucet-swap.mjs needs to (a) submit a FaucetMint via spel and (b) +# drive the swap from the freshly-funded holder2 account: bin/IDL paths, the six +# FaucetMint account ids (recipient/allowance/holding/definition/authority/clock), +# and the token A definition id. Written at the end from the derived values. +FAUCET_MANIFEST_OUT="apps/amm/tests/testnet/faucet.json" + ############################################################################### # Helpers ############################################################################### @@ -203,6 +231,38 @@ program_id() { printf '%s' "$pid" } +# Compute the faucet's singleton mint-authority PDA (base58) for a faucet binary. +# Delegates to the token_mint_authority `mint_authority` example, which decodes the +# bin, computes its ImageID, and derives compute_mint_authority_pda(). Must be run +# against the exact bin being deployed — the PDA is ImageID-dependent. +mint_authority_pda() { + local bin="$1" out pda + out="$(RISC0_DEV_MODE=1 RISC0_SKIP_BUILD=1 cargo run -q -p token_mint_authority_program \ + --example mint_authority -- "$bin" 2>&1)" \ + || { echo "$out" >&2; die "mint_authority example failed for $bin"; } + # The example prints a line like: base58: + pda="$(printf '%s' "$out" | awk -F'base58:[[:space:]]*' 'NF>1 {print $2; exit}' \ + | grep -oE '[1-9A-HJ-NP-Za-km-z]{32,44}' | head -n1 || true)" + [ -n "$pda" ] || { echo "$out" >&2; die "could not parse base58 mint-authority PDA from example output"; } + printf '%s' "$pda" +} + +# Compute the faucet's per-(recipient, definition) mint-allowance PDA (base58). +# Delegates to the token_mint_authority `faucet_allowance` example. This is the +# rate-limit account FaucetMint claims/reads, and a required instruction input, so +# it must be derived up front. ImageID-dependent — run against the deployed bin. +# mint_allowance_pda +mint_allowance_pda() { + local bin="$1" recipient="$2" definition="$3" out pda + out="$(RISC0_DEV_MODE=1 RISC0_SKIP_BUILD=1 cargo run -q -p token_mint_authority_program \ + --example faucet_allowance -- "$bin" "$recipient" "$definition" 2>&1)" \ + || { echo "$out" >&2; die "faucet_allowance example failed for $bin"; } + pda="$(printf '%s' "$out" | awk -F'base58:[[:space:]]*' 'NF>1 {print $2; exit}' \ + | grep -oE '[1-9A-HJ-NP-Za-km-z]{32,44}' | head -n1 || true)" + [ -n "$pda" ] || { echo "$out" >&2; die "could not parse base58 mint-allowance PDA from example output"; } + printf '%s' "$pda" +} + # Resolve a wallet account id (bare base58) from its label. Deterministic under # the test mnemonic. Returns non-zero if the label isn't registered yet. acct_id() { @@ -292,10 +352,11 @@ sec "Preflight" require_cmd wallet require_cmd spel require_cmd cargo -require_file "$TOKEN_BIN"; require_file "$AMM_BIN"; require_file "$TWAP_BIN" -require_file "$TOKEN_IDL"; require_file "$AMM_IDL" +require_file "$TOKEN_BIN"; require_file "$AMM_BIN"; require_file "$TWAP_BIN"; require_file "$MINT_AUTHORITY_BIN" +require_file "$TOKEN_IDL"; require_file "$AMM_IDL"; require_file "$MINT_AUTHORITY_IDL" kv "repo root" "$REPO_ROOT" kv "token bin" "$TOKEN_BIN"; kv "amm bin" "$AMM_BIN"; kv "twap bin" "$TWAP_BIN" +kv "mint-authority bin" "$MINT_AUTHORITY_BIN" # Decide whether keys need restoring from the key material (storage.json), NOT the # home dir — write_wallet_config below creates the dir, so a dir check would always @@ -332,12 +393,17 @@ TOKEN_C_DEF="$(acct_id token-c-def)" || die "token-c-def not registered" TOKEN_C_HOLDING="$(acct_id token-c-holding)" || die "token-c-holding not registered" TOKEN_D_DEF="$(acct_id token-d-def)" || die "token-d-def not registered" TOKEN_D_HOLDING="$(acct_id token-d-holding)" || die "token-d-holding not registered" -for v in TOKEN_A_DEF TOKEN_A_HOLDING TOKEN_B_DEF TOKEN_B_HOLDING USER_HOLDING_LP TOKEN_C_DEF TOKEN_C_HOLDING TOKEN_D_DEF TOKEN_D_HOLDING; do +# "Token A Holder 2" — the faucet recipient/signer and its (initially empty) token A holding. +HOLDER2="$(acct_id holder2)" || die "holder2 not registered — run with FORCE_BOOTSTRAP=1" +HOLDER2_A_HOLDING="$(acct_id holder2-a-holding)" || die "holder2-a-holding not registered" +for v in TOKEN_A_DEF TOKEN_A_HOLDING TOKEN_B_DEF TOKEN_B_HOLDING USER_HOLDING_LP TOKEN_C_DEF TOKEN_C_HOLDING TOKEN_D_DEF TOKEN_D_HOLDING HOLDER2 HOLDER2_A_HOLDING; do [ -n "${!v}" ] || die "failed to resolve account id for $v" done -# Derived roles (the input holding signs; mint authority == holding; authority is the A holding). -TOKEN_A_MINT_AUTH="$TOKEN_A_HOLDING"; TOKEN_B_MINT_AUTH="$TOKEN_B_HOLDING"; TOKEN_C_MINT_AUTH="$TOKEN_C_HOLDING"; TOKEN_D_MINT_AUTH="$TOKEN_D_HOLDING" +# Derived roles: the holding accounts sign creation and seed the pool; the AMM +# authority is the A holding. Mint authority is deliberately NOT the holding — it's +# the faucet PDA, set in step 3 once the faucet binary is deployed and its ImageID +# (hence the PDA) is known. AMM_AUTHORITY="$TOKEN_A_HOLDING" USER_HOLDING_A="$TOKEN_A_HOLDING"; USER_HOLDING_B="$TOKEN_B_HOLDING" @@ -350,21 +416,40 @@ kv "token-c-def" "$TOKEN_C_DEF" kv "token-c-holding" "$TOKEN_C_HOLDING" kv "token-d-def" "$TOKEN_D_DEF" kv "token-d-holding" "$TOKEN_D_HOLDING" +kv "holder2" "$HOLDER2" +kv "holder2-a-holding" "$HOLDER2_A_HOLDING" ############################################################################### # 2. Deploy programs ############################################################################### -run_tx soft "deploy token program" -- wallet deploy-program "$TOKEN_BIN" -run_tx soft "deploy amm program" -- wallet deploy-program "$AMM_BIN" -run_tx soft "deploy twap_oracle program" -- wallet deploy-program "$TWAP_BIN" +run_tx soft "deploy token program" -- wallet deploy-program "$TOKEN_BIN" +run_tx soft "deploy amm program" -- wallet deploy-program "$AMM_BIN" +run_tx soft "deploy twap_oracle program" -- wallet deploy-program "$TWAP_BIN" +run_tx soft "deploy token-mint-authority program" -- wallet deploy-program "$MINT_AUTHORITY_BIN" ############################################################################### # 3. Derive program IDs ############################################################################### sec "Program IDs (derived from the deployed binaries)" -TOKEN_PID="$(program_id "$TOKEN_BIN")"; kv "token program id" "$TOKEN_PID" -AMM_PID="$(program_id "$AMM_BIN")"; kv "amm program id" "$AMM_PID" -TWAP_PID="$(program_id "$TWAP_BIN")"; kv "twap program id" "$TWAP_PID" +TOKEN_PID="$(program_id "$TOKEN_BIN")"; kv "token program id" "$TOKEN_PID" +AMM_PID="$(program_id "$AMM_BIN")"; kv "amm program id" "$AMM_PID" +TWAP_PID="$(program_id "$TWAP_BIN")"; kv "twap program id" "$TWAP_PID" +MINT_AUTHORITY_PID="$(program_id "$MINT_AUTHORITY_BIN")"; kv "mint-authority program id" "$MINT_AUTHORITY_PID" + +# The faucet's singleton mint-authority PDA is derived from the DEPLOYED faucet +# binary's ImageID. Every faucet token's definition sets its mint_authority to this +# PDA, so afterwards only the faucet — via its program seed — can mint more. Derived +# from the exact bin deployed above, so it stays correct across guest rebuilds. +MINT_AUTHORITY_PDA="$(mint_authority_pda "$MINT_AUTHORITY_BIN")" +kv "faucet mint-authority PDA" "$MINT_AUTHORITY_PDA" +TOKEN_A_MINT_AUTH="$MINT_AUTHORITY_PDA"; TOKEN_B_MINT_AUTH="$MINT_AUTHORITY_PDA" +TOKEN_C_MINT_AUTH="$MINT_AUTHORITY_PDA"; TOKEN_D_MINT_AUTH="$MINT_AUTHORITY_PDA" + +# The faucet-swap test's FaucetMint needs holder2's per-(recipient, token A) allowance +# PDA — the rate-limit account it claims on first mint. Derived here (recipient=holder2, +# definition=token A) so the test can pass it straight to spel without touching cargo. +HOLDER2_ALLOWANCE_PDA="$(mint_allowance_pda "$MINT_AUTHORITY_BIN" "$HOLDER2" "$TOKEN_A_DEF")" +kv "holder2 allowance PDA" "$HOLDER2_ALLOWANCE_PDA" ############################################################################### # 4. Create token definitions (mint supply to the holding accounts) @@ -563,7 +648,7 @@ sec "Write UI registry config -> $REGISTRY_CONFIG_OUT" "name": "AMM local registry", "version": "0.1.0", "networks": [ - { "id": "local", "name": "Local", "programIds": { "amm": "$AMM_PID", "token": "$TOKEN_PID" } } + { "id": "local", "name": "Local", "programIds": { "amm": "$AMM_PID", "token": "$TOKEN_PID", "tokenMintAuthority": "$MINT_AUTHORITY_PID" } } ], "tokens": [ { "network": "local", "symbol": "$TOKEN_A_SYMBOL", "name": "$TOKEN_A_NAME", "definitionId": "$TOKEN_A_DEF" }, @@ -596,11 +681,46 @@ sec "Write custom-token store -> $CUSTOM_TOKEN_CONFIG_OUT" printf '%s\n' "[]" > "$CUSTOM_TOKEN_CONFIG_OUT" kv "wrote" "$CUSTOM_TOKEN_CONFIG_OUT (empty)" +############################################################################### +# 13. Write the faucet manifest for the faucet-swap UI test +############################################################################### +sec "Write faucet manifest -> $FAUCET_MANIFEST_OUT" +# Everything apps/amm/tests/faucet-swap.mjs needs. The six faucet accounts are the +# exact, ordered FaucetMint inputs (recipient, mint_allowance, user_holding, +# token_definition, mint_authority, clock). tokenBin/tokenIdl let the test first +# `initialize_account` holder2's token A holding (the faucet only mints into an +# EXISTING holding — it doesn't sign user_holding, so it can't create a fresh one), +# then FaucetMint into it. Paths are repo-relative (resolved against the repo root). +cat > "$FAUCET_MANIFEST_OUT" < token-mint-authority -> token` chained `MintWithAuthority` under +//! the mint-authority PDA seed, the lazily-claimed allowance PDA, and the +//! per-day cooldown across real transactions. + +use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID}; +use lee::{ + program_deployment_transaction::{self, ProgramDeploymentTransaction}, + public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State, +}; +use lee_core::account::{Account, AccountId, Data, Nonce}; +use token_core::{TokenDefinition, TokenHolding}; +use token_mint_authority_core::{ + compute_mint_allowance_pda, compute_mint_authority_pda, MintAllowance, FAUCET_MINT_AMOUNT, + MINT_COOLDOWN_MS, +}; + +struct Keys; +struct Ids; + +/// Faucet-mint wall-clock anchor (Unix milliseconds). +const T0: u64 = 1_700_000_000_000; + +impl Keys { + fn recipient() -> PrivateKey { + PrivateKey::try_new([21; 32]).expect("valid private key") + } + + fn faucet_definition() -> PrivateKey { + PrivateKey::try_new([23; 32]).expect("valid private key") + } + + fn user_holding() -> PrivateKey { + PrivateKey::try_new([24; 32]).expect("valid private key") + } +} + +impl Ids { + fn token_program() -> lee_core::program::ProgramId { + token_methods::TOKEN_ID + } + + fn token_mint_authority_program() -> lee_core::program::ProgramId { + token_mint_authority_methods::TOKEN_MINT_AUTHORITY_ID + } + + fn recipient() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&Keys::recipient())) + } + + fn faucet_definition() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&Keys::faucet_definition())) + } + + fn user_holding() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&Keys::user_holding())) + } + + /// The faucet token's mint authority — a Token-Mint-Authority PDA the deploy step + /// wires into the definition. Uninitialized until first use. + fn mint_authority() -> AccountId { + compute_mint_authority_pda(Ids::token_mint_authority_program()) + } + + /// The recipient's per-token rate-limit PDA. Uninitialized until first use. + fn mint_allowance() -> AccountId { + compute_mint_allowance_pda( + Ids::token_mint_authority_program(), + Ids::recipient(), + Ids::faucet_definition(), + ) + } +} + +/// The faucet token definition: a normal fungible whose mint authority is the +/// Token-Mint-Authority PDA. Starts at zero supply. +fn faucet_definition_init() -> Account { + Account { + program_owner: Ids::token_program(), + balance: 0, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("Faucet Token"), + total_supply: 0, + metadata_id: None, + authority: Some(Ids::mint_authority()), + }), + nonce: Nonce(0), + } +} + +/// The recipient's existing holding for the faucet token (so no holding +/// signature is needed — the Token Program just writes to it). +fn user_holding_init() -> Account { + Account { + program_owner: Ids::token_program(), + balance: 0, + data: Data::from(&TokenHolding::Fungible { + definition_id: Ids::faucet_definition(), + balance: 0, + }), + nonce: Nonce(0), + } +} + +/// The recipient identity that signs and is rate-limited. Non-default owner so +/// its (unchanged) post-state survives the framework output filter as its nonce +/// bumps across transactions. +fn recipient_init() -> Account { + Account { + program_owner: [7u32; 8], + ..Account::default() + } +} + +/// Seed the canonical `CLOCK_01` account at `timestamp`. Non-default owner for +/// the same output-filter reason the stablecoin tests use. +fn seed_clock(state: &mut V03State, timestamp: u64) { + let data = ClockAccountData { + block_id: 0, + timestamp, + } + .to_bytes(); + let clock_account = Account { + program_owner: [8u32; 8], + data: Data::try_from(data).expect("clock account data fits"), + ..Account::default() + }; + state.force_insert_account(CLOCK_01_PROGRAM_ACCOUNT_ID, clock_account); +} + +fn deploy_programs(state: &mut V03State) { + for elf in [ + token_methods::TOKEN_ELF.to_vec(), + token_mint_authority_methods::TOKEN_MINT_AUTHORITY_ELF.to_vec(), + ] { + state + .transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new( + program_deployment_transaction::Message::new(elf), + )) + .expect("program deployment must succeed"); + } +} + +fn state_for_faucet_tests() -> V03State { + let mut state = V03State::new(); + deploy_programs(&mut state); + seed_clock(&mut state, T0); + state.force_insert_account(Ids::faucet_definition(), faucet_definition_init()); + state.force_insert_account(Ids::user_holding(), user_holding_init()); + state.force_insert_account(Ids::recipient(), recipient_init()); + state +} + +fn current_nonce(state: &V03State, account_id: AccountId) -> Nonce { + state.get_account_by_id(account_id).nonce +} + +/// Submit one `FaucetMint`. Only the recipient signs; the mint-authority and +/// allowance PDAs are authorized/claimed by the program via seeds. +fn faucet_mint(state: &mut V03State) -> Result<(), lee::error::LeeError> { + let message = public_transaction::Message::try_new( + Ids::token_mint_authority_program(), + vec![ + Ids::recipient(), + Ids::mint_allowance(), + Ids::user_holding(), + Ids::faucet_definition(), + Ids::mint_authority(), + CLOCK_01_PROGRAM_ACCOUNT_ID, + ], + vec![current_nonce(state, Ids::recipient())], + token_mint_authority_core::Instruction::FaucetMint, + ) + .expect("faucet-mint message is valid"); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::recipient()]); + let tx = PublicTransaction::new(message, witness_set); + state + .transition_from_public_transaction(&tx, 0, 0) + .map(|_| ()) +} + +fn holding_balance(state: &V03State, account_id: AccountId) -> u128 { + match TokenHolding::try_from(&state.get_account_by_id(account_id).data).expect("valid holding") + { + TokenHolding::Fungible { balance, .. } => balance, + TokenHolding::NftMaster { .. } | TokenHolding::NftPrintedCopy { .. } => { + panic!("expected a fungible holding") + } + } +} + +fn definition_supply(state: &V03State, account_id: AccountId) -> u128 { + match TokenDefinition::try_from(&state.get_account_by_id(account_id).data) + .expect("valid definition") + { + TokenDefinition::Fungible { total_supply, .. } => total_supply, + TokenDefinition::NonFungible { .. } => panic!("expected a fungible definition"), + } +} + +fn allowance_last_mint(state: &V03State, account_id: AccountId) -> u64 { + MintAllowance::try_from(&state.get_account_by_id(account_id).data) + .expect("valid allowance") + .last_mint_ms +} + +#[test] +fn faucet_grants_fixed_amount_and_enforces_daily_cooldown() { + let mut state = state_for_faucet_tests(); + + // 1. First mint: the chained MintWithAuthority credits exactly FAUCET_MINT_AMOUNT and the + // allowance PDA is claimed with the current timestamp. + faucet_mint(&mut state).expect("first faucet mint must succeed"); + assert_eq!( + holding_balance(&state, Ids::user_holding()), + FAUCET_MINT_AMOUNT + ); + assert_eq!( + definition_supply(&state, Ids::faucet_definition()), + FAUCET_MINT_AMOUNT + ); + assert_eq!(allowance_last_mint(&state, Ids::mint_allowance()), T0); + // The program owns the allowance PDA now, but never the authority PDA. + assert_eq!( + state.get_account_by_id(Ids::mint_allowance()).program_owner, + Ids::token_mint_authority_program() + ); + + // 2. Second mint at the same clock: blocked by the cooldown, state unchanged. + assert!( + faucet_mint(&mut state).is_err(), + "a second mint within 24h must be rejected" + ); + assert_eq!( + holding_balance(&state, Ids::user_holding()), + FAUCET_MINT_AMOUNT + ); + assert_eq!(allowance_last_mint(&state, Ids::mint_allowance()), T0); + + // 3. Advance the clock past the cooldown: minting is allowed again and stacks. + seed_clock(&mut state, T0 + MINT_COOLDOWN_MS); + faucet_mint(&mut state).expect("mint after the cooldown must succeed"); + assert_eq!( + holding_balance(&state, Ids::user_holding()), + 2 * FAUCET_MINT_AMOUNT + ); + assert_eq!( + definition_supply(&state, Ids::faucet_definition()), + 2 * FAUCET_MINT_AMOUNT + ); + assert_eq!( + allowance_last_mint(&state, Ids::mint_allowance()), + T0 + MINT_COOLDOWN_MS + ); +} + +#[test] +fn faucet_rejects_a_token_whose_authority_is_not_the_mint_authority_pda() { + let mut state = state_for_faucet_tests(); + // Re-point the faucet token's mint authority at some unrelated account: the + // Token-Mint-Authority must refuse to mint a token it does not control. + let mut definition = faucet_definition_init(); + definition.data = Data::from(&TokenDefinition::Fungible { + name: String::from("Faucet Token"), + total_supply: 0, + metadata_id: None, + authority: Some(AccountId::new([0xEE; 32])), + }); + state.force_insert_account(Ids::faucet_definition(), definition); + + assert!( + faucet_mint(&mut state).is_err(), + "minting a token not controlled by the mint-authority PDA must fail" + ); + assert_eq!(holding_balance(&state, Ids::user_holding()), 0); +} diff --git a/programs/token_mint_authority/Cargo.toml b/programs/token_mint_authority/Cargo.toml new file mode 100644 index 00000000..ff678fbf --- /dev/null +++ b/programs/token_mint_authority/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "token_mint_authority_program" +version = "0.1.0" +edition = "2021" + +[dependencies] +lee_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.4", features = ["host"] } +clock_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.4" } +token_mint_authority_core = { path = "core" } +token_core = { path = "../token/core" } + +[dev-dependencies] +risc0-zkvm = { version = "=3.0.5", default-features = false } +# For the `mint_authority` example: decode a built guest `.bin` and compute its ImageID. +risc0-binfmt = { version = "=3.0.4", default-features = false } +hex = "0.4" diff --git a/programs/token_mint_authority/core/Cargo.toml b/programs/token_mint_authority/core/Cargo.toml new file mode 100644 index 00000000..77baecc9 --- /dev/null +++ b/programs/token_mint_authority/core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "token_mint_authority_core" +version = "0.1.0" +edition = "2021" + +[dependencies] +lee_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.4", features = ["host"] } +borsh = { version = "1.5", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } +risc0-zkvm = { version = "=3.0.5", default-features = false } +spel-framework-macros = { git = "https://github.com/logos-co/spel.git", rev = "1ef0500f8fc8ce3ddf95523726ae159db83f744f", package = "spel-framework-macros" } + +[dev-dependencies] +serde_json = "1.0" diff --git a/programs/token_mint_authority/core/src/lib.rs b/programs/token_mint_authority/core/src/lib.rs new file mode 100644 index 00000000..de1eb337 --- /dev/null +++ b/programs/token_mint_authority/core/src/lib.rs @@ -0,0 +1,258 @@ +//! Core data structures and utilities for the Token-Mint-Authority Program. +//! +//! The Token-Mint-Authority is a permissionless testnet faucet. It is a pure delegating +//! proxy in front of the Token Program: the call chain is +//! `user -> token-mint-authority -> token`. Its sole job is to hold the mint authority +//! for one or more faucet token definitions and to let any account mint a fixed +//! amount to itself, rate-limited to once per 24h per token. +//! +//! For a token to be mintable through here, its `TokenDefinition::Fungible` +//! `mint_authority` must be set — at token-creation time, via the Token +//! Program's `NewFungibleDefinition` — to [`compute_mint_authority_pda`] of the +//! deployed Token-Mint-Authority program. The Token-Mint-Authority then authorizes that PDA +//! implicitly (program id + seed) when it delegates the chained +//! `Token::MintWithAuthority`; it stores no key and needs no initialization. + +use borsh::{BorshDeserialize, BorshSerialize}; +use lee_core::{ + account::{AccountId, AccountWithMetadata, Data}, + program::{PdaSeed, ProgramId}, +}; +use serde::{Deserialize, Serialize}; +use spel_framework_macros::account_type; + +/// One whole faucet token in base units (18 decimals), i.e. `1e18`. +pub const ONE_TOKEN: u128 = 1_000_000_000_000_000_000; + +/// Fixed amount minted by every successful [`Instruction::FaucetMint`], in base +/// units: `10_000e18`. The mint amount is not caller-controllable — a fixed +/// grant is the whole point of a faucet and keeps supply drain bounded. +pub const FAUCET_MINT_AMOUNT: u128 = 10_000 * ONE_TOKEN; + +/// Minimum wall-clock gap between two successful faucet mints for the same +/// `(recipient, token definition)` pair: 24 hours in milliseconds. +pub const MINT_COOLDOWN_MS: u64 = 24 * 60 * 60 * 1_000; + +// Stable domain-separation tags for the Token-Mint-Authority PDAs; these must stay +// unchanged for address compatibility. +const MINT_AUTHORITY_PDA_DOMAIN: &[u8] = b"TOKEN_MINT_AUTHORITY__MINT_AUTHORITY"; +const MINT_ALLOWANCE_PDA_DOMAIN: &[u8] = b"TOKEN_MINT_AUTHORITY__MINT_ALLOWANCE"; + +/// Token-Mint-Authority Program Instruction. +#[derive(Debug, Serialize, Deserialize)] +pub enum Instruction { + /// Mint [`FAUCET_MINT_AMOUNT`] of the faucet token to the calling recipient, + /// once per [`MINT_COOLDOWN_MS`] per `(recipient, token definition)`. + /// + /// Required accounts (6), in order: + /// 1. `recipient` — authorized; the account being funded and the rate-limit subject. Also + /// authorizes its own `user_holding`. + /// 2. `mint_allowance` — the per-`(recipient, definition)` rate-limit PDA at + /// [`compute_mint_allowance_pda`]; claimed on first use, then rewritten. + /// 3. `user_holding` — recipient's Token Holding for the faucet token (uninitialized, or + /// initialized and authorized). Mutated by the chained `Token::MintWithAuthority`. + /// 4. `token_definition` — the faucet `TokenDefinition::Fungible`, owned by the Token Program, + /// whose stored `mint_authority` is `mint_authority` below. Mutated by the chained mint. + /// 5. `mint_authority` — the Token-Mint-Authority PDA at [`compute_mint_authority_pda`]; + /// authorized in the chained mint via its seed (`user -> token-mint-authority -> token`), + /// so the Token-Mint-Authority never holds a key of its own. + /// 6. `clock` — the system `CLOCK_01` account; read-only. Anchors the cooldown. (The pinned + /// spel-framework exposes no `ProgramContext` clock, so wall-clock time is read from this + /// account.) + FaucetMint, +} + +/// Per-`(recipient, token definition)` rate-limit state. +/// +/// Stored at the [`compute_mint_allowance_pda`] address and owned by the +/// Token-Mint-Authority program. `recipient_id` / `definition_id` are redundant with the +/// PDA derivation but kept for `spel inspect`-ability and defense in depth. +#[account_type] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct MintAllowance { + /// The funded account. + pub recipient_id: AccountId, + /// The faucet token definition this allowance is scoped to. + pub definition_id: AccountId, + /// Unix milliseconds of the most recent successful faucet mint. + pub last_mint_ms: u64, +} + +impl TryFrom<&Data> for MintAllowance { + type Error = std::io::Error; + + fn try_from(data: &Data) -> Result { + Self::try_from_slice(data.as_ref()) + } +} + +impl From<&MintAllowance> for Data { + fn from(state: &MintAllowance) -> Self { + let len = borsh::object_length(state).expect("MintAllowance length must be known"); + let mut buf = Vec::with_capacity(len); + BorshSerialize::serialize(state, &mut buf) + .expect("MintAllowance serialization should not fail"); + Self::try_from(buf).expect("MintAllowance encoded data should fit into Data") + } +} + +/// PDA seed for the Token-Mint-Authority's singleton mint-authority account. A single +/// authority per deployed program backs every faucet token: creators set their +/// definition's `mint_authority` to [`compute_mint_authority_pda`] of this +/// program, and the program's seed authorizes every chained mint. +#[must_use] +pub fn compute_mint_authority_pda_seed() -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256 as _}; + + let mut out = [0u8; 32]; + out.copy_from_slice(Impl::hash_bytes(MINT_AUTHORITY_PDA_DOMAIN).as_bytes()); + PdaSeed::new(out) +} + +/// Account id of the Token-Mint-Authority's mint-authority PDA under +/// `token_mint_authority_program_id`. +#[must_use] +pub fn compute_mint_authority_pda(token_mint_authority_program_id: ProgramId) -> AccountId { + AccountId::for_public_pda( + &token_mint_authority_program_id, + &compute_mint_authority_pda_seed(), + ) +} + +/// PDA seed for the [`MintAllowance`] of `(recipient_id, definition_id)`. +/// +/// Keyed by both the recipient and the token so each faucet token carries its +/// own independent per-account cooldown. +#[must_use] +pub fn compute_mint_allowance_pda_seed( + recipient_id: AccountId, + definition_id: AccountId, +) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256 as _}; + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&recipient_id.to_bytes()); + bytes.extend_from_slice(&definition_id.to_bytes()); + bytes.extend_from_slice(MINT_ALLOWANCE_PDA_DOMAIN); + + let mut out = [0u8; 32]; + out.copy_from_slice(Impl::hash_bytes(&bytes).as_bytes()); + PdaSeed::new(out) +} + +/// Account id of the [`MintAllowance`] PDA for `(recipient_id, definition_id)` +/// under `token_mint_authority_program_id`. +#[must_use] +pub fn compute_mint_allowance_pda( + token_mint_authority_program_id: ProgramId, + recipient_id: AccountId, + definition_id: AccountId, +) -> AccountId { + AccountId::for_public_pda( + &token_mint_authority_program_id, + &compute_mint_allowance_pda_seed(recipient_id, definition_id), + ) +} + +/// Verify the mint-authority account's address matches `token_mint_authority_program_id` +/// and return its [`PdaSeed`] for use in the chained mint. +/// +/// # Panics +/// If `mint_authority.account_id` does not match the derived PDA. +pub fn verify_mint_authority_and_get_seed( + mint_authority: &AccountWithMetadata, + token_mint_authority_program_id: ProgramId, +) -> PdaSeed { + let seed = compute_mint_authority_pda_seed(); + let expected_id = AccountId::for_public_pda(&token_mint_authority_program_id, &seed); + assert_eq!( + mint_authority.account_id, expected_id, + "Mint authority account ID does not match expected PDA derivation" + ); + seed +} + +/// Verify the allowance account's address matches `(token_mint_authority_program_id, +/// recipient, definition)` and return its [`PdaSeed`] for the post-state claim. +/// +/// # Panics +/// If `mint_allowance.account_id` does not match the derived PDA. +pub fn verify_mint_allowance_and_get_seed( + mint_allowance: &AccountWithMetadata, + recipient_id: AccountId, + definition_id: AccountId, + token_mint_authority_program_id: ProgramId, +) -> PdaSeed { + let seed = compute_mint_allowance_pda_seed(recipient_id, definition_id); + let expected_id = AccountId::for_public_pda(&token_mint_authority_program_id, &seed); + assert_eq!( + mint_allowance.account_id, expected_id, + "Mint allowance account ID does not match expected PDA derivation" + ); + seed +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample() -> MintAllowance { + MintAllowance { + recipient_id: AccountId::new([3u8; 32]), + definition_id: AccountId::new([7u8; 32]), + last_mint_ms: 1_700_000_000_000, + } + } + + #[test] + fn borsh_roundtrip_allowance() { + let state = sample(); + let data: Data = (&state).into(); + let decoded = MintAllowance::try_from(&data).expect("decode"); + assert_eq!(decoded, state); + } + + #[test] + fn faucet_amount_is_ten_thousand_tokens() { + assert_eq!(FAUCET_MINT_AMOUNT, 10_000 * ONE_TOKEN); + assert_eq!(FAUCET_MINT_AMOUNT, 10_000_000_000_000_000_000_000); + } + + #[test] + fn cooldown_is_one_day() { + assert_eq!(MINT_COOLDOWN_MS, 86_400_000); + } + + #[test] + fn authority_pda_is_deterministic_and_singleton() { + let program_id: ProgramId = [9u32; 8]; + assert_eq!( + compute_mint_authority_pda(program_id), + compute_mint_authority_pda(program_id), + ); + } + + #[test] + fn allowance_pda_depends_on_recipient_and_definition() { + let program_id: ProgramId = [9u32; 8]; + let a = AccountId::new([1u8; 32]); + let b = AccountId::new([2u8; 32]); + let def = AccountId::new([5u8; 32]); + let def2 = AccountId::new([6u8; 32]); + // Distinct recipient -> distinct allowance. + assert_ne!( + compute_mint_allowance_pda(program_id, a, def), + compute_mint_allowance_pda(program_id, b, def), + ); + // Distinct token -> distinct allowance (per-account-per-token scoping). + assert_ne!( + compute_mint_allowance_pda(program_id, a, def), + compute_mint_allowance_pda(program_id, a, def2), + ); + // Allowance PDA never collides with the authority PDA. + assert_ne!( + compute_mint_allowance_pda(program_id, a, def), + compute_mint_authority_pda(program_id), + ); + } +} diff --git a/programs/token_mint_authority/examples/faucet_allowance.rs b/programs/token_mint_authority/examples/faucet_allowance.rs new file mode 100644 index 00000000..75cd1786 --- /dev/null +++ b/programs/token_mint_authority/examples/faucet_allowance.rs @@ -0,0 +1,49 @@ +//! Print the mint-allowance PDA for a `(recipient, token definition)` pair. +//! +//! `FaucetMint` rate-limits per `(recipient, definition)` by claiming a +//! [`MintAllowance`] account at [`compute_mint_allowance_pda`]. That address is a +//! required input to the instruction, so anything that submits a faucet mint (the +//! testnet setup script, an e2e test) needs to derive it up front. Like the +//! mint-authority PDA it depends on the deployed faucet binary's ImageID, so run +//! this against the *exact* `.bin` you deploy. +//! +//! Usage: +//! cargo run -p token_mint_authority_program --example faucet_allowance -- \ +//! + +use std::{error::Error, str::FromStr}; + +use lee_core::{account::AccountId, program::ProgramId}; +use risc0_binfmt::ProgramBinary; +use token_mint_authority_core::compute_mint_allowance_pda; + +fn main() -> Result<(), Box> { + let mut args = std::env::args().skip(1); + let usage = "usage: cargo run -p token_mint_authority_program --example faucet_allowance -- \ + "; + let path = args.next().ok_or(usage)?; + let recipient_s = args.next().ok_or(usage)?; + let definition_s = args.next().ok_or(usage)?; + + let bytes = std::fs::read(&path).map_err(|error| format!("failed to read {path}: {error}"))?; + let binary = ProgramBinary::decode(&bytes) + .map_err(|error| format!("failed to decode program binary: {error}"))?; + let program_id: ProgramId = binary + .compute_image_id() + .map_err(|error| format!("failed to compute image id: {error}"))? + .into(); + + let recipient = + AccountId::from_str(&recipient_s).map_err(|_| "recipient must be a base58 account id")?; + let definition = + AccountId::from_str(&definition_s).map_err(|_| "definition must be a base58 account id")?; + + let allowance = compute_mint_allowance_pda(program_id, recipient, definition); + + // Print only the base58 id on the last line so callers can grep it out easily. + println!("recipient: {recipient}"); + println!("definition: {definition}"); + println!("base58: {allowance}"); + + Ok(()) +} diff --git a/programs/token_mint_authority/examples/mint_authority.rs b/programs/token_mint_authority/examples/mint_authority.rs new file mode 100644 index 00000000..6a13f138 --- /dev/null +++ b/programs/token_mint_authority/examples/mint_authority.rs @@ -0,0 +1,59 @@ +//! Print the mint-authority account id for a built token-mint-authority guest binary. +//! +//! Every faucet token's `mint_authority` (set at `NewFungibleDefinition` time) +//! must be this program's singleton mint-authority PDA, which is derived from the +//! deployed binary's ImageID. This helper decodes the `.bin`, computes that +//! ImageID, and prints the resulting account id in base58 and hex so it can be +//! dropped straight into a token definition. +//! +//! Because the id depends on the ImageID, run this against the *exact* binary you +//! deploy (build with the release profile first — see the deployment notes). +//! +//! Usage: +//! make build-programs # produces target/guest/token_mint_authority.bin +//! cargo run -p token_mint_authority_program --example mint_authority -- \ +//! target/guest/token_mint_authority.bin + +use std::error::Error; + +use lee_core::program::ProgramId; +use risc0_binfmt::ProgramBinary; +use token_mint_authority_core::compute_mint_authority_pda; + +fn main() -> Result<(), Box> { + let path = std::env::args().nth(1).ok_or( + "usage: cargo run -p token_mint_authority_program --example mint_authority -- \ + ", + )?; + + let bytes = std::fs::read(&path).map_err(|error| format!("failed to read {path}: {error}"))?; + let binary = ProgramBinary::decode(&bytes) + .map_err(|error| format!("failed to decode program binary: {error}"))?; + let program_id: ProgramId = binary + .compute_image_id() + .map_err(|error| format!("failed to compute image id: {error}"))? + .into(); + + let authority = compute_mint_authority_pda(program_id); + + println!("program binary: {path}"); + println!( + "program id (hex): {}", + hex::encode(program_id_bytes(program_id)) + ); + println!(); + println!("Set this as `mint_authority` on every faucet token definition:"); + println!(" base58: {authority}"); + println!(" hex: {}", hex::encode(authority.to_bytes())); + + Ok(()) +} + +/// The 8×u32 ImageID as its 32-byte little-endian form (the ProgramId `spel inspect` reports). +fn program_id_bytes(program_id: ProgramId) -> [u8; 32] { + let mut bytes = [0u8; 32]; + for (chunk, word) in bytes.chunks_exact_mut(4).zip(program_id) { + chunk.copy_from_slice(&word.to_le_bytes()); + } + bytes +} diff --git a/programs/token_mint_authority/methods/Cargo.toml b/programs/token_mint_authority/methods/Cargo.toml new file mode 100644 index 00000000..ab89018a --- /dev/null +++ b/programs/token_mint_authority/methods/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "token-mint-authority-methods" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = "=3.0.5" + +[dependencies] +risc0-zkvm = { version = "=3.0.5", features = ["std"] } +token_mint_authority_core = { path = "../core" } + +[package.metadata.risc0] +methods = ["guest"] diff --git a/programs/token_mint_authority/methods/build.rs b/programs/token_mint_authority/methods/build.rs new file mode 100644 index 00000000..64657e82 --- /dev/null +++ b/programs/token_mint_authority/methods/build.rs @@ -0,0 +1,4 @@ +//! Build script that embeds the token-mint-authority RISC Zero guest ELF as host-side constants. +fn main() { + risc0_build::embed_methods(); +} diff --git a/programs/token_mint_authority/methods/guest/Cargo.lock b/programs/token_mint_authority/methods/guest/Cargo.lock new file mode 100644 index 00000000..1d6aac72 --- /dev/null +++ b/programs/token_mint_authority/methods/guest/Cargo.lock @@ -0,0 +1,2470 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-r1cs-std", + "ark-std", +] + +[[package]] +name = "ark-crypto-primitives" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0c292754729c8a190e50414fd1a37093c786c709899f29c9f7daccecfa855e" +dependencies = [ + "ahash", + "ark-crypto-primitives-macros", + "ark-ec", + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-snark", + "ark-std", + "blake2", + "derivative", + "digest 0.10.7", + "fnv", + "merlin", + "sha2", +] + +[[package]] +name = "ark-crypto-primitives-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-groth16" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f1d0f3a534bb54188b8dcc104307db6c56cdae574ddc3212aec0625740fc7e" +dependencies = [ + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-r1cs-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-relations", + "ark-std", + "educe", + "num-bigint", + "num-integer", + "num-traits", + "tracing", +] + +[[package]] +name = "ark-relations" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" +dependencies = [ + "ark-ff", + "ark-std", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-snark" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d368e2848c2d4c129ce7679a7d0d2d612b6a274d3ea6a13bad4445d61b381b88" +dependencies = [ + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.8", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base58" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bytesize" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.3.0", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "clock_core" +version = "0.1.0" +source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.4#47eba256479f6f785acbd138834340703cd03401" +dependencies = [ + "borsh", + "lee_core", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "ctutils", + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "include_bytes_aligned" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee796ad498c8d9a1d68e477df8f754ed784ef875de1414ebdaf169f70a6a784" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.2", + "rand_core 0.10.1", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lee_core" +version = "0.1.0" +source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.4#47eba256479f6f785acbd138834340703cd03401" +dependencies = [ + "base58", + "borsh", + "bytemuck", + "bytesize", + "chacha20", + "ml-kem", + "risc0-zkvm", + "serde", + "serde_with", + "thiserror 2.0.20", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak 0.1.6", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8", + "rand_core 0.10.1", + "sha3", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + +[[package]] +name = "no_std_strings" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "unarray", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "risc0-binfmt" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d836c6ad82f4ced7c61d5feedf905a17780312e393aa681d29cc0bbc5131672b" +dependencies = [ + "anyhow", + "borsh", + "bytemuck", + "derive_more", + "elf", + "lazy_static", + "postcard", + "rand 0.9.5", + "risc0-zkp", + "risc0-zkvm-platform", + "ruint", + "semver", + "serde", + "tracing", +] + +[[package]] +name = "risc0-circuit-keccak" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c731e12429eb4457e1ddc69c56ee7343a1e10b86e4aa55bc8f4d2b13734abb9" +dependencies = [ + "anyhow", + "bytemuck", + "paste", + "risc0-binfmt", + "risc0-circuit-recursion", + "risc0-core", + "risc0-zkp", + "tracing", +] + +[[package]] +name = "risc0-circuit-recursion" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40dd640122abcc67d4d4e4f055c68cbc3ad2efb8589c65c2b23d354632971b60" +dependencies = [ + "anyhow", + "bytemuck", + "hex", + "metal", + "risc0-core", + "risc0-zkp", + "tracing", +] + +[[package]] +name = "risc0-circuit-rv32im" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb11231aa4b74bcc0c8d16597893fbd7ea6f6a9ebbc35e16bfd06b467c7ee104" +dependencies = [ + "anyhow", + "bit-vec", + "bytemuck", + "derive_more", + "paste", + "risc0-binfmt", + "risc0-core", + "risc0-zkp", + "serde", + "tracing", +] + +[[package]] +name = "risc0-core" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6eb2d2b2c6cac0e43cbb2202daacee1a2f24d0dfa03fd08887a11dc6defdcc1" +dependencies = [ + "bytemuck", + "rand_core 0.9.5", +] + +[[package]] +name = "risc0-groth16" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0ca702ea7d0162766defe7ed6a79bda4a747ad9e2684000a6edd14df0a6d1f3" +dependencies = [ + "anyhow", + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-groth16", + "ark-serialize", + "bytemuck", + "hex", + "num-bigint", + "num-traits", + "risc0-binfmt", + "risc0-zkp", + "serde", +] + +[[package]] +name = "risc0-zkos-v1compat" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b0b598ba7946354b10ca5c56e382de801e6c7fce9fccad0396ec436bc5072b" +dependencies = [ + "include_bytes_aligned", + "no_std_strings", + "risc0-zkvm-platform", +] + +[[package]] +name = "risc0-zkp" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21c0c921e5e2d44197940d387a45e29c6165e318b5a168fdfdbd50f50ba03678" +dependencies = [ + "anyhow", + "blake2", + "borsh", + "bytemuck", + "cfg-if", + "digest 0.10.7", + "hex", + "hex-literal", + "metal", + "paste", + "rand_core 0.9.5", + "risc0-core", + "risc0-zkvm-platform", + "serde", + "sha2", + "stability", + "tracing", +] + +[[package]] +name = "risc0-zkvm" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22b7eafb5d85be59cbd9da83f662cf47d834f1b836e14f675d1530b12c666867" +dependencies = [ + "anyhow", + "borsh", + "bytemuck", + "derive_more", + "hex", + "risc0-binfmt", + "risc0-circuit-keccak", + "risc0-circuit-recursion", + "risc0-circuit-rv32im", + "risc0-core", + "risc0-groth16", + "risc0-zkos-v1compat", + "risc0-zkp", + "risc0-zkvm-platform", + "rrs-lib", + "semver", + "serde", + "sha2", + "stability", + "tracing", +] + +[[package]] +name = "risc0-zkvm-platform" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb37a97ff7e8e4ee1b2a1c43ec143b4887759883c343507af9e4787a57914cd" +dependencies = [ + "bytemuck", + "cfg-if", + "getrandom 0.2.17", + "getrandom 0.3.4", + "libm", + "num_enum", + "paste", + "stability", +] + +[[package]] +name = "rrs-lib" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4382d3af3a4ebdae7f64ba6edd9114fff92c89808004c4943b393377a25d001" +dependencies = [ + "downcast-rs", + "paste", +] + +[[package]] +name = "ruint" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" +dependencies = [ + "borsh", + "proptest", + "rand 0.8.8", + "rand 0.9.5", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "spel-framework" +version = "0.6.0" +source = "git+https://github.com/logos-co/spel.git?rev=1ef0500f8fc8ce3ddf95523726ae159db83f744f#1ef0500f8fc8ce3ddf95523726ae159db83f744f" +dependencies = [ + "borsh", + "lee_core", + "serde_json", + "spel-framework-core", + "spel-framework-macros", +] + +[[package]] +name = "spel-framework-core" +version = "0.6.0" +source = "git+https://github.com/logos-co/spel.git?rev=1ef0500f8fc8ce3ddf95523726ae159db83f744f#1ef0500f8fc8ce3ddf95523726ae159db83f744f" +dependencies = [ + "base58", + "borsh", + "lee_core", + "proc-macro2", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "thiserror 1.0.69", + "toml", +] + +[[package]] +name = "spel-framework-macros" +version = "0.6.0" +source = "git+https://github.com/logos-co/spel.git?rev=1ef0500f8fc8ce3ddf95523726ae159db83f744f#1ef0500f8fc8ce3ddf95523726ae159db83f744f" +dependencies = [ + "proc-macro2", + "quote", + "serde_json", + "sha2", + "spel-framework-core", + "syn 2.0.119", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "token-mint-authority-guest" +version = "0.1.0" +dependencies = [ + "borsh", + "lee_core", + "risc0-zkvm", + "serde", + "spel-framework", + "token_mint_authority_core", + "token_mint_authority_program", +] + +[[package]] +name = "token_core" +version = "0.1.0" +dependencies = [ + "borsh", + "lee_core", + "serde", + "spel-framework-macros", +] + +[[package]] +name = "token_mint_authority_core" +version = "0.1.0" +dependencies = [ + "borsh", + "lee_core", + "risc0-zkvm", + "serde", + "spel-framework-macros", +] + +[[package]] +name = "token_mint_authority_program" +version = "0.1.0" +dependencies = [ + "clock_core", + "lee_core", + "token_core", + "token_mint_authority_core", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/programs/token_mint_authority/methods/guest/Cargo.toml b/programs/token_mint_authority/methods/guest/Cargo.toml new file mode 100644 index 00000000..37441016 --- /dev/null +++ b/programs/token_mint_authority/methods/guest/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "token-mint-authority-guest" +version = "0.1.0" +edition = "2021" + +[workspace] + +[profile.release] +debug = 0 +strip = "symbols" + +[[bin]] +name = "token_mint_authority" +path = "src/bin/token_mint_authority.rs" + +[dependencies] +spel-framework = { git = "https://github.com/logos-co/spel.git", rev = "1ef0500f8fc8ce3ddf95523726ae159db83f744f", package = "spel-framework" } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.4", package = "lee_core" } +risc0-zkvm = { version = "=3.0.5", default-features = false } +token_mint_authority_core = { path = "../../core" } +token_mint_authority_program = { path = "../..", package = "token_mint_authority_program" } +serde = { version = "1.0", features = ["derive"] } +borsh = "1.5" diff --git a/programs/token_mint_authority/methods/guest/src/bin/token_mint_authority.rs b/programs/token_mint_authority/methods/guest/src/bin/token_mint_authority.rs new file mode 100644 index 00000000..68041a59 --- /dev/null +++ b/programs/token_mint_authority/methods/guest/src/bin/token_mint_authority.rs @@ -0,0 +1,56 @@ +#![cfg_attr(not(test), no_main)] + +use nssa_core::account::AccountWithMetadata; +use spel_framework::context::ProgramContext; +use spel_framework::prelude::*; + +#[cfg(not(test))] +risc0_zkvm::guest::entry!(main); + +#[lez_program(instruction = "token_mint_authority_core::Instruction")] +mod token_mint_authority { + #[allow(unused_imports)] + use super::*; + + /// Mint a fixed faucet grant to the calling recipient, rate-limited to once + /// per 24h per `(recipient, token definition)` (host fn + /// `token_mint_authority_program::faucet_mint::faucet_mint`). + /// + /// Pure delegating proxy: `user -> token-mint-authority -> token`. Emits one chained + /// `Token::MintWithAuthority` authorized by this program's mint-authority PDA + /// seed — the Token-Mint-Authority holds no key of its own. Wall-clock time is read + /// from the system `CLOCK_01` account passed as the 6th input, since the + /// pinned `ProgramContext` exposes no clock. + /// + /// # Errors + /// Returns the host program's panic-converted error if any precondition + /// fails — see the host fn for the full list. + #[instruction] + pub fn faucet_mint( + ctx: ProgramContext, + #[account(signer)] + recipient: AccountWithMetadata, + #[account(mut)] + mint_allowance: AccountWithMetadata, + #[account(mut)] + user_holding: AccountWithMetadata, + #[account(mut)] + token_definition: AccountWithMetadata, + mint_authority: AccountWithMetadata, + clock: AccountWithMetadata, + ) -> SpelResult { + let (post_states, chained_calls) = token_mint_authority_program::faucet_mint::faucet_mint( + recipient, + mint_allowance, + user_holding, + token_definition, + mint_authority, + clock, + ctx.self_program_id, + ); + Ok(spel_framework::SpelOutput::execute( + post_states, + chained_calls, + )) + } +} diff --git a/programs/token_mint_authority/methods/src/lib.rs b/programs/token_mint_authority/methods/src/lib.rs new file mode 100644 index 00000000..3a096025 --- /dev/null +++ b/programs/token_mint_authority/methods/src/lib.rs @@ -0,0 +1,12 @@ +//! Host-side embedding of the token-mint-authority RISC Zero guest ELF. +//! +//! Re-exports the constants produced by `build.rs` via `risc0_build::embed_methods` — +//! `TOKEN_MINT_AUTHORITY_ELF`, `TOKEN_MINT_AUTHORITY_PATH`, and `TOKEN_MINT_AUTHORITY_ID` — used by +//! host code to load and identify the guest binary. + +#![allow( + missing_docs, + reason = "constants below are generated by risc0_build::embed_methods at build time" +)] + +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/programs/token_mint_authority/src/faucet_mint.rs b/programs/token_mint_authority/src/faucet_mint.rs new file mode 100644 index 00000000..5c2c3575 --- /dev/null +++ b/programs/token_mint_authority/src/faucet_mint.rs @@ -0,0 +1,178 @@ +//! Host-side implementation of [`token_mint_authority_core::Instruction::FaucetMint`]. +//! +//! Delegates to the Token Program (`user -> token-mint-authority -> token`): after the +//! per-day rate-limit check, it emits a single chained +//! `Token::MintWithAuthority` that mints [`FAUCET_MINT_AMOUNT`] to the caller, +//! authorized by the Token-Mint-Authority's mint-authority PDA seed. The Token-Mint-Authority +//! holds no key — the PDA seed is its authority. +//! +//! Wall-clock time comes from the system `CLOCK_01` account (the pinned +//! `spel-framework`'s `ProgramContext` exposes no clock), same as the stablecoin +//! program. + +use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID}; +use lee_core::{ + account::{Account, AccountWithMetadata}, + program::{AccountPostState, ChainedCall, Claim, ProgramId}, +}; +use token_core::TokenDefinition; +use token_mint_authority_core::{ + verify_mint_allowance_and_get_seed, verify_mint_authority_and_get_seed, MintAllowance, + FAUCET_MINT_AMOUNT, MINT_COOLDOWN_MS, +}; + +/// Grant [`FAUCET_MINT_AMOUNT`] of the faucet token to `recipient`, at most once +/// per [`MINT_COOLDOWN_MS`] per `(recipient, token definition)`. +/// +/// Returns the six echoed/claimed post-states (in input-account order) and one +/// chained `Token::MintWithAuthority`. +/// +/// The recipient's `user_holding` authorization is deliberately NOT checked +/// here — the Token Program enforces it downstream: an existing holding is just +/// written, while a fresh one is claimed via `Claim::Authorized`, which fails +/// unless the recipient also authorized the holding. So the faucet stays +/// permissive (mint into any existing holding) without letting anyone create a +/// holding they don't control. +/// +/// # Panics +/// - `recipient` is not authorized. +/// - `token_definition` is uninitialized, not a `Fungible`, or its stored `mint_authority` is not +/// `mint_authority` / is renounced. +/// - `mint_authority` / `mint_allowance` do not match their PDA derivations. +/// - `mint_allowance` exists but is not owned by this program, or its cooldown has not elapsed +/// (`FaucetMint cooldown has not elapsed`). +/// - `clock` is not the system `CLOCK_01` account or is uninitialized. +#[allow( + clippy::too_many_arguments, + reason = "six account inputs + program id mirror the host-call ABI; a struct would obscure it" +)] +pub fn faucet_mint( + recipient: AccountWithMetadata, + mint_allowance: AccountWithMetadata, + user_holding: AccountWithMetadata, + token_definition: AccountWithMetadata, + mint_authority: AccountWithMetadata, + clock: AccountWithMetadata, + token_mint_authority_program_id: ProgramId, +) -> (Vec, Vec) { + assert!( + recipient.is_authorized, + "Recipient authorization is missing" + ); + + // The faucet token is owned by the Token Program; that owner is the target + // of the chained mint. + assert_ne!( + token_definition.account, + Account::default(), + "Faucet token definition must be initialized" + ); + let token_program_id = token_definition.account.program_owner; + if user_holding.account != Account::default() { + assert_eq!( + user_holding.account.program_owner, token_program_id, + "User holding must be owned by the same Token Program as the token definition" + ); + } + let definition_id = token_definition.account_id; + // The definition must be a mintable Fungible whose stored authority is this + // program's mint-authority PDA — otherwise the chained mint could never + // succeed, so fail early with a clear message. + let authority_seed = + verify_mint_authority_and_get_seed(&mint_authority, token_mint_authority_program_id); + match TokenDefinition::try_from(&token_definition.account.data) + .expect("Faucet token definition must decode as a TokenDefinition") + { + TokenDefinition::Fungible { authority, .. } => { + let authority = + authority.expect("Faucet token has a renounced mint authority (fixed supply)"); + assert_eq!( + authority, mint_authority.account_id, + "Faucet token mint authority is not this program's mint-authority PDA" + ); + } + TokenDefinition::NonFungible { .. } => { + panic!("Faucet token definition must be Fungible"); + } + } + + let now = read_clock(&clock); + + let allowance_seed = verify_mint_allowance_and_get_seed( + &mint_allowance, + recipient.account_id, + definition_id, + token_mint_authority_program_id, + ); + + // First mint claims the allowance PDA; later mints must respect the cooldown. + // A default (unowned) account means this recipient has never used this faucet + // token, so there is nothing to throttle yet. + if mint_allowance.account != Account::default() { + assert_eq!( + mint_allowance.account.program_owner, token_mint_authority_program_id, + "Mint allowance account is not owned by this program" + ); + let previous = MintAllowance::try_from(&mint_allowance.account.data) + .expect("Mint allowance account must decode as a MintAllowance"); + // `saturating_sub` treats a backwards clock as "no time elapsed", which + // conservatively keeps the faucet throttled rather than opening it. + assert!( + now.saturating_sub(previous.last_mint_ms) >= MINT_COOLDOWN_MS, + "FaucetMint cooldown has not elapsed" + ); + } + + let updated = MintAllowance { + recipient_id: recipient.account_id, + definition_id, + last_mint_ms: now, + }; + let mut allowance_post = mint_allowance.account.clone(); + allowance_post.data = (&updated).into(); + + // Post-states mirror the input account order. `user_holding` and + // `token_definition` are echoed unchanged here; the chained mint applies the + // actual mutation. The allowance PDA is claimed on first use (Claim::Pda) so + // this program owns it and its `last_mint_ms` persists, then rewritten. The + // authority PDA is only echoed: it never holds state, and a default-state + // account is retained by the framework's output filter without a claim — so + // this program takes no ownership of it (the seed alone authorizes the mint). + let post_states = vec![ + AccountPostState::new(recipient.account), + AccountPostState::new_claimed_if_default(allowance_post, Claim::Pda(allowance_seed)), + AccountPostState::new(user_holding.account.clone()), + AccountPostState::new(token_definition.account.clone()), + AccountPostState::new(mint_authority.account.clone()), + AccountPostState::new(clock.account), + ]; + + // Delegate the mint to the Token Program under the mint-authority PDA seed. + // MintWithAuthority account order: [definition, holding, authority]. + let mut authority_authorized = mint_authority; + authority_authorized.is_authorized = true; + let mint_call = ChainedCall::new( + token_program_id, + vec![token_definition, user_holding, authority_authorized], + &token_core::Instruction::MintWithAuthority { + amount_to_mint: FAUCET_MINT_AMOUNT, + }, + ) + .with_pda_seeds(vec![authority_seed]); + + (post_states, vec![mint_call]) +} + +/// Read the millisecond wall-clock timestamp from the system `CLOCK_01` account. +pub(crate) fn read_clock(clock: &AccountWithMetadata) -> u64 { + assert_eq!( + clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID, + "Clock account must be the system CLOCK_01 account" + ); + assert_ne!( + clock.account, + Account::default(), + "Clock account must be initialized" + ); + ClockAccountData::from_bytes(clock.account.data.as_ref()).timestamp +} diff --git a/programs/token_mint_authority/src/lib.rs b/programs/token_mint_authority/src/lib.rs new file mode 100644 index 00000000..5f187b40 --- /dev/null +++ b/programs/token_mint_authority/src/lib.rs @@ -0,0 +1,16 @@ +//! The Token-Mint-Authority Program implementation. +//! +//! A permissionless testnet faucet that delegates to the Token Program: +//! `user -> token-mint-authority -> token`. See [`token_mint_authority_core`] for the account +//! contract and the mint-authority setup requirement. + +pub use token_mint_authority_core as core; + +/// Mint a fixed grant of the faucet token to the caller, rate-limited per day. +pub mod faucet_mint; + +#[cfg(test)] +mod test_support; + +#[cfg(test)] +mod tests; diff --git a/programs/token_mint_authority/src/test_support.rs b/programs/token_mint_authority/src/test_support.rs new file mode 100644 index 00000000..c84dcb8f --- /dev/null +++ b/programs/token_mint_authority/src/test_support.rs @@ -0,0 +1,156 @@ +//! Shared account builders for the `faucet_mint` host-function unit tests. + +use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID}; +use lee_core::{ + account::{Account, AccountId, AccountWithMetadata, Data, Nonce}, + program::ProgramId, +}; +use token_core::{TokenDefinition, TokenHolding}; +use token_mint_authority_core::{ + compute_mint_allowance_pda, compute_mint_authority_pda, MintAllowance, +}; + +pub(crate) const TOKEN_MINT_AUTHORITY_PROGRAM_ID: ProgramId = [11u32; 8]; +pub(crate) const TOKEN_PROGRAM_ID: ProgramId = [2u32; 8]; +pub(crate) const CLOCK_PROGRAM_ID: ProgramId = [5u32; 8]; + +/// The clock timestamp used as "now" in every test (Unix milliseconds). +pub(crate) const NOW: u64 = 1_700_000_000_000; + +pub(crate) fn recipient_id() -> AccountId { + AccountId::new([0xCA; 32]) +} +pub(crate) fn user_holding_id() -> AccountId { + AccountId::new([0xCB; 32]) +} +pub(crate) fn definition_id() -> AccountId { + AccountId::new([0x40; 32]) +} +pub(crate) fn mint_authority_id() -> AccountId { + compute_mint_authority_pda(TOKEN_MINT_AUTHORITY_PROGRAM_ID) +} +pub(crate) fn mint_allowance_id() -> AccountId { + compute_mint_allowance_pda( + TOKEN_MINT_AUTHORITY_PROGRAM_ID, + recipient_id(), + definition_id(), + ) +} + +/// The funded caller: authorized, no state of its own. +pub(crate) fn recipient_account() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: recipient_id(), + } +} + +/// An existing, authorized holding for the faucet token. +pub(crate) fn user_holding_account() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0, + data: Data::from(&TokenHolding::Fungible { + definition_id: definition_id(), + balance: 0, + }), + nonce: Nonce(0), + }, + is_authorized: true, + account_id: user_holding_id(), + } +} + +/// A not-yet-created holding — the Token Program materializes it during the +/// chained mint. Still authorized by the recipient's signature. +pub(crate) fn fresh_user_holding_account() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: user_holding_id(), + } +} + +/// The faucet token definition, minted by this program's authority PDA. +pub(crate) fn faucet_definition_account() -> AccountWithMetadata { + faucet_definition_with_authority(Some(mint_authority_id())) +} + +pub(crate) fn faucet_definition_with_authority( + authority: Option, +) -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("Faucet Token"), + total_supply: 0, + metadata_id: None, + authority, + }), + nonce: Nonce(0), + }, + is_authorized: false, + account_id: definition_id(), + } +} + +/// The mint-authority PDA as passed in: a bare, unclaimed account at the derived +/// address (the runtime authorizes it via the chained call's seed). +pub(crate) fn mint_authority_account() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: mint_authority_id(), + } +} + +/// A never-used allowance: default/unclaimed at the derived address. +pub(crate) fn uninitialized_allowance() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: mint_allowance_id(), + } +} + +/// An existing allowance owned by this program, last minted at `last_mint_ms`. +pub(crate) fn allowance_account(last_mint_ms: u64) -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_MINT_AUTHORITY_PROGRAM_ID, + balance: 0, + data: Data::from(&MintAllowance { + recipient_id: recipient_id(), + definition_id: definition_id(), + last_mint_ms, + }), + nonce: Nonce(0), + }, + is_authorized: false, + account_id: mint_allowance_id(), + } +} + +pub(crate) fn clock_account(timestamp: u64) -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: CLOCK_PROGRAM_ID, + balance: 0, + data: Data::try_from( + ClockAccountData { + block_id: 0, + timestamp, + } + .to_bytes(), + ) + .expect("clock data fits"), + nonce: Nonce(0), + }, + is_authorized: false, + account_id: CLOCK_01_PROGRAM_ACCOUNT_ID, + } +} diff --git a/programs/token_mint_authority/src/tests.rs b/programs/token_mint_authority/src/tests.rs new file mode 100644 index 00000000..b1ea9910 --- /dev/null +++ b/programs/token_mint_authority/src/tests.rs @@ -0,0 +1,267 @@ +//! Unit tests for the `faucet_mint` host function. + +#![allow( + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + reason = "tests deliberately panic on bad state via assert!/#[should_panic] and index fixed-size vectors" +)] + +use lee_core::{ + account::{Account, AccountId, AccountWithMetadata}, + program::{AccountPostState, ChainedCall, Claim}, +}; +use token_core::Instruction as TokenInstruction; +use token_mint_authority_core::{ + compute_mint_allowance_pda_seed, compute_mint_authority_pda_seed, MintAllowance, + FAUCET_MINT_AMOUNT, MINT_COOLDOWN_MS, +}; + +use crate::{ + faucet_mint::faucet_mint, + test_support::{ + allowance_account, clock_account, definition_id, faucet_definition_account, + faucet_definition_with_authority, fresh_user_holding_account, mint_authority_account, + mint_authority_id, recipient_account, recipient_id, uninitialized_allowance, + user_holding_account, user_holding_id, NOW, TOKEN_MINT_AUTHORITY_PROGRAM_ID, + TOKEN_PROGRAM_ID, + }, +}; + +fn invoke(allowance: AccountWithMetadata, now: u64) -> (Vec, Vec) { + faucet_mint( + recipient_account(), + allowance, + user_holding_account(), + faucet_definition_account(), + mint_authority_account(), + clock_account(now), + TOKEN_MINT_AUTHORITY_PROGRAM_ID, + ) +} + +fn decode_token_instruction(call: &ChainedCall) -> TokenInstruction { + risc0_zkvm::serde::from_slice::(&call.instruction_data) + .expect("chained instruction must decode as a token_core::Instruction") +} + +#[test] +fn first_mint_returns_six_post_states_and_one_chained_call() { + let (post_states, chained_calls) = invoke(uninitialized_allowance(), NOW); + assert_eq!(post_states.len(), 6); + assert_eq!(chained_calls.len(), 1); +} + +#[test] +fn first_mint_claims_allowance_pda_and_stamps_now() { + let (post_states, _) = invoke(uninitialized_allowance(), NOW); + // post_states[1] is the allowance (input-order). + assert_eq!( + post_states[1].required_claim(), + Some(Claim::Pda(compute_mint_allowance_pda_seed( + recipient_id(), + definition_id() + ))) + ); + let decoded = MintAllowance::try_from(&post_states[1].account().data).unwrap(); + assert_eq!(decoded.recipient_id, recipient_id()); + assert_eq!(decoded.definition_id, definition_id()); + assert_eq!(decoded.last_mint_ms, NOW); +} + +#[test] +fn first_mint_echoes_authority_pda_without_claiming_it() { + // The authority PDA holds no state; the seed alone authorizes the chained + // mint, so this program takes no ownership of it. + let (post_states, _) = invoke(uninitialized_allowance(), NOW); + assert_eq!(post_states[4].required_claim(), None); +} + +#[test] +fn post_state_order_mirrors_inputs() { + let (post_states, _) = invoke(uninitialized_allowance(), NOW); + // [recipient, allowance, holding, definition, authority, clock] + assert_eq!(post_states[0].account(), &Account::default()); // recipient echoed + assert_eq!(post_states[5].account(), &clock_account(NOW).account); // clock echoed +} + +#[test] +fn chained_call_delegates_fixed_mint_to_token_program() { + let (_, chained_calls) = invoke(uninitialized_allowance(), NOW); + let call = &chained_calls[0]; + + assert_eq!(call.program_id, TOKEN_PROGRAM_ID); + // MintWithAuthority account order: [definition, holding, authority]. + assert_eq!(call.pre_states.len(), 3); + assert_eq!(call.pre_states[0].account_id, definition_id()); + assert_eq!(call.pre_states[1].account_id, user_holding_id()); + assert_eq!(call.pre_states[2].account_id, mint_authority_id()); + // The authority PDA is authorized to the callee via its seed. + assert!(call.pre_states[2].is_authorized); + assert_eq!(call.pda_seeds, vec![compute_mint_authority_pda_seed()]); + + match decode_token_instruction(call) { + TokenInstruction::MintWithAuthority { amount_to_mint } => { + assert_eq!(amount_to_mint, FAUCET_MINT_AMOUNT); + } + _ => panic!("expected chained instruction to be Token::MintWithAuthority"), + } +} + +#[test] +fn mint_exactly_at_cooldown_boundary_is_allowed_and_rewrites_without_claim() { + let (post_states, chained_calls) = invoke(allowance_account(NOW - MINT_COOLDOWN_MS), NOW); + assert_eq!(chained_calls.len(), 1); + // Already owned by this program -> rewritten, not re-claimed. + assert_eq!(post_states[1].required_claim(), None); + let decoded = MintAllowance::try_from(&post_states[1].account().data).unwrap(); + assert_eq!(decoded.last_mint_ms, NOW); +} + +#[test] +fn mint_to_a_fresh_holding_delegates_the_authorized_holding() { + let (post_states, chained_calls) = faucet_mint( + recipient_account(), + uninitialized_allowance(), + fresh_user_holding_account(), + faucet_definition_account(), + mint_authority_account(), + clock_account(NOW), + TOKEN_MINT_AUTHORITY_PROGRAM_ID, + ); + assert_eq!(chained_calls.len(), 1); + // Holding is echoed as default; the chained mint materializes it. + assert_eq!(post_states[2].account(), &Account::default()); + assert_eq!(chained_calls[0].pre_states[1].account_id, user_holding_id()); + assert!(chained_calls[0].pre_states[1].is_authorized); +} + +#[test] +#[should_panic(expected = "FaucetMint cooldown has not elapsed")] +fn rejects_a_second_mint_within_the_cooldown() { + let _ = invoke(allowance_account(NOW - (MINT_COOLDOWN_MS - 1)), NOW); +} + +#[test] +#[should_panic(expected = "FaucetMint cooldown has not elapsed")] +fn rejects_a_second_mint_at_the_same_instant() { + let _ = invoke(allowance_account(NOW), NOW); +} + +#[test] +#[should_panic(expected = "FaucetMint cooldown has not elapsed")] +fn a_backwards_clock_stays_throttled() { + // Clock earlier than the last mint: saturating_sub -> 0 elapsed -> blocked. + let _ = invoke(allowance_account(NOW), NOW - 1_000); +} + +#[test] +#[should_panic(expected = "Recipient authorization is missing")] +fn rejects_unauthorized_recipient() { + let mut recipient = recipient_account(); + recipient.is_authorized = false; + let _ = faucet_mint( + recipient, + uninitialized_allowance(), + user_holding_account(), + faucet_definition_account(), + mint_authority_account(), + clock_account(NOW), + TOKEN_MINT_AUTHORITY_PROGRAM_ID, + ); +} + +#[test] +#[should_panic(expected = "Faucet token definition must be initialized")] +fn rejects_uninitialized_definition() { + let definition = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: definition_id(), + }; + let _ = faucet_mint( + recipient_account(), + uninitialized_allowance(), + user_holding_account(), + definition, + mint_authority_account(), + clock_account(NOW), + TOKEN_MINT_AUTHORITY_PROGRAM_ID, + ); +} + +#[test] +#[should_panic(expected = "Faucet token mint authority is not this program's mint-authority PDA")] +fn rejects_definition_whose_authority_is_not_our_pda() { + let _ = faucet_mint( + recipient_account(), + uninitialized_allowance(), + user_holding_account(), + faucet_definition_with_authority(Some(AccountId::new([0xEE; 32]))), + mint_authority_account(), + clock_account(NOW), + TOKEN_MINT_AUTHORITY_PROGRAM_ID, + ); +} + +#[test] +#[should_panic(expected = "renounced mint authority")] +fn rejects_definition_with_renounced_authority() { + let _ = faucet_mint( + recipient_account(), + uninitialized_allowance(), + user_holding_account(), + faucet_definition_with_authority(None), + mint_authority_account(), + clock_account(NOW), + TOKEN_MINT_AUTHORITY_PROGRAM_ID, + ); +} + +#[test] +#[should_panic(expected = "Mint authority account ID does not match expected PDA derivation")] +fn rejects_wrong_authority_pda() { + let mut authority = mint_authority_account(); + authority.account_id = AccountId::new([0xEE; 32]); + let _ = faucet_mint( + recipient_account(), + uninitialized_allowance(), + user_holding_account(), + faucet_definition_account(), + authority, + clock_account(NOW), + TOKEN_MINT_AUTHORITY_PROGRAM_ID, + ); +} + +#[test] +#[should_panic(expected = "Mint allowance account ID does not match expected PDA derivation")] +fn rejects_wrong_allowance_pda() { + let mut allowance = uninitialized_allowance(); + allowance.account_id = AccountId::new([0xEE; 32]); + let _ = invoke(allowance, NOW); +} + +#[test] +#[should_panic(expected = "Mint allowance account is not owned by this program")] +fn rejects_foreign_owned_allowance() { + let mut allowance = allowance_account(NOW - MINT_COOLDOWN_MS); + allowance.account.program_owner = [9u32; 8]; + let _ = invoke(allowance, NOW); +} + +#[test] +#[should_panic(expected = "Clock account must be the system CLOCK_01 account")] +fn rejects_wrong_clock_account() { + let mut clock = clock_account(NOW); + clock.account_id = AccountId::new([0xC1; 32]); + let _ = faucet_mint( + recipient_account(), + uninitialized_allowance(), + user_holding_account(), + faucet_definition_account(), + mint_authority_account(), + clock, + TOKEN_MINT_AUTHORITY_PROGRAM_ID, + ); +} diff --git a/scripts/build-guests.Dockerfile b/scripts/build-guests.Dockerfile index b11641eb..f7b0135d 100644 --- a/scripts/build-guests.Dockerfile +++ b/scripts/build-guests.Dockerfile @@ -18,7 +18,7 @@ RUN --mount=type=cache,id=${RISC0_BUILD_CACHE_ID}-cargo-git,sharing=locked,targe set -eu target_triple="riscv32im-risc0-zkvm-elf" -programs="amm ata stablecoin token twap_oracle" +programs="amm ata stablecoin token token_mint_authority twap_oracle" unit_separator="$(printf '\037')" guest_rustflags="-C${unit_separator}passes=lower-atomic${unit_separator}-C${unit_separator}link-arg=-Ttext=0x00200800${unit_separator}-C${unit_separator}link-arg=--fatal-warnings${unit_separator}-C${unit_separator}panic=abort${unit_separator}--cfg${unit_separator}getrandom_backend=\"custom\"" export CARGO_ENCODED_RUSTFLAGS="${guest_rustflags}" diff --git a/scripts/build-guests.sh b/scripts/build-guests.sh index 70f6bb6d..74394239 100755 --- a/scripts/build-guests.sh +++ b/scripts/build-guests.sh @@ -28,7 +28,7 @@ out_dir="${repo_root}/target/guest" rm -rf "${out_dir}" mkdir -p "${out_dir}" -for program in amm ata stablecoin token twap_oracle; do +for program in amm ata stablecoin token token_mint_authority twap_oracle; do rm -rf "${repo_root}/programs/${program}/methods/guest/target" install -m 0644 "${staging_dir}/${program}.bin" "${out_dir}/${program}.bin" done