refactor(amm): consolidate the two client FFIs into one JSON crate

The AMM host FFI was split across two crates with two ABI styles: the
typed-C `amm_client_ffi` (swap primitives, under programs/) and the
JSON/wire `amm_client` (new-position flow, under apps/). Fold both into a
single `amm_client` crate exposing one JSON C ABI, and delete
`programs/amm/client-ffi`.

Rust:
- Re-express the swap path as `api/swap.rs` operations on the existing
  `call::<T>` dispatch — swap_pair, resolve_pool, swap_plan, program_id —
  reusing `pair::derive_pair` (no more duplicated PDA derivation) and
  `risc0_zkvm::serde` for the SwapExactInput words (the same encoding the
  guest decodes). The account list and signer flags stay byte-identical
  to the old typed path.
- Generate a single header (`include/amm_client.h`) covering all ops via
  cbindgen; bump cbindgen 0.27 -> 0.28 for `#[unsafe(no_mangle)]` support.

C++:
- Extend the `AmmClient` wrapper with the four swap ops.
- Add `SwapRuntime` (mirrors `NewPositionRuntime`): reads accounts through
  the wallet, drives the swap ops, submits the transaction.
- `AmmUiBackend` swap methods now delegate to `SwapRuntime`, dropping ~390
  lines of typed-FFI and byte-twiddling. `program_id` becomes a JSON op,
  and the swap clock is derived via `derive_pair` (clock_core::CLOCK_01)
  instead of a hardcoded base58 literal — same account, verified.
This commit is contained in:
r4bbit
2026-08-03 13:43:32 +02:00
parent 8358cfa2f1
commit 0b2437d1fc
27 changed files with 680 additions and 1069 deletions
Generated
+4 -16
View File
@@ -83,10 +83,12 @@ dependencies = [
"alloy-primitives",
"amm_core",
"borsh",
"cbindgen",
"clock_core",
"hex",
"lee_core",
"pretty_assertions",
"risc0-binfmt",
"risc0-zkvm",
"serde",
"serde_json",
@@ -95,20 +97,6 @@ dependencies = [
"twap_oracle_core",
]
[[package]]
name = "amm_client_ffi"
version = "0.1.0"
dependencies = [
"amm_core",
"borsh",
"cbindgen",
"lee_core",
"risc0-binfmt",
"risc0-zkvm",
"ruint",
"twap_oracle_core",
]
[[package]]
name = "amm_core"
version = "0.1.0"
@@ -887,9 +875,9 @@ dependencies = [
[[package]]
name = "cbindgen"
version = "0.27.0"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb"
checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff"
dependencies = [
"clap",
"heck 0.4.1",
-1
View File
@@ -7,7 +7,6 @@ members = [
"programs/amm/core",
"programs/amm",
"programs/amm/methods",
"programs/amm/client-ffi",
"programs/ata/core",
"programs/ata",
"programs/ata/methods",
+2 -1
View File
@@ -41,6 +41,8 @@ logos_module(
src/AmmClient.cpp
src/NewPositionRuntime.h
src/NewPositionRuntime.cpp
src/SwapRuntime.h
src/SwapRuntime.cpp
FIND_PACKAGES
Qt6Gui
LINK_LIBRARIES
@@ -49,7 +51,6 @@ logos_module(
LINK_TARGETS
logos_wallet_access
EXTERNAL_LIBS
amm_client_ffi
amm_client
)
+4
View File
@@ -13,6 +13,7 @@ borsh = { workspace = true }
clock_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0" }
hex = "0.4"
nssa_core = { workspace = true }
risc0-binfmt = { version = "=3.0.4", default-features = false }
risc0-zkvm = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
@@ -20,5 +21,8 @@ sha2 = "0.10"
token_core = { workspace = true }
twap_oracle_core = { workspace = true }
[build-dependencies]
cbindgen = "0.28"
[dev-dependencies]
pretty_assertions = "1"
@@ -3,8 +3,7 @@ fn main() {
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo");
cbindgen::generate(&crate_dir)
.expect("cbindgen")
.write_to_file(format!("{crate_dir}/amm_client_ffi.h"));
println!("cargo:rerun-if-changed=src/lib.rs");
.write_to_file(format!("{crate_dir}/include/amm_client.h"));
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed=cbindgen.toml");
}
@@ -1,6 +1,7 @@
language = "C"
include_guard = "AMM_CLIENT_FFI_H"
include_guard = "AMM_CLIENT_H"
pragma_once = true
cpp_compat = true
autogen_warning = "/* Generated by cbindgen. Do not edit. */"
[export]
+33 -4
View File
@@ -1,20 +1,49 @@
#ifndef AMM_CLIENT_H
#define AMM_CLIENT_H
#pragma once
/* Generated by cbindgen. Do not edit. */
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#ifdef __cplusplus
extern "C" {
#endif
#endif // __cplusplus
char *amm_config_id(const char *request_json);
char *amm_token_ids(const char *request_json);
char *amm_pair_ids(const char *request_json);
char *amm_context(const char *request_json);
char *amm_quote(const char *request_json);
char *amm_plan(const char *request_json);
char *amm_swap_pair(const char *request_json);
char *amm_resolve_pool(const char *request_json);
char *amm_swap_plan(const char *request_json);
char *amm_program_id(const char *request_json);
/**
* Releases a string returned by an `amm_*` operation.
*
* # Safety
* `value` must be null or a pointer returned by this library that has not been freed.
*/
void amm_free(char *value);
#ifdef __cplusplus
}
#endif
} // extern "C"
#endif // __cplusplus
#endif
#endif /* AMM_CLIENT_H */
+23 -1
View File
@@ -13,6 +13,7 @@ mod position;
mod quote;
mod quote_error;
mod request;
mod swap;
#[cfg(test)]
mod tests;
@@ -21,7 +22,8 @@ use std::{error::Error, fmt};
pub use request::{
ConfigIdRequest, ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest, PositionRequest,
QuoteRequest, TokenIdsRequest,
ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapPairRequest, SwapPlanRequest,
TokenIdsRequest,
};
use serde_json::Value;
@@ -95,3 +97,23 @@ pub fn quote(request: QuoteRequest) -> AmmResult {
pub fn plan(request: PlanRequest) -> AmmResult {
plan::plan(request).map_err(Into::into)
}
/// Derives the canonical account ids for a swap pair (tokens in either order).
pub fn swap_pair(request: SwapPairRequest) -> AmmResult {
swap::swap_pair(request).map_err(Into::into)
}
/// Decodes a pool account: existence, reserves (canonical order), fee tier.
pub fn resolve_pool(request: ResolvePoolRequest) -> AmmResult {
swap::resolve_pool(request).map_err(Into::into)
}
/// Builds the `SwapExactInput` wallet submission for a token pair.
pub fn swap_plan(request: SwapPlanRequest) -> AmmResult {
swap::swap_plan(request).map_err(Into::into)
}
/// Derives the AMM `ProgramId` (Image ID) from a deployed program binary.
pub fn program_id(request: ProgramIdRequest) -> AmmResult {
swap::program_id(request).map_err(Into::into)
}
+35
View File
@@ -52,6 +52,41 @@ pub struct PairIdsRequest {
pub token_b_id: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SwapPairRequest {
pub amm_program_id: String,
pub token_in_id: String,
pub token_out_id: String,
pub config: AccountRead,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ResolvePoolRequest {
pub pool: AccountRead,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SwapPlanRequest {
pub amm_program_id: String,
pub token_in_id: String,
pub token_out_id: String,
pub config: AccountRead,
pub user_input_holding_id: String,
pub user_output_holding_id: String,
pub amount_in: String,
pub min_out: String,
pub deadline_ms: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProgramIdRequest {
pub elf: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PositionRequest {
+268
View File
@@ -0,0 +1,268 @@
//! Swap operations — pool discovery/decode, swap-transaction planning, and
//! program-id derivation. Same transport-independent pattern as the
//! new-position ops: pure functions returning JSON `Value`, PDAs reused from
//! `pair::derive_pair` so the swap path never re-derives seeds.
use amm_core::PoolDefinition;
use nssa_core::account::AccountId;
use risc0_binfmt::ProgramBinary;
use serde_json::{json, Value};
use super::{
pair::{derive_pair, is_canonical_pair},
ProgramIdRequest, ResolvePoolRequest, SwapPairRequest, SwapPlanRequest,
};
use crate::account::{
account_id_from_hex, account_id_hex, decode_account, parse_program_id, program_id_bytes,
};
/// Orders `(token_in, token_out)` into the pool's canonical `(token_a, token_b)`
/// so derived vault PDAs line up with the pool's stored `vault_a`/`vault_b`.
fn canonical_pair(token_in: AccountId, token_out: AccountId) -> (AccountId, AccountId) {
if is_canonical_pair(token_in, token_out) {
(token_in, token_out)
} else {
(token_out, token_in)
}
}
fn parse_u128(value: &str, label: &str) -> Result<u128, String> {
value
.parse::<u128>()
.map_err(|error| format!("invalid {label}: {error}"))
}
fn parse_u64(value: &str, label: &str) -> Result<u64, String> {
value
.parse::<u64>()
.map_err(|error| format!("invalid {label}: {error}"))
}
/// Derives the canonical account ids for a swap pair — reuses `derive_pair`
/// after ordering `(token_in, token_out)` into `(token_a, token_b)`, so a
/// caller can read the pool account before decoding it. Like `pair_ids`, but
/// accepts the tokens in either order.
pub(super) fn swap_pair(request: SwapPairRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let token_in = account_id_from_hex(&request.token_in_id, "token in id")?;
let token_out = account_id_from_hex(&request.token_out_id, "token out id")?;
if token_in == token_out {
return Ok(json!({ "status": "error", "code": "same_token_pair" }));
}
let (token_a, token_b) = canonical_pair(token_in, token_out);
let Ok(pair) = derive_pair(amm_program, token_a, token_b, &request.config) else {
return Ok(json!({ "status": "error", "code": "config_unavailable" }));
};
Ok(json!({
"status": "ok",
"configId": account_id_hex(pair.config),
"poolId": account_id_hex(pair.pool),
"vaultAId": account_id_hex(pair.vault_a),
"vaultBId": account_id_hex(pair.vault_b),
"currentTickId": account_id_hex(pair.current_tick),
"clockId": account_id_hex(pair.clock),
}))
}
/// Decodes a pool account: whether it holds liquidity, its canonical token ids
/// (`defAHex`/`defBHex`), its reserves (same canonical `a`/`b` order — the
/// caller matches a reserve to its own direction by comparing its token id
/// against `defAHex`), and fee tier. Absent/empty/uninitialized pool →
/// `{ exists: false }`.
pub(super) fn resolve_pool(request: ResolvePoolRequest) -> Result<Value, String> {
if request.pool.status != "ok" {
return Ok(json!({ "exists": false }));
}
let Ok((_, pool_account)) = decode_account(&request.pool) else {
return Ok(json!({ "exists": false }));
};
let Ok(pool) = PoolDefinition::try_from(&pool_account.data) else {
return Ok(json!({ "exists": false }));
};
if pool.liquidity_pool_supply == 0 {
return Ok(json!({ "exists": false }));
}
let fee_bps =
u32::try_from(pool.fees).map_err(|_| String::from("pool fee tier exceeds u32"))?;
Ok(json!({
"exists": true,
"defAHex": account_id_hex(pool.definition_token_a_id),
"defBHex": account_id_hex(pool.definition_token_b_id),
"reserveA": pool.reserve_a.to_string(),
"reserveB": pool.reserve_b.to_string(),
"feeBps": fee_bps,
}))
}
/// Builds the `SwapExactInput` submission for a pair: the fixed 8-account IDL
/// order (vaults canonical, only the user's input holding signs) and the
/// instruction words (`risc0_zkvm::serde` — the same encoding the guest
/// decodes). Mirrors `plan.rs`'s `ready` output shape.
pub(super) fn swap_plan(request: SwapPlanRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let token_in = account_id_from_hex(&request.token_in_id, "token in id")?;
let token_out = account_id_from_hex(&request.token_out_id, "token out id")?;
// Domain errors (a bad pair, an unavailable config) mirror `swap_pair`'s
// `{ status: "error", code }` shape rather than `Err`, which is reserved for
// malformed inputs. `SwapRuntime::swap` treats any non-"ready" status as a
// failed plan, so both map to the same UI outcome.
if token_in == token_out {
return Ok(json!({ "status": "error", "code": "same_token_pair" }));
}
let (token_a, token_b) = canonical_pair(token_in, token_out);
let Ok(pair) = derive_pair(amm_program, token_a, token_b, &request.config) else {
return Ok(json!({ "status": "error", "code": "config_unavailable" }));
};
let user_input_holding =
account_id_from_hex(&request.user_input_holding_id, "user input holding id")?;
let user_output_holding =
account_id_from_hex(&request.user_output_holding_id, "user output holding id")?;
let swap_amount_in = parse_u128(&request.amount_in, "amountIn")?;
let min_amount_out = parse_u128(&request.min_out, "minOut")?;
let deadline = parse_u64(&request.deadline_ms, "deadlineMs")?;
let instruction = risc0_zkvm::serde::to_vec(&amm_core::Instruction::SwapExactInput {
swap_amount_in,
min_amount_out,
deadline,
})
.map_err(|error| format!("instruction serialization failed: {error}"))?;
// Fixed IDL account order for SwapExactInput; only user_input_holding signs.
let account_ids = [
pair.config,
pair.pool,
pair.vault_a,
pair.vault_b,
user_input_holding,
user_output_holding,
pair.current_tick,
pair.clock,
];
let signing_requirements = [false, false, false, false, true, false, false, false];
Ok(json!({
"status": "ready",
"programId": request.amm_program_id,
"accountIds": account_ids.into_iter().map(account_id_hex).collect::<Vec<_>>(),
"signingRequirements": signing_requirements,
"instruction": instruction,
"deadlineMs": deadline.to_string(),
}))
}
/// Computes the AMM `ProgramId` (RISC Zero Image ID) of a deployed program
/// binary. `elf` is the hex-encoded `.bin` (a RISC Zero `ProgramBinary`, not a
/// raw ELF) — decoded, image-id computed, returned as 64-char lowercase hex.
pub(super) fn program_id(request: ProgramIdRequest) -> Result<Value, String> {
let elf = hex::decode(&request.elf).map_err(|error| format!("invalid elf hex: {error}"))?;
let binary = ProgramBinary::decode(&elf).map_err(|error| format!("{error:?}"))?;
let image_id: nssa_core::program::ProgramId = binary
.compute_image_id()
.map_err(|error| format!("{error:?}"))?
.into();
Ok(json!({ "programId": hex::encode(program_id_bytes(image_id)) }))
}
#[cfg(test)]
mod tests {
use amm_core::PoolDefinition;
use nssa_core::account::AccountId;
use super::*;
use crate::account::{AccountRead, WalletAccount};
fn pool_read(pool: &PoolDefinition) -> AccountRead {
AccountRead {
id: "11".repeat(32),
status: String::from("ok"),
account: Some(WalletAccount {
program_owner: "00".repeat(32),
balance: "0".repeat(32),
nonce: "0".repeat(32),
data: hex::encode(borsh::to_vec(pool).unwrap()),
}),
}
}
#[test]
fn resolve_pool_reports_canonical_token_ids() {
let def_a = AccountId::new([0xAA; 32]);
let def_b = AccountId::new([0xBB; 32]);
let pool = PoolDefinition {
definition_token_a_id: def_a,
definition_token_b_id: def_b,
liquidity_pool_supply: 1_000,
reserve_a: 111,
reserve_b: 222,
fees: 30,
..Default::default()
};
let value = resolve_pool(ResolvePoolRequest {
pool: pool_read(&pool),
})
.unwrap();
assert_eq!(value["exists"], true);
// The Swap UI matches reserveA/reserveB to its own sell/buy direction by
// comparing the sold token's id against defAHex — so both ids must be
// present in canonical order.
assert_eq!(value["defAHex"], account_id_hex(def_a));
assert_eq!(value["defBHex"], account_id_hex(def_b));
assert_eq!(value["reserveA"], "111");
assert_eq!(value["reserveB"], "222");
assert_eq!(value["feeBps"], 30);
}
#[test]
fn resolve_pool_absent_when_no_liquidity() {
let pool = PoolDefinition {
liquidity_pool_supply: 0,
..Default::default()
};
let value = resolve_pool(ResolvePoolRequest {
pool: pool_read(&pool),
})
.unwrap();
assert_eq!(value, json!({ "exists": false }));
}
#[test]
fn same_token_is_a_recoverable_domain_error_in_both_ops() {
let program = "00".repeat(32);
let same = "aa".repeat(32);
let expected = json!({ "status": "error", "code": "same_token_pair" });
// Not reached before the same-token check, so its contents don't matter.
let dummy_config = AccountRead {
id: String::new(),
status: String::from("read_failed"),
account: None,
};
let pair = swap_pair(SwapPairRequest {
amm_program_id: program.clone(),
token_in_id: same.clone(),
token_out_id: same.clone(),
config: dummy_config.clone(),
})
.unwrap();
assert_eq!(pair, expected);
let plan = swap_plan(SwapPlanRequest {
amm_program_id: program,
token_in_id: same.clone(),
token_out_id: same,
config: dummy_config,
user_input_holding_id: String::new(),
user_output_holding_id: String::new(),
amount_in: String::new(),
min_out: String::new(),
deadline_ms: String::new(),
})
.unwrap();
assert_eq!(plan, expected);
}
}
+22 -1
View File
@@ -7,7 +7,8 @@ use serde::{de::DeserializeOwned, Serialize};
use crate::api::{
self, AmmApiError, AmmResult, ConfigIdRequest, ContextRequest, PairIdsRequest, PlanRequest,
QuoteRequest, TokenIdsRequest,
ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapPairRequest, SwapPlanRequest,
TokenIdsRequest,
};
#[derive(Serialize)]
@@ -106,6 +107,26 @@ pub extern "C" fn amm_plan(request_json: *const c_char) -> *mut c_char {
call::<PlanRequest>(request_json, api::plan)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_swap_pair(request_json: *const c_char) -> *mut c_char {
call::<SwapPairRequest>(request_json, api::swap_pair)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_resolve_pool(request_json: *const c_char) -> *mut c_char {
call::<ResolvePoolRequest>(request_json, api::resolve_pool)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_swap_plan(request_json: *const c_char) -> *mut c_char {
call::<SwapPlanRequest>(request_json, api::swap_plan)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_program_id(request_json: *const c_char) -> *mut c_char {
call::<ProgramIdRequest>(request_json, api::program_id)
}
/// Releases a string returned by an `amm_*` operation.
///
/// # Safety
+5 -3
View File
@@ -6,7 +6,9 @@ mod ffi;
pub mod api;
pub use api::{
config_id, context, pair_ids, plan, quote, token_ids, AccountRead, AmmApiError, AmmResponse,
AmmResult, ConfigIdRequest, ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest,
PositionRequest, QuoteRequest, TokenIdsRequest, WalletAccount, NEW_POSITION_SCHEMA,
config_id, context, pair_ids, plan, program_id, quote, resolve_pool, swap_pair, swap_plan,
token_ids, AccountRead, AmmApiError, AmmResponse, AmmResult, ConfigIdRequest, ContextRequest,
PairIdsRequest, PairSnapshot, PlanRequest, PositionRequest, ProgramIdRequest, QuoteRequest,
ResolvePoolRequest, SwapPairRequest, SwapPlanRequest, TokenIdsRequest, WalletAccount,
NEW_POSITION_SCHEMA,
};
+3 -3
View File
@@ -30,17 +30,17 @@
amm_client.url = "path:../..";
};
# NOTE: this flake is no longer built standalone. The amm_client_ffi crate
# NOTE: this flake is no longer built standalone. The amm_client crate
# (the Rust C FFI library the AmmUiBackend C++ code links against) lives in
# the repo-root flake, and referencing it from here would require either a
# hardcoded absolute `git+file://` path or a `path:../..` input — the latter
# fails flake evaluation because the app directory is copied into the Nix
# store as its own flake root, so `../..` can't escape it there. Instead,
# the repo-root flake.nix builds this module directly (src = ./apps/amm)
# and resolves amm_client_ffi via `self`. The repo-root flake exposes the UI
# and resolves amm_client via `self`. The repo-root flake exposes the UI
# as a named attribute (there is no bare `default`): run it from the repo root
# with `nix run .#amm-ui`, and build just the FFI crate with
# `nix build .#amm_client_ffi`.
# `nix build .#amm_client`.
outputs = inputs@{ logos-module-builder, shared_wallet, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
-1
View File
@@ -15,7 +15,6 @@
"runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp", "libbase58"]
},
"external_libraries": [
{ "name": "amm_client_ffi" },
{ "name": "amm_client" }
],
"cmake": {
+20
View File
@@ -69,3 +69,23 @@ AmmClientResult BundledAmmClient::plan(const QJsonObject& request) const
{
return call(amm_plan, request);
}
AmmClientResult BundledAmmClient::swapPair(const QJsonObject& request) const
{
return call(amm_swap_pair, request);
}
AmmClientResult BundledAmmClient::resolvePool(const QJsonObject& request) const
{
return call(amm_resolve_pool, request);
}
AmmClientResult BundledAmmClient::swapPlan(const QJsonObject& request) const
{
return call(amm_swap_plan, request);
}
AmmClientResult BundledAmmClient::programId(const QJsonObject& request) const
{
return call(amm_program_id, request);
}
+8
View File
@@ -17,6 +17,10 @@ public:
virtual AmmClientResult context(const QJsonObject& request) const = 0;
virtual AmmClientResult quote(const QJsonObject& request) const = 0;
virtual AmmClientResult plan(const QJsonObject& request) const = 0;
virtual AmmClientResult swapPair(const QJsonObject& request) const = 0;
virtual AmmClientResult resolvePool(const QJsonObject& request) const = 0;
virtual AmmClientResult swapPlan(const QJsonObject& request) const = 0;
virtual AmmClientResult programId(const QJsonObject& request) const = 0;
};
class BundledAmmClient final : public AmmClient {
@@ -27,4 +31,8 @@ public:
AmmClientResult context(const QJsonObject& request) const override;
AmmClientResult quote(const QJsonObject& request) const override;
AmmClientResult plan(const QJsonObject& request) const override;
AmmClientResult swapPair(const QJsonObject& request) const override;
AmmClientResult resolvePool(const QJsonObject& request) const override;
AmmClientResult swapPlan(const QJsonObject& request) const override;
AmmClientResult programId(const QJsonObject& request) const override;
};
+18 -382
View File
@@ -21,37 +21,11 @@
#include "AmmClient.h"
#include "LogosWalletProvider.h"
#include "NewPositionRuntime.h"
#include "SwapRuntime.h"
#include "WalletController.h"
#include "logos_api.h"
#include "logos_sdk.h"
extern "C" {
#include "amm_client_ffi.h"
}
// Debug tracing for the swap path (resolvePool / swapExactInput), gated behind
// the AMM_DEBUG env var so normal runs stay quiet. When disabled, the streamed
// arguments — including the account_id_to_base58 round-trips used to render
// accounts — are NOT evaluated, so there's no per-call overhead.
//
// NOTE: `send_generic_public_transaction(account_ids, signing_requirements,
// instruction, program_id_hex)`. `instruction` is a byte string (`bstr`):
// declaring it as Vec<u32> makes the module's Qt/QtRO glue downgrade it to an
// opaque `any` the separate module process can't deserialize, silently dropping
// every argument. We send `instruction` as the little-endian bytes of the u32
// words, and the deployed program by its id hex (not the raw ELF). Requires the
// wallet module built with the byte-string param; see
// docs/amm-swap-qtro-serialization-bug.md.
static bool ammDebugEnabled()
{
static const bool on = qEnvironmentVariableIsSet("AMM_DEBUG");
return on;
}
// AMM_DBG() << ... behaves like qWarning().noquote() but only when AMM_DEBUG is
// set; otherwise the whole statement (and its arguments) is skipped.
#define AMM_DBG() \
if (ammDebugEnabled()) qWarning().noquote()
namespace {
// Absolute path to the deployed AMM program's RISC Zero program binary
// (amm.bin — the `ProgramBinary` `.bin` from the docker guest build, decoded
@@ -67,78 +41,6 @@ namespace {
// (see apps/amm/README.md). Config-driven so the Swap view's token picker
// doesn't need a hardcoded/dummy token list.
const char TOKENS_CONFIG_ENV[] = "TOKENS_CONFIG";
QString bytes32ToHex(const uint8_t (&b)[32]) {
return QString::fromLatin1(QByteArray(reinterpret_cast<const char*>(b), 32).toHex());
}
bool hexToBytes32(const QString& hex, uint8_t (&out)[32]) {
const QByteArray bytes = QByteArray::fromHex(hex.toUtf8());
if (bytes.size() != 32)
return false;
for (int i = 0; i < 32; ++i)
out[i] = static_cast<uint8_t>(bytes[i]);
return true;
}
// Little-endian 16-byte u128 -> decimal string. QString has no direct u128
// constructor, so accumulate into unsigned __int128 and extract digits.
QString u128leToDecimal(const uint8_t (&le)[16]) {
unsigned __int128 value = 0;
for (int i = 15; i >= 0; --i)
value = (value << 8) | static_cast<unsigned __int128>(le[i]);
if (value == 0)
return QStringLiteral("0");
QString digits;
while (value > 0) {
const unsigned int digit = static_cast<unsigned int>(value % 10);
digits.prepend(QChar(static_cast<char16_t>('0' + digit)));
value /= 10;
}
return digits;
}
// Decimal string -> little-endian 16-byte u128. Inverse of u128leToDecimal.
// Returns false (leaving `out` unwritten) on an empty string or a
// non-digit character.
bool decimalToU128Le(const QString& decimal, uint8_t (&out)[16]) {
const QString trimmed = decimal.trimmed();
if (trimmed.isEmpty())
return false;
const unsigned __int128 kMax = ~static_cast<unsigned __int128>(0);
unsigned __int128 value = 0;
for (const QChar ch : trimmed) {
if (!ch.isDigit())
return false;
const unsigned int d = static_cast<unsigned int>(ch.digitValue());
if (value > (kMax - d) / 10)
return false; // would overflow u128
value = value * 10 + d;
}
for (int i = 0; i < 16; ++i) {
out[i] = static_cast<uint8_t>(value & 0xFF);
value >>= 8;
}
return true;
}
// Base58 address of the fixed clock account consumed by SwapExactInput's
// deadline check. Resolved to hex via the wallet module rather than
// hardcoding a guessed hex encoding.
const char CLOCK_ACCOUNT_BASE58[] = "4BdcjoXkq786TMWcBGGHqcxeLYMZmn17rL4eM9ZyRWNU";
// Extracts the hex-encoded raw account bytes from a get_account_public()
// JSON reply. Returns an empty string if the account has no data (i.e. is
// uninitialized/nonexistent) — note the module always includes the "data"
// key, set to "" rather than omitted, when there's nothing to read.
QString accountDataHex(const QString& accountJson) {
const QJsonObject obj = QJsonDocument::fromJson(accountJson.toUtf8()).object();
return obj.value(QStringLiteral("data")).toString();
}
}
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
@@ -149,7 +51,8 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
m_walletController(std::make_unique<WalletController>(
*m_wallet, QStringLiteral("AmmUI"))),
m_ammClient(std::make_unique<BundledAmmClient>()),
m_newPosition(std::make_unique<NewPositionRuntime>(m_wallet.get(), m_ammClient.get()))
m_newPosition(std::make_unique<NewPositionRuntime>(m_wallet.get(), m_ammClient.get())),
m_swap(std::make_unique<SwapRuntime>(m_wallet.get(), m_ammClient.get()))
{
setWalletStateReady(false);
setNewPositionContext(m_newPosition->context(
@@ -291,24 +194,16 @@ QString AmmUiBackend::ammProgramIdHex()
const QByteArray elf = loadAmmElf();
if (elf.isEmpty())
return QString();
ProgramId ammId{};
if (!amm_client_program_id_from_elf(reinterpret_cast<const uint8_t*>(elf.constData()),
static_cast<uintptr_t>(elf.size()), &ammId)) {
qWarning() << "AmmUiBackend::ammProgramIdHex: amm_client_program_id_from_elf failed";
// Hand the deployed program binary to the amm_client program_id op, which
// decodes it and computes the Image ID — 64-char lowercase hex, little-endian
// per u32 word (matches `spel program-id` and the on-chain *_program_id fields).
const AmmClientResult result = m_ammClient->programId(
QJsonObject { { QStringLiteral("elf"), QString::fromLatin1(elf.toHex()) } });
if (!result.ok) {
qWarning() << "AmmUiBackend::ammProgramIdHex: amm_program_id failed";
return QString();
}
// 32 bytes, little-endian per u32 word, lowercase hex — same encoding as
// swapExactInput's program-id and `spel program-id`.
QByteArray bytes;
bytes.reserve(32);
for (int i = 0; i < 8; ++i) {
const uint32_t word = ammId[i];
bytes.append(static_cast<char>(word & 0xff));
bytes.append(static_cast<char>((word >> 8) & 0xff));
bytes.append(static_cast<char>((word >> 16) & 0xff));
bytes.append(static_cast<char>((word >> 24) & 0xff));
}
return QString::fromLatin1(bytes.toHex());
return result.value.value(QStringLiteral("programId")).toString();
}
ActiveNetworkSnapshot AmmUiBackend::networkSnapshot()
@@ -395,278 +290,19 @@ QByteArray AmmUiBackend::loadAmmElf()
QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
{
QVariantMap out;
out[QStringLiteral("exists")] = false;
// 1. Load the deployed AMM program's ELF. We can't derive this ourselves —
// it must match whatever is actually deployed on the target sequencer.
const QByteArray elf = loadAmmElf();
if (elf.isEmpty()) {
out[QStringLiteral("error")] = QStringLiteral("no_program_bin");
return out;
}
// 2. Derive the AMM program id from the ELF bytes.
ProgramId ammId{};
if (!amm_client_program_id_from_elf(reinterpret_cast<const uint8_t*>(elf.constData()),
static_cast<uintptr_t>(elf.size()), &ammId)) {
qWarning() << "AmmUiBackend::resolvePool: amm_client_program_id_from_elf failed";
out[QStringLiteral("error")] = QStringLiteral("bad_program_bin");
return out;
}
// 3. Read + decode the AMM config account to discover the TWAP oracle
// program id (needed for the current-tick PDA below). No data means the
// AMM hasn't been initialized on this sequencer yet.
uint8_t configPda[32];
amm_client_config_pda(&ammId, &configPda);
const QString configHex = bytes32ToHex(configPda);
// Debug: log every derived account (base58 + hex) and every on-chain read so
// a failed resolve can be traced directly against `spel inspect`.
// account_id_to_base58 is a LOCAL encode (safe even when on-chain reads
// time out).
auto b58 = [this](const QString& hex) {
const QString s = m_logos->logos_execution_zone.account_id_to_base58(hex);
return s.isEmpty() ? hex : QStringLiteral("%1 (hex %2)").arg(s, hex);
};
AMM_DBG() << "[amm-debug] resolvePool: defA=" << b58(defAHex)
<< "defB=" << b58(defBHex);
AMM_DBG() << "[amm-debug] resolvePool: reading config account" << b58(configHex);
const QString configJson = m_logos->logos_execution_zone.get_account_public(configHex);
AMM_DBG() << "[amm-debug] resolvePool: config get_account_public ->"
<< configJson.size() << "chars; raw:" << configJson.left(400);
const QString configDataHex = accountDataHex(configJson);
if (configDataHex.isEmpty()) {
qWarning() << "AmmUiBackend::resolvePool: AMM config read returned no data"
<< "(a failed/timed-out RPC or an uninitialized account)";
out[QStringLiteral("error")] = QStringLiteral("amm_not_initialized");
return out;
}
const QByteArray configBytes = QByteArray::fromHex(configDataHex.toUtf8());
FfiConfigView configView{};
if (!amm_client_decode_config(reinterpret_cast<const uint8_t*>(configBytes.constData()),
static_cast<uintptr_t>(configBytes.size()), &configView)) {
qWarning() << "AmmUiBackend::resolvePool: amm_client_decode_config failed";
out[QStringLiteral("error")] = QStringLiteral("bad_config");
return out;
}
// 4. Derive the pool PDA and read + decode its account. No data means
// there's no pool (or no liquidity) for this token pair yet.
uint8_t defA[32];
uint8_t defB[32];
if (!hexToBytes32(defAHex, defA) || !hexToBytes32(defBHex, defB)) {
qWarning() << "AmmUiBackend::resolvePool: invalid defAHex/defBHex";
out[QStringLiteral("error")] = QStringLiteral("bad_config");
return out;
}
uint8_t poolPda[32];
amm_client_pool_pda(&ammId, &defA, &defB, &poolPda);
const QString poolHex = bytes32ToHex(poolPda);
AMM_DBG() << "[amm-debug] resolvePool: reading pool account" << b58(poolHex);
const QString poolJson = m_logos->logos_execution_zone.get_account_public(poolHex);
AMM_DBG() << "[amm-debug] resolvePool: pool get_account_public ->"
<< poolJson.size() << "chars; raw:" << poolJson.left(400);
const QString poolDataHex = accountDataHex(poolJson);
if (poolDataHex.isEmpty()) {
// Not a warning: this is the normal "no liquidity yet" state.
out[QStringLiteral("error")] = QStringLiteral("no_pool");
return out;
}
const QByteArray poolBytes = QByteArray::fromHex(poolDataHex.toUtf8());
FfiPoolView poolView{};
if (!amm_client_decode_pool(reinterpret_cast<const uint8_t*>(poolBytes.constData()),
static_cast<uintptr_t>(poolBytes.size()), &poolView)) {
qWarning() << "AmmUiBackend::resolvePool: amm_client_decode_pool failed";
out[QStringLiteral("error")] = QStringLiteral("bad_config");
return out;
}
// 5. Derive the TWAP oracle's current-tick PDA for this pool.
uint8_t currentTickPda[32];
amm_client_current_tick_pda(&configView.twap_oracle_program_id, &poolPda, &currentTickPda);
// 6. Assemble the result.
out[QStringLiteral("exists")] = true;
out[QStringLiteral("configHex")] = configHex;
out[QStringLiteral("poolIdHex")] = poolHex;
out[QStringLiteral("defAHex")] = bytes32ToHex(poolView.def_a);
out[QStringLiteral("defBHex")] = bytes32ToHex(poolView.def_b);
out[QStringLiteral("vaultAHex")] = bytes32ToHex(poolView.vault_a);
out[QStringLiteral("vaultBHex")] = bytes32ToHex(poolView.vault_b);
out[QStringLiteral("currentTickHex")] = bytes32ToHex(currentTickPda);
out[QStringLiteral("reserveA")] = u128leToDecimal(poolView.reserve_a);
out[QStringLiteral("reserveB")] = u128leToDecimal(poolView.reserve_b);
out[QStringLiteral("feeBps")] = static_cast<int>(poolView.fees);
AMM_DBG() << "[amm-debug] resolvePool: RESOLVED"
<< "pool=" << b58(poolHex)
<< "poolDefA=" << b58(out[QStringLiteral("defAHex")].toString())
<< "poolDefB=" << b58(out[QStringLiteral("defBHex")].toString())
<< "vaultA=" << b58(out[QStringLiteral("vaultAHex")].toString())
<< "vaultB=" << b58(out[QStringLiteral("vaultBHex")].toString())
<< "currentTick=" << b58(out[QStringLiteral("currentTickHex")].toString())
<< "reserveA=" << out[QStringLiteral("reserveA")].toString()
<< "reserveB=" << out[QStringLiteral("reserveB")].toString()
<< "feeBps=" << out[QStringLiteral("feeBps")].toInt();
return out;
return m_swap->resolvePool(defAHex, defBHex, networkSnapshot());
}
QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex,
QString userOutputHoldingHex, QString amountInDecimal,
QString minOutDecimal, QString deadlineDecimal)
{
AMM_DBG() << "[amm-debug] swapExactInput: ARGS"
<< "defA=" << defAHex << "defB=" << defBHex
<< "userInputHolding=" << userInputHoldingHex
<< "userOutputHolding=" << userOutputHoldingHex
<< "amountIn=" << amountInDecimal << "minOut=" << minOutDecimal
<< "deadline=" << deadlineDecimal;
// 1. Resolve the pool's PDAs; refuse if there's no pool (or no liquidity)
// for this token pair.
const QVariantMap pool = resolvePool(defAHex, defBHex);
if (!pool.value(QStringLiteral("exists")).toBool()) {
qWarning() << "AmmUiBackend::swapExactInput: no pool for the given token pair";
return QString();
}
// 2. Load the deployed AMM program's ELF (must match resolvePool's — both
// read the same AMM_PROGRAM_BIN — since the instruction is proven against
// this exact binary's image id).
const QByteArray elf = loadAmmElf();
if (elf.isEmpty()) {
qWarning() << "AmmUiBackend::swapExactInput: failed to load AMM_PROGRAM_BIN";
return QString();
}
// 3. Convert amounts/deadline to the wire types amm_client_swap_words expects.
uint8_t amtIn[16];
uint8_t minOut[16];
if (!decimalToU128Le(amountInDecimal, amtIn) || !decimalToU128Le(minOutDecimal, minOut)) {
qWarning() << "AmmUiBackend::swapExactInput: invalid amountInDecimal/minOutDecimal";
return QString();
}
bool deadlineOk = false;
const quint64 deadline = deadlineDecimal.toULongLong(&deadlineOk);
if (!deadlineOk) {
qWarning() << "AmmUiBackend::swapExactInput: invalid deadlineDecimal";
return QString();
}
// 4. Build the RISC0 instruction words for SwapExactInput.
const AmmWords w = amm_client_swap_words(&amtIn, &minOut, deadline);
if (!w.ok) {
qWarning() << "AmmUiBackend::swapExactInput: amm_client_swap_words failed";
return QString();
}
const std::vector<uint32_t> instruction(w.ptr, w.ptr + w.len);
amm_client_free_words(w);
// 5. Assemble the account id list (exact IDL order) and parallel signer
// flags. The swap's direction is derived from the input holding's own
// token, so the two user holdings occupy fixed role slots: user_input_holding
// (the token being sold) then user_output_holding (received). Only the input
// holding signs — the guest debits it via the downstream token transfer; the
// output holding only receives and needs no signature.
const QString clockHex =
m_logos->logos_execution_zone.account_id_from_base58(QString::fromLatin1(CLOCK_ACCOUNT_BASE58));
if (clockHex.isEmpty()) {
qWarning() << "AmmUiBackend::swapExactInput: failed to resolve clock account id";
return QString();
}
const QStringList accounts = {
pool.value(QStringLiteral("configHex")).toString(),
pool.value(QStringLiteral("poolIdHex")).toString(),
pool.value(QStringLiteral("vaultAHex")).toString(),
pool.value(QStringLiteral("vaultBHex")).toString(),
userInputHoldingHex, // user_input_holding — the token being sold (signed)
userOutputHoldingHex, // user_output_holding — receives the token being bought
pool.value(QStringLiteral("currentTickHex")).toString(),
clockHex,
};
const QVariantList signers = { false, false, false, false, true, false, false, false };
// Debug: dump the exact accounts/signers we submit (base58 for `spel`
// comparison), plus the instruction/elf sizes.
auto b58s = [this](const QString& hex) {
const QString s = m_logos->logos_execution_zone.account_id_to_base58(hex);
return s.isEmpty() ? hex : QStringLiteral("%1 (hex %2)").arg(s, hex);
};
static const char* const kSlot[] = { "config", "pool",
"vault_a", "vault_b",
"user_input_holding", "user_output_holding",
"current_tick", "clock" };
AMM_DBG() << "[amm-debug] swapExactInput: SUBMIT"
<< "instruction_words=" << static_cast<int>(instruction.size())
<< "elf_bytes=" << elf.size();
for (int i = 0; i < accounts.size(); ++i) {
AMM_DBG() << "[amm-debug] account[" << i << "]"
<< (i < 8 ? kSlot[i] : "?") << "=" << b58s(accounts[i])
<< "signer=" << (i < signers.size() && signers[i].toBool());
}
// 6. Submit through the wallet module's generic public-transaction entry
// point. The module API takes `instruction` (as bytes — see the note atop
// this file) plus the program's id as hex, not the raw ELF: the program is
// already deployed and referenced by id. There is no program_dependencies
// arg — the sequencer resolves the AMM's chained token/twap calls on-chain.
// Serialize the u32 instruction words to bytes explicitly as little-endian,
// matching the protocol's LE-u32 wire format. A reinterpret_cast of the
// in-memory vector would be host-endian-dependent and byte-swap on a
// big-endian host, making the guest decode a different instruction.
QByteArray instructionBytes;
instructionBytes.reserve(static_cast<int>(instruction.size() * sizeof(uint32_t)));
for (const uint32_t w : instruction) {
instructionBytes.append(static_cast<char>(w & 0xFF));
instructionBytes.append(static_cast<char>((w >> 8) & 0xFF));
instructionBytes.append(static_cast<char>((w >> 16) & 0xFF));
instructionBytes.append(static_cast<char>((w >> 24) & 0xFF));
}
// Derive the deployed AMM program id from the ELF and hex-encode it as the
// canonical 32-byte hex (little-endian per u32 word — matches `spel
// program-id` and the on-chain config's *_program_id fields).
ProgramId ammId;
if (!amm_client_program_id_from_elf(reinterpret_cast<const uint8_t*>(elf.constData()),
static_cast<uintptr_t>(elf.size()), &ammId)) {
qWarning() << "AmmUiBackend::swapExactInput: amm_client_program_id_from_elf failed";
return QString();
}
QByteArray programIdBytes;
programIdBytes.reserve(32);
for (int i = 0; i < 8; ++i) {
const uint32_t w = ammId[i];
programIdBytes.append(static_cast<char>(w & 0xff));
programIdBytes.append(static_cast<char>((w >> 8) & 0xff));
programIdBytes.append(static_cast<char>((w >> 16) & 0xff));
programIdBytes.append(static_cast<char>((w >> 24) & 0xff));
}
const QString programIdHex = QString::fromLatin1(programIdBytes.toHex());
AMM_DBG() << "[amm-debug] swapExactInput: program_id_hex=" << programIdHex;
// `instruction` is a QVariant (bstr); `program_id_hex` is a plain QString
// (tstr) in the generated proxy — pass it directly, not QVariant-wrapped.
const QString resultJson = m_logos->logos_execution_zone.send_generic_public_transaction(
accounts, signers, QVariant::fromValue(instructionBytes), programIdHex);
AMM_DBG() << "[amm-debug] swapExactInput: send_generic_public_transaction ->"
<< resultJson.size() << "chars; raw:" << resultJson.left(600);
const QJsonObject obj = QJsonDocument::fromJson(resultJson.toUtf8()).object();
if (!obj.value(QStringLiteral("success")).toBool()) {
qWarning() << "AmmUiBackend::swapExactInput: transaction failed:" << resultJson;
return QString();
}
refreshBalances();
return obj.value(QStringLiteral("tx_hash")).toString();
const QString txHash = m_swap->swap(defAHex, defBHex, userInputHoldingHex, userOutputHoldingHex,
amountInDecimal, minOutDecimal, deadlineDecimal,
networkSnapshot(), isWalletOpen());
if (!txHash.isEmpty())
refreshBalances();
return txHash;
}
QVariantList AmmUiBackend::tokenList()
+2 -4
View File
@@ -15,15 +15,12 @@
#include "ActiveNetwork.h"
#include "WalletAccountModel.h"
extern "C" {
#include "amm_client_ffi.h"
}
class LogosAPI;
struct LogosModules;
class AmmClient;
class LogosWalletProvider;
class NewPositionRuntime;
class SwapRuntime;
class WalletController;
// Source-side implementation of the AmmUiBackend .rep interface.
@@ -100,6 +97,7 @@ private:
std::unique_ptr<WalletController> m_walletController;
std::unique_ptr<AmmClient> m_ammClient;
std::unique_ptr<NewPositionRuntime> m_newPosition;
std::unique_ptr<SwapRuntime> m_swap;
QVariantMap m_newPositionHints;
+169
View File
@@ -0,0 +1,169 @@
#include "SwapRuntime.h"
#include <QJsonArray>
#include <QStringList>
#include <QVector>
#include "AmmClient.h"
#include "WalletProvider.h"
namespace {
QJsonObject accountReadJson(const WalletAccountRead& read)
{
QJsonObject result {
{ QStringLiteral("id"), read.accountId },
{ QStringLiteral("status"), read.status },
};
if (read.ok()) {
result.insert(QStringLiteral("account"), QJsonObject {
{ QStringLiteral("program_owner"), read.programOwner },
{ QStringLiteral("balance"), read.balanceHex },
{ QStringLiteral("nonce"), read.nonceHex },
{ QStringLiteral("data"), read.dataHex },
});
}
return result;
}
QStringList jsonStringList(const QJsonArray& values)
{
QStringList result;
result.reserve(values.size());
for (const QJsonValue& value : values)
result.append(value.toString());
return result;
}
QVector<bool> jsonBoolList(const QJsonArray& values)
{
QVector<bool> result;
result.reserve(values.size());
for (const QJsonValue& value : values)
result.append(value.toBool());
return result;
}
QVector<quint32> jsonUIntList(const QJsonArray& values)
{
QVector<quint32> result;
result.reserve(values.size());
for (const QJsonValue& value : values)
result.append(static_cast<quint32>(value.toInteger()));
return result;
}
}
SwapRuntime::SwapRuntime(WalletProvider* wallet, AmmClient* client)
: m_wallet(wallet),
m_client(client)
{
}
QJsonObject SwapRuntime::readConfig(const ActiveNetworkSnapshot& network) const
{
const AmmClientResult configResult = m_client->configId(
QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } });
if (!configResult.ok)
return {};
return accountReadJson(m_wallet->readPublicAccount(
configResult.value.value(QStringLiteral("configId")).toString()));
}
QVariantMap SwapRuntime::resolvePool(const QString& tokenInId,
const QString& tokenOutId,
const ActiveNetworkSnapshot& network)
{
const QVariantMap absent { { QStringLiteral("exists"), false } };
// Attaches a diagnostic code the Swap UI surfaces verbatim (SwapCard.qml:
// pool.error). The normal "no pool / no liquidity yet" state stays
// code-less — that's the bare `absent` / resolve_pool `{exists:false}`
// below, which the UI renders as its neutral "no pool" message, not an error.
const auto failure = [](const QString& code) {
return QVariantMap {
{ QStringLiteral("exists"), false },
{ QStringLiteral("error"), code },
};
};
// Network still resolving AMM_PROGRAM_BIN: transient startup state, not a
// diagnostic — surface nothing so the UI keeps its "loading" affordance.
if (network.status != QStringLiteral("ready"))
return absent;
// readConfig returns {} only when the config_id op itself fails (a client
// bug, not a chain state) — as opposed to the config account merely being
// unreadable, which surfaces as swap_pair's `config_unavailable` below.
const QJsonObject config = readConfig(network);
if (config.isEmpty())
return failure(QStringLiteral("backend_error"));
const AmmClientResult pairResult = m_client->swapPair(QJsonObject {
{ QStringLiteral("ammProgramId"), network.ammProgramId },
{ QStringLiteral("tokenInId"), tokenInId },
{ QStringLiteral("tokenOutId"), tokenOutId },
{ QStringLiteral("config"), config },
});
if (!pairResult.ok)
return failure(QStringLiteral("backend_error"));
if (pairResult.value.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) {
// swap_pair reports `config_unavailable` when the AMM config account is
// missing/uninitialized on this network, `same_token_pair` for an
// invalid pair, etc. Propagate its code rather than flattening to
// "no pool", so a misconfigured network is distinguishable from an
// empty one.
const QString code = pairResult.value.value(QStringLiteral("code")).toString();
return failure(code.isEmpty() ? QStringLiteral("backend_error") : code);
}
const QJsonObject pool = accountReadJson(m_wallet->readPublicAccount(
pairResult.value.value(QStringLiteral("poolId")).toString()));
const AmmClientResult resolveResult =
m_client->resolvePool(QJsonObject { { QStringLiteral("pool"), pool } });
if (!resolveResult.ok)
return failure(QStringLiteral("backend_error"));
return resolveResult.value.toVariantMap();
}
QString SwapRuntime::swap(const QString& tokenInId,
const QString& tokenOutId,
const QString& userInputHoldingId,
const QString& userOutputHoldingId,
const QString& amountInDecimal,
const QString& minOutDecimal,
const QString& deadlineMs,
const ActiveNetworkSnapshot& network,
bool walletOpen)
{
if (network.status != QStringLiteral("ready") || !walletOpen)
return {};
const QJsonObject config = readConfig(network);
if (config.isEmpty())
return {};
const AmmClientResult planResult = m_client->swapPlan(QJsonObject {
{ QStringLiteral("ammProgramId"), network.ammProgramId },
{ QStringLiteral("tokenInId"), tokenInId },
{ QStringLiteral("tokenOutId"), tokenOutId },
{ QStringLiteral("config"), config },
{ QStringLiteral("userInputHoldingId"), userInputHoldingId },
{ QStringLiteral("userOutputHoldingId"), userOutputHoldingId },
{ QStringLiteral("amountIn"), amountInDecimal },
{ QStringLiteral("minOut"), minOutDecimal },
{ QStringLiteral("deadlineMs"), deadlineMs },
});
if (!planResult.ok
|| planResult.value.value(QStringLiteral("status")).toString() != QStringLiteral("ready"))
return {};
const QJsonObject plan = planResult.value;
const WalletSubmission submission = m_wallet->submitPublicTransaction({
plan.value(QStringLiteral("programId")).toString(),
jsonStringList(plan.value(QStringLiteral("accountIds")).toArray()),
jsonBoolList(plan.value(QStringLiteral("signingRequirements")).toArray()),
jsonUIntList(plan.value(QStringLiteral("instruction")).toArray()),
});
if (!submission.accepted())
return {};
return submission.nativeHash;
}
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <QJsonObject>
#include <QString>
#include <QVariantMap>
#include "ActiveNetwork.h"
class AmmClient;
class WalletProvider;
// Off-chain orchestration for the Swap view: reads accounts through the wallet,
// drives the amm_client swap ops, and submits the swap transaction. The AMM
// domain logic lives in the amm_client crate; this class only glues account
// reads and submission to it. Mirrors NewPositionRuntime.
class SwapRuntime {
public:
SwapRuntime(WalletProvider* wallet, AmmClient* client);
// { exists, reserveA, reserveB, feeBps } for the (tokenIn, tokenOut) pool.
// reserveA/reserveB follow the pool's canonical def order (the caller maps
// them to sell/buy). exists=false when the pool is absent or has no liquidity.
QVariantMap resolvePool(const QString& tokenInId,
const QString& tokenOutId,
const ActiveNetworkSnapshot& network);
// Builds and submits a SwapExactInput transaction; returns the native tx
// hash on success, an empty string on any failure.
QString swap(const QString& tokenInId,
const QString& tokenOutId,
const QString& userInputHoldingId,
const QString& userOutputHoldingId,
const QString& amountInDecimal,
const QString& minOutDecimal,
const QString& deadlineMs,
const ActiveNetworkSnapshot& network,
bool walletOpen);
private:
// Derives the config account id (config_id) and reads it. Returns an empty
// object only when the config_id op itself fails.
QJsonObject readConfig(const ActiveNetworkSnapshot& network) const;
WalletProvider* m_wallet;
AmmClient* m_client;
};
+15 -40
View File
@@ -11,7 +11,7 @@
};
# The AMM QML UI module (apps/amm) is built from this same flake so it can
# reference the amm_client_ffi crate package via `self` — no filesystem
# reference the amm_client crate package via `self` — no filesystem
# path or git-remote reference to this repo is needed (see apps/amm/flake.nix
# history: a `git+file://` URL pointing at a local checkout is
# machine-specific and not portable).
@@ -54,19 +54,19 @@
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
# Whole workspace: crane needs Cargo.lock + all path deps (amm_core,
# twap_oracle_core, token_core, ...) to resolve `-p amm_client_ffi`.
# twap_oracle_core, token_core, ...) to resolve `-p amm_client`.
src = ./.;
commonArgs = {
inherit src;
strictDeps = true;
pname = "amm_client_ffi";
pname = "amm_client";
version = "0.1.0";
# CRITICAL: scope to ONLY this crate. The workspace also contains
# `amm/methods` etc. whose build.rs compiles the risc0 guest (which
# WOULD invoke Metal on darwin). `-p amm_client_ffi` never builds
# WOULD invoke Metal on darwin). `-p amm_client` never builds
# those crates or their build scripts.
cargoExtraArgs = "-p amm_client_ffi";
cargoExtraArgs = "-p amm_client";
doCheck = false;
# NOTE: cbindgen is used here as a Cargo *build-dependency*
# (invoked from build.rs via its Rust library API), not as the
@@ -76,14 +76,17 @@
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
ammClientFfi = craneLib.buildPackage (
# The single AMM host FFI crate (apps/amm/client): swap + new-position
# operations behind one JSON C ABI. Its header is cbindgen-generated
# into include/amm_client.h at build time.
ammClient = craneLib.buildPackage (
commonArgs
// {
inherit cargoArtifacts;
postInstall =
''
mkdir -p $out/include
cp programs/amm/client-ffi/amm_client_ffi.h $out/include/
cp apps/amm/client/include/amm_client.h $out/include/
''
+ pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
# Set the dylib's install-name to its ABSOLUTE store path (NOT
@@ -92,31 +95,6 @@
# @rpath id fails to dlopen at launch. An absolute /nix/store id
# is recorded in the plugin's LC_LOAD_DYLIB, kept in the closure
# by Nix, and resolved directly at runtime no rpath needed.
if [ -f $out/lib/libamm_client_ffi.dylib ]; then
install_name_tool -id "$out/lib/libamm_client_ffi.dylib" $out/lib/libamm_client_ffi.dylib
fi
'';
}
);
# Second AMM client crate (apps/amm/client) — the new-position/pool
# flow's protocol lib. Built alongside amm_client_ffi (they coexist:
# symbols are prefixed amm_* vs amm_client_*). TODO: consolidate the two
# into a single AMM client FFI.
ammClientArgs = commonArgs // {
pname = "amm_client";
cargoExtraArgs = "-p amm_client";
};
ammClient = craneLib.buildPackage (
ammClientArgs
// {
cargoArtifacts = craneLib.buildDepsOnly ammClientArgs;
postInstall =
''
mkdir -p $out/include
cp apps/amm/client/include/amm_client.h $out/include/
''
+ pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
if [ -f $out/lib/libamm_client.dylib ]; then
install_name_tool -id "$out/lib/libamm_client.dylib" $out/lib/libamm_client.dylib
fi
@@ -125,14 +103,13 @@
);
in
{
packages.default = ammClientFfi;
packages.amm_client_ffi = ammClientFfi;
packages.default = ammClient;
packages.amm_client = ammClient;
}
);
# The AMM QML UI module (apps/amm). Its external_libraries entry
# (amm_client_ffi) is resolved to `self.packages.${system}.amm_client_ffi`
# (amm_client) is resolved to `self.packages.${system}.amm_client`
# above — the module builder's resolveExtInput reads
# `flakeInput.packages.${system}.${pkgName}`, and passing `self` here
# works because `self` is this very flake, which already exposes that
@@ -142,7 +119,6 @@
configFile = ./apps/amm/metadata.json;
flakeInputs = inputs;
externalLibInputs = {
amm_client_ffi = { input = self; packages.default = "amm_client_ffi"; };
amm_client = { input = self; packages.default = "amm_client"; };
};
# The AMM UI links the shared C++ wallet access lib and bundles the
@@ -178,20 +154,19 @@
appPkgs = appOutputs.packages or { };
# Wrap the app launcher to export DYLD_FALLBACK_LIBRARY_PATH pointing at the
# amm_client_ffi lib. The logos module builder links the plugin against
# @rpath/libamm_client_ffi.dylib but does NOT stage that dylib into the
# amm_client lib. The logos module builder links the plugin against
# @rpath/libamm_client.dylib but does NOT stage that dylib into the
# plugin-dir it loads at runtime, so dlopen fails with "Failed to load UI
# plugin". Adding the crate's store lib dir to DYLD's fallback search path
# lets the loader find it (the store path stays in the closure).
wrapWithDyld = system: app:
let
pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlays.default ]; };
ammFfi = crateOutputs.packages.${system}.amm_client_ffi;
ammClient = crateOutputs.packages.${system}.amm_client;
in
app // {
program = "${pkgs.writeShellScript "run-amm-ui" ''
export DYLD_FALLBACK_LIBRARY_PATH="${ammFfi}/lib:${ammClient}/lib''${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}"
export DYLD_FALLBACK_LIBRARY_PATH="${ammClient}/lib''${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}"
exec ${app.program} "$@"
''}";
};
-32
View File
@@ -1,32 +0,0 @@
[package]
name = "amm_client_ffi"
version = "0.1.0"
# 2021 (not the repo's 2024): matches amm_core and keeps plain #[no_mangle]
# (edition 2024 requires #[unsafe(no_mangle)]).
edition = "2021"
[lib]
name = "amm_client_ffi"
# The logos module builder's macOS find_library only locates SHARED libs
# (lib<name>.dylib), never a static .a — so we must ship the cdylib. The flake
# gives the dylib an ABSOLUTE store-path install-name (not @rpath), so the plugin
# links it by its /nix/store path (kept in the closure) and dlopen resolves it at
# runtime without any rpath staging. staticlib/rlib kept for other consumers/tests.
crate-type = ["staticlib", "cdylib", "rlib"]
[lints]
workspace = true
[dependencies]
amm_core = { path = "../core" }
twap_oracle_core = { path = "../../twap_oracle/core" }
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0", features = ["host"], package = "lee_core" }
borsh = { version = "1.5", features = ["derive"] }
risc0-zkvm = { version = "=3.0.5", default-features = false }
risc0-binfmt = { version = "=3.0.4", default-features = false }
# amm_core's ruint is default-features=false (for the no_std guest); enable std
# here (host-only) so its Uint::root compiles. Guest build is unaffected.
ruint = { version = "=1.17.0", default-features = false, features = ["std"] }
[build-dependencies]
cbindgen = "0.27"
-155
View File
@@ -1,155 +0,0 @@
#ifndef AMM_CLIENT_FFI_H
#define AMM_CLIENT_FFI_H
#pragma once
/* Generated by cbindgen. Do not edit. */
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
/**
* Heap-allocated buffer of RISC0 instruction words, returned across the C
* boundary. Free with `amm_client_free_words`.
*/
typedef struct AmmWords {
uint32_t *ptr;
uintptr_t len;
bool ok;
} AmmWords;
/**
* C-ABI representation of `nssa_core::program::ProgramId` (an Image ID),
* which is itself defined as `[u32; 8]`. Declared as a concrete array here
* (rather than an alias through `nssa_core::program::ProgramId`) so
* cbindgen — which only inspects this crate's source, not its
* dependencies' — can emit a real typedef instead of an opaque,
* self-referential `ProgramId` in the generated header. Since both are
* plain type aliases to `[u32; 8]`, they remain interchangeable to rustc.
*/
typedef uint32_t ProgramId[8];
/**
* C-ABI mirror of `pool::PoolView`. u128 fields are little-endian bytes.
*/
typedef struct FfiPoolView {
uint8_t def_a[32];
uint8_t def_b[32];
uint8_t vault_a[32];
uint8_t vault_b[32];
uint8_t reserve_a[16];
uint8_t reserve_b[16];
uint8_t liquidity_supply[16];
uint32_t fees;
bool ok;
} FfiPoolView;
/**
* C-ABI mirror of `pool::ConfigView`.
*/
typedef struct FfiConfigView {
uint32_t token_program_id[8];
uint32_t twap_oracle_program_id[8];
uint8_t authority[32];
bool ok;
} FfiConfigView;
/**
* Builds the RISC0 instruction words for a `SwapExactInput`. `amount_in` and
* `min_out` are little-endian encoded `u128` values (`[u8; 16]`). The
* returned buffer must be freed with `amm_client_free_words`.
*
* # Safety
* `amount_in` and `min_out` must be valid, non-null pointers to readable
* memory of the indicated sizes.
*/
struct AmmWords amm_client_swap_words(const uint8_t (*amount_in)[16],
const uint8_t (*min_out)[16],
uint64_t deadline);
/**
* Frees a buffer previously returned by `amm_client_swap_words`.
*
* # Safety
* `w` must be a value previously returned by `amm_client_swap_words` that
* has not already been freed.
*/
void amm_client_free_words(struct AmmWords w);
/**
* Computes the `ProgramId` (Image ID) of a compiled guest ELF. Returns
* `false` on an invalid ELF, leaving `out` unwritten.
*
* # Safety
* `elf` must be valid for reads of `elf_len` bytes, and `out` must be a
* valid, non-null pointer to writable memory for a `ProgramId`.
*/
bool amm_client_program_id_from_elf(const uint8_t *elf, uintptr_t elf_len, ProgramId *out);
/**
* Fills `out` with the AMM config PDA.
*
* # Safety
* `amm` must be a valid, non-null pointer to a readable `ProgramId`, and
* `out` must be a valid, non-null pointer to writable memory.
*/
void amm_client_config_pda(const ProgramId *amm, uint8_t (*out)[32]);
/**
* Fills `out` with the pool PDA for the two definition ids.
*
* # Safety
* All pointer arguments must be valid, non-null, and point to readable (or,
* for `out`, writable) memory of the indicated sizes.
*/
void amm_client_pool_pda(const ProgramId *amm,
const uint8_t (*def_a)[32],
const uint8_t (*def_b)[32],
uint8_t (*out)[32]);
/**
* Fills `out` with the vault PDA for a pool + token definition.
*
* # Safety
* All pointer arguments must be valid, non-null, and point to readable (or,
* for `out`, writable) memory of the indicated sizes.
*/
void amm_client_vault_pda(const ProgramId *amm,
const uint8_t (*pool)[32],
const uint8_t (*def)[32],
uint8_t (*out)[32]);
/**
* Fills `out` with the TWAP oracle current-tick PDA for a pool.
*
* # Safety
* All pointer arguments must be valid, non-null, and point to readable (or,
* for `out`, writable) memory of the indicated sizes.
*/
void amm_client_current_tick_pda(const ProgramId *twap,
const uint8_t (*pool)[32],
uint8_t (*out)[32]);
/**
* Decodes a `PoolDefinition` account's raw bytes into `out`. Returns `false`
* on decode failure, leaving `out` unwritten.
*
* # Safety
* `bytes` must be valid for reads of `len` bytes, and `out` must be a
* valid, non-null pointer to writable memory for a `FfiPoolView`.
*/
bool amm_client_decode_pool(const uint8_t *bytes, uintptr_t len, struct FfiPoolView *out);
/**
* Decodes an `AmmConfig` account's raw bytes into `out`. Returns `false` on
* decode failure, leaving `out` unwritten.
*
* # Safety
* `bytes` must be valid for reads of `len` bytes, and `out` must be a
* valid, non-null pointer to writable memory for a `FfiConfigView`.
*/
bool amm_client_decode_config(const uint8_t *bytes, uintptr_t len, struct FfiConfigView *out);
#endif /* AMM_CLIENT_FFI_H */
-244
View File
@@ -1,244 +0,0 @@
//! C-ABI client helpers for calling the AMM program from host apps.
//! All AMM instruction/PDA logic is delegated to `amm_core`; encoding is
//! delegated to `lee` — this crate only bridges to a C ABI.
#![allow(
unsafe_code,
reason = "this crate exists solely to expose a C ABI; every unsafe fn \
is a documented pointer dereference at the FFI boundary"
)]
mod pda;
mod pool;
mod swap;
use nssa_core::account::AccountId;
/// C-ABI representation of `nssa_core::program::ProgramId` (an Image ID),
/// which is itself defined as `[u32; 8]`. Declared as a concrete array here
/// (rather than an alias through `nssa_core::program::ProgramId`) so
/// cbindgen — which only inspects this crate's source, not its
/// dependencies' — can emit a real typedef instead of an opaque,
/// self-referential `ProgramId` in the generated header. Since both are
/// plain type aliases to `[u32; 8]`, they remain interchangeable to rustc.
pub type ProgramId = [u32; 8];
/// Heap-allocated buffer of RISC0 instruction words, returned across the C
/// boundary. Free with `amm_client_free_words`.
#[repr(C)]
pub struct AmmWords {
pub ptr: *mut u32,
pub len: usize,
pub ok: bool,
}
/// C-ABI mirror of `pool::PoolView`. u128 fields are little-endian bytes.
#[repr(C)]
pub struct FfiPoolView {
pub def_a: [u8; 32],
pub def_b: [u8; 32],
pub vault_a: [u8; 32],
pub vault_b: [u8; 32],
pub reserve_a: [u8; 16],
pub reserve_b: [u8; 16],
pub liquidity_supply: [u8; 16],
pub fees: u32,
pub ok: bool,
}
/// C-ABI mirror of `pool::ConfigView`.
#[repr(C)]
pub struct FfiConfigView {
pub token_program_id: [u32; 8],
pub twap_oracle_program_id: [u32; 8],
pub authority: [u8; 32],
pub ok: bool,
}
/// # Safety
/// `p` must be a valid, non-null pointer to a readable `[u8; 32]`.
unsafe fn acc(p: *const [u8; 32]) -> AccountId {
AccountId::new(*p)
}
/// Builds the RISC0 instruction words for a `SwapExactInput`. `amount_in` and
/// `min_out` are little-endian encoded `u128` values (`[u8; 16]`). The
/// returned buffer must be freed with `amm_client_free_words`.
///
/// # Safety
/// `amount_in` and `min_out` must be valid, non-null pointers to readable
/// memory of the indicated sizes.
#[no_mangle]
pub unsafe extern "C" fn amm_client_swap_words(
amount_in: *const [u8; 16],
min_out: *const [u8; 16],
deadline: u64,
) -> AmmWords {
let (a, m) = (
u128::from_le_bytes(*amount_in),
u128::from_le_bytes(*min_out),
);
match swap::swap_exact_input_words(a, m, deadline) {
Ok(w) => {
let boxed = w.into_boxed_slice();
let len = boxed.len();
let ptr = Box::into_raw(boxed).cast::<u32>();
AmmWords { ptr, len, ok: true }
}
Err(_) => AmmWords {
ptr: core::ptr::null_mut(),
len: 0,
ok: false,
},
}
}
/// Frees a buffer previously returned by `amm_client_swap_words`.
///
/// # Safety
/// `w` must be a value previously returned by `amm_client_swap_words` that
/// has not already been freed.
#[no_mangle]
pub unsafe extern "C" fn amm_client_free_words(w: AmmWords) {
if !w.ptr.is_null() {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
w.ptr, w.len,
)));
}
}
/// Computes the `ProgramId` (Image ID) of a deployed program binary — the RISC
/// Zero `ProgramBinary` (`.bin`) format, NOT a raw ELF (the `elf` bytes are
/// decoded via `ProgramBinary::decode`). Returns `false` on invalid input,
/// leaving `out` unwritten.
///
/// # Safety
/// `elf` must be valid for reads of `elf_len` bytes, and `out` must be a
/// valid, non-null pointer to writable memory for a `ProgramId`.
#[no_mangle]
pub unsafe extern "C" fn amm_client_program_id_from_elf(
elf: *const u8,
elf_len: usize,
out: *mut ProgramId,
) -> bool {
let bytes = core::slice::from_raw_parts(elf, elf_len);
match pda::program_id_from_elf(bytes) {
Ok(id) => {
*out = id;
true
}
Err(_) => false,
}
}
/// Fills `out` with the AMM config PDA.
///
/// # Safety
/// `amm` must be a valid, non-null pointer to a readable `ProgramId`, and
/// `out` must be a valid, non-null pointer to writable memory.
#[no_mangle]
pub unsafe extern "C" fn amm_client_config_pda(amm: *const ProgramId, out: *mut [u8; 32]) {
*out = pda::config_pda(*amm).into_value();
}
/// Fills `out` with the pool PDA for the two definition ids.
///
/// # Safety
/// All pointer arguments must be valid, non-null, and point to readable (or,
/// for `out`, writable) memory of the indicated sizes.
#[no_mangle]
pub unsafe extern "C" fn amm_client_pool_pda(
amm: *const ProgramId,
def_a: *const [u8; 32],
def_b: *const [u8; 32],
out: *mut [u8; 32],
) {
*out = pda::pool_pda(*amm, acc(def_a), acc(def_b)).into_value();
}
/// Fills `out` with the vault PDA for a pool + token definition.
///
/// # Safety
/// All pointer arguments must be valid, non-null, and point to readable (or,
/// for `out`, writable) memory of the indicated sizes.
#[no_mangle]
pub unsafe extern "C" fn amm_client_vault_pda(
amm: *const ProgramId,
pool: *const [u8; 32],
def: *const [u8; 32],
out: *mut [u8; 32],
) {
*out = pda::vault_pda(*amm, acc(pool), acc(def)).into_value();
}
/// Fills `out` with the TWAP oracle current-tick PDA for a pool.
///
/// # Safety
/// All pointer arguments must be valid, non-null, and point to readable (or,
/// for `out`, writable) memory of the indicated sizes.
#[no_mangle]
pub unsafe extern "C" fn amm_client_current_tick_pda(
twap: *const ProgramId,
pool: *const [u8; 32],
out: *mut [u8; 32],
) {
*out = pda::current_tick_pda(*twap, acc(pool)).into_value();
}
/// Decodes a `PoolDefinition` account's raw bytes into `out`. Returns `false`
/// on decode failure, leaving `out` unwritten.
///
/// # Safety
/// `bytes` must be valid for reads of `len` bytes, and `out` must be a
/// valid, non-null pointer to writable memory for a `FfiPoolView`.
#[no_mangle]
pub unsafe extern "C" fn amm_client_decode_pool(
bytes: *const u8,
len: usize,
out: *mut FfiPoolView,
) -> bool {
let b = core::slice::from_raw_parts(bytes, len);
match pool::decode_pool(b) {
Ok(v) => {
*out = FfiPoolView {
def_a: v.def_a,
def_b: v.def_b,
vault_a: v.vault_a,
vault_b: v.vault_b,
reserve_a: v.reserve_a.to_le_bytes(),
reserve_b: v.reserve_b.to_le_bytes(),
liquidity_supply: v.liquidity_supply.to_le_bytes(),
fees: v.fees,
ok: true,
};
true
}
Err(_) => false,
}
}
/// Decodes an `AmmConfig` account's raw bytes into `out`. Returns `false` on
/// decode failure, leaving `out` unwritten.
///
/// # Safety
/// `bytes` must be valid for reads of `len` bytes, and `out` must be a
/// valid, non-null pointer to writable memory for a `FfiConfigView`.
#[no_mangle]
pub unsafe extern "C" fn amm_client_decode_config(
bytes: *const u8,
len: usize,
out: *mut FfiConfigView,
) -> bool {
let b = core::slice::from_raw_parts(bytes, len);
match pool::decode_config(b) {
Ok(v) => {
*out = FfiConfigView {
token_program_id: v.token_program_id,
twap_oracle_program_id: v.twap_oracle_program_id,
authority: v.authority,
ok: true,
};
true
}
Err(_) => false,
}
}
-49
View File
@@ -1,49 +0,0 @@
//! PDA derivation and program-id helpers, delegating to `amm_core` /
//! `twap_oracle_core` so the client never re-implements seed hashing.
use amm_core::{compute_config_pda, compute_pool_pda, compute_vault_pda};
use nssa_core::{account::AccountId, program::ProgramId};
use risc0_binfmt::ProgramBinary;
use twap_oracle_core::compute_current_tick_account_pda;
/// Computes the `ProgramId` (Image ID) of a deployed program binary — the RISC
/// Zero `ProgramBinary` (`.bin`) format produced by the guest build, NOT a raw
/// ELF (the `elf` bytes are decoded via `ProgramBinary::decode`) — the same way
/// the sequencer/wallet does when deploying a program.
pub fn program_id_from_elf(elf: &[u8]) -> Result<ProgramId, String> {
let binary = ProgramBinary::decode(elf).map_err(|e| format!("{e:?}"))?;
let id = binary.compute_image_id().map_err(|e| format!("{e:?}"))?;
Ok(id.into())
}
pub fn config_pda(amm: ProgramId) -> AccountId {
compute_config_pda(amm)
}
pub fn pool_pda(amm: ProgramId, def_a: AccountId, def_b: AccountId) -> AccountId {
compute_pool_pda(amm, def_a, def_b)
}
pub fn vault_pda(amm: ProgramId, pool: AccountId, def: AccountId) -> AccountId {
compute_vault_pda(amm, pool, def)
}
pub fn current_tick_pda(twap: ProgramId, pool: AccountId) -> AccountId {
compute_current_tick_account_pda(twap, pool)
}
#[cfg(test)]
mod tests {
use amm_core::compute_pool_pda;
use nssa_core::{account::AccountId, program::ProgramId};
use super::*;
#[test]
fn pool_pda_matches_core() {
let amm: ProgramId = [1u32; 8];
let a = AccountId::new([2u8; 32]);
let b = AccountId::new([3u8; 32]);
assert_eq!(pool_pda(amm, a, b), compute_pool_pda(amm, a, b));
}
}
-77
View File
@@ -1,77 +0,0 @@
//! Decoding for the AMM's on-chain `PoolDefinition` account, so client apps
//! can read reserves / fees / vault ids without depending on `amm_core`
//! directly.
use amm_core::{AmmConfig, PoolDefinition};
pub struct PoolView {
pub def_a: [u8; 32],
pub def_b: [u8; 32],
pub vault_a: [u8; 32],
pub vault_b: [u8; 32],
pub reserve_a: u128,
pub reserve_b: u128,
pub liquidity_supply: u128,
/// Fee tier in basis points. Source is `u128` but supported tiers are all
/// <= 100, so downcasting to `u32` is safe.
pub fees: u32,
}
pub fn decode_pool(bytes: &[u8]) -> Result<PoolView, String> {
let p: PoolDefinition = borsh::from_slice(bytes).map_err(|e| format!("{e:?}"))?;
Ok(PoolView {
def_a: p.definition_token_a_id.into_value(),
def_b: p.definition_token_b_id.into_value(),
vault_a: p.vault_a_id.into_value(),
vault_b: p.vault_b_id.into_value(),
reserve_a: p.reserve_a,
reserve_b: p.reserve_b,
liquidity_supply: p.liquidity_pool_supply,
fees: u32::try_from(p.fees).map_err(|_| format!("pool fee {} exceeds u32::MAX", p.fees))?,
})
}
pub struct ConfigView {
pub token_program_id: [u32; 8],
pub twap_oracle_program_id: [u32; 8],
pub authority: [u8; 32],
}
pub fn decode_config(bytes: &[u8]) -> Result<ConfigView, String> {
let c: AmmConfig = borsh::from_slice(bytes).map_err(|e| format!("{e:?}"))?;
Ok(ConfigView {
token_program_id: c.token_program_id,
twap_oracle_program_id: c.twap_oracle_program_id,
authority: c.authority.into_value(),
})
}
#[cfg(test)]
mod tests {
use amm_core::PoolDefinition;
use nssa_core::account::AccountId;
use super::*;
#[test]
fn decode_roundtrip() {
let p = PoolDefinition::default();
let bytes = borsh::to_vec(&p).unwrap();
let v = decode_pool(&bytes).unwrap();
assert_eq!(v.reserve_a, 0);
}
#[test]
fn decode_config_roundtrip() {
let c = AmmConfig {
token_program_id: [7u32; 8],
twap_oracle_program_id: [9u32; 8],
authority: AccountId::new([0u8; 32]),
};
let bytes = borsh::to_vec(&c).unwrap();
let v = decode_config(&bytes).unwrap();
assert_eq!(v.token_program_id, [7u32; 8]);
assert_eq!(v.twap_oracle_program_id, [9u32; 8]);
assert_eq!(v.authority, [0u8; 32]);
}
}
-52
View File
@@ -1,52 +0,0 @@
use amm_core::Instruction;
/// Build the RISC0 instruction words for a SwapExactInput, via the exact
/// serializer the public-transaction path and the guest decoder share.
pub fn swap_exact_input_words(
amount_in: u128,
min_out: u128,
deadline: u64,
) -> Result<Vec<u32>, String> {
let instruction = Instruction::SwapExactInput {
swap_amount_in: amount_in,
min_amount_out: min_out,
deadline,
};
risc0_zkvm::serde::to_vec(&instruction).map_err(|e| format!("{e:?}"))
}
#[cfg(test)]
mod tests {
use amm_core::Instruction;
use super::*;
#[test]
fn words_roundtrip_to_same_instruction() {
let words = swap_exact_input_words(1000, 1, u64::MAX).unwrap();
// Deserialize back through the exact serde the guest decoder uses.
// `Instruction` derives neither `Debug` nor `PartialEq`, so assert
// field-by-field instead of matching with a `{:?}` fallback arm.
let decoded: Instruction = risc0_zkvm::serde::from_slice(&words).unwrap();
match decoded {
Instruction::SwapExactInput {
swap_amount_in,
min_amount_out,
deadline,
} => {
assert_eq!(swap_amount_in, 1000);
assert_eq!(min_amount_out, 1);
assert_eq!(deadline, u64::MAX);
}
Instruction::Initialize { .. }
| Instruction::UpdateConfig { .. }
| Instruction::CreatePriceObservations { .. }
| Instruction::CreateOraclePriceAccount { .. }
| Instruction::NewDefinition { .. }
| Instruction::AddLiquidity { .. }
| Instruction::RemoveLiquidity { .. }
| Instruction::SwapExactOutput { .. }
| Instruction::SyncReserves => panic!("wrong variant: expected SwapExactInput"),
}
}
}