mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(amm): add oracle setup ops (createPriceObservations / createOraclePriceAccount)
Expose the two TWAP oracle-setup instructions as module ops so a pool's price feeds can be seeded from the app. Both chain into the configured oracle, seeded from validated pool state (initial tick read on-chain) — nothing is caller-priced, and each window is a distinct feed account.
This commit is contained in:
@@ -201,6 +201,27 @@ QVariantMap AmmUiBackend::transferOwnership(QVariantMap request)
|
||||
return m_logos->amm_module.transferOwnership(request);
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::createPriceObservations(QVariantMap request)
|
||||
{
|
||||
if (!isWalletOpen())
|
||||
return QVariantMap {
|
||||
{ QStringLiteral("status"), QStringLiteral("error") },
|
||||
{ QStringLiteral("error"), QStringLiteral("wallet_unavailable") },
|
||||
};
|
||||
// Oracle setup doesn't touch token balances — no refresh.
|
||||
return m_logos->amm_module.createPriceObservations(request);
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::createOraclePriceAccount(QVariantMap request)
|
||||
{
|
||||
if (!isWalletOpen())
|
||||
return QVariantMap {
|
||||
{ QStringLiteral("status"), QStringLiteral("error") },
|
||||
{ QStringLiteral("error"), QStringLiteral("wallet_unavailable") },
|
||||
};
|
||||
return m_logos->amm_module.createOraclePriceAccount(request);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex,
|
||||
QString userOutputHoldingHex, QString amountInDecimal,
|
||||
QString minOutDecimal, QString deadlineDecimal)
|
||||
|
||||
@@ -57,6 +57,8 @@ public slots:
|
||||
QVariantMap resolvePoolAccount(QString defAHex, QString defBHex) override;
|
||||
QVariantMap configAccount() override;
|
||||
QVariantMap transferOwnership(QVariantMap request) override;
|
||||
QVariantMap createPriceObservations(QVariantMap request) override;
|
||||
QVariantMap createOraclePriceAccount(QVariantMap request) override;
|
||||
QString swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex,
|
||||
QString userOutputHoldingHex, QString amountInDecimal,
|
||||
QString minOutDecimal, QString deadlineDecimal) override;
|
||||
|
||||
@@ -59,6 +59,14 @@ class AmmUiBackend
|
||||
// transactionId:<hex> } or { status:"error", error:<code> } (wallet_unavailable,
|
||||
// config_missing, invalid_account_id, wallet_submission_failed, backend_error).
|
||||
SLOT(QVariantMap transferOwnership(QVariantMap request))
|
||||
// Oracle setup (keeper): seed a pool's TWAP feed / create its oracle price account for a
|
||||
// window. `request` carries { tokenAId, tokenBId, windowDurationMs }. Direct submits
|
||||
// (chained into the oracle, seeded from validated pool state — nothing signs). Return
|
||||
// { status:"ok", error:"", transactionId:<hex> } or { status:"error", error:<code> }
|
||||
// (wallet_unavailable, config_missing, invalid_token_id, invalid_window, same_token_pair,
|
||||
// config_unavailable, already_exists, wallet_submission_failed, backend_error).
|
||||
SLOT(QVariantMap createPriceObservations(QVariantMap request))
|
||||
SLOT(QVariantMap createOraclePriceAccount(QVariantMap request))
|
||||
// Submits a real on-chain SwapExactInput transaction against the pool for
|
||||
// (defAHex, defBHex). amountInDecimal/minOutDecimal are decimal-string
|
||||
// u128 amounts in base units; deadlineDecimal is a decimal-string u64 unix
|
||||
|
||||
@@ -52,6 +52,10 @@ char *amm_sync_reserves_plan(const char *request_json);
|
||||
|
||||
char *amm_transfer_ownership_plan(const char *request_json);
|
||||
|
||||
char *amm_create_price_observations_plan(const char *request_json);
|
||||
|
||||
char *amm_create_oracle_price_account_plan(const char *request_json);
|
||||
|
||||
char *amm_token_holdings(const char *request_json);
|
||||
|
||||
char *amm_program_id(const char *request_json);
|
||||
|
||||
@@ -6,6 +6,7 @@ mod context;
|
||||
mod fee;
|
||||
mod holding;
|
||||
mod liquidity;
|
||||
mod oracle;
|
||||
mod pair;
|
||||
mod quote;
|
||||
mod request;
|
||||
@@ -19,7 +20,8 @@ use std::{error::Error, fmt};
|
||||
|
||||
pub use request::{
|
||||
AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigAccountRequest, ConfigIdRequest,
|
||||
CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest,
|
||||
CreateOraclePriceAccountPlanRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest,
|
||||
CreatePriceObservationsPlanRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest,
|
||||
ProgramIdRequest, RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest,
|
||||
ResolveTokensRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
|
||||
SwapExactOutQuoteRequest, SwapPairRequest, SyncReservesPlanRequest, TokenHoldingsRequest,
|
||||
@@ -153,6 +155,16 @@ pub fn transfer_ownership_plan(request: TransferOwnershipPlanRequest) -> AmmResu
|
||||
admin::transfer_ownership_plan(request).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Builds the `CreatePriceObservations` submission — seeds a pool's TWAP observations feed.
|
||||
pub fn create_price_observations_plan(request: CreatePriceObservationsPlanRequest) -> AmmResult {
|
||||
oracle::create_price_observations_plan(request).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Builds the `CreateOraclePriceAccount` submission — creates a pool's TWAP oracle price account.
|
||||
pub fn create_oracle_price_account_plan(request: CreateOraclePriceAccountPlanRequest) -> AmmResult {
|
||||
oracle::create_oracle_price_account_plan(request).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Lists the wallet's fungible token holdings for the account selector.
|
||||
pub fn token_holdings(request: TokenHoldingsRequest) -> AmmResult {
|
||||
token_holdings::token_holdings(request).map_err(Into::into)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
use amm_core::Instruction;
|
||||
use nssa_core::account::AccountId;
|
||||
use serde_json::{json, Value};
|
||||
use twap_oracle_core::{compute_oracle_price_account_pda, compute_price_observations_pda};
|
||||
|
||||
use super::{
|
||||
pair::derive_pair, CreateOraclePriceAccountPlanRequest, CreatePriceObservationsPlanRequest,
|
||||
};
|
||||
use crate::account::{account_id_from_hex, account_id_hex, parse_program_id};
|
||||
|
||||
/// The tx-submission envelope shared by the two oracle-setup plans: the fixed IDL account ids as
|
||||
/// hex, their signer flags (nothing signs — both are chained calls into the TWAP oracle seeded
|
||||
/// from validated pool state), and the risc0-encoded instruction words.
|
||||
fn plan_response(program_id: &str, account_ids: &[AccountId], instruction: Vec<u32>) -> Value {
|
||||
let signing_requirements = vec![false; account_ids.len()];
|
||||
json!({
|
||||
"programId": program_id,
|
||||
"accountIds": account_ids.iter().copied().map(account_id_hex).collect::<Vec<_>>(),
|
||||
"signingRequirements": signing_requirements,
|
||||
"instruction": instruction,
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolves the pair's config / pool / current-tick / clock, returning them plus the TWAP oracle
|
||||
/// program id (for the window-seeded PDAs). Shared prelude for the two oracle-setup plans.
|
||||
fn resolve(
|
||||
request_amm_program_id: &str,
|
||||
token_a_id: &str,
|
||||
token_b_id: &str,
|
||||
config: &crate::account::AccountRead,
|
||||
) -> Result<super::pair::PairIds, String> {
|
||||
let amm_program = parse_program_id(request_amm_program_id)?;
|
||||
let token_a = account_id_from_hex(token_a_id, "token A id")?;
|
||||
let token_b = account_id_from_hex(token_b_id, "token B id")?;
|
||||
if token_a == token_b {
|
||||
return Err(String::from("same_token_pair"));
|
||||
}
|
||||
derive_pair(amm_program, token_a, token_b, config)
|
||||
.map_err(|_| String::from("config_unavailable"))
|
||||
}
|
||||
|
||||
/// Builds the `CreatePriceObservations` submission: creates the pool's TWAP observations account
|
||||
/// for `window_duration_ms`. A chained call into the configured oracle — the feed's initial tick
|
||||
/// is read on-chain from the pool's current-tick account, so nothing is caller-priced.
|
||||
pub(super) fn create_price_observations_plan(
|
||||
request: CreatePriceObservationsPlanRequest,
|
||||
) -> Result<Value, String> {
|
||||
let pair = resolve(
|
||||
&request.amm_program_id,
|
||||
&request.token_a_id,
|
||||
&request.token_b_id,
|
||||
&request.config,
|
||||
)?;
|
||||
let window = request.window_duration_ms;
|
||||
if window < u64::from(twap_oracle_core::OBSERVATIONS_CAPACITY) {
|
||||
return Err(String::from("invalid_window"));
|
||||
}
|
||||
let price_observations =
|
||||
compute_price_observations_pda(pair.twap_oracle_program, pair.pool, window);
|
||||
|
||||
let instruction = risc0_zkvm::serde::to_vec(&Instruction::CreatePriceObservations {
|
||||
window_duration: window,
|
||||
})
|
||||
.map_err(|error| format!("instruction serialization failed: {error}"))?;
|
||||
|
||||
// Fixed IDL account order: config, pool (the price source), current_tick (supplies the initial
|
||||
// tick), price_observations (init), clock. Nothing signs.
|
||||
let account_ids = [
|
||||
pair.config,
|
||||
pair.pool,
|
||||
pair.current_tick,
|
||||
price_observations,
|
||||
pair.clock,
|
||||
];
|
||||
Ok(plan_response(
|
||||
&request.amm_program_id,
|
||||
&account_ids,
|
||||
instruction,
|
||||
))
|
||||
}
|
||||
|
||||
/// Builds the `CreateOraclePriceAccount` submission: creates the pool's TWAP oracle price account
|
||||
/// for `window_duration_ms`. A chained call into the configured oracle — no caller pricing.
|
||||
pub(super) fn create_oracle_price_account_plan(
|
||||
request: CreateOraclePriceAccountPlanRequest,
|
||||
) -> Result<Value, String> {
|
||||
let pair = resolve(
|
||||
&request.amm_program_id,
|
||||
&request.token_a_id,
|
||||
&request.token_b_id,
|
||||
&request.config,
|
||||
)?;
|
||||
let window = request.window_duration_ms;
|
||||
let oracle_price_account =
|
||||
compute_oracle_price_account_pda(pair.twap_oracle_program, pair.pool, window);
|
||||
|
||||
let instruction = risc0_zkvm::serde::to_vec(&Instruction::CreateOraclePriceAccount {
|
||||
window_duration: window,
|
||||
})
|
||||
.map_err(|error| format!("instruction serialization failed: {error}"))?;
|
||||
|
||||
// Fixed IDL account order: config, pool (the price source), oracle_price_account (init), clock.
|
||||
// Nothing signs.
|
||||
let account_ids = [pair.config, pair.pool, oracle_price_account, pair.clock];
|
||||
Ok(plan_response(
|
||||
&request.amm_program_id,
|
||||
&account_ids,
|
||||
instruction,
|
||||
))
|
||||
}
|
||||
@@ -22,6 +22,8 @@ pub(super) struct PairIds {
|
||||
pub(super) lp_lock_holding: AccountId,
|
||||
pub(super) current_tick: AccountId,
|
||||
pub(super) clock: AccountId,
|
||||
/// The configured TWAP oracle program — the price/observation PDAs are seeded by it.
|
||||
pub(super) twap_oracle_program: ProgramId,
|
||||
}
|
||||
|
||||
pub(super) fn pair_ids(request: PairIdsRequest) -> Result<Value, String> {
|
||||
@@ -69,6 +71,7 @@ pub(super) fn derive_pair(
|
||||
lp_lock_holding: compute_lp_lock_holding_pda(amm_program, pool),
|
||||
current_tick: compute_current_tick_account_pda(config.twap_oracle_program_id, pool),
|
||||
clock: CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
twap_oracle_program: config.twap_oracle_program_id,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,31 @@ pub struct TransferOwnershipPlanRequest {
|
||||
pub new_authority_id: String,
|
||||
}
|
||||
|
||||
/// Builds the `CreatePriceObservations` submission — seeds the pool's TWAP observations feed for a
|
||||
/// window. `config` is the read of the config PDA (its `twap_oracle_program_id` seeds the feed
|
||||
/// PDAs); `token_ids` are hex; `window_duration_ms` is the TWAP window in milliseconds.
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatePriceObservationsPlanRequest {
|
||||
pub amm_program_id: String,
|
||||
pub config: AccountRead,
|
||||
pub token_a_id: String,
|
||||
pub token_b_id: String,
|
||||
pub window_duration_ms: u64,
|
||||
}
|
||||
|
||||
/// Builds the `CreateOraclePriceAccount` submission — creates the pool's TWAP oracle price account
|
||||
/// for a window. Same inputs as `CreatePriceObservationsPlanRequest`.
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateOraclePriceAccountPlanRequest {
|
||||
pub amm_program_id: String,
|
||||
pub config: AccountRead,
|
||||
pub token_a_id: String,
|
||||
pub token_b_id: String,
|
||||
pub window_duration_ms: u64,
|
||||
}
|
||||
|
||||
/// Resolves an app-provided set of token ids into selector rows. `token_ids` are hex — the
|
||||
/// module normalizes base58→hex and reads each definition into `token_definitions` (keyed by
|
||||
/// hex id) plus the wallet accounts; the FFI is stateless and reads nothing itself.
|
||||
|
||||
@@ -11,18 +11,23 @@ use nssa_core::{
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
use twap_oracle_core::compute_current_tick_account_pda;
|
||||
use twap_oracle_core::{
|
||||
compute_current_tick_account_pda, compute_oracle_price_account_pda,
|
||||
compute_price_observations_pda,
|
||||
};
|
||||
|
||||
use super::{
|
||||
admin::transfer_ownership_plan,
|
||||
config::config_account as decode_config_account,
|
||||
context::resolve_tokens,
|
||||
holding::{select_holding, SelectedHolding},
|
||||
oracle::{create_oracle_price_account_plan, create_price_observations_plan},
|
||||
pair::{is_canonical_pair, pair_ids, PairIds},
|
||||
quote::{div_ceil_u256, minimum_opening_pair, Q64},
|
||||
swap::{swap_exact_in_plan, swap_exact_out_plan},
|
||||
ConfigAccountRequest, PairIdsRequest, ResolveTokensRequest, SwapExactInPlanRequest,
|
||||
SwapExactOutPlanRequest, TransferOwnershipPlanRequest,
|
||||
ConfigAccountRequest, CreateOraclePriceAccountPlanRequest, CreatePriceObservationsPlanRequest,
|
||||
PairIdsRequest, ResolveTokensRequest, SwapExactInPlanRequest, SwapExactOutPlanRequest,
|
||||
TransferOwnershipPlanRequest,
|
||||
};
|
||||
use crate::{
|
||||
account::{account_id_hex, account_read, decode_account, program_id_base58, program_id_bytes},
|
||||
@@ -95,6 +100,7 @@ fn ids() -> PairIds {
|
||||
lp_lock_holding: compute_lp_lock_holding_pda(AMM_PROGRAM, pool),
|
||||
current_tick: compute_current_tick_account_pda(TWAP_PROGRAM, pool),
|
||||
clock: CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
twap_oracle_program: TWAP_PROGRAM,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +285,99 @@ fn transfer_ownership_plan_targets_config_and_current_admin() {
|
||||
assert_eq!(decoded, new_authority);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_price_observations_plan_targets_the_window_feed_accounts() {
|
||||
let token_a = AccountId::new([2; 32]);
|
||||
let token_b = AccountId::new([1; 32]);
|
||||
let config_id = compute_config_pda(AMM_PROGRAM);
|
||||
let pool = compute_pool_pda(AMM_PROGRAM, token_a, token_b);
|
||||
let window = 3_600_000_u64;
|
||||
|
||||
let plan = create_price_observations_plan(CreatePriceObservationsPlanRequest {
|
||||
amm_program_id: amm_program_id(),
|
||||
config: account_read(config_id, &config_account()),
|
||||
token_a_id: account_id_hex(token_a),
|
||||
token_b_id: account_id_hex(token_b),
|
||||
window_duration_ms: window,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// config, pool (price source), current_tick (initial tick), price_observations (init), clock.
|
||||
assert_eq!(
|
||||
plan["accountIds"],
|
||||
json!([
|
||||
account_id_hex(config_id),
|
||||
account_id_hex(pool),
|
||||
account_id_hex(compute_current_tick_account_pda(TWAP_PROGRAM, pool)),
|
||||
account_id_hex(compute_price_observations_pda(TWAP_PROGRAM, pool, window)),
|
||||
account_id_hex(CLOCK_01_PROGRAM_ACCOUNT_ID),
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
plan["signingRequirements"],
|
||||
json!([false, false, false, false, false])
|
||||
);
|
||||
|
||||
let words: Vec<u32> = plan["instruction"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|word| word.as_u64().unwrap() as u32)
|
||||
.collect();
|
||||
let Instruction::CreatePriceObservations { window_duration } =
|
||||
risc0_zkvm::serde::from_slice(&words).unwrap()
|
||||
else {
|
||||
panic!("expected CreatePriceObservations");
|
||||
};
|
||||
assert_eq!(window_duration, window);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_oracle_price_account_plan_targets_the_window_price_account() {
|
||||
let token_a = AccountId::new([2; 32]);
|
||||
let token_b = AccountId::new([1; 32]);
|
||||
let config_id = compute_config_pda(AMM_PROGRAM);
|
||||
let pool = compute_pool_pda(AMM_PROGRAM, token_a, token_b);
|
||||
let window = 900_000_u64;
|
||||
|
||||
let plan = create_oracle_price_account_plan(CreateOraclePriceAccountPlanRequest {
|
||||
amm_program_id: amm_program_id(),
|
||||
config: account_read(config_id, &config_account()),
|
||||
token_a_id: account_id_hex(token_a),
|
||||
token_b_id: account_id_hex(token_b),
|
||||
window_duration_ms: window,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// config, pool (price source), oracle_price_account (init), clock.
|
||||
assert_eq!(
|
||||
plan["accountIds"],
|
||||
json!([
|
||||
account_id_hex(config_id),
|
||||
account_id_hex(pool),
|
||||
account_id_hex(compute_oracle_price_account_pda(TWAP_PROGRAM, pool, window)),
|
||||
account_id_hex(CLOCK_01_PROGRAM_ACCOUNT_ID),
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
plan["signingRequirements"],
|
||||
json!([false, false, false, false])
|
||||
);
|
||||
|
||||
let words: Vec<u32> = plan["instruction"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|word| word.as_u64().unwrap() as u32)
|
||||
.collect();
|
||||
let Instruction::CreateOraclePriceAccount { window_duration } =
|
||||
risc0_zkvm::serde::from_slice(&words).unwrap()
|
||||
else {
|
||||
panic!("expected CreateOraclePriceAccount");
|
||||
};
|
||||
assert_eq!(window_duration, window);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_account_decodes_authority_and_program_ids() {
|
||||
let config_id = compute_config_pda(AMM_PROGRAM);
|
||||
|
||||
@@ -7,7 +7,8 @@ use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
use crate::api::{
|
||||
self, AddLiquidityPlanRequest, AddLiquidityQuoteRequest, AmmApiError, AmmResult,
|
||||
ConfigAccountRequest, ConfigIdRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest,
|
||||
ConfigAccountRequest, ConfigIdRequest, CreateOraclePriceAccountPlanRequest,
|
||||
CreatePoolPlanRequest, CreatePoolQuoteRequest, CreatePriceObservationsPlanRequest,
|
||||
FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest, RemoveLiquidityPlanRequest,
|
||||
RemoveLiquidityQuoteRequest, ResolvePoolRequest, ResolveTokensRequest, SwapExactInPlanRequest,
|
||||
SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest,
|
||||
@@ -175,6 +176,16 @@ pub extern "C" fn amm_transfer_ownership_plan(request_json: *const c_char) -> *m
|
||||
call::<TransferOwnershipPlanRequest>(request_json, api::transfer_ownership_plan)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn amm_create_price_observations_plan(request_json: *const c_char) -> *mut c_char {
|
||||
call::<CreatePriceObservationsPlanRequest>(request_json, api::create_price_observations_plan)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn amm_create_oracle_price_account_plan(request_json: *const c_char) -> *mut c_char {
|
||||
call::<CreateOraclePriceAccountPlanRequest>(request_json, api::create_oracle_price_account_plan)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn amm_token_holdings(request_json: *const c_char) -> *mut c_char {
|
||||
call::<TokenHoldingsRequest>(request_json, api::token_holdings)
|
||||
|
||||
@@ -6,11 +6,13 @@ mod ffi;
|
||||
pub mod api;
|
||||
|
||||
pub use api::{
|
||||
config_account, config_id, create_pool_plan, create_pool_quote, fee_tiers, pair_ids, pool_id,
|
||||
program_id, resolve_pool, resolve_tokens, swap_exact_in_plan, swap_exact_in_quote,
|
||||
swap_exact_out_plan, swap_exact_out_quote, swap_pair, transfer_ownership_plan, AccountRead,
|
||||
AmmApiError, AmmResponse, AmmResult, ConfigAccountRequest, ConfigIdRequest,
|
||||
CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest,
|
||||
config_account, config_id, create_oracle_price_account_plan, create_pool_plan,
|
||||
create_pool_quote, create_price_observations_plan, fee_tiers, pair_ids, pool_id, program_id,
|
||||
resolve_pool, resolve_tokens, swap_exact_in_plan, swap_exact_in_quote, swap_exact_out_plan,
|
||||
swap_exact_out_quote, swap_pair, transfer_ownership_plan, AccountRead, AmmApiError,
|
||||
AmmResponse, AmmResult, ConfigAccountRequest, ConfigIdRequest,
|
||||
CreateOraclePriceAccountPlanRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest,
|
||||
CreatePriceObservationsPlanRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest,
|
||||
ProgramIdRequest, ResolvePoolRequest, ResolveTokensRequest, SwapExactInPlanRequest,
|
||||
SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest,
|
||||
TransferOwnershipPlanRequest, WalletAccount,
|
||||
|
||||
@@ -466,6 +466,91 @@ LogosMap AmmModuleImpl::transferOwnership(const LogosMap& request) {
|
||||
return LogosMap{{"status", "ok"}, {"error", ""}, {"transactionId", jStr(obj, "tx_hash")}};
|
||||
}
|
||||
|
||||
LogosMap AmmModuleImpl::createPriceObservations(const LogosMap& request) {
|
||||
return oracleSetupSubmit(request, /*observations=*/true);
|
||||
}
|
||||
|
||||
LogosMap AmmModuleImpl::createOraclePriceAccount(const LogosMap& request) {
|
||||
return oracleSetupSubmit(request, /*observations=*/false);
|
||||
}
|
||||
|
||||
LogosMap AmmModuleImpl::oracleSetupSubmit(const LogosMap& request, bool observations) {
|
||||
auto error = [](const std::string& err) {
|
||||
return LogosMap{{"status", "error"}, {"error", err}};
|
||||
};
|
||||
|
||||
const std::string amm_program_id = ammProgramId();
|
||||
if (amm_program_id.empty())
|
||||
return error("config_missing");
|
||||
|
||||
// The plan derives the feed PDAs from the config's twap_oracle_program_id + the pool.
|
||||
const json config = readConfig(amm_program_id);
|
||||
if (config.is_null())
|
||||
return error("config_missing");
|
||||
|
||||
const std::string token_a = normalizeAccountId(jStr(request, "tokenAId"));
|
||||
const std::string token_b = normalizeAccountId(jStr(request, "tokenBId"));
|
||||
if (token_a.empty() || token_b.empty())
|
||||
return error("invalid_token_id");
|
||||
|
||||
// windowDurationMs may arrive as a JSON number (UI) or a decimal string (CLI); the FFI wants
|
||||
// a u64. A zero / unparsable window is rejected — each window is a distinct feed PDA.
|
||||
const json window_json = request.value("windowDurationMs", json());
|
||||
uint64_t window = 0;
|
||||
if (window_json.is_number_unsigned()) {
|
||||
window = window_json.get<uint64_t>();
|
||||
} else if (window_json.is_number_integer() && window_json.get<int64_t>() > 0) {
|
||||
window = window_json.get<uint64_t>();
|
||||
} else if (window_json.is_string()) {
|
||||
try {
|
||||
window = std::stoull(window_json.get<std::string>());
|
||||
} catch (...) {
|
||||
window = 0;
|
||||
}
|
||||
}
|
||||
if (window == 0)
|
||||
return error("invalid_window");
|
||||
|
||||
auto* const plan_op =
|
||||
observations ? amm_create_price_observations_plan : amm_create_oracle_price_account_plan;
|
||||
const FfiResult planResult = call(plan_op, json{
|
||||
{"ammProgramId", amm_program_id},
|
||||
{"config", config},
|
||||
{"tokenAId", token_a},
|
||||
{"tokenBId", token_b},
|
||||
{"windowDurationMs", window},
|
||||
});
|
||||
if (!planResult.ok)
|
||||
return error(planResult.error.empty() ? "backend_error" : planResult.error);
|
||||
const json plan = planResult.value;
|
||||
|
||||
const std::vector<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
||||
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
||||
const std::vector<uint8_t> instruction =
|
||||
jsonWordsToLeBytes(plan.value("instruction", json::array()));
|
||||
const std::string program_id = jStr(plan, "programId");
|
||||
|
||||
// Surface a stable error code when the target PDA already exists instead of failing at submit.
|
||||
const size_t target_index = observations ? 3 : 2;
|
||||
if (accounts.size() > target_index) {
|
||||
const json existing = readPublicAccount(accounts[target_index]);
|
||||
if (jStr(existing, "status") == "ok")
|
||||
return error("already_exists");
|
||||
}
|
||||
|
||||
AMM_TRACE("oracleSetup(" << (observations ? "observations" : "priceAccount")
|
||||
<< "): SUBMIT programId=" << program_id << " accounts=" << accounts.size());
|
||||
const std::string reply = modules().logos_execution_zone.send_generic_public_transaction(
|
||||
accounts, signers, instruction, program_id);
|
||||
AMM_TRACE("oracleSetup: tx reply=" << reply);
|
||||
|
||||
const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false);
|
||||
if (!obj.is_object() || !obj.value("success", false))
|
||||
return error("wallet_submission_failed");
|
||||
|
||||
return LogosMap{{"status", "ok"}, {"error", ""}, {"transactionId", jStr(obj, "tx_hash")}};
|
||||
}
|
||||
|
||||
LogosMap AmmModuleImpl::swapExactInQuote(const std::string& token_in_hex,
|
||||
const std::string& token_out_hex,
|
||||
const nlohmann::json& amount_in,
|
||||
|
||||
@@ -54,6 +54,17 @@ public:
|
||||
/// `wallet_submission_failed`, `backend_error`, or a plan code (e.g. `config_unavailable`).
|
||||
LogosMap transferOwnership(const LogosMap& request);
|
||||
|
||||
/// Submits `CreatePriceObservations` / `CreateOraclePriceAccount` — seeds a pool's TWAP feed /
|
||||
/// creates its oracle price account for `request.windowDurationMs` (a distinct account per
|
||||
/// window). `request` carries `{ tokenAId, tokenBId, windowDurationMs }`. Both are direct
|
||||
/// submits (chained into the oracle, seeded from validated pool state — nothing signs). On
|
||||
/// success `{ status:"ok", error:"", transactionId:<hex> }`; on failure
|
||||
/// `{ status:"error", error:<code> }` — `config_missing`, `invalid_token_id`,
|
||||
/// `invalid_window`, `same_token_pair`, `config_unavailable`, `wallet_submission_failed`,
|
||||
/// `backend_error` (`already_exists` if the target account is already created).
|
||||
LogosMap createPriceObservations(const LogosMap& request);
|
||||
LogosMap createOraclePriceAccount(const LogosMap& request);
|
||||
|
||||
/// Prices a `SwapExactInput` for the (token_in_hex, token_out_hex) pair:
|
||||
/// reads the pool and returns `{ status:"ok", error:"", expectedOutRaw,
|
||||
/// minReceivedRaw, priceImpactBps }`, oriented and computed server-side via
|
||||
@@ -271,4 +282,8 @@ private:
|
||||
// The user's own public account reads, fresh each call (empty when the wallet
|
||||
// is closed). Each read is a live sequencer round-trip.
|
||||
nlohmann::json walletAccountReads(bool wallet_open);
|
||||
|
||||
// Shared body for createPriceObservations / createOraclePriceAccount: reads the config,
|
||||
// builds the window-seeded oracle plan (observations vs price account), and submits it.
|
||||
LogosMap oracleSetupSubmit(const LogosMap& request, bool observations);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user