LogosWalletProvider::submitPublicTransaction passed the RISC0 instruction
words to send_generic_public_transaction as a QVariantList<u32>. That param
is a byte string (bstr): the module's QtRO glue mangles a list-of-u32 crossing
the module process boundary, so the guest deserialized a corrupted Instruction
variant and panicked, e.g.
Guest panicked: called `Result::unwrap()` on an `Err` value:
Custom("invalid value: integer `53765`, expected variant index 0 <= i < 10")
(53765 = 0x0000D205 — the guest read a 4-byte word where 0x05, the AddLiquidity
variant, is a single byte.) This broke every submit through the shared provider,
including the new-position / add-liquidity flow.
Send the little-endian bytes of the u32 words as a QByteArray instead — the same
encoding the AMM swap path already uses (it calls the module directly and never
went through this provider). signingRequirements stays a QVariantList<bool>;
only the instruction needed the bstr encoding.
The create-pool / new-position flow carried its own network layer:
AMM_UI_NETWORK + AMM_UI_DEVNET_FILE (a devnet.json) or a bundled
config/networks.json supplied the AMM program id and token set, and a
JSON-RPC channel/checkpoint "identity probe" gated the flow to a verified
network. Main already exposes all of this the way the Swap view consumes it,
so collapse onto those sources instead of a parallel system:
- ammProgramId <- $AMM_PROGRAM_BIN (derived like swapExactInput's program id;
doubles as the quote's network fingerprint so a quote can't
be replayed against a different deployment)
- tokenIds <- $TOKENS_CONFIG (amm-tokens.json), same as the Swap picker
- sequencer <- the wallet config (already surfaced via syncWalletState)
networkSnapshot() builds the ActiveNetworkSnapshot from those; status is
"ready"/"config_missing", gated to "loading" until wallet state resolves so no
module reads happen during construction. The channel probe is gone — submit
needs no channelId (the wallet module supplies the channel via
submitPublicTransaction), so the whole verification apparatus was overhead.
Removes: AMM_UI_NETWORK / AMM_UI_DEVNET_FILE, devnet.json / networks.json, the
ActiveNetwork class (+ its test) and its QNetwork channel probe, and Qt6Network.
ActiveNetwork.h keeps only the ActiveNetworkSnapshot struct. Run command drops
the AMM_UI_* vars:
```
LEE_WALLET_HOME_DIR=… AMM_PROGRAM_BIN=… TOKENS_CONFIG=… nix run .#amm-ui
```
Keep wallet assets as token choices without automatically selecting a pair. Keep pool-probe amounts internal so active-pool quotes do not populate the form before user input.
Add a "Create Pool" flow to the AMM app that lets a user open a new
liquidity position — seeding a pool's initial liquidity — from token
selection and amount entry, through a confirmation dialog, to on-chain
submission.
- client crate (apps/amm/client): pure, testable protocol logic — account
decoding, pair/position modelling, and quote/plan computation — exposed to
the app over a C ABI (config/networks.json drives network selection).
- C++ runtime + backend: AmmClient, ActiveNetwork, and NewPositionRuntime,
wired into AmmUiBackend (new resolve/quote/submit slots).
- QML flow: NewPositionForm, NewPositionFlow state, NewPositionConfirmation-
Dialog, TokenSelectorModal, and reusable Amm* presentational components
(theme, surfaces, buttons) + AmountMath.js.
- tests: C++ (NewPositionRuntimeTest, ActiveNetworkTest) and QML
(tst_NewPositionForm, tst_LiquidityPage, tst_TokenAmountInput, …).
The Logos.Wallet QML plugin was installed with an ELF "$ORIGIN" rpath.
macOS dyld does not expand "$ORIGIN", so loading the plugin failed with
Cannot load library liblogos_wallet_qmlplugin.dylib:
Library not loaded: @rpath/liblogos_wallet_qml.dylib
even though liblogos_wallet_qml.dylib sits in the same directory. Select
the rpath per platform: @loader_path on Apple, $ORIGIN elsewhere, so
@rpath/liblogos_wallet_qml.dylib resolves to the sibling library.
Replace the `token_definition_id_in` argument with a role-based account
interface: swaps now take a `user_input_holding` and a `user_output_holding`
instead of positional token-A/token-B holdings. Direction is derived from the
input holding's own token definition, and the input slot is a framework-level
`#[account(signer)]` so authorization is enforced before execution rather than
delegated solely to the downstream token transfer. The output holding only
receives and needs no signature.
This removes the ambiguity the previous arg-based model carried (the arg could
disagree with the signed holding) and makes the IDL express the signing rule:
`user_input_holding` is `signer: true`, `user_output_holding` is `signer: false`.
The two user-holding post-states are echoed in the guest's declared slot order
(input, then output); the framework matches post-states to accounts by
position, so the internal A/B mapping used for reserve bookkeeping must not leak
into the returned order.
BREAKING CHANGE: The AMM swap instruction interface changed and the guest
ImageID/ProgramId changes as a result.
Move the spel-framework dependency from the 0x-r4bbit/spel fork
(v0.5.0 @ 91023c9, the refactor/lez-v020-compat branch) to the released
logos-co/spel tag v0.6.0.
spel v0.6.0 is built against the final logos-execution-zone v0.2.0, not
the v0.2.0-rc6 this repo pinned. The two must match: with rc6, the guest
ELF build fails because spel's lee_core::program::ValidityWindow and the
repo's own copy are distinct types the #[lez_program] macro can't unify.
So this also bumps every logos-execution-zone pin (lee/lee_core aliased
as nssa/nssa_core, plus clock_core) from v0.2.0-rc6 to the final v0.2.0.
- 10 Cargo.toml switched to logos-co/spel tag v0.6.0
- 24 logos-execution-zone pins across 18 Cargo.toml moved rc6 -> v0.2.0
- All 7 Cargo.lock files re-resolved (root + 5 guest workspaces +
benchmark); pulls in lee_core v0.1.0 (v0.2.0) as a spel transitive dep
Guest ImageIDs change as a result: the ID hashes the whole guest ELF,
which links the updated spel-framework and lee_core object code, even
though the program sources are unchanged. Update any ImageID-derived
values (deployed program IDs, PDA addresses, AMM/ATA program-id inputs)
before submitting transactions.
The AMM guest lockfile had drifted to enum-ordinalize 4.4.1 (and
enum-ordinalize-derive 4.4.1), both of which require rustc 1.89. The
RISC Zero guest toolchain is 1.88.0-dev, so `cargo +risc0 build
--locked` failed the "build programs" CI task:
error: rustc 1.88.0-dev is not supported by the following packages:
enum-ordinalize@4.4.1 requires rustc 1.89
enum-ordinalize-derive@4.4.1 requires rustc 1.89
enum-ordinalize is pulled in transitively via educe 0.6.0 (^4.3), so
4.3.2 satisfies the requirement and matches what every other guest
lockfile already resolves to. Pin both crates back to 4.3.2.
The host workspace Cargo.lock keeps 4.4.1 — it builds on 1.94.0 per
rust-toolchain.toml and is unaffected.
Add an optional mint authority to fungible tokens for controlled supply:
create with a designated minter, mint additional supply, rotate the
authority to a new key, or permanently revoke it to fix the supply.
The authority is stored inline on `TokenDefinition::Fungible` as
`authority: Option<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` rather than a custom
wrapper type leaves account state decodable by `spel inspect`; the
require/rotate/revoke guard logic lives inline in the handlers.
LEZ rejects a transaction that lists the same account id twice, so one
instruction cannot statically express both "the definition account is the
authority and signs" (self/PDA authority) and "a distinct rotated account
signs" (external authority) — they need opposite signer markers. Each
privileged operation is therefore split into a self and an external
variant:
- `Mint` / `SetAuthority` — the definition account is the signer.
- `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority
account is the signer; the definition account does not sign.
Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero
authority id is rejected. The AMM's LP token uses self/PDA authority — its
stored authority is the LP definition PDA, minted only by the pool via
chained calls.
Covered by token unit tests and zkVM integration tests: creation with and
without an authority, self- and external-authority mint, rotation, and
external rotate/revoke. IDLs regenerated.
Turns the dummy-data AMM UI into a real client of the on-chain LEZ wallet.
Adds a hand-written ui_qml C++ backend (src/AmmUi*) over the core
logos_execution_zone module: create/open a local wallet, create and list
public/private accounts, and a navbar Connect / Connected + account-selector
+ Disconnect flow. Onboarding is password-only (no path picking) with a
per-app wallet at ~/.lee/amm-wallet (override: AMM_WALLET_HOME_DIR);
standalone gets its own wallet, Basecamp shares accounts via adopt-on-start.
Requires Nix with flakes; macOS also needs `sandbox = false` (the default).
The logos_execution_zone input is pinned to a module rev whose LEZ (lssa)
already includes the macOS Metal-build fix, so no `--override-input` is
needed — plain `nix run .` works:
cd apps/amm
nix run .
- create_new now returns the new wallet's BIP39 mnemonic (not an int status);
the app currently discards it, so the wallet can't yet be recovered. Surfacing
it in onboarding (+ restore_storage) is a follow-up.
- The wallet password is currently a no-op upstream (storage.rs: "TODO: use
password for storage encryption"); storage.json is plaintext. So Disconnect
is a UI-level lock and reconnect does not (cannot yet) re-prompt for it.
- wallet-ffi requires explicit config/storage paths; a *_default() FFI would
let the app drop its path handling.
- Bundled network config: connects to whatever WalletConfig::default() points
at; real testnet endpoints still TBD.
The AMM multiplied amounts in u128 — `token_a * token_b` for the initial
LP in `new_definition`, `reserve * amount` in swaps, and the mul/div steps
in add/remove liquidity. For realistic 18-decimal token amounts the
intermediate product exceeds `u128::MAX` (~3.4e38): opening a pool with
100/200 tokens is `1e20 * 2e20 = 2e40`, which panicked and caused the
sequencer to skip the transaction.
Widen the intermediate arithmetic, not the stored types. Add
`mul_div_floor`, `mul_div_ceil`, and `isqrt_product` to `amm_core` (using
`alloy_primitives::U256`, as `spot_price_q64_64` already does): they
compute the product/division/sqrt in U256 and downcast the result back to
u128. Route `new_definition`, `swap_exact_input`/`swap_exact_output`,
`add_liquidity`, and `remove_liquidity` through them. `swap_exact_output`
keeps its ceil rounding (required input rounded up, in the pool's favour)
via `mul_div_ceil`.
Balances, reserves, and LP supply stay u128, so account data formats,
IDLs, and the token/ata/stablecoin programs are unchanged. This lifts the
usable amount range to the full u128.
Bump the LEZ dependency from the `lez-core-v0.2.0` tag to `v0.2.0-rc6` across
the workspace and all guest manifests (still resolving via the renamed
`lee_core`/`lee` packages), and regenerate the lockfiles to match.
rc6 moved the clock program out of `nssa` into a separate system-programs crate
(gated behind the guest-building `artifacts` feature), so adapt the tests:
- Import `ClockAccountData` and `CLOCK_01_PROGRAM_ACCOUNT_ID` from `clock_core`
instead of `nssa`, and build clock data via `ClockAccountData::to_bytes()`
rather than hand-encoding the Borsh layout.
- `V03State::new()` no longer auto-creates the clock account, so AMM tests seed
the canonical 1-block clock explicitly before ops that read it.
- `advance_clock` now writes the clock account directly via
`force_insert_account` (the clock can no longer be ticked with a real
transaction), matching how upstream rc6 state-machine tests seed accounts.
- Add the `clock_core` dependency to integration_tests/benchmark.
Bump the LEZ dependency from the `v0.2.0-rc3` tags to the released
`lez-core-v0.2.0` tag across the workspace and all guest manifests. The crate
was renamed upstream, so `nssa_core`/`nssa` now resolve via the `lee_core`/`lee`
packages, and spel-framework points at the `refactor/lez-v020-compat` fork
branch for compatibility.
Adapt the integration tests to the new API surface:
- `NssaError` is now `LeeError` (error variants unchanged).
- Account inputs move from numeric mask vectors (`vec![2, 0, 0]`) to typed
`InputAccountIdentity` values (e.g. `PrivateUnauthorized { epk, view_tag,
npk, ssk, identifier }`).
- `ViewingPublicKey::from_scalar` → `from_seed(d, z)`; `AccountId::from(&npk)`
→ `AccountId::for_regular_private_account(&npk, 0)`; ephemeral-key/shared-
secret setup → `SharedSecretKey::encapsulate_deterministic(...)` with the
circuit filling the EPK.
Regenerate all guest Cargo.lock files and the workspace lockfile to match.
`idl-gen` emits IDL instructions in source order, and `spel` uses each
instruction's IDL position as its serde variant index. When the
`#[instruction]` function order diverges from the `twap_oracle_core::Instruction`
enum order, spel addresses the wrong instruction.
Move `update_current_tick` ahead of the TWAP-computation instruction so the
function order in twap_oracle.rs lines up with the enum variant order, and
regenerate artifacts/twap_oracle-idl.json to match.
No behavioral change — the instruction bodies are unchanged, only reordered.
The swap and add-liquidity instructions debited user-owned token holdings
without requiring those accounts to be signers. Mark them `signer` so a
transaction can't move a user's tokens without their authorization:
- add liquidity: `user_holding_lp` is now `#[account(mut, signer)]`
- swap (both directions): `user_holding_a` and `user_holding_b` are now
`#[account(mut, signer)]`
Regenerate artifacts/amm-idl.json to reflect the new signer metadata.
Update integration tests accordingly: swaps now sign and supply nonces for
both user holdings (incrementing both nonces), and
`amm_new_definition_precreated_zero_balance_user_lp` becomes
`amm_new_definition_precreated_user_lp_unsigned_fails`, asserting an unsigned
pre-existing LP holding is rejected and the transaction reverts.
Configure guest release profiles with debug = 0 and strip = "symbols" so deployed RISC Zero artifacts use stripped binaries.
Document that release-profile ImageIDs are canonical for testnet and mainnet deployments and dependent values must be refreshed.
Add the first zkVM-path coverage of the oracle's price-account output, which
previously existed only as native unit tests:
- amm_twap_create_oracle_price_account: creates the OraclePriceAccount via a
signing price source and checks the initialized state (price, timestamp,
source/base/quote, confidence).
- amm_twap_publish_price_publishes_window_average: full pipeline — real swaps +
RecordTick build the observations, then PublishPrice consumes them. With the
clock at the newest observation (empty tail) the published price is the
stored-window average tick converted to a Q64.64 price, stamped with now.
- amm_twap_publish_price_extrapolates_tail_to_now: advances the clock past the
last record with no new observation; asserts the published timestamp is now
(a fresh price, not a stale window) and the value reflects the extrapolated
tail.
- amm_twap_publish_price_noop_with_fewer_than_two_observations: PublishPrice
leaves the price account untouched when there is nothing to average.
Add a CreateOraclePriceAccount instruction mirroring CreatePriceObservations:
anyone can register the consumer-facing OraclePriceAccount for a pool feed, and
the AMM authorizes the pool as the price source via its pool PDA seed through a
single chained call to the configured TWAP oracle program.
Add the first end-to-end coverage of the oracle's RecordTick path, which
previously existed only as native unit tests:
- amm_twap_observations_accumulate_across_swaps_and_yield_time_weighted_average:
drives swaps + RecordTick across simulated time, then checks the cumulative
accumulator and the consulted time-weighted average.
- amm_twap_record_tick_sampling_guard_skips_calls_below_min_interval: exercises
the min-interval sampling guard through the real instruction path.
Running RecordTick through the zkVM surfaced that committing the oracle-owned
~100 KiB observations account costs ~50.9M cycles — over the 2^25 (~33.5M)
public-execution limit — so the instruction aborted on chain. Reduce
OBSERVATIONS_CAPACITY 6396 -> 2048 (~16.8M cycles, ~half the limit); window
coverage is unchanged, only sampling resolution.
Add programs/benchmark, a standalone crate (excluded from the workspace so CI
and the Makefile skip it) that runs the guest ELF through the RISC Zero
executor and reports the per-instruction cycle split, reproducing the on-chain
pass/fail at the limit. Its cost-vs-capacity sweep still spans to 6396, guarding
against bumping capacity back into the over-budget range.