Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/amm/src/AmmUiBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
connect(m_registry.get(), &RegistryLoader::changed, this, [this]() {
m_logos->amm_module.setAmmProgramId(QVariantMap{
{QStringLiteral("ammProgramId"), m_registry->activeAmmProgramId()}});
// Point ops at the active network's AMM instance by its config-PDA id
// (empty ⇒ the module falls back to AMM_CONFIG_ID).
m_logos->amm_module.setConfigId(QVariantMap{
{QStringLiteral("configId"), m_registry->activeAmmConfigId()}});
setNetworks(m_registry->networks());
setActiveNetwork(m_registry->activeNetwork());
setRegistryRevision(m_registry->revision());
Expand Down
8 changes: 6 additions & 2 deletions apps/amm/src/RegistryLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,20 +210,24 @@ bool RegistryLoader::applySelection()
if (activeId.isEmpty()) {
qWarning() << "AMM registry: no networks declared; nothing applied";
m_activeAmmProgramId.clear();
m_activeAmmConfigId.clear();
publish({}, {}, m_lastSource, {});
return false;
}

// Adopt the active network's declared AMM program id so the backend can point
// ops at it (setAmmProgramId) without an AMM_PROGRAM_BIN.
// Adopt the active network's declared AMM program id and config-PDA id so the
// backend can point ops at that instance (setAmmProgramId / setConfigId) without
// an AMM_PROGRAM_BIN / AMM_CONFIG_ID.
m_activeAmmProgramId.clear();
m_activeAmmConfigId.clear();
for (const QJsonValue& entry : networks) {
const QJsonObject net = entry.toObject();
if (net.value(QStringLiteral("id")).toString() == activeId) {
m_activeAmmProgramId = net.value(QStringLiteral("programIds"))
.toObject()
.value(QStringLiteral("amm"))
.toString();
m_activeAmmConfigId = net.value(QStringLiteral("ammConfigId")).toString();
break;
}
}
Expand Down
6 changes: 6 additions & 0 deletions apps/amm/src/RegistryLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ class RegistryLoader : public QObject {
// target this network without an AMM_PROGRAM_BIN.
QString activeAmmProgramId() const { return m_activeAmmProgramId; }

// The active network's AMM instance, identified by the account id of its config
// PDA (registry field `ammConfigId`). The backend hands it to the module
// (setConfigId) so ops target that instance. Empty when the network omits it.
QString activeAmmConfigId() const { return m_activeAmmConfigId; }

// Whether a local-file source (TOKENS_CONFIG / AMM_POOLS_CONFIG) is configured —
// it takes precedence over the remote registry (local-replaces-remote).
static bool hasLocalSource();
Expand Down Expand Up @@ -99,6 +104,7 @@ public slots:
QString m_source = QStringLiteral("none");
QString m_activeNetwork;
QString m_activeAmmProgramId;
QString m_activeAmmConfigId;
QString m_configuredUrl; // UI-configured registry URL (env overrides)

// The last-loaded registry document, kept so selectNetwork() can re-filter to a
Expand Down
20 changes: 15 additions & 5 deletions apps/amm/tests/testnet/setup-amm-testnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ TEST_SEQ_POLL_TIMEOUT="${TEST_SEQ_POLL_TIMEOUT:-3s}"
# the faucet mints into. They deliberately start with NO token A so the test can prove
# the account only appears in the swap picker after a faucet mint + UI refresh. The
# faucet requires recipient != user_holding, hence two accounts.
ACCOUNT_LABELS=(token-a-def token-a-holding token-b-def token-b-holding lp-holding token-c-def token-c-holding token-d-def token-d-holding holder2 holder2-a-holding)
# `amm-owner` is a dedicated account that signs `initialize` — the AMM instance's
# namespace owner. All three are appended last so the existing accounts keep their ids.
ACCOUNT_LABELS=(token-a-def token-a-holding token-b-def token-b-holding lp-holding token-c-def token-c-holding token-d-def token-d-holding holder2 holder2-a-holding amm-owner)

###############################################################################
# CONFIG — non-account parameters (edit freely)
Expand Down Expand Up @@ -396,7 +398,9 @@ TOKEN_D_HOLDING="$(acct_id token-d-holding)" || die "token-d-holding not registe
# "Token A Holder 2" — the faucet recipient/signer and its (initially empty) token A holding.
HOLDER2="$(acct_id holder2)" || die "holder2 not registered — run with FORCE_BOOTSTRAP=1"
HOLDER2_A_HOLDING="$(acct_id holder2-a-holding)" || die "holder2-a-holding not registered"
for v in TOKEN_A_DEF TOKEN_A_HOLDING TOKEN_B_DEF TOKEN_B_HOLDING USER_HOLDING_LP TOKEN_C_DEF TOKEN_C_HOLDING TOKEN_D_DEF TOKEN_D_HOLDING HOLDER2 HOLDER2_A_HOLDING; do
# `amm-owner` signs initialize — the AMM instance's namespace owner.
AMM_OWNER="$(acct_id amm-owner)" || die "amm-owner not registered"
for v in TOKEN_A_DEF TOKEN_A_HOLDING TOKEN_B_DEF TOKEN_B_HOLDING USER_HOLDING_LP TOKEN_C_DEF TOKEN_C_HOLDING TOKEN_D_DEF TOKEN_D_HOLDING HOLDER2 HOLDER2_A_HOLDING AMM_OWNER; do
[ -n "${!v}" ] || die "failed to resolve account id for $v"
done

Expand All @@ -405,6 +409,9 @@ done
# the faucet PDA, set in step 3 once the faucet binary is deployed and its ImageID
# (hence the PDA) is known.
AMM_AUTHORITY="$TOKEN_A_HOLDING"
# Namespace of the AMM instance: `amm-owner` signs initialize; the all-zero nonce is
# its default instance. The config PDA (below) is derived from (AMM_OWNER, AMM_NONCE).
AMM_NONCE="0000000000000000000000000000000000000000000000000000000000000000"
USER_HOLDING_A="$TOKEN_A_HOLDING"; USER_HOLDING_B="$TOKEN_B_HOLDING"

kv "token-a-def" "$TOKEN_A_DEF"
Expand All @@ -418,6 +425,7 @@ kv "token-d-def" "$TOKEN_D_DEF"
kv "token-d-holding" "$TOKEN_D_HOLDING"
kv "holder2" "$HOLDER2"
kv "holder2-a-holding" "$HOLDER2_A_HOLDING"
kv "amm-owner" "$AMM_OWNER"

###############################################################################
# 2. Deploy programs
Expand Down Expand Up @@ -502,9 +510,9 @@ inspect "$TOKEN_IDL" "$TOKEN_D_HOLDING" "TokenHolding"
# 6. Derive AMM PDAs from the program ids + token pair
###############################################################################
sec "Deriving AMM PDAs (amm_pdas example)"
log "${DIM}\$ cargo run -q -p amm_program --example amm_pdas -- $AMM_PID $TWAP_PID $TOKEN_A_DEF $TOKEN_B_DEF${RST}"
log "${DIM}\$ cargo run -q -p amm_program --example amm_pdas -- $AMM_PID $AMM_OWNER $TWAP_PID $TOKEN_A_DEF $TOKEN_B_DEF${RST}"
PDAS="$(RISC0_DEV_MODE=1 RISC0_SKIP_BUILD=1 cargo run -q -p amm_program --example amm_pdas -- \
"$AMM_PID" "$TWAP_PID" "$TOKEN_A_DEF" "$TOKEN_B_DEF")"
"$AMM_PID" "$AMM_OWNER" "$TWAP_PID" "$TOKEN_A_DEF" "$TOKEN_B_DEF")"
printf '%s\n' "$PDAS"

pda() { printf '%s' "$PDAS" | awk -v k="$1" '$1==k {print $2; exit}'; }
Expand Down Expand Up @@ -534,7 +542,9 @@ kv "current_tick_account" "$TICK"
###############################################################################
run_tx strict "initialize AMM config" -- \
spel --idl "$AMM_IDL" --program "$AMM_BIN" -- initialize \
--owner "$AMM_OWNER" \
--config "$CONFIG" \
--nonce "$AMM_NONCE" \
--token-program-id "$TOKEN_PID" \
--twap-oracle-program-id "$TWAP_PID" \
--authority "$AMM_AUTHORITY"
Expand Down Expand Up @@ -648,7 +658,7 @@ sec "Write UI registry config -> $REGISTRY_CONFIG_OUT"
"name": "AMM local registry",
"version": "0.1.0",
"networks": [
{ "id": "local", "name": "Local", "programIds": { "amm": "$AMM_PID", "token": "$TOKEN_PID", "tokenMintAuthority": "$MINT_AUTHORITY_PID" } }
{ "id": "local", "name": "Local", "programIds": { "amm": "$AMM_PID", "token": "$TOKEN_PID", "tokenMintAuthority": "$MINT_AUTHORITY_PID" }, "ammConfigId": "$CONFIG" }
],
"tokens": [
{ "network": "local", "symbol": "$TOKEN_A_SYMBOL", "name": "$TOKEN_A_NAME", "definitionId": "$TOKEN_A_DEF" },
Expand Down
15 changes: 15 additions & 0 deletions artifacts/amm-idl.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
{
"name": "initialize",
"accounts": [
{
"name": "owner",
"writable": true,
"signer": true,
"init": false
},
{
"name": "config",
"writable": true,
Expand All @@ -13,6 +19,15 @@
}
],
"args": [
{
"name": "nonce",
"type": {
"array": [
"u8",
32
]
}
},
{
"name": "token_program_id",
"type": "program_id"
Expand Down
6 changes: 3 additions & 3 deletions modules/amm/ffi/src/api/admin.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use amm_core::{compute_config_pda, Instruction};
use amm_core::Instruction;
use serde_json::{json, Value};

use super::{config::load_config, TransferOwnershipPlanRequest};
Expand All @@ -13,7 +13,7 @@ pub(super) fn transfer_ownership_plan(
) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let new_authority = account_id_from_hex(&request.new_authority_id, "new authority id")?;
let Ok(config) = load_config(amm_program, &request.config) else {
let Ok((config_id, config)) = load_config(amm_program, &request.config) else {
return Err(String::from("config_unavailable"));
};

Expand All @@ -23,7 +23,7 @@ pub(super) fn transfer_ownership_plan(
// Fixed IDL account order for UpdateConfig: the config account (mut, updated in place, not a
// signer) and the current admin authority (signs). `new_authority` is instruction data, not
// an account.
let account_ids = [compute_config_pda(amm_program), config.authority];
let account_ids = [config_id, config.authority];
let signing_requirements = [false, true];

Ok(json!({
Expand Down
40 changes: 29 additions & 11 deletions modules/amm/ffi/src/api/config.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
use amm_core::{compute_config_pda, AmmConfig};
use lee_core::{account::Account, program::ProgramId};
use lee_core::{
account::{Account, AccountId},
program::ProgramId,
};
use serde_json::{json, Value};

use super::{ConfigAccountRequest, ConfigIdRequest};
use crate::account::{
account_id_hex, decode_account, parse_program_id, program_id_base58, AccountRead,
account_id_hex, decode_account, parse_base58_id, parse_hex_32, parse_program_id,
program_id_base58, AccountRead,
};

pub(super) fn config_id(request: ConfigIdRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let owner = parse_base58_id(&request.owner, "owner")?;
// An omitted / empty nonce selects the owner's default (all-zero) instance.
let nonce = if request.nonce.is_empty() {
[0_u8; 32]
} else {
parse_hex_32(&request.nonce, "nonce")?
};
Ok(json!({
"status": "ok",
"configId": account_id_hex(compute_config_pda(amm_program)),
"configId": account_id_hex(compute_config_pda(amm_program, owner, nonce)),
}))
}

Expand All @@ -21,27 +32,34 @@ pub(super) fn config_id(request: ConfigIdRequest) -> Result<Value, String> {
/// `config_id` for address derivation.
pub(super) fn config_account(request: ConfigAccountRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let Ok(config) = load_config(amm_program, &request.config) else {
let Ok((config_id, config)) = load_config(amm_program, &request.config) else {
return Ok(json!({ "status": "error", "error": "config_unavailable" }));
};
Ok(json!({
"status": "ok",
"error": "",
"configId": compute_config_pda(amm_program).to_string(),
"configId": config_id.to_string(),
"ammProgramId": program_id_base58(amm_program),
"authority": config.authority.to_string(),
"tokenProgramId": program_id_base58(config.token_program_id),
"twapOracleProgramId": program_id_base58(config.twap_oracle_program_id),
}))
}

pub(super) fn load_config(amm_program: ProgramId, read: &AccountRead) -> Result<AmmConfig, String> {
/// Decodes and validates a passed AMM config account, returning its id (the namespace root
/// callers derive pools under) alongside the decoded config. Since the config PDA is now
/// namespaced by `(owner, nonce)`, the id can no longer be recomputed here without those
/// inputs — the caller-supplied account's id IS the namespace root. Program ownership and a
/// non-default, parseable account are still enforced.
pub(super) fn load_config(
amm_program: ProgramId,
read: &AccountRead,
) -> Result<(AccountId, AmmConfig), String> {
let (id, account) = decode_account(read)?;
if id != compute_config_pda(amm_program)
|| account.program_owner != amm_program
|| account == Account::default()
{
if account.program_owner != amm_program || account == Account::default() {
return Err(String::from("AMM config is unavailable"));
}
AmmConfig::try_from(&account.data).map_err(|_| String::from("AMM config is invalid"))
let config =
AmmConfig::try_from(&account.data).map_err(|_| String::from("AMM config is invalid"))?;
Ok((id, config))
}
2 changes: 1 addition & 1 deletion modules/amm/ffi/src/api/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::account::{account_id_from_hex, decode_account, parse_program_id, Acco
/// requested id with no returned row as unresolved/unavailable.
pub(super) fn resolve_tokens(request: ResolveTokensRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let Ok(config) = load_config(amm_program, &request.config) else {
let Ok((_, config)) = load_config(amm_program, &request.config) else {
return Ok(json!({ "status": "error", "code": "config_unavailable", "tokens": [] }));
};

Expand Down
25 changes: 16 additions & 9 deletions modules/amm/ffi/src/api/liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,13 @@ mod tests {
}
}

/// The namespace root (config PDA id) the plan tests derive pools under. A fixed
/// `(owner, nonce)` instance is enough — the tests only need it to be consistent between
/// `valid_config` and the expected `compute_pool_pda`.
fn config_id(amm: lee_core::program::ProgramId) -> AccountId {
compute_config_pda(amm, AccountId::new([0x07; 32]), [0; 32])
}

/// A valid AMM config account read so `derive_pair` succeeds in plan tests.
fn valid_config(amm: lee_core::program::ProgramId) -> AccountRead {
let token_program = parse_program_id(&"01".repeat(32)).unwrap();
Expand All @@ -669,7 +676,7 @@ mod tests {
}),
..Account::default()
};
account_read(compute_config_pda(amm), &account)
account_read(config_id(amm), &account)
}

#[test]
Expand Down Expand Up @@ -797,8 +804,8 @@ mod tests {
.map(|value| value.as_bool().unwrap())
.collect();

let pool = compute_pool_pda(amm, canonical_a, canonical_b);
assert_eq!(ids[0], account_id_hex(compute_config_pda(amm)));
let pool = compute_pool_pda(amm, config_id(amm), canonical_a, canonical_b);
assert_eq!(ids[0], account_id_hex(config_id(amm)));
assert_eq!(ids[1], account_id_hex(pool));
// Canonical vaults, in canonical order.
assert_eq!(
Expand Down Expand Up @@ -1062,10 +1069,10 @@ mod tests {
serde_json::json!(words.iter().map(|w| u64::from(*w)).collect::<Vec<u64>>())
};
let assert_aligned = |ids: &[String], instruction: &serde_json::Value| {
assert_eq!(ids[0], account_id_hex(compute_config_pda(amm)));
assert_eq!(ids[0], account_id_hex(config_id(amm)));
assert_eq!(
ids[1],
account_id_hex(compute_pool_pda(amm, token_a, token_b))
account_id_hex(compute_pool_pda(amm, config_id(amm), token_a, token_b))
);
assert_eq!(ids[2], account_id_hex(vault_a));
assert_eq!(ids[3], account_id_hex(vault_b));
Expand Down Expand Up @@ -1363,10 +1370,10 @@ mod tests {
]);
let assert_aligned =
|ids: &[String], instruction: &serde_json::Value, signers: &serde_json::Value| {
assert_eq!(ids[0], account_id_hex(compute_config_pda(amm)));
assert_eq!(ids[0], account_id_hex(config_id(amm)));
assert_eq!(
ids[1],
account_id_hex(compute_pool_pda(amm, token_a, token_b))
account_id_hex(compute_pool_pda(amm, config_id(amm), token_a, token_b))
);
assert_eq!(ids[2], account_id_hex(vault_a));
assert_eq!(ids[3], account_id_hex(vault_b));
Expand Down Expand Up @@ -1475,10 +1482,10 @@ mod tests {
.map(|v| v.as_str().unwrap().to_string())
.collect::<Vec<String>>();
assert_eq!(ids.len(), 6);
assert_eq!(ids[0], account_id_hex(compute_config_pda(amm)));
assert_eq!(ids[0], account_id_hex(config_id(amm)));
assert_eq!(
ids[1],
account_id_hex(compute_pool_pda(amm, token_a, token_b))
account_id_hex(compute_pool_pda(amm, config_id(amm), token_a, token_b))
);
assert_eq!(ids[2], account_id_hex(vault_a)); // pool's stored vaults
assert_eq!(ids[3], account_id_hex(vault_b));
Expand Down
8 changes: 3 additions & 5 deletions modules/amm/ffi/src/api/pair.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use amm_core::{
compute_config_pda, compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda,
compute_vault_pda,
compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda, compute_vault_pda,
};
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
use lee_core::{account::AccountId, program::ProgramId};
Expand Down Expand Up @@ -57,9 +56,8 @@ pub(super) fn derive_pair(
token_b: AccountId,
config_read: &AccountRead,
) -> Result<PairIds, String> {
let config_id = compute_config_pda(amm_program);
let config = load_config(amm_program, config_read)?;
let pool = compute_pool_pda(amm_program, token_a, token_b);
let (config_id, config) = load_config(amm_program, config_read)?;
let pool = compute_pool_pda(amm_program, config_id, token_a, token_b);
Ok(PairIds {
token_a,
token_b,
Expand Down
10 changes: 10 additions & 0 deletions modules/amm/ffi/src/api/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ use serde::Deserialize;

use crate::account::AccountRead;

/// Derives a namespaced AMM instance's config PDA. `owner` (base58 account id) and `nonce`
/// (64-char hex; omitted / empty ⇒ the all-zero default, i.e. the owner's default instance)
/// select the instance. This is the one op that supplies the namespace directly — every other
/// op derives it from the config account it is passed.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ConfigIdRequest {
pub amm_program_id: String,
pub owner: String,
#[serde(default)]
pub nonce: String,
}

/// Decodes the singleton AMM config account. `config` is the read of the config PDA the module
Expand Down Expand Up @@ -124,6 +131,9 @@ pub struct PoolIdRequest {
pub amm_program_id: String,
pub token_in_id: String,
pub token_out_id: String,
/// AMM config account read — its id is the namespace root the pool PDA is derived under
/// (pools are namespaced by config since the namespacing change).
pub config: AccountRead,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
Expand Down
Loading
Loading