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:
r4bbit
2026-08-13 22:43:48 +02:00
parent ca8adfc4af
commit 7e45e44eac
13 changed files with 407 additions and 10 deletions
+21
View File
@@ -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)
+2
View File
@@ -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;
+8
View File
@@ -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
+4
View File
@@ -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);
+13 -1
View File
@@ -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)
+110
View File
@@ -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,
))
}
+3
View File
@@ -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,
})
}
+25
View File
@@ -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.
+102 -3
View File
@@ -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);
+12 -1
View File
@@ -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)
+7 -5
View File
@@ -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,
+85
View File
@@ -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,
+15
View File
@@ -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);
};