mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
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:
Generated
+4
-16
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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]
|
||||
@@ -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 */
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 = ./.;
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp", "libbase58"]
|
||||
},
|
||||
"external_libraries": [
|
||||
{ "name": "amm_client_ffi" },
|
||||
{ "name": "amm_client" }
|
||||
],
|
||||
"cmake": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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, ¤tTickPda);
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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} "$@"
|
||||
''}";
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
@@ -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 */
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user