mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-21 14:29:41 +00:00
feat: Introduce SequencerClient for enhanced account management and transaction querying
- Added SequencerClient class to handle asynchronous account reads and transaction queries. - Updated NewPositionRuntime to integrate SequencerClient, allowing for improved wallet account management. - Modified existing tests to accommodate changes in the new position schema from v1 to v2. - Enhanced QML tests to validate new position form behavior with updated holding selections and context handling. - Refactored code to ensure compatibility with the new SequencerClient functionalities.
This commit is contained in:
parent
90ba65f2d0
commit
0e01962bec
@ -42,6 +42,8 @@ logos_module(
|
|||||||
src/AmmClient.cpp
|
src/AmmClient.cpp
|
||||||
src/NewPositionRuntime.h
|
src/NewPositionRuntime.h
|
||||||
src/NewPositionRuntime.cpp
|
src/NewPositionRuntime.cpp
|
||||||
|
src/SequencerClient.h
|
||||||
|
src/SequencerClient.cpp
|
||||||
FIND_PACKAGES
|
FIND_PACKAGES
|
||||||
Qt6Gui
|
Qt6Gui
|
||||||
Qt6Network
|
Qt6Network
|
||||||
@ -73,10 +75,13 @@ if(BUILD_TESTING)
|
|||||||
add_executable(amm_new_position_runtime_test
|
add_executable(amm_new_position_runtime_test
|
||||||
tests/cpp/NewPositionRuntimeTest.cpp
|
tests/cpp/NewPositionRuntimeTest.cpp
|
||||||
src/NewPositionRuntime.cpp
|
src/NewPositionRuntime.cpp
|
||||||
|
src/SequencerClient.cpp
|
||||||
|
src/SequencerClient.h
|
||||||
)
|
)
|
||||||
target_include_directories(amm_new_position_runtime_test PRIVATE src)
|
target_include_directories(amm_new_position_runtime_test PRIVATE src)
|
||||||
target_link_libraries(amm_new_position_runtime_test PRIVATE
|
target_link_libraries(amm_new_position_runtime_test PRIVATE
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
|
Qt6::Network
|
||||||
PkgConfig::BASE58
|
PkgConfig::BASE58
|
||||||
logos_wallet_access
|
logos_wallet_access
|
||||||
)
|
)
|
||||||
|
|||||||
@ -11,6 +11,7 @@ char *amm_pair_ids(const char *request_json);
|
|||||||
char *amm_context(const char *request_json);
|
char *amm_context(const char *request_json);
|
||||||
char *amm_quote(const char *request_json);
|
char *amm_quote(const char *request_json);
|
||||||
char *amm_plan(const char *request_json);
|
char *amm_plan(const char *request_json);
|
||||||
|
char *amm_normalize_account_rpc(const char *request_json);
|
||||||
void amm_free(char *value);
|
void amm_free(char *value);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@ -5,6 +5,9 @@ use nssa_core::{
|
|||||||
program::ProgramId,
|
program::ProgramId,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use crate::api::NormalizeAccountRpcRequest;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@ -137,6 +140,47 @@ pub(crate) fn decode_account(read: &AccountRead) -> Result<(AccountId, Account),
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RpcEnvelope {
|
||||||
|
#[serde(default)]
|
||||||
|
result: Option<RpcAccount>,
|
||||||
|
#[serde(default)]
|
||||||
|
error: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RpcAccount {
|
||||||
|
program_owner: ProgramId,
|
||||||
|
balance: u128,
|
||||||
|
data: Vec<u8>,
|
||||||
|
nonce: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn normalize_account_rpc(request: NormalizeAccountRpcRequest) -> Result<Value, String> {
|
||||||
|
parse_hex_32(&request.account_id, "account id")?;
|
||||||
|
let envelope: RpcEnvelope = serde_json::from_str(&request.response)
|
||||||
|
.map_err(|error| format!("invalid sequencer response: {error}"))?;
|
||||||
|
if envelope.error.is_some() {
|
||||||
|
return Err(String::from("sequencer returned an account-read error"));
|
||||||
|
}
|
||||||
|
let account = envelope
|
||||||
|
.result
|
||||||
|
.ok_or_else(|| String::from("sequencer returned no account"))?;
|
||||||
|
let data = Data::try_from(account.data)
|
||||||
|
.map_err(|error| format!("account data is too large: {error}"))?;
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"id": request.account_id,
|
||||||
|
"status": "ok",
|
||||||
|
"account": {
|
||||||
|
"program_owner": hex::encode(program_id_bytes(account.program_owner)),
|
||||||
|
"balance": hex::encode(account.balance.to_le_bytes()),
|
||||||
|
"nonce": hex::encode(account.nonce.to_le_bytes()),
|
||||||
|
"data": hex::encode(data.as_ref()),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn account_read(id: AccountId, account: &Account) -> AccountRead {
|
pub(crate) fn account_read(id: AccountId, account: &Account) -> AccountRead {
|
||||||
AccountRead {
|
AccountRead {
|
||||||
@ -150,3 +194,35 @@ pub(crate) fn account_read(id: AccountId, account: &Account) -> AccountRead {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalizes_rpc_u128_without_precision_loss() {
|
||||||
|
let account_id = hex::encode([7_u8; 32]);
|
||||||
|
let value = normalize_account_rpc(NormalizeAccountRpcRequest {
|
||||||
|
account_id: account_id.clone(),
|
||||||
|
response: String::from(
|
||||||
|
r#"{"jsonrpc":"2.0","id":1,"result":{"program_owner":[1,2,3,4,5,6,7,8],"balance":340282366920938463463374607431768211455,"data":[0,255],"nonce":18446744073709551617}}"#,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(value["id"], account_id);
|
||||||
|
assert_eq!(
|
||||||
|
value["account"]["balance"],
|
||||||
|
"ffffffffffffffffffffffffffffffff"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
value["account"]["nonce"],
|
||||||
|
"01000000000000000100000000000000"
|
||||||
|
);
|
||||||
|
assert_eq!(value["account"]["data"], "00ff");
|
||||||
|
assert_eq!(
|
||||||
|
value["account"]["program_owner"],
|
||||||
|
"0100000002000000030000000400000005000000060000000700000008000000"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use token_core::TokenDefinition;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
config::load_config,
|
config::load_config,
|
||||||
holding::{select_holding, wallet_holdings, SelectedHolding},
|
holding::{holding_options, wallet_holdings, SelectedHolding},
|
||||||
quote_error::issue,
|
quote_error::issue,
|
||||||
ContextRequest, TokenIdsRequest, SCHEMA,
|
ContextRequest, TokenIdsRequest, SCHEMA,
|
||||||
};
|
};
|
||||||
@ -85,8 +85,11 @@ pub(super) fn context(request: ContextRequest) -> Result<Value, String> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected = select_holding(&holdings, token_id);
|
let options = holding_options(&holdings, token_id);
|
||||||
let mut row = json!({
|
let total_balance = options.iter().fold(0_u128, |total, holding| {
|
||||||
|
total.saturating_add(holding.balance)
|
||||||
|
});
|
||||||
|
let row = json!({
|
||||||
"definitionId": token_id.to_string(),
|
"definitionId": token_id.to_string(),
|
||||||
"name": name,
|
"name": name,
|
||||||
"metadataId": metadata_id.map(|id| id.to_string()),
|
"metadataId": metadata_id.map(|id| id.to_string()),
|
||||||
@ -98,17 +101,22 @@ pub(super) fn context(request: ContextRequest) -> Result<Value, String> {
|
|||||||
"status": "available",
|
"status": "available",
|
||||||
"code": "available",
|
"code": "available",
|
||||||
"sources": sources,
|
"sources": sources,
|
||||||
|
"balanceRaw": total_balance.to_string(),
|
||||||
|
"holdings": options.into_iter().map(|holding| json!({
|
||||||
|
"holdingId": holding.id.to_string(),
|
||||||
|
"balanceRaw": holding.balance.to_string(),
|
||||||
|
})).collect::<Vec<_>>(),
|
||||||
});
|
});
|
||||||
if let Some(selected) = selected {
|
|
||||||
row["holdingId"] = json!(selected.id.to_string());
|
|
||||||
row["balanceRaw"] = json!(selected.balance.to_string());
|
|
||||||
}
|
|
||||||
rows.push(row);
|
rows.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.sort_by(|left, right| {
|
rows.sort_by(|left, right| {
|
||||||
let left_holding = left.get("holdingId").is_some();
|
let left_holding = left["holdings"]
|
||||||
let right_holding = right.get("holdingId").is_some();
|
.as_array()
|
||||||
|
.is_some_and(|rows| !rows.is_empty());
|
||||||
|
let right_holding = right["holdings"]
|
||||||
|
.as_array()
|
||||||
|
.is_some_and(|rows| !rows.is_empty());
|
||||||
right_holding.cmp(&left_holding).then_with(|| {
|
right_holding.cmp(&left_holding).then_with(|| {
|
||||||
left["definitionId"]
|
left["definitionId"]
|
||||||
.as_str()
|
.as_str()
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use nssa_core::{
|
|||||||
};
|
};
|
||||||
use token_core::TokenHolding;
|
use token_core::TokenHolding;
|
||||||
|
|
||||||
use crate::account::{decode_account, AccountRead};
|
use crate::account::{decode_account, parse_base58_id, AccountRead};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(super) struct SelectedHolding {
|
pub(super) struct SelectedHolding {
|
||||||
@ -51,14 +51,24 @@ pub(super) fn decode_fungible_holding(
|
|||||||
pub(super) fn select_holding(
|
pub(super) fn select_holding(
|
||||||
holdings: &[SelectedHolding],
|
holdings: &[SelectedHolding],
|
||||||
definition_id: AccountId,
|
definition_id: AccountId,
|
||||||
|
requested_id: Option<&str>,
|
||||||
) -> Option<SelectedHolding> {
|
) -> Option<SelectedHolding> {
|
||||||
|
let requested_id = parse_base58_id(requested_id?, "holding id").ok()?;
|
||||||
holdings
|
holdings
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|holding| holding.definition_id == definition_id)
|
.find(|holding| holding.definition_id == definition_id && holding.id == requested_id)
|
||||||
.max_by(|left, right| {
|
|
||||||
left.balance
|
|
||||||
.cmp(&right.balance)
|
|
||||||
.then_with(|| right.id.cmp(&left.id))
|
|
||||||
})
|
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn holding_options(
|
||||||
|
holdings: &[SelectedHolding],
|
||||||
|
definition_id: AccountId,
|
||||||
|
) -> Vec<SelectedHolding> {
|
||||||
|
let mut options = holdings
|
||||||
|
.iter()
|
||||||
|
.filter(|holding| holding.definition_id == definition_id)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
options.sort_by_key(|holding| holding.id);
|
||||||
|
options
|
||||||
|
}
|
||||||
|
|||||||
@ -20,15 +20,15 @@ mod tests;
|
|||||||
use std::{error::Error, fmt};
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
pub use request::{
|
pub use request::{
|
||||||
ConfigIdRequest, ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest, PositionRequest,
|
ConfigIdRequest, ContextRequest, NormalizeAccountRpcRequest, PairIdsRequest, PairSnapshot,
|
||||||
QuoteRequest, TokenIdsRequest,
|
PlanRequest, PositionRequest, QuoteRequest, TokenIdsRequest,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
pub use crate::account::{AccountRead, WalletAccount};
|
pub use crate::account::{AccountRead, WalletAccount};
|
||||||
|
|
||||||
/// Schema identifier expected by position quote and plan requests.
|
/// Schema identifier expected by position quote and plan requests.
|
||||||
pub const NEW_POSITION_SCHEMA: &str = "new-position.v1";
|
pub const NEW_POSITION_SCHEMA: &str = "new-position.v2";
|
||||||
|
|
||||||
pub(crate) const SCHEMA: &str = NEW_POSITION_SCHEMA;
|
pub(crate) const SCHEMA: &str = NEW_POSITION_SCHEMA;
|
||||||
|
|
||||||
@ -95,3 +95,8 @@ pub fn quote(request: QuoteRequest) -> AmmResult {
|
|||||||
pub fn plan(request: PlanRequest) -> AmmResult {
|
pub fn plan(request: PlanRequest) -> AmmResult {
|
||||||
plan::plan(request).map_err(Into::into)
|
plan::plan(request).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts one raw sequencer `getAccount` response into precision-safe account-read JSON.
|
||||||
|
pub fn normalize_account_rpc(request: NormalizeAccountRpcRequest) -> AmmResult {
|
||||||
|
crate::account::normalize_account_rpc(request).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|||||||
@ -77,6 +77,7 @@ pub(super) fn plan(input: PlanRequest) -> Result<Value, String> {
|
|||||||
return Ok(plan_error("transaction_deadline_expired"));
|
return Ok(plan_error("transaction_deadline_expired"));
|
||||||
}
|
}
|
||||||
let NewPositionPlan { accounts, branch } = plan;
|
let NewPositionPlan { accounts, branch } = plan;
|
||||||
|
let affected_account_ids = accounts.writable_account_ids(fresh_lp)?;
|
||||||
let (account_ids, signing_requirements) = accounts.wallet_args(fresh_lp)?;
|
let (account_ids, signing_requirements) = accounts.wallet_args(fresh_lp)?;
|
||||||
let instruction = match branch {
|
let instruction = match branch {
|
||||||
QuoteBranch::Missing { amount_a, amount_b } => {
|
QuoteBranch::Missing { amount_a, amount_b } => {
|
||||||
@ -116,6 +117,10 @@ pub(super) fn plan(input: PlanRequest) -> Result<Value, String> {
|
|||||||
"status": "ready",
|
"status": "ready",
|
||||||
"programId": input.amm_program_id,
|
"programId": input.amm_program_id,
|
||||||
"accountIds": account_ids.into_iter().map(account_id_hex).collect::<Vec<_>>(),
|
"accountIds": account_ids.into_iter().map(account_id_hex).collect::<Vec<_>>(),
|
||||||
|
"affectedAccountIds": affected_account_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(account_id_hex)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
"signingRequirements": signing_requirements,
|
"signingRequirements": signing_requirements,
|
||||||
"instruction": instruction,
|
"instruction": instruction,
|
||||||
"deadlineMs": deadline.to_string(),
|
"deadlineMs": deadline.to_string(),
|
||||||
|
|||||||
@ -193,6 +193,23 @@ impl AccountPlan {
|
|||||||
}
|
}
|
||||||
Ok((account_ids, signing_requirements))
|
Ok((account_ids, signing_requirements))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn writable_account_ids(
|
||||||
|
&self,
|
||||||
|
fresh_lp: Option<AccountId>,
|
||||||
|
) -> Result<Vec<AccountId>, String> {
|
||||||
|
self.rows
|
||||||
|
.iter()
|
||||||
|
.filter(|row| row.action != "read")
|
||||||
|
.map(|row| match row.account_id {
|
||||||
|
Some(account_id) => Ok(account_id),
|
||||||
|
None if row.role == "user_holding_lp" => {
|
||||||
|
fresh_lp.ok_or_else(|| String::from("transaction plan has no LP holding"))
|
||||||
|
}
|
||||||
|
None => Err(String::from("transaction plan has an unresolved account")),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountPlanRow {
|
impl AccountPlanRow {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ use super::{
|
|||||||
commitment::{QuoteCommitment, RequestCommitment},
|
commitment::{QuoteCommitment, RequestCommitment},
|
||||||
context::fungible_definition,
|
context::fungible_definition,
|
||||||
funding::{funding_commitments, funding_issues, hash_quote},
|
funding::{funding_commitments, funding_issues, hash_quote},
|
||||||
holding::{decode_fungible_holding, select_holding, wallet_holdings},
|
holding::{decode_fungible_holding, holding_options, select_holding, wallet_holdings},
|
||||||
pair::{derive_pair, is_canonical_pair, PairIds},
|
pair::{derive_pair, is_canonical_pair, PairIds},
|
||||||
position::{
|
position::{
|
||||||
AccountPlan, AccountPlanHoldings, EvaluatedQuote, NewPositionPlan, QuoteBranch,
|
AccountPlan, AccountPlanHoldings, EvaluatedQuote, NewPositionPlan, QuoteBranch,
|
||||||
@ -213,9 +213,18 @@ fn compute_missing_quote(
|
|||||||
let expected_lp = initial_lp - MINIMUM_LIQUIDITY;
|
let expected_lp = initial_lp - MINIMUM_LIQUIDITY;
|
||||||
|
|
||||||
let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
|
let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
|
||||||
let holding_a = select_holding(&holdings, pair.token_a);
|
let holding_a = select_holding(
|
||||||
let holding_b = select_holding(&holdings, pair.token_b);
|
&holdings,
|
||||||
let funding = funding_issues(
|
pair.token_a,
|
||||||
|
input.request.holding_a_id.as_deref(),
|
||||||
|
);
|
||||||
|
let holding_b = select_holding(
|
||||||
|
&holdings,
|
||||||
|
pair.token_b,
|
||||||
|
input.request.holding_b_id.as_deref(),
|
||||||
|
);
|
||||||
|
let mut funding = holding_selection_issues(input, pair, &holdings, &holding_a, &holding_b);
|
||||||
|
funding.extend(funding_issues(
|
||||||
input.snapshot.wallet_available,
|
input.snapshot.wallet_available,
|
||||||
pair,
|
pair,
|
||||||
&holding_a,
|
&holding_a,
|
||||||
@ -223,7 +232,7 @@ fn compute_missing_quote(
|
|||||||
&holding_b,
|
&holding_b,
|
||||||
amount_b,
|
amount_b,
|
||||||
["amountARaw", "amountBRaw"],
|
["amountARaw", "amountBRaw"],
|
||||||
);
|
));
|
||||||
let can_submit = funding.is_empty();
|
let can_submit = funding.is_empty();
|
||||||
let mut account_plan = missing_account_plan(
|
let mut account_plan = missing_account_plan(
|
||||||
input,
|
input,
|
||||||
@ -440,11 +449,31 @@ fn compute_active_quote(
|
|||||||
return Ok(error);
|
return Ok(error);
|
||||||
}
|
}
|
||||||
let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
|
let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
|
||||||
let holding_a = select_holding(&holdings, pair.token_a);
|
let holding_a = select_holding(
|
||||||
let holding_b = select_holding(&holdings, pair.token_b);
|
&holdings,
|
||||||
let lp_holding = select_holding(&holdings, pair.lp_definition);
|
pair.token_a,
|
||||||
let requires_fresh_lp = lp_holding.is_none();
|
input.request.holding_a_id.as_deref(),
|
||||||
let funding = funding_issues(
|
);
|
||||||
|
let holding_b = select_holding(
|
||||||
|
&holdings,
|
||||||
|
pair.token_b,
|
||||||
|
input.request.holding_b_id.as_deref(),
|
||||||
|
);
|
||||||
|
let lp_options = holding_options(&holdings, pair.lp_definition);
|
||||||
|
let lp_holding = if input.request.create_fresh_lp {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
select_holding(
|
||||||
|
&holdings,
|
||||||
|
pair.lp_definition,
|
||||||
|
input.request.lp_holding_id.as_deref(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let lp_destination_selected =
|
||||||
|
input.request.create_fresh_lp || lp_holding.is_some() || lp_options.is_empty();
|
||||||
|
let requires_fresh_lp = input.request.create_fresh_lp || lp_options.is_empty();
|
||||||
|
let mut funding = holding_selection_issues(input, pair, &holdings, &holding_a, &holding_b);
|
||||||
|
funding.extend(funding_issues(
|
||||||
input.snapshot.wallet_available,
|
input.snapshot.wallet_available,
|
||||||
pair,
|
pair,
|
||||||
&holding_a,
|
&holding_a,
|
||||||
@ -452,7 +481,15 @@ fn compute_active_quote(
|
|||||||
&holding_b,
|
&holding_b,
|
||||||
actual_b,
|
actual_b,
|
||||||
["maxAmountARaw", "maxAmountBRaw"],
|
["maxAmountARaw", "maxAmountBRaw"],
|
||||||
);
|
));
|
||||||
|
if !lp_destination_selected {
|
||||||
|
funding.push(issue(
|
||||||
|
"lp_destination_required",
|
||||||
|
"Select an LP token destination.",
|
||||||
|
&["lpHoldingId", "createFreshLp"],
|
||||||
|
json!({ "available": lp_options.len() }),
|
||||||
|
));
|
||||||
|
}
|
||||||
let can_submit = funding.is_empty();
|
let can_submit = funding.is_empty();
|
||||||
let warnings = if slippage_bps >= HIGH_SLIPPAGE_BPS {
|
let warnings = if slippage_bps >= HIGH_SLIPPAGE_BPS {
|
||||||
vec![issue(
|
vec![issue(
|
||||||
@ -532,6 +569,12 @@ fn compute_active_quote(
|
|||||||
"minimumLpRaw": minimum_lp.to_string(),
|
"minimumLpRaw": minimum_lp.to_string(),
|
||||||
"initialPriceRealRaw": spot_price_q64_64(reserve_a, reserve_b).to_string(),
|
"initialPriceRealRaw": spot_price_q64_64(reserve_a, reserve_b).to_string(),
|
||||||
"requiresFreshLp": requires_fresh_lp,
|
"requiresFreshLp": requires_fresh_lp,
|
||||||
|
"lpDestinationRequired": !lp_destination_selected,
|
||||||
|
"lpHoldingOptions": lp_options.iter().map(|holding| json!({
|
||||||
|
"holdingId": holding.id.to_string(),
|
||||||
|
"balanceRaw": holding.balance.to_string(),
|
||||||
|
})).collect::<Vec<_>>(),
|
||||||
|
"selectedLpHoldingId": lp_holding.as_ref().map(|holding| holding.id.to_string()),
|
||||||
"accountPreview": preview,
|
"accountPreview": preview,
|
||||||
"errors": funding,
|
"errors": funding,
|
||||||
"warnings": warnings,
|
"warnings": warnings,
|
||||||
@ -556,6 +599,52 @@ fn compute_active_quote(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn holding_selection_issues(
|
||||||
|
input: &QuoteRequest,
|
||||||
|
pair: PairIds,
|
||||||
|
holdings: &[super::holding::SelectedHolding],
|
||||||
|
holding_a: &Option<super::holding::SelectedHolding>,
|
||||||
|
holding_b: &Option<super::holding::SelectedHolding>,
|
||||||
|
) -> Vec<Value> {
|
||||||
|
if !input.snapshot.wallet_available {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for (definition_id, requested, selected, field) in [
|
||||||
|
(
|
||||||
|
pair.token_a,
|
||||||
|
input.request.holding_a_id.as_deref(),
|
||||||
|
holding_a,
|
||||||
|
"holdingAId",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
pair.token_b,
|
||||||
|
input.request.holding_b_id.as_deref(),
|
||||||
|
holding_b,
|
||||||
|
"holdingBId",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let options = holding_options(holdings, definition_id);
|
||||||
|
if !options.is_empty() && selected.is_none() {
|
||||||
|
errors.push(issue(
|
||||||
|
if requested.is_some() {
|
||||||
|
"invalid_holding_selection"
|
||||||
|
} else {
|
||||||
|
"holding_selection_required"
|
||||||
|
},
|
||||||
|
"Select a wallet holding for this token.",
|
||||||
|
&[field],
|
||||||
|
json!({
|
||||||
|
"tokenId": definition_id.to_string(),
|
||||||
|
"available": options.len(),
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_active_accounts(
|
fn validate_active_accounts(
|
||||||
input: &QuoteRequest,
|
input: &QuoteRequest,
|
||||||
pair: PairIds,
|
pair: PairIds,
|
||||||
|
|||||||
@ -2,6 +2,13 @@ use serde::Deserialize;
|
|||||||
|
|
||||||
use crate::account::AccountRead;
|
use crate::account::AccountRead;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NormalizeAccountRpcRequest {
|
||||||
|
pub account_id: String,
|
||||||
|
pub response: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ConfigIdRequest {
|
pub struct ConfigIdRequest {
|
||||||
@ -60,6 +67,14 @@ pub struct PositionRequest {
|
|||||||
pub token_b_id: String,
|
pub token_b_id: String,
|
||||||
pub fee_bps: u32,
|
pub fee_bps: u32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub holding_a_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub holding_b_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub lp_holding_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub create_fresh_lp: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub amount_a_raw: Option<String>,
|
pub amount_a_raw: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub amount_b_raw: Option<String>,
|
pub amount_b_raw: Option<String>,
|
||||||
|
|||||||
@ -144,6 +144,10 @@ fn request(pair: PairIds) -> PositionRequest {
|
|||||||
token_a_id: pair.token_a.to_string(),
|
token_a_id: pair.token_a.to_string(),
|
||||||
token_b_id: pair.token_b.to_string(),
|
token_b_id: pair.token_b.to_string(),
|
||||||
fee_bps: 30,
|
fee_bps: 30,
|
||||||
|
holding_a_id: Some(AccountId::new([61; 32]).to_string()),
|
||||||
|
holding_b_id: Some(AccountId::new([62; 32]).to_string()),
|
||||||
|
lp_holding_id: None,
|
||||||
|
create_fresh_lp: false,
|
||||||
amount_a_raw: None,
|
amount_a_raw: None,
|
||||||
amount_b_raw: None,
|
amount_b_raw: None,
|
||||||
max_amount_a_raw: None,
|
max_amount_a_raw: None,
|
||||||
@ -244,8 +248,16 @@ fn account_plan_sources_follow_pool_branch() {
|
|||||||
let pair = scenario.pair;
|
let pair = scenario.pair;
|
||||||
let input = scenario.quote_request();
|
let input = scenario.quote_request();
|
||||||
let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
|
let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
|
||||||
let holding_a = select_holding(&holdings, pair.token_a);
|
let holding_a = select_holding(
|
||||||
let holding_b = select_holding(&holdings, pair.token_b);
|
&holdings,
|
||||||
|
pair.token_a,
|
||||||
|
input.request.holding_a_id.as_deref(),
|
||||||
|
);
|
||||||
|
let holding_b = select_holding(
|
||||||
|
&holdings,
|
||||||
|
pair.token_b,
|
||||||
|
input.request.holding_b_id.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
let missing = missing_account_plan(
|
let missing = missing_account_plan(
|
||||||
&input,
|
&input,
|
||||||
@ -337,7 +349,7 @@ fn minimum_pair_is_minimal_on_price_base_side() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn highest_balance_holding_wins_then_lowest_id() {
|
fn holding_selection_uses_requested_id() {
|
||||||
let definition = AccountId::new([9; 32]);
|
let definition = AccountId::new([9; 32]);
|
||||||
let holding = |id: u8, balance| SelectedHolding {
|
let holding = |id: u8, balance| SelectedHolding {
|
||||||
id: AccountId::new([id; 32]),
|
id: AccountId::new([id; 32]),
|
||||||
@ -354,9 +366,10 @@ fn highest_balance_holding_wins_then_lowest_id() {
|
|||||||
let selected = select_holding(
|
let selected = select_holding(
|
||||||
&[holding(4, 10), holding(2, 20), holding(1, 20)],
|
&[holding(4, 10), holding(2, 20), holding(1, 20)],
|
||||||
definition,
|
definition,
|
||||||
|
Some(&AccountId::new([4; 32]).to_string()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(selected.id, AccountId::new([1; 32]));
|
assert_eq!(selected.id, AccountId::new([4; 32]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -511,6 +524,43 @@ fn context_selects_tokens_without_holdings() {
|
|||||||
assert!(value["tokens"][0].get("holdingId").is_none());
|
assert!(value["tokens"][0].get("holdingId").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_lists_all_holdings_without_preselecting_one() {
|
||||||
|
let token_id = AccountId::new([3; 32]);
|
||||||
|
let first = AccountId::new([4; 32]);
|
||||||
|
let second = AccountId::new([5; 32]);
|
||||||
|
let config_id = compute_config_pda(AMM_PROGRAM);
|
||||||
|
let value = context(ContextRequest {
|
||||||
|
network_id: String::from("testnet"),
|
||||||
|
network_fingerprint: String::from("block10:abc"),
|
||||||
|
amm_program_id: amm_program_id(),
|
||||||
|
wallet_available: true,
|
||||||
|
config: account_read(config_id, &config_account()),
|
||||||
|
wallet_accounts: vec![
|
||||||
|
account_read(second, &token_holding(token_id, 20)),
|
||||||
|
account_read(first, &token_holding(token_id, 10)),
|
||||||
|
],
|
||||||
|
token_definitions: vec![account_read(
|
||||||
|
token_id,
|
||||||
|
&token_definition("Token", 1_000_000),
|
||||||
|
)],
|
||||||
|
configured_token_ids: vec![account_id_hex(token_id)],
|
||||||
|
recent_token_ids: Vec::new(),
|
||||||
|
resolved_token_ids: Vec::new(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(value["tokens"][0].get("holdingId").is_none());
|
||||||
|
assert_eq!(value["tokens"][0]["balanceRaw"], "30");
|
||||||
|
assert_eq!(
|
||||||
|
value["tokens"][0]["holdings"],
|
||||||
|
json!([
|
||||||
|
{ "holdingId": first.to_string(), "balanceRaw": "10" },
|
||||||
|
{ "holdingId": second.to_string(), "balanceRaw": "20" },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_pool_snapshot_defaults_remain_real_accounts() {
|
fn missing_pool_snapshot_defaults_remain_real_accounts() {
|
||||||
let id = AccountId::new([5; 32]);
|
let id = AccountId::new([5; 32]);
|
||||||
@ -529,6 +579,8 @@ fn missing_pool_quote_and_plan_use_current_account_order() {
|
|||||||
assert_eq!(quote_value["canSubmit"], true);
|
assert_eq!(quote_value["canSubmit"], true);
|
||||||
assert_eq!(quote_value["accountPreview"].as_array().unwrap().len(), 11);
|
assert_eq!(quote_value["accountPreview"].as_array().unwrap().len(), 11);
|
||||||
let quote_hash = quote_value["quoteHash"].as_str().unwrap().to_owned();
|
let quote_hash = quote_value["quoteHash"].as_str().unwrap().to_owned();
|
||||||
|
let config_id = scenario.pair.config;
|
||||||
|
let clock_id = scenario.pair.clock;
|
||||||
|
|
||||||
let fresh_lp = AccountId::new([63; 32]);
|
let fresh_lp = AccountId::new([63; 32]);
|
||||||
let plan_value = scenario.plan(quote_hash, Some(default_read(fresh_lp)));
|
let plan_value = scenario.plan(quote_hash, Some(default_read(fresh_lp)));
|
||||||
@ -538,6 +590,10 @@ fn missing_pool_quote_and_plan_use_current_account_order() {
|
|||||||
assert_eq!(plan_value["signingRequirements"][6], true);
|
assert_eq!(plan_value["signingRequirements"][6], true);
|
||||||
assert_eq!(plan_value["signingRequirements"][7], true);
|
assert_eq!(plan_value["signingRequirements"][7], true);
|
||||||
assert_eq!(plan_value["signingRequirements"][8], true);
|
assert_eq!(plan_value["signingRequirements"][8], true);
|
||||||
|
let affected = plan_value["affectedAccountIds"].as_array().unwrap();
|
||||||
|
assert_eq!(affected.len(), 9);
|
||||||
|
assert!(!affected.contains(&json!(account_id_hex(config_id))));
|
||||||
|
assert!(!affected.contains(&json!(account_id_hex(clock_id))));
|
||||||
assert_preview_matches_plan("e_value, &plan_value, Some(fresh_lp));
|
assert_preview_matches_plan("e_value, &plan_value, Some(fresh_lp));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -661,6 +717,15 @@ fn active_pool_quote_uses_ratio_and_existing_lp_holding() {
|
|||||||
scenario.request.max_amount_b_raw = Some(String::from("3000"));
|
scenario.request.max_amount_b_raw = Some(String::from("3000"));
|
||||||
scenario.request.slippage_bps = Some(50);
|
scenario.request.slippage_bps = Some(50);
|
||||||
|
|
||||||
|
let awaiting_destination = scenario.quote();
|
||||||
|
assert_eq!(awaiting_destination["lpDestinationRequired"], true);
|
||||||
|
assert_eq!(awaiting_destination["canSubmit"], false);
|
||||||
|
assert_eq!(
|
||||||
|
awaiting_destination["errors"][0]["code"],
|
||||||
|
"lp_destination_required"
|
||||||
|
);
|
||||||
|
scenario.request.lp_holding_id = Some(lp_holding.to_string());
|
||||||
|
|
||||||
let quote_value = scenario.quote();
|
let quote_value = scenario.quote();
|
||||||
assert_eq!(quote_value["poolStatus"], "active_pool");
|
assert_eq!(quote_value["poolStatus"], "active_pool");
|
||||||
assert_eq!(quote_value["actualAmountARaw"], "1000");
|
assert_eq!(quote_value["actualAmountARaw"], "1000");
|
||||||
|
|||||||
@ -6,8 +6,8 @@ use std::{
|
|||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
use crate::api::{
|
use crate::api::{
|
||||||
self, AmmApiError, AmmResult, ConfigIdRequest, ContextRequest, PairIdsRequest, PlanRequest,
|
self, AmmApiError, AmmResult, ConfigIdRequest, ContextRequest, NormalizeAccountRpcRequest,
|
||||||
QuoteRequest, TokenIdsRequest,
|
PairIdsRequest, PlanRequest, QuoteRequest, TokenIdsRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -106,6 +106,11 @@ pub extern "C" fn amm_plan(request_json: *const c_char) -> *mut c_char {
|
|||||||
call::<PlanRequest>(request_json, api::plan)
|
call::<PlanRequest>(request_json, api::plan)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn amm_normalize_account_rpc(request_json: *const c_char) -> *mut c_char {
|
||||||
|
call::<NormalizeAccountRpcRequest>(request_json, api::normalize_account_rpc)
|
||||||
|
}
|
||||||
|
|
||||||
/// Releases a string returned by an `amm_*` operation.
|
/// Releases a string returned by an `amm_*` operation.
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
|
|||||||
@ -9,5 +9,5 @@ fn direct_rust_api_does_not_require_ffi() {
|
|||||||
|
|
||||||
assert_eq!(response["status"], "ok");
|
assert_eq!(response["status"], "ok");
|
||||||
assert!(response["configId"].is_string());
|
assert!(response["configId"].is_string());
|
||||||
assert_eq!(NEW_POSITION_SCHEMA, "new-position.v1");
|
assert_eq!(NEW_POSITION_SCHEMA, "new-position.v2");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@ -6,6 +9,8 @@ ColumnLayout {
|
|||||||
|
|
||||||
property var snapshot: ({})
|
property var snapshot: ({})
|
||||||
|
|
||||||
|
signal snapshotEdited(var snapshot)
|
||||||
|
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
function actionText(instruction) {
|
function actionText(instruction) {
|
||||||
@ -22,6 +27,40 @@ ColumnLayout {
|
|||||||
value: root.snapshot.pairText || "-"
|
value: root.snapshot.pairText || "-"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 5
|
||||||
|
visible: root.snapshot.instruction === "AddLiquidity"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("LP destination")
|
||||||
|
color: "#a1a1aa"
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: lpDestinationPicker
|
||||||
|
|
||||||
|
objectName: "lpDestinationPicker"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
model: root.destinationRows()
|
||||||
|
enabled: model.length > 1
|
||||||
|
currentIndex: root.destinationIndex()
|
||||||
|
displayText: currentIndex >= 0
|
||||||
|
? model[currentIndex].label : qsTr("Select destination")
|
||||||
|
Accessible.name: qsTr("LP token destination")
|
||||||
|
onActivated: function(index) {
|
||||||
|
root.selectDestination(model[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
required property var modelData
|
||||||
|
width: lpDestinationPicker.width
|
||||||
|
text: modelData.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SummaryRow {
|
SummaryRow {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
label: qsTr("Action")
|
label: qsTr("Action")
|
||||||
@ -48,4 +87,55 @@ ColumnLayout {
|
|||||||
label: qsTr("Expected LP")
|
label: qsTr("Expected LP")
|
||||||
value: root.snapshot.expectedLpText || "-"
|
value: root.snapshot.expectedLpText || "-"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function destinationRows() {
|
||||||
|
var rows = []
|
||||||
|
var options = root.snapshot.lpHoldingOptions || []
|
||||||
|
for (var i = 0; i < options.length; ++i) {
|
||||||
|
var id = String(options[i].holdingId || "")
|
||||||
|
rows.push({
|
||||||
|
"holdingId": id,
|
||||||
|
"createFresh": false,
|
||||||
|
"label": qsTr("%1 · balance %2")
|
||||||
|
.arg(root.shortId(id))
|
||||||
|
.arg(String(options[i].balanceRaw || "0"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rows.push({
|
||||||
|
"holdingId": "",
|
||||||
|
"createFresh": true,
|
||||||
|
"label": qsTr("Create new LP holding")
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function destinationIndex() {
|
||||||
|
var rows = root.destinationRows()
|
||||||
|
for (var i = 0; i < rows.length; ++i) {
|
||||||
|
if (rows[i].createFresh === (root.snapshot.createFreshLp === true)
|
||||||
|
&& (rows[i].createFresh
|
||||||
|
|| rows[i].holdingId === root.snapshot.selectedLpHoldingId)) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDestination(destination) {
|
||||||
|
var next = JSON.parse(JSON.stringify(root.snapshot || ({})))
|
||||||
|
next.request = next.request || ({})
|
||||||
|
delete next.request.lpHoldingId
|
||||||
|
next.request.createFreshLp = destination.createFresh === true
|
||||||
|
if (!destination.createFresh)
|
||||||
|
next.request.lpHoldingId = destination.holdingId
|
||||||
|
next.selectedLpHoldingId = destination.holdingId
|
||||||
|
next.createFreshLp = destination.createFresh === true
|
||||||
|
next.lpDestinationRequired = false
|
||||||
|
next.quoteReady = false
|
||||||
|
root.snapshotEdited(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortId(value) {
|
||||||
|
return value.length > 14 ? value.slice(0, 7) + "…" + value.slice(-5) : value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,8 @@ AmmActionCard {
|
|||||||
property var flowState: ({})
|
property var flowState: ({})
|
||||||
property string selectedTokenAId: ""
|
property string selectedTokenAId: ""
|
||||||
property string selectedTokenBId: ""
|
property string selectedTokenBId: ""
|
||||||
|
property string selectedHoldingAId: ""
|
||||||
|
property string selectedHoldingBId: ""
|
||||||
property int selectedFeeBps: 30
|
property int selectedFeeBps: 30
|
||||||
property int slippageBps: 50
|
property int slippageBps: 50
|
||||||
property string amountA: ""
|
property string amountA: ""
|
||||||
@ -47,6 +49,9 @@ AmmActionCard {
|
|||||||
readonly property bool contextLoading: root.flowState.contextLoading === true
|
readonly property bool contextLoading: root.flowState.contextLoading === true
|
||||||
readonly property bool quoteLoading: root.flowState.quoteLoading === true
|
readonly property bool quoteLoading: root.flowState.quoteLoading === true
|
||||||
readonly property bool submitting: root.flowState.submitting === true
|
readonly property bool submitting: root.flowState.submitting === true
|
||||||
|
readonly property bool walletCanSubmit: root.flowState.walletCanSubmit === true
|
||||||
|
readonly property string walletSyncStatus: String(
|
||||||
|
root.flowState.walletSyncStatus || "closed")
|
||||||
readonly property bool quoteStale: root.flowState.quoteStale === true
|
readonly property bool quoteStale: root.flowState.quoteStale === true
|
||||||
readonly property bool poolCreationPending: root.flowState.poolCreationPending === true
|
readonly property bool poolCreationPending: root.flowState.poolCreationPending === true
|
||||||
readonly property string submitError: root.flowState.errorCode
|
readonly property string submitError: root.flowState.errorCode
|
||||||
@ -65,6 +70,12 @@ AmmActionCard {
|
|||||||
? root.newPositionContext.feeTiers : []
|
? root.newPositionContext.feeTiers : []
|
||||||
readonly property var tokenA: root.tokenById(root.selectedTokenAId)
|
readonly property var tokenA: root.tokenById(root.selectedTokenAId)
|
||||||
readonly property var tokenB: root.tokenById(root.selectedTokenBId)
|
readonly property var tokenB: root.tokenById(root.selectedTokenBId)
|
||||||
|
readonly property var holdingsA: root.tokenHoldings(root.tokenA)
|
||||||
|
readonly property var holdingsB: root.tokenHoldings(root.tokenB)
|
||||||
|
readonly property var holdingA: root.holdingById(root.holdingsA,
|
||||||
|
root.selectedHoldingAId)
|
||||||
|
readonly property var holdingB: root.holdingById(root.holdingsB,
|
||||||
|
root.selectedHoldingBId)
|
||||||
readonly property int decimalsA: 0
|
readonly property int decimalsA: 0
|
||||||
readonly property int decimalsB: 0
|
readonly property int decimalsB: 0
|
||||||
readonly property bool displayIsCanonical: root.selectedTokenAId.length > 0
|
readonly property bool displayIsCanonical: root.selectedTokenAId.length > 0
|
||||||
@ -87,9 +98,13 @@ AmmActionCard {
|
|||||||
&& root.selectedTokenBId.length > 0
|
&& root.selectedTokenBId.length > 0
|
||||||
&& root.selectedTokenAId !== root.selectedTokenBId
|
&& root.selectedTokenAId !== root.selectedTokenBId
|
||||||
readonly property bool resolvingToken: root.resolvingTokenId.length > 0
|
readonly property bool resolvingToken: root.resolvingTokenId.length > 0
|
||||||
readonly property bool canConfirm: root.quotePayload.schema === "new-position.v1"
|
readonly property bool lpDestinationRequired: root.quotePayload.status === "ok"
|
||||||
|
&& root.quotePayload.lpDestinationRequired === true
|
||||||
|
&& root.onlyLpDestinationBlocks()
|
||||||
|
readonly property bool canConfirm: root.quotePayload.schema === "new-position.v2"
|
||||||
&& root.quotePayload.status === "ok"
|
&& root.quotePayload.status === "ok"
|
||||||
&& root.quotePayload.canSubmit === true
|
&& (root.quotePayload.canSubmit === true
|
||||||
|
|| root.lpDestinationRequired)
|
||||||
&& root.quoteMatchesPair()
|
&& root.quoteMatchesPair()
|
||||||
&& String(root.quotePayload.quoteHash || "").length > 0
|
&& String(root.quotePayload.quoteHash || "").length > 0
|
||||||
&& !root.contextLoading
|
&& !root.contextLoading
|
||||||
@ -97,6 +112,7 @@ AmmActionCard {
|
|||||||
&& !root.quoteStale
|
&& !root.quoteStale
|
||||||
&& !root.submitting
|
&& !root.submitting
|
||||||
&& !root.poolCreationPending
|
&& !root.poolCreationPending
|
||||||
|
&& root.walletCanSubmit
|
||||||
|
|
||||||
signal quoteRequested(bool immediate, var quoteRequest)
|
signal quoteRequested(bool immediate, var quoteRequest)
|
||||||
signal confirmationRequested(var snapshot)
|
signal confirmationRequested(var snapshot)
|
||||||
@ -116,6 +132,7 @@ AmmActionCard {
|
|||||||
root.finishTokenResolution()
|
root.finishTokenResolution()
|
||||||
else
|
else
|
||||||
root.reconcileSelection()
|
root.reconcileSelection()
|
||||||
|
root.reconcileHoldings()
|
||||||
}
|
}
|
||||||
onQuotePayloadChanged: {
|
onQuotePayloadChanged: {
|
||||||
if (root.quoteStale)
|
if (root.quoteStale)
|
||||||
@ -236,7 +253,8 @@ AmmActionCard {
|
|||||||
theme: root.theme
|
theme: root.theme
|
||||||
text: root.amountA
|
text: root.amountA
|
||||||
label: qsTr("Token A amount")
|
label: qsTr("Token A amount")
|
||||||
balance: root.contextLoading ? "" : root.balanceText(root.tokenA, root.decimalsA)
|
balance: root.contextLoading ? "" : root.holdingBalanceText(root.holdingA,
|
||||||
|
root.decimalsA)
|
||||||
helperText: root.missingPool && !root.compact
|
helperText: root.missingPool && !root.compact
|
||||||
? root.minimumAmountText("A") : ""
|
? root.minimumAmountText("A") : ""
|
||||||
errorText: root.formErrorText()
|
errorText: root.formErrorText()
|
||||||
@ -246,6 +264,9 @@ AmmActionCard {
|
|||||||
tokenData: root.tokenA.definitionId ? root.tokenA : null
|
tokenData: root.tokenA.definitionId ? root.tokenA : null
|
||||||
tokens: root.tokens
|
tokens: root.tokens
|
||||||
selectedTokenId: root.selectedTokenAId
|
selectedTokenId: root.selectedTokenAId
|
||||||
|
holdings: root.holdingsA
|
||||||
|
selectedHoldingId: root.selectedHoldingAId
|
||||||
|
holdingSelectionEnabled: !root.submitting
|
||||||
tokenInvalid: root.tokenHasError("A")
|
tokenInvalid: root.tokenHasError("A")
|
||||||
tokenSelectionEnabled: !root.contextLoading && !root.submitting
|
tokenSelectionEnabled: !root.contextLoading && !root.submitting
|
||||||
adjustment: root.missingPool ? priceAmountAAdjustment : null
|
adjustment: root.missingPool ? priceAmountAAdjustment : null
|
||||||
@ -268,6 +289,9 @@ AmmActionCard {
|
|||||||
onMaxClicked: root.useMaximum()
|
onMaxClicked: root.useMaximum()
|
||||||
onTokenSelected: function(tokenId) { root.resolveToken("A", tokenId) }
|
onTokenSelected: function(tokenId) { root.resolveToken("A", tokenId) }
|
||||||
onTokenEntered: function(value) { root.resolveToken("A", value) }
|
onTokenEntered: function(value) { root.resolveToken("A", value) }
|
||||||
|
onHoldingSelected: function(holdingId) {
|
||||||
|
root.selectHolding("A", holdingId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AmmPairSeparator {
|
AmmPairSeparator {
|
||||||
@ -285,7 +309,8 @@ AmmActionCard {
|
|||||||
theme: root.theme
|
theme: root.theme
|
||||||
text: root.amountB
|
text: root.amountB
|
||||||
label: qsTr("Token B amount")
|
label: qsTr("Token B amount")
|
||||||
balance: root.contextLoading ? "" : root.balanceText(root.tokenB, root.decimalsB)
|
balance: root.contextLoading ? "" : root.holdingBalanceText(root.holdingB,
|
||||||
|
root.decimalsB)
|
||||||
helperText: root.missingPool && !root.compact
|
helperText: root.missingPool && !root.compact
|
||||||
? root.minimumAmountText("B") : ""
|
? root.minimumAmountText("B") : ""
|
||||||
invalid: root.fieldHasError("amountB")
|
invalid: root.fieldHasError("amountB")
|
||||||
@ -294,6 +319,9 @@ AmmActionCard {
|
|||||||
tokenData: root.tokenB.definitionId ? root.tokenB : null
|
tokenData: root.tokenB.definitionId ? root.tokenB : null
|
||||||
tokens: root.tokens
|
tokens: root.tokens
|
||||||
selectedTokenId: root.selectedTokenBId
|
selectedTokenId: root.selectedTokenBId
|
||||||
|
holdings: root.holdingsB
|
||||||
|
selectedHoldingId: root.selectedHoldingBId
|
||||||
|
holdingSelectionEnabled: !root.submitting
|
||||||
tokenInvalid: root.tokenHasError("B")
|
tokenInvalid: root.tokenHasError("B")
|
||||||
tokenSelectionEnabled: !root.contextLoading && !root.submitting
|
tokenSelectionEnabled: !root.contextLoading && !root.submitting
|
||||||
adjustment: root.missingPool ? priceAmountBAdjustment : null
|
adjustment: root.missingPool ? priceAmountBAdjustment : null
|
||||||
@ -316,6 +344,9 @@ AmmActionCard {
|
|||||||
onMaxClicked: root.useMaximum()
|
onMaxClicked: root.useMaximum()
|
||||||
onTokenSelected: function(tokenId) { root.resolveToken("B", tokenId) }
|
onTokenSelected: function(tokenId) { root.resolveToken("B", tokenId) }
|
||||||
onTokenEntered: function(value) { root.resolveToken("B", value) }
|
onTokenEntered: function(value) { root.resolveToken("B", value) }
|
||||||
|
onHoldingSelected: function(holdingId) {
|
||||||
|
root.selectHolding("B", holdingId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -601,6 +632,10 @@ AmmActionCard {
|
|||||||
text: root.submitting
|
text: root.submitting
|
||||||
? qsTr("Submitting…")
|
? qsTr("Submitting…")
|
||||||
: root.contextLoading ? qsTr("Loading…")
|
: root.contextLoading ? qsTr("Loading…")
|
||||||
|
: !root.walletCanSubmit && root.walletSyncStatus === "syncing"
|
||||||
|
? qsTr("Syncing wallet…")
|
||||||
|
: !root.walletCanSubmit && root.walletSyncStatus === "opening"
|
||||||
|
? qsTr("Opening wallet…")
|
||||||
: root.poolCreationPending ? qsTr("Waiting for pool")
|
: root.poolCreationPending ? qsTr("Waiting for pool")
|
||||||
: root.missingPool ? qsTr("Create pool") : qsTr("Add liquidity")
|
: root.missingPool ? qsTr("Create pool") : qsTr("Add liquidity")
|
||||||
enabled: root.canConfirm
|
enabled: root.canConfirm
|
||||||
@ -700,6 +735,40 @@ AmmActionCard {
|
|||||||
return root.emptyToken
|
return root.emptyToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tokenHoldings(token) {
|
||||||
|
return token && token.holdings ? token.holdings : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function holdingById(holdings, holdingId) {
|
||||||
|
for (var i = 0; i < holdings.length; ++i) {
|
||||||
|
if (String(holdings[i].holdingId || "") === holdingId)
|
||||||
|
return holdings[i]
|
||||||
|
}
|
||||||
|
return ({})
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileHoldings() {
|
||||||
|
root.selectedHoldingAId = root.reconciledHoldingId(
|
||||||
|
root.holdingsA, root.selectedHoldingAId)
|
||||||
|
root.selectedHoldingBId = root.reconciledHoldingId(
|
||||||
|
root.holdingsB, root.selectedHoldingBId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconciledHoldingId(holdings, selectedId) {
|
||||||
|
if (holdings.length === 1)
|
||||||
|
return String(holdings[0].holdingId || "")
|
||||||
|
return root.holdingById(holdings, selectedId).holdingId ? selectedId : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectHolding(side, holdingId) {
|
||||||
|
if (side === "A")
|
||||||
|
root.selectedHoldingAId = holdingId
|
||||||
|
else
|
||||||
|
root.selectedHoldingBId = holdingId
|
||||||
|
root.noteDraftChanged()
|
||||||
|
root.requestQuote(true)
|
||||||
|
}
|
||||||
|
|
||||||
function selectableTokenIds() {
|
function selectableTokenIds() {
|
||||||
var result = []
|
var result = []
|
||||||
for (var i = 0; i < root.tokens.length; ++i) {
|
for (var i = 0; i < root.tokens.length; ++i) {
|
||||||
@ -823,6 +892,9 @@ AmmActionCard {
|
|||||||
root.selectedTokenAId = root.selectedTokenBId
|
root.selectedTokenAId = root.selectedTokenBId
|
||||||
root.selectedTokenBId = tokenId
|
root.selectedTokenBId = tokenId
|
||||||
}
|
}
|
||||||
|
root.selectedHoldingAId = ""
|
||||||
|
root.selectedHoldingBId = ""
|
||||||
|
root.reconcileHoldings()
|
||||||
root.resetPairDraft()
|
root.resetPairDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -830,6 +902,9 @@ AmmActionCard {
|
|||||||
var tokenId = root.selectedTokenAId
|
var tokenId = root.selectedTokenAId
|
||||||
root.selectedTokenAId = root.selectedTokenBId
|
root.selectedTokenAId = root.selectedTokenBId
|
||||||
root.selectedTokenBId = tokenId
|
root.selectedTokenBId = tokenId
|
||||||
|
var holdingId = root.selectedHoldingAId
|
||||||
|
root.selectedHoldingAId = root.selectedHoldingBId
|
||||||
|
root.selectedHoldingBId = holdingId
|
||||||
var amount = root.amountA
|
var amount = root.amountA
|
||||||
root.amountA = root.amountB
|
root.amountA = root.amountB
|
||||||
root.amountB = amount
|
root.amountB = amount
|
||||||
@ -858,7 +933,7 @@ AmmActionCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function acceptPoolActivation(quote) {
|
function acceptPoolActivation(quote) {
|
||||||
if (!quote || quote.schema !== "new-position.v1"
|
if (!quote || quote.schema !== "new-position.v2"
|
||||||
|| quote.status !== "ok"
|
|| quote.status !== "ok"
|
||||||
|| quote.poolStatus !== "active_pool"
|
|| quote.poolStatus !== "active_pool"
|
||||||
|| !root.quoteMatchesSelectedPair(quote)) {
|
|| !root.quoteMatchesSelectedPair(quote)) {
|
||||||
@ -985,6 +1060,10 @@ AmmActionCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var request = root.pairRequest()
|
var request = root.pairRequest()
|
||||||
|
if (root.holdingsA.length > 0 && root.selectedHoldingAId.length === 0)
|
||||||
|
errors.push(root.localIssue("holding_selection_required", ["holdingAId"]))
|
||||||
|
if (root.holdingsB.length > 0 && root.selectedHoldingBId.length === 0)
|
||||||
|
errors.push(root.localIssue("holding_selection_required", ["holdingBId"]))
|
||||||
|
|
||||||
if (root.activePool) {
|
if (root.activePool) {
|
||||||
var parsedA = AmountMath.parseHuman(root.amountA, root.decimalsA)
|
var parsedA = AmountMath.parseHuman(root.amountA, root.decimalsA)
|
||||||
@ -1076,14 +1155,23 @@ AmmActionCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pairRequest() {
|
function pairRequest() {
|
||||||
return {
|
var request = {
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"tokenAId": root.displayIsCanonical
|
"tokenAId": root.displayIsCanonical
|
||||||
? root.selectedTokenAId : root.selectedTokenBId,
|
? root.selectedTokenAId : root.selectedTokenBId,
|
||||||
"tokenBId": root.displayIsCanonical
|
"tokenBId": root.displayIsCanonical
|
||||||
? root.selectedTokenBId : root.selectedTokenAId,
|
? root.selectedTokenBId : root.selectedTokenAId,
|
||||||
"feeBps": root.selectedFeeBps
|
"feeBps": root.selectedFeeBps
|
||||||
}
|
}
|
||||||
|
var holdingAId = root.displayIsCanonical
|
||||||
|
? root.selectedHoldingAId : root.selectedHoldingBId
|
||||||
|
var holdingBId = root.displayIsCanonical
|
||||||
|
? root.selectedHoldingBId : root.selectedHoldingAId
|
||||||
|
if (holdingAId.length > 0)
|
||||||
|
request.holdingAId = holdingAId
|
||||||
|
if (holdingBId.length > 0)
|
||||||
|
request.holdingBId = holdingBId
|
||||||
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestQuote(immediate) {
|
function requestQuote(immediate) {
|
||||||
@ -1095,7 +1183,11 @@ AmmActionCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function probeRaw(token, decimals) {
|
function probeRaw(token, decimals) {
|
||||||
var balance = String(token.balanceRaw || "0")
|
var holdings = root.tokenHoldings(token)
|
||||||
|
var selectedId = token.definitionId === root.tokenA.definitionId
|
||||||
|
? root.selectedHoldingAId : root.selectedHoldingBId
|
||||||
|
var selected = root.holdingById(holdings, selectedId)
|
||||||
|
var balance = String(selected.balanceRaw || "0")
|
||||||
var simulated = AmountMath.multiply(AmountMath.pow10(decimals), "1000")
|
var simulated = AmountMath.multiply(AmountMath.pow10(decimals), "1000")
|
||||||
if (AmountMath.isUnsigned(balance) && AmountMath.compare(balance, simulated) > 0)
|
if (AmountMath.isUnsigned(balance) && AmountMath.compare(balance, simulated) > 0)
|
||||||
return balance
|
return balance
|
||||||
@ -1195,6 +1287,9 @@ AmmActionCard {
|
|||||||
"invalid_amount_precision": qsTr("Token amounts must use whole raw units."),
|
"invalid_amount_precision": qsTr("Token amounts must use whole raw units."),
|
||||||
"invalid_raw_amount": qsTr("Value is outside the supported range."),
|
"invalid_raw_amount": qsTr("Value is outside the supported range."),
|
||||||
"amount_exceeds_balance": qsTr("Amount exceeds the selected holding balance."),
|
"amount_exceeds_balance": qsTr("Amount exceeds the selected holding balance."),
|
||||||
|
"holding_selection_required": qsTr("Select a wallet holding for this token."),
|
||||||
|
"invalid_holding_selection": qsTr("Selected wallet holding is unavailable."),
|
||||||
|
"lp_destination_required": qsTr("Select where LP tokens should be deposited."),
|
||||||
"amount_too_low": qsTr("Value is too low for this pool."),
|
"amount_too_low": qsTr("Value is too low for this pool."),
|
||||||
"invalid_token_id": qsTr("Enter a valid base58 TokenDefinition ID."),
|
"invalid_token_id": qsTr("Enter a valid base58 TokenDefinition ID."),
|
||||||
"deposit_ratio_mismatch": qsTr("Deposit amounts must match the initial price."),
|
"deposit_ratio_mismatch": qsTr("Deposit amounts must match the initial price."),
|
||||||
@ -1203,6 +1298,7 @@ AmmActionCard {
|
|||||||
"fee_tier_mismatch": qsTr("Select the existing pool fee tier."),
|
"fee_tier_mismatch": qsTr("Select the existing pool fee tier."),
|
||||||
"no_wallet": qsTr("Connect a wallet to submit this position."),
|
"no_wallet": qsTr("Connect a wallet to submit this position."),
|
||||||
"wallet_unavailable": qsTr("Wallet is unavailable."),
|
"wallet_unavailable": qsTr("Wallet is unavailable."),
|
||||||
|
"wallet_syncing": qsTr("Wallet is still syncing. Review the quote while it finishes."),
|
||||||
"wallet_submission_failed": qsTr("Wallet submission failed. Review and retry manually."),
|
"wallet_submission_failed": qsTr("Wallet submission failed. Review and retry manually."),
|
||||||
"signature_rejected": qsTr("Wallet approval was rejected."),
|
"signature_rejected": qsTr("Wallet approval was rejected."),
|
||||||
"quote_changed": qsTr("Pool or wallet state changed. Review the refreshed quote."),
|
"quote_changed": qsTr("Pool or wallet state changed. Review the refreshed quote."),
|
||||||
@ -1217,6 +1313,7 @@ AmmActionCard {
|
|||||||
"network_unknown": qsTr("Network identity could not be verified. Refresh and retry."),
|
"network_unknown": qsTr("Network identity could not be verified. Refresh and retry."),
|
||||||
"network_mismatch": qsTr("Connected wallet uses a different network."),
|
"network_mismatch": qsTr("Connected wallet uses a different network."),
|
||||||
"config_missing": qsTr("Network configuration is missing."),
|
"config_missing": qsTr("Network configuration is missing."),
|
||||||
|
"sequencer_config_required": qsTr("Sequencer endpoint is missing from wallet_config.json."),
|
||||||
"account_read_failed": qsTr("Required on-chain state could not be read."),
|
"account_read_failed": qsTr("Required on-chain state could not be read."),
|
||||||
"pool_unavailable": qsTr("Pool state is unavailable."),
|
"pool_unavailable": qsTr("Pool state is unavailable."),
|
||||||
"config_unavailable": qsTr("AMM configuration is unavailable."),
|
"config_unavailable": qsTr("AMM configuration is unavailable."),
|
||||||
@ -1264,8 +1361,8 @@ AmmActionCard {
|
|||||||
var reserveB = root.poolReserve("B")
|
var reserveB = root.poolReserve("B")
|
||||||
if (!reserveA || !reserveB || reserveA === "0" || reserveB === "0")
|
if (!reserveA || !reserveB || reserveA === "0" || reserveB === "0")
|
||||||
return
|
return
|
||||||
var balanceA = String(root.tokenA.balanceRaw || "0")
|
var balanceA = String(root.holdingA.balanceRaw || "0")
|
||||||
var balanceB = String(root.tokenB.balanceRaw || "0")
|
var balanceB = String(root.holdingB.balanceRaw || "0")
|
||||||
var fitA = AmountMath.mulDivFloor(balanceB, reserveA, reserveB)
|
var fitA = AmountMath.mulDivFloor(balanceB, reserveA, reserveB)
|
||||||
var rawA = AmountMath.compare(balanceA, fitA) < 0 ? balanceA : fitA
|
var rawA = AmountMath.compare(balanceA, fitA) < 0 ? balanceA : fitA
|
||||||
var rawB = AmountMath.mulDivFloor(rawA, reserveB, reserveA)
|
var rawB = AmountMath.mulDivFloor(rawA, reserveB, reserveA)
|
||||||
@ -1459,10 +1556,27 @@ AmmActionCard {
|
|||||||
"depositAText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A"),
|
"depositAText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A"),
|
||||||
"depositBText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "B"),
|
"depositBText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "B"),
|
||||||
"expectedLpText": root.rawLpText(root.quotePayload.expectedLpRaw),
|
"expectedLpText": root.rawLpText(root.quotePayload.expectedLpRaw),
|
||||||
"instruction": String(root.quotePayload.instruction || "")
|
"instruction": String(root.quotePayload.instruction || ""),
|
||||||
|
"lpHoldingOptions": root.quotePayload.lpHoldingOptions || [],
|
||||||
|
"selectedLpHoldingId": String(root.quotePayload.selectedLpHoldingId || ""),
|
||||||
|
"createFreshLp": root.quotePayload.requiresFreshLp === true
|
||||||
|
&& root.quotePayload.lpDestinationRequired !== true,
|
||||||
|
"lpDestinationRequired": root.quotePayload.lpDestinationRequired === true,
|
||||||
|
"quoteReady": root.quotePayload.canSubmit === true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onlyLpDestinationBlocks() {
|
||||||
|
var errors = root.quotePayload.errors || []
|
||||||
|
if (errors.length === 0)
|
||||||
|
return false
|
||||||
|
for (var i = 0; i < errors.length; ++i) {
|
||||||
|
if (errors[i].code !== "lp_destination_required")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function shortTokenName(token) {
|
function shortTokenName(token) {
|
||||||
if (token && token.name && token.name.length > 0)
|
if (token && token.name && token.name.length > 0)
|
||||||
return token.name
|
return token.name
|
||||||
@ -1473,6 +1587,12 @@ AmmActionCard {
|
|||||||
return AmountMath.formatRaw(String(token.balanceRaw || "0"), decimals)
|
return AmountMath.formatRaw(String(token.balanceRaw || "0"), decimals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function holdingBalanceText(holding, decimals) {
|
||||||
|
if (!holding || holding.balanceRaw === undefined)
|
||||||
|
return ""
|
||||||
|
return AmountMath.formatRaw(String(holding.balanceRaw || "0"), decimals)
|
||||||
|
}
|
||||||
|
|
||||||
function tokenBalanceDetail(token) {
|
function tokenBalanceDetail(token) {
|
||||||
return qsTr("Available %1").arg(root.balanceText(token, 0))
|
return qsTr("Available %1").arg(root.balanceText(token, 0))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
import "../shared"
|
import "../shared"
|
||||||
|
|
||||||
@ -16,6 +18,9 @@ AmmTokenAmountSurface {
|
|||||||
property string selectedTokenId: ""
|
property string selectedTokenId: ""
|
||||||
property bool tokenInvalid: false
|
property bool tokenInvalid: false
|
||||||
property bool tokenSelectionEnabled: true
|
property bool tokenSelectionEnabled: true
|
||||||
|
property var holdings: []
|
||||||
|
property string selectedHoldingId: ""
|
||||||
|
property bool holdingSelectionEnabled: true
|
||||||
property bool editPending: false
|
property bool editPending: false
|
||||||
property string pendingValue: ""
|
property string pendingValue: ""
|
||||||
property var disabledReasonForCode: function(code) {
|
property var disabledReasonForCode: function(code) {
|
||||||
@ -31,13 +36,15 @@ AmmTokenAmountSurface {
|
|||||||
signal maxClicked
|
signal maxClicked
|
||||||
signal tokenSelected(string tokenId)
|
signal tokenSelected(string tokenId)
|
||||||
signal tokenEntered(string value)
|
signal tokenEntered(string value)
|
||||||
|
signal holdingSelected(string holdingId)
|
||||||
|
|
||||||
amount: root.text
|
amount: root.text
|
||||||
supportingText: root.helperText
|
supportingText: root.helperText
|
||||||
supportingActionText: root.showMaxButton ? qsTr("MAX") : ""
|
supportingActionText: root.showMaxButton ? qsTr("MAX") : ""
|
||||||
accessory: tokenActions
|
accessory: tokenActions
|
||||||
accessoryWidth: width < 360 ? 132 : 180
|
accessoryWidth: width < 360 ? 132 : 180
|
||||||
accessoryHeight: root.balance.length > 0 ? 58 : 40
|
accessoryHeight: root.holdings.length > 1 ? 88
|
||||||
|
: root.balance.length > 0 ? 58 : 40
|
||||||
|
|
||||||
onAmountEdited: function(value) {
|
onAmountEdited: function(value) {
|
||||||
root.pendingValue = value
|
root.pendingValue = value
|
||||||
@ -68,17 +75,46 @@ AmmTokenAmountSurface {
|
|||||||
Component {
|
Component {
|
||||||
id: tokenActions
|
id: tokenActions
|
||||||
|
|
||||||
AmmTokenAccessory {
|
ColumnLayout {
|
||||||
theme: root.theme
|
spacing: 4
|
||||||
enabled: root.tokenSelectionEnabled
|
|
||||||
invalid: root.tokenInvalid
|
AmmTokenAccessory {
|
||||||
hasToken: root.tokenData !== null
|
Layout.fillWidth: true
|
||||||
tokenColor: root.tokenColor(root.tokenData)
|
theme: root.theme
|
||||||
tokenLetter: root.tokenLetter(root.tokenData)
|
enabled: root.tokenSelectionEnabled
|
||||||
tokenText: root.tokenText(root.tokenData)
|
invalid: root.tokenInvalid
|
||||||
balance: root.balance
|
hasToken: root.tokenData !== null
|
||||||
accessibleName: qsTr("Select %1").arg(root.label)
|
tokenColor: root.tokenColor(root.tokenData)
|
||||||
onClicked: tokenModal.open()
|
tokenLetter: root.tokenLetter(root.tokenData)
|
||||||
|
tokenText: root.tokenText(root.tokenData)
|
||||||
|
balance: root.balance
|
||||||
|
accessibleName: qsTr("Select %1").arg(root.label)
|
||||||
|
onClicked: tokenModal.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: holdingPicker
|
||||||
|
|
||||||
|
objectName: "holdingPicker"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: root.holdings.length > 1
|
||||||
|
enabled: root.holdingSelectionEnabled && root.holdings.length > 1
|
||||||
|
model: root.holdings
|
||||||
|
currentIndex: root.holdingIndex()
|
||||||
|
displayText: currentIndex >= 0
|
||||||
|
? root.holdingLabel(root.holdings[currentIndex])
|
||||||
|
: qsTr("Select holding")
|
||||||
|
Accessible.name: qsTr("Wallet holding for %1").arg(root.label)
|
||||||
|
onActivated: function(index) {
|
||||||
|
root.holdingSelected(String(root.holdings[index].holdingId || ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
required property var modelData
|
||||||
|
width: holdingPicker.width
|
||||||
|
text: root.holdingLabel(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,4 +168,20 @@ AmmTokenAmountSurface {
|
|||||||
var text = String(value || "")
|
var text = String(value || "")
|
||||||
return text.length > 14 ? text.slice(0, 7) + "..." + text.slice(-5) : text
|
return text.length > 14 ? text.slice(0, 7) + "..." + text.slice(-5) : text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function holdingIndex() {
|
||||||
|
for (var i = 0; i < root.holdings.length; ++i) {
|
||||||
|
if (String(root.holdings[i].holdingId || "") === root.selectedHoldingId)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function holdingLabel(holding) {
|
||||||
|
if (!holding)
|
||||||
|
return qsTr("Select holding")
|
||||||
|
return qsTr("%1 · %2")
|
||||||
|
.arg(root.shortId(String(holding.holdingId || "")))
|
||||||
|
.arg(String(holding.balanceRaw || "0"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -255,6 +255,10 @@ Item {
|
|||||||
function onQuoteRefreshRequested(immediate) {
|
function onQuoteRefreshRequested(immediate) {
|
||||||
form.requestQuote(immediate)
|
form.requestQuote(immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onConfirmationQuoteReady(snapshot) {
|
||||||
|
confirmationDialog.updateSnapshot(snapshot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
@ -269,8 +273,16 @@ Item {
|
|||||||
title: qsTr("Confirm new position")
|
title: qsTr("Confirm new position")
|
||||||
confirmText: qsTr("Submit")
|
confirmText: qsTr("Submit")
|
||||||
busy: newPositionFlow.submitting
|
busy: newPositionFlow.submitting
|
||||||
|
|| (opened && snapshot.quoteReady === false
|
||||||
|
&& newPositionFlow.quoteLoading)
|
||||||
|
confirmEnabled: snapshot.quoteReady === true
|
||||||
|
&& newPositionFlow.walletCanSubmit
|
||||||
summary: liquidityConfirmationSummary
|
summary: liquidityConfirmationSummary
|
||||||
|
|
||||||
|
onSummaryEdited: function(snapshot) {
|
||||||
|
newPositionFlow.requoteConfirmation(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
onConfirmed: function(snapshot) {
|
onConfirmed: function(snapshot) {
|
||||||
newPositionFlow.confirm(snapshot)
|
newPositionFlow.confirm(snapshot)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,19 +7,24 @@ QtObject {
|
|||||||
property var runtime: null
|
property var runtime: null
|
||||||
property bool active: false
|
property bool active: false
|
||||||
|
|
||||||
readonly property bool walletStateReady: root.backend !== null
|
readonly property bool backendReady: root.backend !== null
|
||||||
&& root.backend.walletStateReady === true
|
readonly property bool walletCanSubmit: root.backendReady
|
||||||
readonly property var newPositionContext: root.walletStateReady
|
&& root.backend.walletCanSubmit === true
|
||||||
|
readonly property var newPositionContext: root.backendReady
|
||||||
&& root.backend.newPositionContext
|
&& root.backend.newPositionContext
|
||||||
|
&& root.backend.newPositionContext.schema
|
||||||
? root.backend.newPositionContext
|
? root.backend.newPositionContext
|
||||||
: root.loadingContext()
|
: root.loadingContext()
|
||||||
readonly property var viewState: ({
|
readonly property var viewState: ({
|
||||||
"quote": root.newPositionQuote,
|
"quote": root.newPositionQuote,
|
||||||
"contextLoading": root.contextLoading || !root.walletStateReady
|
"contextLoading": root.contextLoading
|
||||||
|| root.newPositionContext.status === "loading",
|
|| root.newPositionContext.status === "loading",
|
||||||
"quoteLoading": root.quoteLoading,
|
"quoteLoading": root.quoteLoading,
|
||||||
"quoteStale": root.quoteStale,
|
"quoteStale": root.quoteStale,
|
||||||
"submitting": root.submitting,
|
"submitting": root.submitting,
|
||||||
|
"walletCanSubmit": root.walletCanSubmit,
|
||||||
|
"walletSyncStatus": root.backendReady
|
||||||
|
? String(root.backend.walletSyncStatus || "closed") : "closed",
|
||||||
"poolCreationPending": root.selectedPoolCreationPending(),
|
"poolCreationPending": root.selectedPoolCreationPending(),
|
||||||
"transactionId": root.transactionId,
|
"transactionId": root.transactionId,
|
||||||
"errorCode": root.flowErrorCode || root.contextErrorCode
|
"errorCode": root.flowErrorCode || root.contextErrorCode
|
||||||
@ -28,8 +33,13 @@ QtObject {
|
|||||||
|
|
||||||
property var newPositionQuote: ({})
|
property var newPositionQuote: ({})
|
||||||
property var resolvedTokenIds: []
|
property var resolvedTokenIds: []
|
||||||
property int contextSerial: 0
|
|
||||||
property int quoteSerial: 0
|
property int quoteSerial: 0
|
||||||
|
property int operationSerial: 0
|
||||||
|
property int activeQuoteRequestId: 0
|
||||||
|
property int submitRequestId: 0
|
||||||
|
property int poolProbeRequestId: 0
|
||||||
|
property var pendingSubmitSnapshot: ({})
|
||||||
|
property var pendingConfirmationSnapshot: null
|
||||||
property bool contextLoading: false
|
property bool contextLoading: false
|
||||||
property bool quoteLoading: false
|
property bool quoteLoading: false
|
||||||
property bool quoteStale: true
|
property bool quoteStale: true
|
||||||
@ -38,6 +48,7 @@ QtObject {
|
|||||||
property string flowErrorCode: ""
|
property string flowErrorCode: ""
|
||||||
property string contextErrorCode: ""
|
property string contextErrorCode: ""
|
||||||
property string quoteErrorCode: ""
|
property string quoteErrorCode: ""
|
||||||
|
property var pendingContextCompletion: null
|
||||||
property var pendingQuoteRequest: ({ "ok": false, "request": ({}) })
|
property var pendingQuoteRequest: ({ "ok": false, "request": ({}) })
|
||||||
property var pendingPoolProbes: []
|
property var pendingPoolProbes: []
|
||||||
property bool poolProbeInFlight: false
|
property bool poolProbeInFlight: false
|
||||||
@ -48,6 +59,7 @@ QtObject {
|
|||||||
signal quoteRefreshRequested(bool immediate)
|
signal quoteRefreshRequested(bool immediate)
|
||||||
signal submitSucceeded
|
signal submitSucceeded
|
||||||
signal submitFailed
|
signal submitFailed
|
||||||
|
signal confirmationQuoteReady(var snapshot)
|
||||||
|
|
||||||
objectName: "newPositionFlow"
|
objectName: "newPositionFlow"
|
||||||
|
|
||||||
@ -64,25 +76,37 @@ QtObject {
|
|||||||
onTriggered: root.pollPendingPool()
|
onTriggered: root.pollPendingPool()
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewPositionContextChanged: root.invalidateQuote()
|
property Connections backendConnections: Connections {
|
||||||
|
target: root.backend
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
|
||||||
onWalletStateReadyChanged: {
|
function onNewPositionQuoteResultChanged() {
|
||||||
++root.contextSerial
|
root.acceptQuoteResult(root.backend.newPositionQuoteResult || ({}))
|
||||||
if (!root.walletStateReady)
|
}
|
||||||
root.contextLoading = false
|
|
||||||
|
function onNewPositionSubmitResultChanged() {
|
||||||
|
root.acceptSubmitResult(root.backend.newPositionSubmitResult || ({}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewPositionContextChanged: {
|
||||||
|
root.contextLoading = false
|
||||||
|
root.contextErrorCode = root.newPositionContext.status === "error"
|
||||||
|
? String(root.newPositionContext.code || "backend_error") : ""
|
||||||
root.invalidateQuote()
|
root.invalidateQuote()
|
||||||
|
const completed = root.pendingContextCompletion
|
||||||
|
root.pendingContextCompletion = null
|
||||||
|
root.tokenResolutionFinished(true)
|
||||||
|
if (completed)
|
||||||
|
completed()
|
||||||
}
|
}
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (!root.active)
|
if (root.active && root.backendReady)
|
||||||
return
|
Qt.callLater(function() { root.quoteRefreshRequested(true) })
|
||||||
Qt.callLater(function() {
|
|
||||||
if (root.active && root.walletStateReady)
|
|
||||||
root.quoteRefreshRequested(true)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function contextHints(refreshWalletAccounts) {
|
function contextHints(refreshPublicData) {
|
||||||
const request = root.pendingQuoteRequest.request || {}
|
const request = root.pendingQuoteRequest.request || {}
|
||||||
const recent = []
|
const recent = []
|
||||||
if (request.tokenAId)
|
if (request.tokenAId)
|
||||||
@ -92,48 +116,24 @@ QtObject {
|
|||||||
return {
|
return {
|
||||||
"recentTokenIds": recent,
|
"recentTokenIds": recent,
|
||||||
"resolvedTokenIds": root.resolvedTokenIds,
|
"resolvedTokenIds": root.resolvedTokenIds,
|
||||||
"refreshWalletAccounts": refreshWalletAccounts === true
|
"refreshWalletAccounts": refreshPublicData === true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshContext(refreshWalletAccounts, completed) {
|
function refreshContext(refreshPublicData, completed) {
|
||||||
const serial = ++root.contextSerial
|
|
||||||
root.contextLoading = true
|
root.contextLoading = true
|
||||||
if (!root.walletStateReady || root.runtime === null) {
|
root.pendingContextCompletion = completed || null
|
||||||
|
if (!root.backendReady) {
|
||||||
root.contextLoading = false
|
root.contextLoading = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
root.runtime.watch(root.backend.refreshNewPositionContext(
|
root.backend.refreshNewPositionContext(root.contextHints(refreshPublicData))
|
||||||
root.contextHints(refreshWalletAccounts)),
|
} catch (_error) {
|
||||||
function() {
|
root.contextLoading = false
|
||||||
root.finishContextRefresh(serial, completed)
|
root.contextErrorCode = "backend_error"
|
||||||
},
|
root.tokenResolutionFailed("backend_error")
|
||||||
function(error) {
|
}
|
||||||
root.failContextRefresh(serial)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishContextRefresh(serial, completed) {
|
|
||||||
if (serial !== root.contextSerial)
|
|
||||||
return
|
|
||||||
root.contextLoading = false
|
|
||||||
root.contextErrorCode = ""
|
|
||||||
Qt.callLater(function() {
|
|
||||||
if (serial !== root.contextSerial)
|
|
||||||
return
|
|
||||||
root.tokenResolutionFinished(true)
|
|
||||||
if (completed)
|
|
||||||
completed()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function failContextRefresh(serial) {
|
|
||||||
if (serial !== root.contextSerial)
|
|
||||||
return
|
|
||||||
root.contextLoading = false
|
|
||||||
root.contextErrorCode = "backend_error"
|
|
||||||
root.tokenResolutionFailed("backend_error")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveToken(tokenId) {
|
function resolveToken(tokenId) {
|
||||||
@ -152,9 +152,9 @@ QtObject {
|
|||||||
++root.quoteSerial
|
++root.quoteSerial
|
||||||
root.pendingQuoteRequest = quoteRequest
|
root.pendingQuoteRequest = quoteRequest
|
||||||
root.quoteStale = true
|
root.quoteStale = true
|
||||||
root.quoteLoading = root.walletStateReady && root.active
|
root.quoteLoading = root.backendReady && root.active
|
||||||
root.quoteDebounce.stop()
|
root.quoteDebounce.stop()
|
||||||
if (!root.walletStateReady || !root.active)
|
if (!root.backendReady || !root.active)
|
||||||
return
|
return
|
||||||
if (immediate)
|
if (immediate)
|
||||||
root.requestQuoteNow(root.quoteSerial)
|
root.requestQuoteNow(root.quoteSerial)
|
||||||
@ -166,86 +166,107 @@ QtObject {
|
|||||||
if (serial !== root.quoteSerial)
|
if (serial !== root.quoteSerial)
|
||||||
return
|
return
|
||||||
const built = root.pendingQuoteRequest
|
const built = root.pendingQuoteRequest
|
||||||
if (!built.ok) {
|
if (!built.ok || !root.backendReady || !root.active) {
|
||||||
root.quoteLoading = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!root.walletStateReady || !root.active || root.runtime === null) {
|
|
||||||
root.quoteLoading = false
|
root.quoteLoading = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
root.activeQuoteRequestId = ++root.operationSerial
|
||||||
|
root.backend.requestNewPositionQuote(
|
||||||
|
built.request, root.activeQuoteRequestId, true)
|
||||||
|
}
|
||||||
|
|
||||||
root.runtime.watch(root.backend.quoteNewPosition(built.request),
|
function acceptQuoteResult(result) {
|
||||||
function(quote) {
|
const requestId = Number(result.requestId || 0)
|
||||||
if (serial !== root.quoteSerial)
|
if (requestId === root.poolProbeRequestId) {
|
||||||
return
|
root.finishPoolProbe(root.pendingPoolProbes[0], result)
|
||||||
root.quoteLoading = false
|
return
|
||||||
root.quoteStale = false
|
}
|
||||||
root.quoteErrorCode = ""
|
if (requestId !== root.activeQuoteRequestId)
|
||||||
if (!quote || quote.schema !== "new-position.v1")
|
return
|
||||||
root.newPositionQuote = root.quoteError("unsupported_schema")
|
root.quoteLoading = false
|
||||||
else
|
root.quoteStale = false
|
||||||
root.newPositionQuote = quote
|
root.quoteErrorCode = ""
|
||||||
},
|
root.newPositionQuote = result.schema === "new-position.v2"
|
||||||
function(error) {
|
? result : root.quoteError("unsupported_schema")
|
||||||
if (serial !== root.quoteSerial)
|
if (root.pendingConfirmationSnapshot) {
|
||||||
return
|
var snapshot = root.pendingConfirmationSnapshot
|
||||||
root.quoteLoading = false
|
root.pendingConfirmationSnapshot = null
|
||||||
root.quoteStale = true
|
snapshot.quoteHash = String(root.newPositionQuote.quoteHash || "")
|
||||||
root.quoteErrorCode = "backend_error"
|
snapshot.expectedLpText = String(root.newPositionQuote.expectedLpRaw || "")
|
||||||
})
|
+ " raw LP"
|
||||||
|
snapshot.instruction = String(root.newPositionQuote.instruction || "")
|
||||||
|
snapshot.lpHoldingOptions = root.newPositionQuote.lpHoldingOptions || []
|
||||||
|
snapshot.selectedLpHoldingId = String(
|
||||||
|
root.newPositionQuote.selectedLpHoldingId || "")
|
||||||
|
snapshot.createFreshLp = root.newPositionQuote.requiresFreshLp === true
|
||||||
|
snapshot.lpDestinationRequired =
|
||||||
|
root.newPositionQuote.lpDestinationRequired === true
|
||||||
|
snapshot.quoteReady = root.newPositionQuote.canSubmit === true
|
||||||
|
root.confirmationQuoteReady(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requoteConfirmation(snapshot) {
|
||||||
|
root.pendingConfirmationSnapshot = snapshot
|
||||||
|
root.scheduleQuote(true, {
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"request": snapshot.request
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirm(snapshot) {
|
function confirm(snapshot) {
|
||||||
if (root.submitting)
|
if (root.submitting)
|
||||||
return
|
return
|
||||||
root.submitting = true
|
if (!root.walletCanSubmit) {
|
||||||
root.flowErrorCode = ""
|
root.finishSubmitFailure(root.quoteError("wallet_syncing"))
|
||||||
|
|
||||||
if (!root.backend || root.runtime === null) {
|
|
||||||
root.finishSubmitFailure(root.quoteError("wallet_unavailable"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
root.submitting = true
|
||||||
|
root.flowErrorCode = ""
|
||||||
|
root.submitRequestId = ++root.operationSerial
|
||||||
|
root.pendingSubmitSnapshot = snapshot
|
||||||
|
root.backend.requestNewPositionSubmit(
|
||||||
|
snapshot.request, snapshot.quoteHash, root.submitRequestId)
|
||||||
|
}
|
||||||
|
|
||||||
root.runtime.watch(root.backend.submitNewPosition(snapshot.request, snapshot.quoteHash),
|
function acceptSubmitResult(result) {
|
||||||
function(result) {
|
if (Number(result.requestId || 0) !== root.submitRequestId)
|
||||||
if (result && result.schema === "new-position.v1"
|
return
|
||||||
&& result.status === "submitted"
|
if (result.schema === "new-position.v2"
|
||||||
&& /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(
|
&& result.status === "submitted"
|
||||||
String(result.transactionId || ""))) {
|
&& /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(
|
||||||
if (snapshot.request.initialPriceRealRaw !== undefined)
|
String(result.transactionId || ""))) {
|
||||||
root.watchPoolCreation(snapshot.poolProbeRequest, result.deadlineMs)
|
root.submitting = false
|
||||||
root.submitting = false
|
root.transactionId = result.transactionId
|
||||||
root.transactionId = result.transactionId
|
root.flowErrorCode = ""
|
||||||
root.flowErrorCode = ""
|
root.contextErrorCode = ""
|
||||||
root.contextErrorCode = ""
|
root.quoteErrorCode = ""
|
||||||
root.quoteErrorCode = ""
|
const poolProbe = root.pendingSubmitSnapshot.poolProbeRequest || null
|
||||||
root.invalidateQuote()
|
if (String(root.pendingSubmitSnapshot.instruction || "")
|
||||||
root.submitSucceeded()
|
=== "NewDefinition" && poolProbe)
|
||||||
return
|
root.watchPoolCreation(poolProbe, result.deadlineMs)
|
||||||
}
|
root.invalidateQuote()
|
||||||
root.finishSubmitFailure(result || root.quoteError("wallet_submission_failed"))
|
root.submitSucceeded()
|
||||||
},
|
return
|
||||||
function(error) {
|
}
|
||||||
root.finishSubmitFailure(root.quoteError("wallet_submission_failed"))
|
root.finishSubmitFailure(result || root.quoteError("wallet_submission_failed"))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishSubmitFailure(result) {
|
function finishSubmitFailure(result) {
|
||||||
root.submitting = false
|
root.submitting = false
|
||||||
const hasFreshQuote = result && result.quote
|
const hasFreshQuote = result && result.quote
|
||||||
&& result.quote.schema === "new-position.v1"
|
&& result.quote.schema === "new-position.v2"
|
||||||
if (hasFreshQuote) {
|
if (hasFreshQuote) {
|
||||||
root.newPositionQuote = result.quote
|
root.newPositionQuote = result.quote
|
||||||
root.quoteLoading = false
|
root.quoteLoading = false
|
||||||
root.quoteStale = false
|
root.quoteStale = false
|
||||||
}
|
}
|
||||||
const code = result && result.code ? result.code : "wallet_submission_failed"
|
root.flowErrorCode = result && result.code
|
||||||
root.flowErrorCode = code
|
? result.code : "wallet_submission_failed"
|
||||||
root.submitFailed()
|
root.submitFailed()
|
||||||
if (hasFreshQuote)
|
if (!hasFreshQuote)
|
||||||
return
|
root.scheduleQuote(true, root.pendingQuoteRequest)
|
||||||
root.scheduleQuote(true, root.pendingQuoteRequest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function watchPoolCreation(request, deadlineMs) {
|
function watchPoolCreation(request, deadlineMs) {
|
||||||
@ -256,40 +277,33 @@ QtObject {
|
|||||||
const pending = root.pendingPoolProbes.filter(function(item) {
|
const pending = root.pendingPoolProbes.filter(function(item) {
|
||||||
return item.key !== key
|
return item.key !== key
|
||||||
})
|
})
|
||||||
pending.push({
|
pending.push({ "key": key, "request": request, "deadlineMs": deadline })
|
||||||
"key": key,
|
|
||||||
"request": request,
|
|
||||||
"deadlineMs": deadline
|
|
||||||
})
|
|
||||||
root.pendingPoolProbes = pending
|
root.pendingPoolProbes = pending
|
||||||
Qt.callLater(root.pollPendingPool)
|
Qt.callLater(root.pollPendingPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pollPendingPool() {
|
function pollPendingPool() {
|
||||||
if (root.poolProbeInFlight || root.pendingPoolProbes.length === 0
|
if (root.poolProbeInFlight || root.pendingPoolProbes.length === 0
|
||||||
|| !root.walletStateReady || root.runtime === null) {
|
|| !root.backendReady)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
const pending = root.pendingPoolProbes[0]
|
const pending = root.pendingPoolProbes[0]
|
||||||
root.poolProbeInFlight = true
|
root.poolProbeInFlight = true
|
||||||
root.runtime.watch(root.backend.quoteNewPosition(pending.request),
|
root.poolProbeRequestId = ++root.operationSerial
|
||||||
function(quote) {
|
root.backend.requestNewPositionQuote(pending.request, root.poolProbeRequestId, true)
|
||||||
root.finishPoolProbe(pending, quote)
|
|
||||||
},
|
|
||||||
function(error) {
|
|
||||||
root.finishPoolProbe(pending, null)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishPoolProbe(pending, quote) {
|
function finishPoolProbe(pending, quote) {
|
||||||
|
if (!pending)
|
||||||
|
return
|
||||||
root.poolProbeInFlight = false
|
root.poolProbeInFlight = false
|
||||||
if (quote && quote.schema === "new-position.v1"
|
root.poolProbeRequestId = 0
|
||||||
|
if (quote && quote.schema === "new-position.v2"
|
||||||
&& quote.poolStatus === "active_pool") {
|
&& quote.poolStatus === "active_pool") {
|
||||||
root.removePendingPool(pending.key)
|
root.removePendingPool(pending.key)
|
||||||
if (root.matchesSelectedPair(pending.request)) {
|
if (root.matchesSelectedPair(pending.request)) {
|
||||||
root.poolActivated(quote)
|
root.poolActivated(quote)
|
||||||
root.invalidateQuote()
|
root.invalidateQuote()
|
||||||
root.refreshContext(true)
|
root.refreshContext(false)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -313,9 +327,7 @@ QtObject {
|
|||||||
if (!request.tokenAId || !request.tokenBId)
|
if (!request.tokenAId || !request.tokenBId)
|
||||||
return false
|
return false
|
||||||
const selected = root.pairKey(request)
|
const selected = root.pairKey(request)
|
||||||
return root.pendingPoolProbes.some(function(item) {
|
return root.pendingPoolProbes.some(function(item) { return item.key === selected })
|
||||||
return item.key === selected
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePendingPool(key) {
|
function removePendingPool(key) {
|
||||||
@ -342,6 +354,7 @@ QtObject {
|
|||||||
|
|
||||||
function invalidateQuote() {
|
function invalidateQuote() {
|
||||||
++root.quoteSerial
|
++root.quoteSerial
|
||||||
|
root.activeQuoteRequestId = 0
|
||||||
root.quoteDebounce.stop()
|
root.quoteDebounce.stop()
|
||||||
root.quoteLoading = false
|
root.quoteLoading = false
|
||||||
root.quoteStale = true
|
root.quoteStale = true
|
||||||
@ -349,7 +362,7 @@ QtObject {
|
|||||||
|
|
||||||
function loadingContext() {
|
function loadingContext() {
|
||||||
return {
|
return {
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "loading",
|
"status": "loading",
|
||||||
"tokens": [],
|
"tokens": [],
|
||||||
"feeTiers": []
|
"feeTiers": []
|
||||||
@ -358,7 +371,7 @@ QtObject {
|
|||||||
|
|
||||||
function quoteError(code) {
|
function quoteError(code) {
|
||||||
return {
|
return {
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"canSubmit": false,
|
"canSubmit": false,
|
||||||
"code": code,
|
"code": code,
|
||||||
|
|||||||
@ -69,3 +69,8 @@ AmmClientResult BundledAmmClient::plan(const QJsonObject& request) const
|
|||||||
{
|
{
|
||||||
return call(amm_plan, request);
|
return call(amm_plan, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AmmClientResult BundledAmmClient::normalizeAccountRpc(const QJsonObject& request) const
|
||||||
|
{
|
||||||
|
return call(amm_normalize_account_rpc, request);
|
||||||
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ public:
|
|||||||
virtual AmmClientResult context(const QJsonObject& request) const = 0;
|
virtual AmmClientResult context(const QJsonObject& request) const = 0;
|
||||||
virtual AmmClientResult quote(const QJsonObject& request) const = 0;
|
virtual AmmClientResult quote(const QJsonObject& request) const = 0;
|
||||||
virtual AmmClientResult plan(const QJsonObject& request) const = 0;
|
virtual AmmClientResult plan(const QJsonObject& request) const = 0;
|
||||||
|
virtual AmmClientResult normalizeAccountRpc(const QJsonObject& request) const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class BundledAmmClient final : public AmmClient {
|
class BundledAmmClient final : public AmmClient {
|
||||||
@ -27,4 +28,5 @@ public:
|
|||||||
AmmClientResult context(const QJsonObject& request) const override;
|
AmmClientResult context(const QJsonObject& request) const override;
|
||||||
AmmClientResult quote(const QJsonObject& request) const override;
|
AmmClientResult quote(const QJsonObject& request) const override;
|
||||||
AmmClientResult plan(const QJsonObject& request) const override;
|
AmmClientResult plan(const QJsonObject& request) const override;
|
||||||
|
AmmClientResult normalizeAccountRpc(const QJsonObject& request) const override;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonParseError>
|
#include <QJsonParseError>
|
||||||
|
#include <QDateTime>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
@ -13,6 +14,7 @@
|
|||||||
#include "AmmClient.h"
|
#include "AmmClient.h"
|
||||||
#include "LogosWalletProvider.h"
|
#include "LogosWalletProvider.h"
|
||||||
#include "NewPositionRuntime.h"
|
#include "NewPositionRuntime.h"
|
||||||
|
#include "SequencerClient.h"
|
||||||
#include "WalletController.h"
|
#include "WalletController.h"
|
||||||
#include "logos_api.h"
|
#include "logos_api.h"
|
||||||
|
|
||||||
@ -63,22 +65,24 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
|
|||||||
m_walletController(std::make_unique<WalletController>(
|
m_walletController(std::make_unique<WalletController>(
|
||||||
*m_wallet, QStringLiteral("AmmUI"))),
|
*m_wallet, QStringLiteral("AmmUI"))),
|
||||||
m_ammClient(std::make_unique<BundledAmmClient>()),
|
m_ammClient(std::make_unique<BundledAmmClient>()),
|
||||||
m_newPosition(std::make_unique<NewPositionRuntime>(m_wallet.get(), m_ammClient.get())),
|
m_sequencer(std::make_unique<SequencerClient>(m_ammClient.get(), this)),
|
||||||
m_net(new QNetworkAccessManager(this))
|
m_newPosition(std::make_unique<NewPositionRuntime>(
|
||||||
|
m_wallet.get(), m_ammClient.get(), m_sequencer.get())),
|
||||||
|
m_net(new QNetworkAccessManager(this)),
|
||||||
|
m_transactionTimer(new QTimer(this))
|
||||||
{
|
{
|
||||||
setWalletStateReady(false);
|
setNewPositionQuoteResult({});
|
||||||
|
setNewPositionSubmitResult({});
|
||||||
|
m_transactionTimer->setInterval(5000);
|
||||||
|
connect(m_transactionTimer, &QTimer::timeout,
|
||||||
|
this, &AmmUiBackend::pollTransactions);
|
||||||
m_network.load();
|
m_network.load();
|
||||||
setNewPositionContext(m_newPosition->context(
|
|
||||||
QVariantMap(), m_network.snapshot(), false, false));
|
|
||||||
|
|
||||||
connect(m_walletController.get(), &WalletController::stateChanged,
|
connect(m_walletController.get(), &WalletController::stateChanged,
|
||||||
this, &AmmUiBackend::syncWalletState);
|
this, &AmmUiBackend::syncWalletState);
|
||||||
syncWalletState();
|
syncWalletState();
|
||||||
|
refreshNewPositionContext({});
|
||||||
m_walletController->start();
|
m_walletController->start();
|
||||||
QTimer::singleShot(0, this, [this]() {
|
|
||||||
setWalletStateReady(true);
|
|
||||||
syncWalletState();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AmmUiBackend::~AmmUiBackend() = default;
|
AmmUiBackend::~AmmUiBackend() = default;
|
||||||
@ -90,28 +94,22 @@ WalletAccountModel* AmmUiBackend::accountModel() const
|
|||||||
|
|
||||||
QString AmmUiBackend::createNewDefault(QString password)
|
QString AmmUiBackend::createNewDefault(QString password)
|
||||||
{
|
{
|
||||||
setWalletStateReady(false);
|
|
||||||
const QString mnemonic = m_walletController->createDefaultWallet(password);
|
const QString mnemonic = m_walletController->createDefaultWallet(password);
|
||||||
setWalletStateReady(true);
|
|
||||||
syncWalletState();
|
syncWalletState();
|
||||||
return mnemonic;
|
return mnemonic;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString AmmUiBackend::createNew(QString configPath, QString storagePath, QString password)
|
QString AmmUiBackend::createNew(QString configPath, QString storagePath, QString password)
|
||||||
{
|
{
|
||||||
setWalletStateReady(false);
|
|
||||||
const QString mnemonic =
|
const QString mnemonic =
|
||||||
m_walletController->createWallet(configPath, storagePath, password);
|
m_walletController->createWallet(configPath, storagePath, password);
|
||||||
setWalletStateReady(true);
|
|
||||||
syncWalletState();
|
syncWalletState();
|
||||||
return mnemonic;
|
return mnemonic;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AmmUiBackend::openExisting()
|
bool AmmUiBackend::openExisting()
|
||||||
{
|
{
|
||||||
setWalletStateReady(false);
|
|
||||||
const bool opened = m_walletController->open();
|
const bool opened = m_walletController->open();
|
||||||
setWalletStateReady(true);
|
|
||||||
syncWalletState();
|
syncWalletState();
|
||||||
return opened;
|
return opened;
|
||||||
}
|
}
|
||||||
@ -119,7 +117,6 @@ bool AmmUiBackend::openExisting()
|
|||||||
void AmmUiBackend::disconnectWallet()
|
void AmmUiBackend::disconnectWallet()
|
||||||
{
|
{
|
||||||
m_walletController->disconnect();
|
m_walletController->disconnect();
|
||||||
setWalletStateReady(true);
|
|
||||||
m_newPosition->clearWalletAccounts();
|
m_newPosition->clearWalletAccounts();
|
||||||
refreshNewPositionContext(QVariantMap());
|
refreshNewPositionContext(QVariantMap());
|
||||||
}
|
}
|
||||||
@ -162,19 +159,38 @@ void AmmUiBackend::refreshNewPositionContext(QVariantMap request)
|
|||||||
}
|
}
|
||||||
if (m_network.status() == QStringLiteral("network_unknown"))
|
if (m_network.status() == QStringLiteral("network_unknown"))
|
||||||
probeNetworkIdentity();
|
probeNetworkIdentity();
|
||||||
setNewPositionContext(m_newPosition->context(
|
const quint64 generation = ++m_contextGeneration;
|
||||||
request, m_network.snapshot(), isWalletOpen(), refreshWalletAccounts));
|
m_newPosition->contextAsync(
|
||||||
|
request, m_network.snapshot(), isWalletOpen(), refreshWalletAccounts,
|
||||||
|
[this, generation](QVariantMap result) {
|
||||||
|
if (generation == m_contextGeneration)
|
||||||
|
setNewPositionContext(std::move(result));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap AmmUiBackend::quoteNewPosition(QVariantMap request)
|
void AmmUiBackend::requestNewPositionQuote(QVariantMap request,
|
||||||
|
int requestId,
|
||||||
|
bool forceRefresh)
|
||||||
{
|
{
|
||||||
return m_newPosition->quote(request, m_network.snapshot(), isWalletOpen());
|
m_newPosition->quoteAsync(
|
||||||
|
request, m_network.snapshot(), isWalletOpen(), forceRefresh,
|
||||||
|
[this, requestId](QVariantMap result) {
|
||||||
|
result.insert(QStringLiteral("requestId"), requestId);
|
||||||
|
setNewPositionQuoteResult(std::move(result));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap AmmUiBackend::submitNewPosition(QVariantMap request, QString quoteHash)
|
void AmmUiBackend::requestNewPositionSubmit(QVariantMap request,
|
||||||
|
QString quoteHash,
|
||||||
|
int requestId)
|
||||||
{
|
{
|
||||||
return m_newPosition->submit(
|
m_newPosition->submitAsync(
|
||||||
request, quoteHash, m_network.snapshot(), isWalletOpen());
|
request, quoteHash, m_network.snapshot(), walletCanSubmit(),
|
||||||
|
[this, requestId](QVariantMap result) {
|
||||||
|
result.insert(QStringLiteral("requestId"), requestId);
|
||||||
|
setNewPositionSubmitResult(result);
|
||||||
|
watchTransaction(result);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void AmmUiBackend::syncWalletState()
|
void AmmUiBackend::syncWalletState()
|
||||||
@ -185,6 +201,11 @@ void AmmUiBackend::syncWalletState()
|
|||||||
const QString previousAddress = sequencerAddr();
|
const QString previousAddress = sequencerAddr();
|
||||||
|
|
||||||
setIsWalletOpen(state.isWalletOpen);
|
setIsWalletOpen(state.isWalletOpen);
|
||||||
|
setWalletSyncStatus(state.syncStatus);
|
||||||
|
setWalletSyncError(state.syncError);
|
||||||
|
setWalletCanSubmit(state.canSubmit());
|
||||||
|
setWalletStateReady(state.syncStatus != QStringLiteral("opening")
|
||||||
|
&& state.syncStatus != QStringLiteral("syncing"));
|
||||||
setWalletExists(state.walletExists);
|
setWalletExists(state.walletExists);
|
||||||
setConfigPath(state.configPath);
|
setConfigPath(state.configPath);
|
||||||
setStoragePath(state.storagePath);
|
setStoragePath(state.storagePath);
|
||||||
@ -194,14 +215,27 @@ void AmmUiBackend::syncWalletState()
|
|||||||
setSequencerAddr(state.sequencerAddress);
|
setSequencerAddr(state.sequencerAddress);
|
||||||
setSequencerReachable(state.sequencerReachable);
|
setSequencerReachable(state.sequencerReachable);
|
||||||
|
|
||||||
|
if (m_sequencerConfigPath != state.configPath || !m_sequencer->isConfigured()) {
|
||||||
|
m_sequencerConfigPath = state.configPath;
|
||||||
|
m_sequencer->configure(state.configPath);
|
||||||
|
}
|
||||||
|
|
||||||
const bool addressChanged = previousAddress != state.sequencerAddress;
|
const bool addressChanged = previousAddress != state.sequencerAddress;
|
||||||
if (addressChanged)
|
if (addressChanged) {
|
||||||
|
m_pendingTransactions.clear();
|
||||||
|
m_transactionTimer->stop();
|
||||||
m_network.sequencerChanged(!state.sequencerAddress.isEmpty());
|
m_network.sequencerChanged(!state.sequencerAddress.isEmpty());
|
||||||
|
}
|
||||||
if (addressChanged || wasReachable != state.sequencerReachable) {
|
if (addressChanged || wasReachable != state.sequencerReachable) {
|
||||||
m_network.reachabilityChanged(state.sequencerReachable, wasReachable);
|
m_network.reachabilityChanged(state.sequencerReachable, wasReachable);
|
||||||
}
|
}
|
||||||
if (walletWasOpen && !state.isWalletOpen)
|
if (walletWasOpen && !state.isWalletOpen)
|
||||||
m_newPosition->clearWalletAccounts();
|
m_newPosition->clearWalletAccounts();
|
||||||
|
if (state.canSubmit()) {
|
||||||
|
const WalletSnapshot snapshot = m_wallet->snapshot();
|
||||||
|
if (snapshot.ok())
|
||||||
|
m_newPosition->setWalletAccounts(snapshot.accounts);
|
||||||
|
}
|
||||||
|
|
||||||
publishNetworkContext();
|
publishNetworkContext();
|
||||||
if (state.sequencerReachable && m_network.needsIdentityProbe())
|
if (state.sequencerReachable && m_network.needsIdentityProbe())
|
||||||
@ -230,6 +264,7 @@ void AmmUiBackend::probeNetworkIdentity()
|
|||||||
QNetworkRequest request{QUrl(address)};
|
QNetworkRequest request{QUrl(address)};
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||||
request.setTransferTimeout(4000);
|
request.setTransferTimeout(4000);
|
||||||
|
m_sequencer->applyAuthorization(request);
|
||||||
QNetworkReply* reply = m_net->post(request, jsonRpcBody(method, params));
|
QNetworkReply* reply = m_net->post(request, jsonRpcBody(method, params));
|
||||||
connect(reply, &QNetworkReply::finished, this, [this, reply, address, devnet]() {
|
connect(reply, &QNetworkReply::finished, this, [this, reply, address, devnet]() {
|
||||||
m_identityProbeInFlight = false;
|
m_identityProbeInFlight = false;
|
||||||
@ -255,6 +290,78 @@ void AmmUiBackend::probeNetworkIdentity()
|
|||||||
|
|
||||||
void AmmUiBackend::publishNetworkContext()
|
void AmmUiBackend::publishNetworkContext()
|
||||||
{
|
{
|
||||||
setNewPositionContext(m_newPosition->context(
|
refreshNewPositionContext(m_newPositionHints);
|
||||||
m_newPositionHints, m_network.snapshot(), isWalletOpen(), false));
|
}
|
||||||
|
|
||||||
|
void AmmUiBackend::watchTransaction(const QVariantMap& result)
|
||||||
|
{
|
||||||
|
if (result.value(QStringLiteral("status")).toString()
|
||||||
|
!= QStringLiteral("submitted")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString nativeHash = result.value(
|
||||||
|
QStringLiteral("nativeTransactionHash")).toString();
|
||||||
|
bool deadlineValid = false;
|
||||||
|
const qint64 deadline = result.value(QStringLiteral("deadlineMs"))
|
||||||
|
.toString().toLongLong(&deadlineValid);
|
||||||
|
QStringList affected;
|
||||||
|
for (const QVariant& value : result.value(
|
||||||
|
QStringLiteral("affectedAccountIds")).toList()) {
|
||||||
|
affected.append(value.toString());
|
||||||
|
}
|
||||||
|
if (nativeHash.isEmpty() || !deadlineValid || affected.isEmpty())
|
||||||
|
return;
|
||||||
|
m_pendingTransactions.insert(nativeHash, {
|
||||||
|
nativeHash,
|
||||||
|
affected,
|
||||||
|
deadline,
|
||||||
|
});
|
||||||
|
if (!m_transactionTimer->isActive())
|
||||||
|
m_transactionTimer->start();
|
||||||
|
QTimer::singleShot(0, this, &AmmUiBackend::pollTransactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AmmUiBackend::pollTransactions()
|
||||||
|
{
|
||||||
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
const QList<PendingTransaction> pending = m_pendingTransactions.values();
|
||||||
|
for (const PendingTransaction& transaction : pending) {
|
||||||
|
if (now >= transaction.deadlineMs) {
|
||||||
|
m_pendingTransactions.remove(transaction.nativeHash);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_sequencer->queryTransaction(transaction.nativeHash,
|
||||||
|
[this, transaction](bool ok, bool included) {
|
||||||
|
if (!ok || !included
|
||||||
|
|| !m_pendingTransactions.contains(transaction.nativeHash)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_pendingTransactions.remove(transaction.nativeHash);
|
||||||
|
refreshAffectedAccounts(transaction.affectedAccountIds);
|
||||||
|
if (m_pendingTransactions.isEmpty())
|
||||||
|
m_transactionTimer->stop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (m_pendingTransactions.isEmpty())
|
||||||
|
m_transactionTimer->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AmmUiBackend::refreshAffectedAccounts(const QStringList& accountIds, int attempt)
|
||||||
|
{
|
||||||
|
m_sequencer->readAccounts(accountIds, true,
|
||||||
|
[this, accountIds, attempt](QVector<WalletAccountRead> reads) {
|
||||||
|
QStringList failed;
|
||||||
|
for (qsizetype index = 0; index < reads.size(); ++index) {
|
||||||
|
if (!reads.at(index).ok())
|
||||||
|
failed.append(accountIds.value(index));
|
||||||
|
}
|
||||||
|
if (!failed.isEmpty() && attempt < 2) {
|
||||||
|
QTimer::singleShot(1000, this,
|
||||||
|
[this, failed, attempt]() {
|
||||||
|
refreshAffectedAccounts(failed, attempt + 1);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshNewPositionContext({});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QHash>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
#include "rep_AmmUiBackend_source.h"
|
#include "rep_AmmUiBackend_source.h"
|
||||||
@ -17,6 +19,8 @@ class AmmClient;
|
|||||||
class LogosWalletProvider;
|
class LogosWalletProvider;
|
||||||
class NewPositionRuntime;
|
class NewPositionRuntime;
|
||||||
class QNetworkAccessManager;
|
class QNetworkAccessManager;
|
||||||
|
class QTimer;
|
||||||
|
class SequencerClient;
|
||||||
class WalletController;
|
class WalletController;
|
||||||
|
|
||||||
// Source-side implementation of the AmmUiBackend .rep interface.
|
// Source-side implementation of the AmmUiBackend .rep interface.
|
||||||
@ -40,8 +44,12 @@ public slots:
|
|||||||
void refreshBalances() override;
|
void refreshBalances() override;
|
||||||
QString getBalance(QString accountIdHex, bool isPublic) override;
|
QString getBalance(QString accountIdHex, bool isPublic) override;
|
||||||
void refreshNewPositionContext(QVariantMap request) override;
|
void refreshNewPositionContext(QVariantMap request) override;
|
||||||
QVariantMap quoteNewPosition(QVariantMap request) override;
|
void requestNewPositionQuote(QVariantMap request,
|
||||||
QVariantMap submitNewPosition(QVariantMap request, QString quoteHash) override;
|
int requestId,
|
||||||
|
bool forceRefresh) override;
|
||||||
|
void requestNewPositionSubmit(QVariantMap request,
|
||||||
|
QString quoteHash,
|
||||||
|
int requestId) override;
|
||||||
// Return the new wallet's BIP39 mnemonic (empty string on failure) so the
|
// Return the new wallet's BIP39 mnemonic (empty string on failure) so the
|
||||||
// UI can force a one-time seed-phrase backup step.
|
// UI can force a one-time seed-phrase backup step.
|
||||||
QString createNewDefault(QString password) override;
|
QString createNewDefault(QString password) override;
|
||||||
@ -53,18 +61,31 @@ private:
|
|||||||
void syncWalletState();
|
void syncWalletState();
|
||||||
void probeNetworkIdentity();
|
void probeNetworkIdentity();
|
||||||
void publishNetworkContext();
|
void publishNetworkContext();
|
||||||
|
void watchTransaction(const QVariantMap& result);
|
||||||
|
void pollTransactions();
|
||||||
|
void refreshAffectedAccounts(const QStringList& accountIds, int attempt = 0);
|
||||||
|
|
||||||
LogosAPI* m_logosAPI;
|
LogosAPI* m_logosAPI;
|
||||||
std::unique_ptr<LogosWalletProvider> m_wallet;
|
std::unique_ptr<LogosWalletProvider> m_wallet;
|
||||||
std::unique_ptr<WalletController> m_walletController;
|
std::unique_ptr<WalletController> m_walletController;
|
||||||
std::unique_ptr<AmmClient> m_ammClient;
|
std::unique_ptr<AmmClient> m_ammClient;
|
||||||
|
std::unique_ptr<SequencerClient> m_sequencer;
|
||||||
std::unique_ptr<NewPositionRuntime> m_newPosition;
|
std::unique_ptr<NewPositionRuntime> m_newPosition;
|
||||||
|
|
||||||
QNetworkAccessManager* m_net;
|
QNetworkAccessManager* m_net;
|
||||||
|
QTimer* m_transactionTimer;
|
||||||
|
|
||||||
ActiveNetwork m_network;
|
ActiveNetwork m_network;
|
||||||
QVariantMap m_newPositionHints;
|
QVariantMap m_newPositionHints;
|
||||||
|
QString m_sequencerConfigPath;
|
||||||
bool m_identityProbeInFlight = false;
|
bool m_identityProbeInFlight = false;
|
||||||
|
quint64 m_contextGeneration = 0;
|
||||||
|
struct PendingTransaction {
|
||||||
|
QString nativeHash;
|
||||||
|
QStringList affectedAccountIds;
|
||||||
|
qint64 deadlineMs = 0;
|
||||||
|
};
|
||||||
|
QHash<QString, PendingTransaction> m_pendingTransactions;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // AMM_UI_BACKEND_H
|
#endif // AMM_UI_BACKEND_H
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// QtRO view contract for the AMM UI backend. PROPs auto-sync to every QML
|
// QtRO view contract for the AMM UI backend. PROPs auto-sync to every QML
|
||||||
// replica; SLOTs are the async surface QML calls via logos.watch(...).
|
// replica; SLOTs dispatch asynchronous backend work.
|
||||||
// The account list is exposed separately as a Q_PROPERTY model on the backend
|
// The account list is exposed separately as a Q_PROPERTY model on the backend
|
||||||
// (reached from QML via logos.model("amm_ui", "accountModel")).
|
// (reached from QML via logos.model("amm_ui", "accountModel")).
|
||||||
class AmmUiBackend
|
class AmmUiBackend
|
||||||
@ -8,6 +8,9 @@ class AmmUiBackend
|
|||||||
// False while startup or reconnect is still resolving wallet state. This
|
// False while startup or reconnect is still resolving wallet state. This
|
||||||
// stays distinct from isWalletOpen because a disconnected wallet is ready.
|
// stays distinct from isWalletOpen because a disconnected wallet is ready.
|
||||||
PROP(bool walletStateReady READONLY)
|
PROP(bool walletStateReady READONLY)
|
||||||
|
PROP(QString walletSyncStatus READONLY)
|
||||||
|
PROP(QString walletSyncError READONLY)
|
||||||
|
PROP(bool walletCanSubmit READONLY)
|
||||||
PROP(bool walletExists READONLY)
|
PROP(bool walletExists READONLY)
|
||||||
PROP(QString configPath READONLY)
|
PROP(QString configPath READONLY)
|
||||||
PROP(QString storagePath READONLY)
|
PROP(QString storagePath READONLY)
|
||||||
@ -26,13 +29,15 @@ class AmmUiBackend
|
|||||||
SLOT(void refreshBalances())
|
SLOT(void refreshBalances())
|
||||||
SLOT(QString getBalance(QString accountIdHex, bool isPublic))
|
SLOT(QString getBalance(QString accountIdHex, bool isPublic))
|
||||||
|
|
||||||
// New Position backend surface. QML calls these through logos.watch(...).
|
// New Position backend surface. QML dispatches request slots and observes
|
||||||
// The QVariant payloads are stable maps/lists so the UI never assembles AMM
|
// request-tagged result properties. QVariant payloads are stable maps/lists
|
||||||
// transactions or duplicates quote state.
|
// so the UI never assembles AMM transactions or duplicates quote state.
|
||||||
PROP(QVariantMap newPositionContext READONLY)
|
PROP(QVariantMap newPositionContext READONLY)
|
||||||
|
PROP(QVariantMap newPositionQuoteResult READONLY)
|
||||||
|
PROP(QVariantMap newPositionSubmitResult READONLY)
|
||||||
SLOT(void refreshNewPositionContext(QVariantMap request))
|
SLOT(void refreshNewPositionContext(QVariantMap request))
|
||||||
SLOT(QVariantMap quoteNewPosition(QVariantMap request))
|
SLOT(void requestNewPositionQuote(QVariantMap request, int requestId, bool forceRefresh))
|
||||||
SLOT(QVariantMap submitNewPosition(QVariantMap request, QString quoteHash))
|
SLOT(void requestNewPositionSubmit(QVariantMap request, QString quoteHash, int requestId))
|
||||||
|
|
||||||
// Wallet lifecycle. createNewDefault() is the happy path: it creates a
|
// Wallet lifecycle. createNewDefault() is the happy path: it creates a
|
||||||
// fresh wallet at the canonical walletHome with no path picking. createNew()
|
// fresh wallet at the canonical walletHome with no path picking. createNew()
|
||||||
|
|||||||
@ -11,10 +11,11 @@
|
|||||||
#include <libbase58.h>
|
#include <libbase58.h>
|
||||||
|
|
||||||
#include "AmmClient.h"
|
#include "AmmClient.h"
|
||||||
|
#include "SequencerClient.h"
|
||||||
#include "WalletProvider.h"
|
#include "WalletProvider.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
const char SCHEMA[] = "new-position.v1";
|
const char SCHEMA[] = "new-position.v2";
|
||||||
constexpr qsizetype HASH_BYTES = 32;
|
constexpr qsizetype HASH_BYTES = 32;
|
||||||
constexpr std::size_t BASE58_BUFFER_SIZE = 45;
|
constexpr std::size_t BASE58_BUFFER_SIZE = 45;
|
||||||
|
|
||||||
@ -35,6 +36,21 @@ namespace {
|
|||||||
encoded.data(), static_cast<qsizetype>(size - 1));
|
encoded.data(), static_cast<qsizetype>(size - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString accountIdHex(const QString& accountId)
|
||||||
|
{
|
||||||
|
const QByteArray encoded = accountId.toLatin1();
|
||||||
|
std::array<unsigned char, HASH_BYTES> bytes {};
|
||||||
|
std::size_t size = bytes.size();
|
||||||
|
if (!b58tobin(bytes.data(), &size, encoded.constData(),
|
||||||
|
static_cast<std::size_t>(encoded.size()))
|
||||||
|
|| size != bytes.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return QString::fromLatin1(
|
||||||
|
QByteArray(reinterpret_cast<const char*>(bytes.data()),
|
||||||
|
static_cast<qsizetype>(bytes.size())).toHex());
|
||||||
|
}
|
||||||
|
|
||||||
QJsonObject issue(const QString& code,
|
QJsonObject issue(const QString& code,
|
||||||
const QJsonArray& blockingFields = {})
|
const QJsonArray& blockingFields = {})
|
||||||
{
|
{
|
||||||
@ -142,17 +158,30 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NewPositionRuntime::NewPositionRuntime(WalletProvider* wallet, AmmClient* client)
|
NewPositionRuntime::NewPositionRuntime(WalletProvider* wallet,
|
||||||
|
AmmClient* client,
|
||||||
|
SequencerClient* sequencer)
|
||||||
: m_wallet(wallet),
|
: m_wallet(wallet),
|
||||||
m_client(client)
|
m_client(client),
|
||||||
|
m_sequencer(sequencer)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewPositionRuntime::clearWalletAccounts()
|
void NewPositionRuntime::clearWalletAccounts()
|
||||||
{
|
{
|
||||||
|
m_walletPublicAccountIds.clear();
|
||||||
m_wallet->clearSnapshot();
|
m_wallet->clearSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NewPositionRuntime::setWalletAccounts(const QVector<WalletAccount>& accounts)
|
||||||
|
{
|
||||||
|
m_walletPublicAccountIds.clear();
|
||||||
|
for (const WalletAccount& account : accounts) {
|
||||||
|
if (account.isPublic)
|
||||||
|
m_walletPublicAccountIds.append(account.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QJsonArray NewPositionRuntime::walletAccountReads(bool walletOpen, bool refresh) const
|
QJsonArray NewPositionRuntime::walletAccountReads(bool walletOpen, bool refresh) const
|
||||||
{
|
{
|
||||||
if (!walletOpen)
|
if (!walletOpen)
|
||||||
@ -221,6 +250,356 @@ QJsonObject NewPositionRuntime::buildQuoteInput(const QVariantMap& request,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NewPositionRuntime::contextAsync(const QVariantMap& request,
|
||||||
|
const ActiveNetworkSnapshot& network,
|
||||||
|
bool walletOpen,
|
||||||
|
bool refreshPublicData,
|
||||||
|
ResultCallback callback)
|
||||||
|
{
|
||||||
|
if (network.status != QStringLiteral("ready")) {
|
||||||
|
callback(contextState(network.status, network).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!m_sequencer || !m_sequencer->isConfigured()) {
|
||||||
|
callback(contextState(QStringLiteral("error"), network,
|
||||||
|
QStringLiteral("sequencer_config_required")).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AmmClientResult configResult = m_client->configId(
|
||||||
|
QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } });
|
||||||
|
if (!configResult.ok) {
|
||||||
|
callback(contextState(QStringLiteral("error"), network,
|
||||||
|
QStringLiteral("backend_error")).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString configId = configResult.value.value(
|
||||||
|
QStringLiteral("configId")).toString();
|
||||||
|
m_sequencer->readAccounts({ configId }, refreshPublicData,
|
||||||
|
[this, request, network, walletOpen, refreshPublicData,
|
||||||
|
callback = std::move(callback)](QVector<WalletAccountRead> configReads) mutable {
|
||||||
|
const QJsonObject config = accountReadJson(configReads.value(0));
|
||||||
|
m_sequencer->readAccounts(m_walletPublicAccountIds, false,
|
||||||
|
[this, request, network, walletOpen, refreshPublicData, config,
|
||||||
|
callback = std::move(callback)](
|
||||||
|
QVector<WalletAccountRead> walletReads) mutable {
|
||||||
|
const QJsonObject hints = QJsonObject::fromVariantMap(request);
|
||||||
|
QJsonArray configured;
|
||||||
|
for (const QString& id : network.tokenIds)
|
||||||
|
configured.append(id);
|
||||||
|
const QJsonArray recent = variantStringArray(
|
||||||
|
hints.value(QStringLiteral("recentTokenIds")).toVariant());
|
||||||
|
const QJsonArray resolved = variantStringArray(
|
||||||
|
hints.value(QStringLiteral("resolvedTokenIds")).toVariant());
|
||||||
|
const QJsonArray walletAccounts = accountReadsJson(walletReads);
|
||||||
|
const AmmClientResult tokenResult = m_client->tokenIds(QJsonObject {
|
||||||
|
{ QStringLiteral("ammProgramId"), network.ammProgramId },
|
||||||
|
{ QStringLiteral("config"), config },
|
||||||
|
{ QStringLiteral("walletAccounts"), walletAccounts },
|
||||||
|
{ QStringLiteral("configuredTokenIds"), configured },
|
||||||
|
{ QStringLiteral("recentTokenIds"), recent },
|
||||||
|
{ QStringLiteral("resolvedTokenIds"), resolved },
|
||||||
|
});
|
||||||
|
if (!tokenResult.ok
|
||||||
|
|| tokenResult.value.value(QStringLiteral("status")).toString()
|
||||||
|
!= QStringLiteral("ok")) {
|
||||||
|
const QString code = tokenResult.ok
|
||||||
|
? tokenResult.value.value(QStringLiteral("code")).toString()
|
||||||
|
: QStringLiteral("backend_error");
|
||||||
|
callback(contextState(QStringLiteral("error"), network,
|
||||||
|
code.isEmpty() ? QStringLiteral("backend_error") : code)
|
||||||
|
.toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList definitionIds;
|
||||||
|
for (const QJsonValue& id : tokenResult.value
|
||||||
|
.value(QStringLiteral("tokenIds")).toArray()) {
|
||||||
|
definitionIds.append(id.toString());
|
||||||
|
}
|
||||||
|
m_sequencer->readAccounts(definitionIds, refreshPublicData,
|
||||||
|
[this, network, walletOpen, config, walletAccounts,
|
||||||
|
configured, recent, resolved,
|
||||||
|
callback = std::move(callback)](
|
||||||
|
QVector<WalletAccountRead> definitions) mutable {
|
||||||
|
const AmmClientResult result = m_client->context(QJsonObject {
|
||||||
|
{ QStringLiteral("networkId"), network.id },
|
||||||
|
{ QStringLiteral("networkFingerprint"), network.fingerprint },
|
||||||
|
{ QStringLiteral("ammProgramId"), network.ammProgramId },
|
||||||
|
{ QStringLiteral("walletAvailable"), walletOpen },
|
||||||
|
{ QStringLiteral("config"), config },
|
||||||
|
{ QStringLiteral("walletAccounts"), walletAccounts },
|
||||||
|
{ QStringLiteral("tokenDefinitions"),
|
||||||
|
accountReadsJson(definitions) },
|
||||||
|
{ QStringLiteral("configuredTokenIds"), configured },
|
||||||
|
{ QStringLiteral("recentTokenIds"), recent },
|
||||||
|
{ QStringLiteral("resolvedTokenIds"), resolved },
|
||||||
|
});
|
||||||
|
callback((result.ok
|
||||||
|
? result.value
|
||||||
|
: contextState(QStringLiteral("error"), network,
|
||||||
|
QStringLiteral("backend_error"))).toVariantMap());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewPositionRuntime::buildQuoteInputAsync(
|
||||||
|
const QVariantMap& request,
|
||||||
|
const ActiveNetworkSnapshot& network,
|
||||||
|
bool walletOpen,
|
||||||
|
bool forceRefresh,
|
||||||
|
std::function<void(QJsonObject, QJsonObject)> callback) const
|
||||||
|
{
|
||||||
|
if (network.status != QStringLiteral("ready")) {
|
||||||
|
callback({}, publicError(network.status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!m_sequencer || !m_sequencer->isConfigured()) {
|
||||||
|
callback({}, publicError(QStringLiteral("sequencer_config_required")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const AmmClientResult configResult = m_client->configId(
|
||||||
|
QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } });
|
||||||
|
if (!configResult.ok) {
|
||||||
|
callback({}, publicError(QStringLiteral("backend_error")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString configId = configResult.value.value(
|
||||||
|
QStringLiteral("configId")).toString();
|
||||||
|
m_sequencer->readAccounts({ configId }, forceRefresh,
|
||||||
|
[this, request, network, walletOpen, forceRefresh,
|
||||||
|
callback = std::move(callback)](QVector<WalletAccountRead> configReads) mutable {
|
||||||
|
const QJsonObject config = accountReadJson(configReads.value(0));
|
||||||
|
const QJsonObject requestObject = QJsonObject::fromVariantMap(request);
|
||||||
|
const AmmClientResult pairResult = m_client->pairIds(QJsonObject {
|
||||||
|
{ QStringLiteral("ammProgramId"), network.ammProgramId },
|
||||||
|
{ QStringLiteral("config"), config },
|
||||||
|
{ QStringLiteral("tokenAId"),
|
||||||
|
requestObject.value(QStringLiteral("tokenAId")) },
|
||||||
|
{ QStringLiteral("tokenBId"),
|
||||||
|
requestObject.value(QStringLiteral("tokenBId")) },
|
||||||
|
});
|
||||||
|
if (!pairResult.ok
|
||||||
|
|| pairResult.value.value(QStringLiteral("status")).toString()
|
||||||
|
!= QStringLiteral("ok")) {
|
||||||
|
const QString code = pairResult.ok
|
||||||
|
? pairResult.value.value(QStringLiteral("code")).toString()
|
||||||
|
: QStringLiteral("backend_error");
|
||||||
|
callback({}, publicError(code.isEmpty()
|
||||||
|
? QStringLiteral("backend_error") : code));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QJsonObject pair = pairResult.value;
|
||||||
|
const QStringList fixedIds {
|
||||||
|
pair.value(QStringLiteral("tokenAId")).toString(),
|
||||||
|
pair.value(QStringLiteral("tokenBId")).toString(),
|
||||||
|
pair.value(QStringLiteral("poolId")).toString(),
|
||||||
|
pair.value(QStringLiteral("vaultAId")).toString(),
|
||||||
|
pair.value(QStringLiteral("vaultBId")).toString(),
|
||||||
|
pair.value(QStringLiteral("lpDefinitionId")).toString(),
|
||||||
|
pair.value(QStringLiteral("lpLockHoldingId")).toString(),
|
||||||
|
pair.value(QStringLiteral("currentTickId")).toString(),
|
||||||
|
pair.value(QStringLiteral("clockId")).toString(),
|
||||||
|
};
|
||||||
|
m_sequencer->readAccounts(fixedIds, forceRefresh,
|
||||||
|
[this, requestObject, network, walletOpen, config, pair,
|
||||||
|
callback = std::move(callback)](
|
||||||
|
QVector<WalletAccountRead> fixedReads) mutable {
|
||||||
|
m_sequencer->readAccounts(m_walletPublicAccountIds, false,
|
||||||
|
[this, requestObject, network, walletOpen, config, pair,
|
||||||
|
fixedReads = std::move(fixedReads),
|
||||||
|
callback = std::move(callback)](
|
||||||
|
QVector<WalletAccountRead> walletReads) mutable {
|
||||||
|
QStringList selectedIds;
|
||||||
|
for (const QString& key : {
|
||||||
|
QStringLiteral("holdingAId"),
|
||||||
|
QStringLiteral("holdingBId"),
|
||||||
|
QStringLiteral("lpHoldingId") }) {
|
||||||
|
const QString id = accountIdHex(
|
||||||
|
requestObject.value(key).toString());
|
||||||
|
if (!id.isEmpty() && !selectedIds.contains(id))
|
||||||
|
selectedIds.append(id);
|
||||||
|
}
|
||||||
|
auto finish = [this, requestObject, network, walletOpen,
|
||||||
|
config, pair,
|
||||||
|
fixedReads = std::move(fixedReads),
|
||||||
|
walletReads = std::move(walletReads),
|
||||||
|
callback = std::move(callback)](
|
||||||
|
QVector<WalletAccountRead> selectedReads) mutable {
|
||||||
|
QHash<QString, WalletAccountRead> walletById;
|
||||||
|
for (const WalletAccountRead& read : walletReads)
|
||||||
|
walletById.insert(read.accountId, read);
|
||||||
|
for (const WalletAccountRead& read : selectedReads)
|
||||||
|
walletById.insert(read.accountId, read);
|
||||||
|
const QJsonArray walletAccounts = accountReadsJson(
|
||||||
|
walletById.values());
|
||||||
|
const QJsonObject snapshot {
|
||||||
|
{ QStringLiteral("config"), config },
|
||||||
|
{ QStringLiteral("tokenA"), accountReadJson(fixedReads.value(0)) },
|
||||||
|
{ QStringLiteral("tokenB"), accountReadJson(fixedReads.value(1)) },
|
||||||
|
{ QStringLiteral("pool"), accountReadJson(fixedReads.value(2)) },
|
||||||
|
{ QStringLiteral("vaultA"), accountReadJson(fixedReads.value(3)) },
|
||||||
|
{ QStringLiteral("vaultB"), accountReadJson(fixedReads.value(4)) },
|
||||||
|
{ QStringLiteral("lpDefinition"), accountReadJson(fixedReads.value(5)) },
|
||||||
|
{ QStringLiteral("lpLockHolding"), accountReadJson(fixedReads.value(6)) },
|
||||||
|
{ QStringLiteral("currentTick"), accountReadJson(fixedReads.value(7)) },
|
||||||
|
{ QStringLiteral("clock"), accountReadJson(fixedReads.value(8)) },
|
||||||
|
{ QStringLiteral("walletAvailable"), walletOpen },
|
||||||
|
{ QStringLiteral("walletAccounts"), walletAccounts },
|
||||||
|
};
|
||||||
|
callback(QJsonObject {
|
||||||
|
{ QStringLiteral("networkId"), network.id },
|
||||||
|
{ QStringLiteral("networkFingerprint"), network.fingerprint },
|
||||||
|
{ QStringLiteral("ammProgramId"), network.ammProgramId },
|
||||||
|
{ QStringLiteral("request"), requestObject },
|
||||||
|
{ QStringLiteral("snapshot"), snapshot },
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
if (selectedIds.isEmpty())
|
||||||
|
finish({});
|
||||||
|
else
|
||||||
|
m_sequencer->readAccounts(
|
||||||
|
selectedIds, true, std::move(finish));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewPositionRuntime::quoteAsync(const QVariantMap& request,
|
||||||
|
const ActiveNetworkSnapshot& network,
|
||||||
|
bool walletOpen,
|
||||||
|
bool forceRefresh,
|
||||||
|
ResultCallback callback)
|
||||||
|
{
|
||||||
|
buildQuoteInputAsync(request, network, walletOpen, forceRefresh,
|
||||||
|
[this, callback = std::move(callback)](
|
||||||
|
QJsonObject input, QJsonObject error) mutable {
|
||||||
|
if (!error.isEmpty()) {
|
||||||
|
callback(error.toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const AmmClientResult result = m_client->quote(input);
|
||||||
|
callback((result.ok ? result.value
|
||||||
|
: publicError(QStringLiteral("backend_error")))
|
||||||
|
.toVariantMap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewPositionRuntime::submitAsync(const QVariantMap& request,
|
||||||
|
const QString& quoteHash,
|
||||||
|
const ActiveNetworkSnapshot& network,
|
||||||
|
bool walletCanSubmit,
|
||||||
|
ResultCallback callback)
|
||||||
|
{
|
||||||
|
if (m_submitInFlight) {
|
||||||
|
callback(publicError(QStringLiteral("submit_in_progress")).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!walletCanSubmit) {
|
||||||
|
callback(publicError(QStringLiteral("wallet_syncing")).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_submitInFlight = true;
|
||||||
|
auto finish = [this, callback = std::move(callback)](QVariantMap result) mutable {
|
||||||
|
m_submitInFlight = false;
|
||||||
|
callback(std::move(result));
|
||||||
|
};
|
||||||
|
buildQuoteInputAsync(request, network, true, true,
|
||||||
|
[this, quoteHash, finish = std::move(finish)](
|
||||||
|
QJsonObject input, QJsonObject error) mutable {
|
||||||
|
if (!error.isEmpty()) {
|
||||||
|
finish(error.toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const AmmClientResult quoteResult = m_client->quote(input);
|
||||||
|
if (!quoteResult.ok) {
|
||||||
|
finish(publicError(QStringLiteral("backend_error")).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QJsonObject quote = quoteResult.value;
|
||||||
|
if (quote.value(QStringLiteral("quoteHash")).toString() != quoteHash) {
|
||||||
|
QJsonObject result = publicError(QStringLiteral("quote_changed"));
|
||||||
|
result.insert(QStringLiteral("quote"), quote);
|
||||||
|
finish(result.toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!quote.value(QStringLiteral("canSubmit")).toBool(false)) {
|
||||||
|
QJsonObject result = publicError(QStringLiteral("quote_not_submittable"));
|
||||||
|
result.insert(QStringLiteral("quote"), quote);
|
||||||
|
finish(result.toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonValue freshLp;
|
||||||
|
if (quote.value(QStringLiteral("requiresFreshLp")).toBool(false)) {
|
||||||
|
const WalletAccountCreation creation = m_wallet->createAccount(true);
|
||||||
|
if (!creation.ok() || !creation.publicAccount.ok()) {
|
||||||
|
finish(publicError(QStringLiteral("wallet_submission_failed")).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
freshLp = accountReadJson(creation.publicAccount);
|
||||||
|
if (!m_walletPublicAccountIds.contains(creation.accountId))
|
||||||
|
m_walletPublicAccountIds.append(creation.accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject planInput = input;
|
||||||
|
planInput.insert(QStringLiteral("quoteHash"), quoteHash);
|
||||||
|
planInput.insert(QStringLiteral("nowMs"), QDateTime::currentMSecsSinceEpoch());
|
||||||
|
if (!freshLp.isUndefined())
|
||||||
|
planInput.insert(QStringLiteral("freshLp"), freshLp);
|
||||||
|
const AmmClientResult planResult = m_client->plan(planInput);
|
||||||
|
if (!planResult.ok) {
|
||||||
|
finish(publicError(QStringLiteral("backend_error")).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QJsonObject plan = planResult.value;
|
||||||
|
if (plan.value(QStringLiteral("status")).toString()
|
||||||
|
!= QStringLiteral("ready")) {
|
||||||
|
const QString code = plan.value(QStringLiteral("code")).toString();
|
||||||
|
finish(publicError(code.isEmpty()
|
||||||
|
? QStringLiteral("wallet_submission_failed") : code).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList accountIds = jsonStringList(
|
||||||
|
plan.value(QStringLiteral("accountIds")).toArray());
|
||||||
|
const QVector<bool> signingRequirements = jsonBoolList(
|
||||||
|
plan.value(QStringLiteral("signingRequirements")).toArray());
|
||||||
|
const QVector<quint32> instruction = jsonUIntList(
|
||||||
|
plan.value(QStringLiteral("instruction")).toArray());
|
||||||
|
bool deadlineValid = false;
|
||||||
|
const qulonglong deadline = plan.value(QStringLiteral("deadlineMs"))
|
||||||
|
.toString().toULongLong(&deadlineValid);
|
||||||
|
if (!deadlineValid
|
||||||
|
|| static_cast<qulonglong>(QDateTime::currentMSecsSinceEpoch()) >= deadline) {
|
||||||
|
finish(publicError(QStringLiteral("transaction_deadline_expired")).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const WalletSubmission submission = m_wallet->submitPublicTransaction({
|
||||||
|
plan.value(QStringLiteral("programId")).toString(),
|
||||||
|
accountIds,
|
||||||
|
signingRequirements,
|
||||||
|
instruction,
|
||||||
|
});
|
||||||
|
const QString transactionId = submission.accepted()
|
||||||
|
? base58TransactionId(submission.nativeHash) : QString();
|
||||||
|
if (transactionId.isEmpty()) {
|
||||||
|
finish(publicError(QStringLiteral("wallet_submission_failed")).toVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finish(QJsonObject {
|
||||||
|
{ QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
|
||||||
|
{ QStringLiteral("status"), QStringLiteral("submitted") },
|
||||||
|
{ QStringLiteral("transactionId"), transactionId },
|
||||||
|
{ QStringLiteral("nativeTransactionHash"), submission.nativeHash },
|
||||||
|
{ QStringLiteral("deadlineMs"), plan.value(QStringLiteral("deadlineMs")) },
|
||||||
|
{ QStringLiteral("affectedAccountIds"),
|
||||||
|
plan.value(QStringLiteral("affectedAccountIds")) },
|
||||||
|
}.toVariantMap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QVariantMap NewPositionRuntime::context(const QVariantMap& request,
|
QVariantMap NewPositionRuntime::context(const QVariantMap& request,
|
||||||
const ActiveNetworkSnapshot& network,
|
const ActiveNetworkSnapshot& network,
|
||||||
bool walletOpen,
|
bool walletOpen,
|
||||||
@ -385,6 +764,7 @@ QVariantMap NewPositionRuntime::submit(const QVariantMap& request,
|
|||||||
{ QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
|
{ QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
|
||||||
{ QStringLiteral("status"), QStringLiteral("submitted") },
|
{ QStringLiteral("status"), QStringLiteral("submitted") },
|
||||||
{ QStringLiteral("transactionId"), transactionId },
|
{ QStringLiteral("transactionId"), transactionId },
|
||||||
|
{ QStringLiteral("nativeTransactionHash"), submission.nativeHash },
|
||||||
{ QStringLiteral("deadlineMs"), plan.value(QStringLiteral("deadlineMs")) },
|
{ QStringLiteral("deadlineMs"), plan.value(QStringLiteral("deadlineMs")) },
|
||||||
}.toVariantMap();
|
}.toVariantMap();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,17 +4,43 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariantMap>
|
#include <QVariantMap>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include "ActiveNetwork.h"
|
#include "ActiveNetwork.h"
|
||||||
|
|
||||||
class AmmClient;
|
class AmmClient;
|
||||||
class WalletProvider;
|
class WalletProvider;
|
||||||
|
class SequencerClient;
|
||||||
|
struct WalletAccount;
|
||||||
|
|
||||||
class NewPositionRuntime {
|
class NewPositionRuntime {
|
||||||
public:
|
public:
|
||||||
NewPositionRuntime(WalletProvider* wallet, AmmClient* client);
|
using ResultCallback = std::function<void(QVariantMap)>;
|
||||||
|
|
||||||
|
NewPositionRuntime(WalletProvider* wallet,
|
||||||
|
AmmClient* client,
|
||||||
|
SequencerClient* sequencer = nullptr);
|
||||||
|
|
||||||
void clearWalletAccounts();
|
void clearWalletAccounts();
|
||||||
|
void setWalletAccounts(const QVector<WalletAccount>& accounts);
|
||||||
|
|
||||||
|
void contextAsync(const QVariantMap& request,
|
||||||
|
const ActiveNetworkSnapshot& network,
|
||||||
|
bool walletOpen,
|
||||||
|
bool refreshPublicData,
|
||||||
|
ResultCallback callback);
|
||||||
|
void quoteAsync(const QVariantMap& request,
|
||||||
|
const ActiveNetworkSnapshot& network,
|
||||||
|
bool walletOpen,
|
||||||
|
bool forceRefresh,
|
||||||
|
ResultCallback callback);
|
||||||
|
void submitAsync(const QVariantMap& request,
|
||||||
|
const QString& quoteHash,
|
||||||
|
const ActiveNetworkSnapshot& network,
|
||||||
|
bool walletCanSubmit,
|
||||||
|
ResultCallback callback);
|
||||||
|
|
||||||
QVariantMap context(const QVariantMap& request,
|
QVariantMap context(const QVariantMap& request,
|
||||||
const ActiveNetworkSnapshot& network,
|
const ActiveNetworkSnapshot& network,
|
||||||
@ -35,8 +61,15 @@ private:
|
|||||||
bool walletOpen,
|
bool walletOpen,
|
||||||
bool freshWalletAccounts,
|
bool freshWalletAccounts,
|
||||||
QJsonObject* error) const;
|
QJsonObject* error) const;
|
||||||
|
void buildQuoteInputAsync(const QVariantMap& request,
|
||||||
|
const ActiveNetworkSnapshot& network,
|
||||||
|
bool walletOpen,
|
||||||
|
bool forceRefresh,
|
||||||
|
std::function<void(QJsonObject, QJsonObject)> callback) const;
|
||||||
|
|
||||||
WalletProvider* m_wallet;
|
WalletProvider* m_wallet;
|
||||||
AmmClient* m_client;
|
AmmClient* m_client;
|
||||||
|
SequencerClient* m_sequencer;
|
||||||
|
QStringList m_walletPublicAccountIds;
|
||||||
bool m_submitInFlight = false;
|
bool m_submitInFlight = false;
|
||||||
};
|
};
|
||||||
|
|||||||
302
apps/amm/src/SequencerClient.cpp
Normal file
302
apps/amm/src/SequencerClient.cpp
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
#include "SequencerClient.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <libbase58.h>
|
||||||
|
|
||||||
|
#include "AmmClient.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int MAX_CONCURRENT_READS = 8;
|
||||||
|
constexpr qsizetype ACCOUNT_ID_HEX_SIZE = 64;
|
||||||
|
constexpr std::size_t BASE58_BUFFER_SIZE = 45;
|
||||||
|
|
||||||
|
bool isLowerHex(const QString& value, qsizetype size)
|
||||||
|
{
|
||||||
|
if (value.size() != size)
|
||||||
|
return false;
|
||||||
|
return std::all_of(value.cbegin(), value.cend(), [](QChar character) {
|
||||||
|
return character.isDigit()
|
||||||
|
|| (character >= QLatin1Char('a') && character <= QLatin1Char('f'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QString accountIdBase58(const QString& accountId)
|
||||||
|
{
|
||||||
|
if (!isLowerHex(accountId, ACCOUNT_ID_HEX_SIZE))
|
||||||
|
return {};
|
||||||
|
const QByteArray bytes = QByteArray::fromHex(accountId.toLatin1());
|
||||||
|
std::array<char, BASE58_BUFFER_SIZE> encoded {};
|
||||||
|
std::size_t size = encoded.size();
|
||||||
|
if (!b58enc(encoded.data(), &size, bytes.constData(),
|
||||||
|
static_cast<std::size_t>(bytes.size()))) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return QString::fromLatin1(encoded.data(), static_cast<qsizetype>(size - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray rpcBody(const QString& method, const QJsonArray& params)
|
||||||
|
{
|
||||||
|
return QJsonDocument(QJsonObject {
|
||||||
|
{ QStringLiteral("jsonrpc"), QStringLiteral("2.0") },
|
||||||
|
{ QStringLiteral("id"), 1 },
|
||||||
|
{ QStringLiteral("method"), method },
|
||||||
|
{ QStringLiteral("params"), params },
|
||||||
|
}).toJson(QJsonDocument::Compact);
|
||||||
|
}
|
||||||
|
|
||||||
|
WalletAccountRead accountRead(const QJsonObject& value, const QString& fallbackId)
|
||||||
|
{
|
||||||
|
WalletAccountRead read;
|
||||||
|
read.accountId = value.value(QStringLiteral("id")).toString(fallbackId);
|
||||||
|
read.status = value.value(QStringLiteral("status")).toString(
|
||||||
|
QStringLiteral("read_failed"));
|
||||||
|
const QJsonObject account = value.value(QStringLiteral("account")).toObject();
|
||||||
|
if (read.ok()) {
|
||||||
|
read.programOwner = account.value(QStringLiteral("program_owner")).toString();
|
||||||
|
read.balanceHex = account.value(QStringLiteral("balance")).toString();
|
||||||
|
read.nonceHex = account.value(QStringLiteral("nonce")).toString();
|
||||||
|
read.dataHex = account.value(QStringLiteral("data")).toString();
|
||||||
|
}
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SequencerClient::SequencerClient(AmmClient* client, QObject* parent)
|
||||||
|
: QObject(parent),
|
||||||
|
m_client(client),
|
||||||
|
m_network(new QNetworkAccessManager(this))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SequencerClient::configure(const QString& configPath)
|
||||||
|
{
|
||||||
|
++m_generation;
|
||||||
|
cancelPendingReads();
|
||||||
|
clear();
|
||||||
|
QFile file(configPath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
m_endpoint = QUrl();
|
||||||
|
m_authorization.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
|
||||||
|
const QJsonObject config = document.object();
|
||||||
|
const QUrl endpoint(config.value(QStringLiteral("sequencer_addr")).toString());
|
||||||
|
const QString scheme = endpoint.scheme().toLower();
|
||||||
|
if (!document.isObject() || !endpoint.isValid() || endpoint.host().isEmpty()
|
||||||
|
|| (scheme != QStringLiteral("http") && scheme != QStringLiteral("https"))) {
|
||||||
|
m_endpoint = QUrl();
|
||||||
|
m_authorization.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_endpoint = endpoint;
|
||||||
|
m_authorization.clear();
|
||||||
|
const QJsonObject basicAuth = config.value(QStringLiteral("basic_auth")).toObject();
|
||||||
|
const QString username = basicAuth.value(QStringLiteral("username")).toString();
|
||||||
|
const QString password = basicAuth.value(QStringLiteral("password")).toString();
|
||||||
|
if (!username.isEmpty()) {
|
||||||
|
m_authorization = QByteArrayLiteral("Basic ")
|
||||||
|
+ (username + QLatin1Char(':') + password).toUtf8().toBase64();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerClient::readAccounts(const QStringList& accountIds,
|
||||||
|
bool forceRefresh,
|
||||||
|
AccountsCallback callback)
|
||||||
|
{
|
||||||
|
if (accountIds.isEmpty()) {
|
||||||
|
QTimer::singleShot(0, this, [callback = std::move(callback)]() mutable {
|
||||||
|
callback({});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Batch {
|
||||||
|
QVector<WalletAccountRead> reads;
|
||||||
|
qsizetype remaining = 0;
|
||||||
|
AccountsCallback callback;
|
||||||
|
};
|
||||||
|
auto batch = std::make_shared<Batch>();
|
||||||
|
batch->reads.resize(accountIds.size());
|
||||||
|
batch->remaining = accountIds.size();
|
||||||
|
batch->callback = std::move(callback);
|
||||||
|
for (qsizetype index = 0; index < accountIds.size(); ++index) {
|
||||||
|
readAccount(accountIds.at(index), forceRefresh,
|
||||||
|
[batch, index](WalletAccountRead read) mutable {
|
||||||
|
batch->reads[index] = std::move(read);
|
||||||
|
if (--batch->remaining == 0)
|
||||||
|
batch->callback(std::move(batch->reads));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerClient::readAccount(const QString& accountId,
|
||||||
|
bool forceRefresh,
|
||||||
|
AccountCallback callback)
|
||||||
|
{
|
||||||
|
if (!isConfigured() || !isLowerHex(accountId, ACCOUNT_ID_HEX_SIZE)) {
|
||||||
|
QTimer::singleShot(0, this,
|
||||||
|
[callback = std::move(callback), accountId]() mutable {
|
||||||
|
callback(WalletAccountRead { accountId });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!forceRefresh && m_cache.contains(accountId)) {
|
||||||
|
const WalletAccountRead cached = m_cache.value(accountId);
|
||||||
|
QTimer::singleShot(0, this,
|
||||||
|
[callback = std::move(callback), cached]() mutable { callback(cached); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool alreadyPending = m_waiters.contains(accountId);
|
||||||
|
m_waiters[accountId].append(std::move(callback));
|
||||||
|
if (!alreadyPending)
|
||||||
|
m_pending.enqueue({ accountId });
|
||||||
|
startPendingReads();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerClient::startPendingReads()
|
||||||
|
{
|
||||||
|
while (m_activeReads < MAX_CONCURRENT_READS && !m_pending.isEmpty()) {
|
||||||
|
const PendingRead pending = m_pending.dequeue();
|
||||||
|
++m_activeReads;
|
||||||
|
startRead(pending.accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerClient::startRead(const QString& accountId)
|
||||||
|
{
|
||||||
|
const quint64 generation = m_generation;
|
||||||
|
const QString encodedId = accountIdBase58(accountId);
|
||||||
|
if (encodedId.isEmpty()) {
|
||||||
|
completeRead(accountId, WalletAccountRead { accountId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply* reply = m_network->post(
|
||||||
|
request(), rpcBody(QStringLiteral("getAccount"), QJsonArray { encodedId }));
|
||||||
|
connect(reply, &QNetworkReply::finished, this,
|
||||||
|
[this, reply, accountId, generation]() {
|
||||||
|
if (generation != m_generation) {
|
||||||
|
reply->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WalletAccountRead read { accountId };
|
||||||
|
const int status = reply->attribute(
|
||||||
|
QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
const QByteArray payload = reply->readAll();
|
||||||
|
if (reply->error() == QNetworkReply::NoError
|
||||||
|
&& status >= 200 && status < 300) {
|
||||||
|
const AmmClientResult normalized =
|
||||||
|
m_client->normalizeAccountRpc(QJsonObject {
|
||||||
|
{ QStringLiteral("accountId"), accountId },
|
||||||
|
{ QStringLiteral("response"), QString::fromUtf8(payload) },
|
||||||
|
});
|
||||||
|
if (normalized.ok)
|
||||||
|
read = accountRead(normalized.value, accountId);
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
completeRead(accountId, read);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerClient::completeRead(const QString& accountId,
|
||||||
|
const WalletAccountRead& read)
|
||||||
|
{
|
||||||
|
if (read.ok())
|
||||||
|
m_cache.insert(accountId, read);
|
||||||
|
const QVector<AccountCallback> callbacks = m_waiters.take(accountId);
|
||||||
|
--m_activeReads;
|
||||||
|
for (const AccountCallback& callback : callbacks)
|
||||||
|
callback(read);
|
||||||
|
startPendingReads();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerClient::queryTransaction(const QString& nativeHash,
|
||||||
|
TransactionCallback callback)
|
||||||
|
{
|
||||||
|
if (!isConfigured() || !isLowerHex(nativeHash, ACCOUNT_ID_HEX_SIZE)) {
|
||||||
|
QTimer::singleShot(0, this,
|
||||||
|
[callback = std::move(callback)]() mutable { callback(false, false); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const quint64 generation = m_generation;
|
||||||
|
QNetworkReply* reply = m_network->post(
|
||||||
|
request(), rpcBody(QStringLiteral("getTransaction"), QJsonArray { nativeHash }));
|
||||||
|
connect(reply, &QNetworkReply::finished, this,
|
||||||
|
[this, reply, generation, callback = std::move(callback)]() mutable {
|
||||||
|
if (generation != m_generation) {
|
||||||
|
reply->deleteLater();
|
||||||
|
callback(false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int status = reply->attribute(
|
||||||
|
QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
const QByteArray payload = reply->readAll();
|
||||||
|
QJsonParseError error;
|
||||||
|
const QJsonDocument document = QJsonDocument::fromJson(payload, &error);
|
||||||
|
const QJsonObject envelope = document.object();
|
||||||
|
const bool ok = reply->error() == QNetworkReply::NoError
|
||||||
|
&& status >= 200 && status < 300
|
||||||
|
&& error.error == QJsonParseError::NoError
|
||||||
|
&& document.isObject()
|
||||||
|
&& (!envelope.contains(QStringLiteral("error"))
|
||||||
|
|| envelope.value(QStringLiteral("error")).isNull())
|
||||||
|
&& envelope.contains(QStringLiteral("result"));
|
||||||
|
const bool included = ok
|
||||||
|
&& !envelope.value(QStringLiteral("result")).isNull();
|
||||||
|
reply->deleteLater();
|
||||||
|
callback(ok, included);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerClient::clear()
|
||||||
|
{
|
||||||
|
m_cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerClient::cancelPendingReads()
|
||||||
|
{
|
||||||
|
decltype(m_waiters) waiters;
|
||||||
|
waiters.swap(m_waiters);
|
||||||
|
m_pending.clear();
|
||||||
|
m_activeReads = 0;
|
||||||
|
for (auto iterator = waiters.cbegin(); iterator != waiters.cend(); ++iterator) {
|
||||||
|
const WalletAccountRead failed { iterator.key() };
|
||||||
|
for (const AccountCallback& callback : iterator.value()) {
|
||||||
|
QTimer::singleShot(0, this, [callback, failed]() {
|
||||||
|
callback(failed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerClient::applyAuthorization(QNetworkRequest& request) const
|
||||||
|
{
|
||||||
|
if (!m_authorization.isEmpty())
|
||||||
|
request.setRawHeader(QByteArrayLiteral("Authorization"), m_authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkRequest SequencerClient::request() const
|
||||||
|
{
|
||||||
|
QNetworkRequest request(m_endpoint);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader,
|
||||||
|
QStringLiteral("application/json"));
|
||||||
|
request.setTransferTimeout(4000);
|
||||||
|
applyAuthorization(request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
61
apps/amm/src/SequencerClient.h
Normal file
61
apps/amm/src/SequencerClient.h
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQueue>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
#include "WalletProvider.h"
|
||||||
|
|
||||||
|
class AmmClient;
|
||||||
|
class QNetworkAccessManager;
|
||||||
|
class QNetworkRequest;
|
||||||
|
|
||||||
|
class SequencerClient final : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
using AccountsCallback = std::function<void(QVector<WalletAccountRead>)>;
|
||||||
|
using TransactionCallback = std::function<void(bool ok, bool included)>;
|
||||||
|
|
||||||
|
explicit SequencerClient(AmmClient* client, QObject* parent = nullptr);
|
||||||
|
|
||||||
|
bool configure(const QString& configPath);
|
||||||
|
QString endpoint() const { return m_endpoint.toString(); }
|
||||||
|
bool isConfigured() const { return m_endpoint.isValid() && !m_endpoint.isEmpty(); }
|
||||||
|
void applyAuthorization(QNetworkRequest& request) const;
|
||||||
|
|
||||||
|
void readAccounts(const QStringList& accountIds,
|
||||||
|
bool forceRefresh,
|
||||||
|
AccountsCallback callback);
|
||||||
|
void queryTransaction(const QString& nativeHash, TransactionCallback callback);
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
private:
|
||||||
|
using AccountCallback = std::function<void(WalletAccountRead)>;
|
||||||
|
|
||||||
|
struct PendingRead {
|
||||||
|
QString accountId;
|
||||||
|
};
|
||||||
|
|
||||||
|
void readAccount(const QString& accountId, bool forceRefresh, AccountCallback callback);
|
||||||
|
void startPendingReads();
|
||||||
|
void startRead(const QString& accountId);
|
||||||
|
void completeRead(const QString& accountId, const WalletAccountRead& read);
|
||||||
|
void cancelPendingReads();
|
||||||
|
QNetworkRequest request() const;
|
||||||
|
|
||||||
|
AmmClient* m_client;
|
||||||
|
QNetworkAccessManager* m_network;
|
||||||
|
QUrl m_endpoint;
|
||||||
|
QByteArray m_authorization;
|
||||||
|
QHash<QString, WalletAccountRead> m_cache;
|
||||||
|
QHash<QString, QVector<AccountCallback>> m_waiters;
|
||||||
|
QQueue<PendingRead> m_pending;
|
||||||
|
int m_activeReads = 0;
|
||||||
|
quint64 m_generation = 0;
|
||||||
|
};
|
||||||
@ -19,6 +19,11 @@ namespace {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void connectAsync(const WalletPaths&, SessionCallback callback) override
|
||||||
|
{
|
||||||
|
callback({});
|
||||||
|
}
|
||||||
|
|
||||||
WalletCreation createWallet(const WalletPaths&, const QString&) override
|
WalletCreation createWallet(const WalletPaths&, const QString&) override
|
||||||
{
|
{
|
||||||
return {};
|
return {};
|
||||||
@ -29,6 +34,11 @@ namespace {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void snapshotAsync(bool, SnapshotCallback callback) override
|
||||||
|
{
|
||||||
|
callback({});
|
||||||
|
}
|
||||||
|
|
||||||
void clearSnapshot() override {}
|
void clearSnapshot() override {}
|
||||||
|
|
||||||
WalletAccountCreation createAccount(bool isPublic) override
|
WalletAccountCreation createAccount(bool isPublic) override
|
||||||
@ -102,7 +112,7 @@ namespace {
|
|||||||
AmmClientResult quote(const QJsonObject&) const override
|
AmmClientResult quote(const QJsonObject&) const override
|
||||||
{
|
{
|
||||||
return success({
|
return success({
|
||||||
{ QStringLiteral("schema"), QStringLiteral("new-position.v1") },
|
{ QStringLiteral("schema"), QStringLiteral("new-position.v2") },
|
||||||
{ QStringLiteral("status"), QStringLiteral("ok") },
|
{ QStringLiteral("status"), QStringLiteral("ok") },
|
||||||
{ QStringLiteral("canSubmit"), true },
|
{ QStringLiteral("canSubmit"), true },
|
||||||
{ QStringLiteral("quoteHash"), quoteHash },
|
{ QStringLiteral("quoteHash"), quoteHash },
|
||||||
@ -116,6 +126,8 @@ namespace {
|
|||||||
return success({
|
return success({
|
||||||
{ QStringLiteral("status"), QStringLiteral("ready") },
|
{ QStringLiteral("status"), QStringLiteral("ready") },
|
||||||
{ QStringLiteral("accountIds"), QJsonArray { QStringLiteral("account") } },
|
{ QStringLiteral("accountIds"), QJsonArray { QStringLiteral("account") } },
|
||||||
|
{ QStringLiteral("affectedAccountIds"),
|
||||||
|
QJsonArray { QStringLiteral("account") } },
|
||||||
{ QStringLiteral("signingRequirements"), QJsonArray { true } },
|
{ QStringLiteral("signingRequirements"), QJsonArray { true } },
|
||||||
{ QStringLiteral("instruction"), QJsonArray { 1 } },
|
{ QStringLiteral("instruction"), QJsonArray { 1 } },
|
||||||
{ QStringLiteral("programId"), QStringLiteral("program") },
|
{ QStringLiteral("programId"), QStringLiteral("program") },
|
||||||
@ -124,6 +136,11 @@ namespace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AmmClientResult normalizeAccountRpc(const QJsonObject&) const override
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
static AmmClientResult success(const QJsonObject& value)
|
static AmmClientResult success(const QJsonObject& value)
|
||||||
{
|
{
|
||||||
return { true, value };
|
return { true, value };
|
||||||
@ -149,7 +166,7 @@ namespace {
|
|||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
const QVariantMap request {
|
const QVariantMap request {
|
||||||
{ QStringLiteral("schema"), QStringLiteral("new-position.v1") },
|
{ QStringLiteral("schema"), QStringLiteral("new-position.v2") },
|
||||||
{ QStringLiteral("tokenAId"), QStringLiteral("token-a") },
|
{ QStringLiteral("tokenAId"), QStringLiteral("token-a") },
|
||||||
{ QStringLiteral("tokenBId"), QStringLiteral("token-b") },
|
{ QStringLiteral("tokenBId"), QStringLiteral("token-b") },
|
||||||
{ QStringLiteral("feeBps"), 30 },
|
{ QStringLiteral("feeBps"), 30 },
|
||||||
@ -168,6 +185,10 @@ int main()
|
|||||||
== QStringLiteral("1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"),
|
== QStringLiteral("1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"),
|
||||||
"native hash should become the expected base58 transaction ID"))
|
"native hash should become the expected base58 transaction ID"))
|
||||||
return 1;
|
return 1;
|
||||||
|
if (!expect(result.value(QStringLiteral("nativeTransactionHash")).toString()
|
||||||
|
== wallet.transactionHash,
|
||||||
|
"submitted result should preserve the native hash for polling"))
|
||||||
|
return 1;
|
||||||
if (!expect(wallet.createdAccounts == 1 && client.sawFreshLp,
|
if (!expect(wallet.createdAccounts == 1 && client.sawFreshLp,
|
||||||
"fresh LP account should enter the plan"))
|
"fresh LP account should enter the plan"))
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@ -18,6 +18,11 @@ TestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SignalSpy {
|
||||||
|
id: editedSpy
|
||||||
|
signalName: "snapshotEdited"
|
||||||
|
}
|
||||||
|
|
||||||
function test_protocolActionsUseDisplayLabels() {
|
function test_protocolActionsUseDisplayLabels() {
|
||||||
var summary = createTemporaryObject(summaryComponent, testCase)
|
var summary = createTemporaryObject(summaryComponent, testCase)
|
||||||
verify(summary)
|
verify(summary)
|
||||||
@ -26,4 +31,32 @@ TestCase {
|
|||||||
compare(summary.actionText("AddLiquidity"), "Add liquidity")
|
compare(summary.actionText("AddLiquidity"), "Add liquidity")
|
||||||
compare(summary.actionText(""), "-")
|
compare(summary.actionText(""), "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function test_lpDestinationOffersExistingHoldingsAndCreateNew() {
|
||||||
|
var summary = createTemporaryObject(summaryComponent, testCase, {
|
||||||
|
"snapshot": {
|
||||||
|
"instruction": "AddLiquidity",
|
||||||
|
"request": ({ "schema": "new-position.v2" }),
|
||||||
|
"lpHoldingOptions": [{
|
||||||
|
"holdingId": "44444444444444444444444444444444",
|
||||||
|
"balanceRaw": "7"
|
||||||
|
}],
|
||||||
|
"lpDestinationRequired": true,
|
||||||
|
"quoteReady": false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
verify(summary)
|
||||||
|
editedSpy.target = summary
|
||||||
|
editedSpy.clear()
|
||||||
|
|
||||||
|
var rows = summary.destinationRows()
|
||||||
|
compare(rows.length, 2)
|
||||||
|
compare(rows[1].createFresh, true)
|
||||||
|
summary.selectDestination(rows[0])
|
||||||
|
compare(editedSpy.count, 1)
|
||||||
|
compare(editedSpy.signalArguments[0][0].request.lpHoldingId,
|
||||||
|
"44444444444444444444444444444444")
|
||||||
|
compare(editedSpy.signalArguments[0][0].quoteReady, false)
|
||||||
|
editedSpy.target = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,26 +17,54 @@ TestCase {
|
|||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
property bool walletStateReady: false
|
property bool walletStateReady: false
|
||||||
|
property bool walletCanSubmit: true
|
||||||
|
property bool isWalletOpen: true
|
||||||
|
property string walletSyncStatus: "ready"
|
||||||
property var submitResult: ({})
|
property var submitResult: ({})
|
||||||
property var quoteResult: ({
|
property var quoteResult: ({
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"poolStatus": "missing_pool"
|
"poolStatus": "missing_pool"
|
||||||
})
|
})
|
||||||
|
property var newPositionQuoteResult: ({})
|
||||||
|
property var newPositionSubmitResult: ({})
|
||||||
property var newPositionContext: ({
|
property var newPositionContext: ({
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "ready",
|
"status": "ready",
|
||||||
"tokens": [],
|
"tokens": [],
|
||||||
"feeTiers": []
|
"feeTiers": []
|
||||||
})
|
})
|
||||||
|
property int contextRefreshCalls: 0
|
||||||
|
property int submitCalls: 0
|
||||||
|
property var lastContextRefreshRequest: ({})
|
||||||
|
|
||||||
function submitNewPosition(request, quoteHash) {
|
function requestNewPositionSubmit(request, quoteHash, requestId) {
|
||||||
return submitResult
|
++submitCalls
|
||||||
|
var result = JSON.parse(JSON.stringify(submitResult || ({})))
|
||||||
|
result.requestId = requestId
|
||||||
|
newPositionSubmitResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
function quoteNewPosition(request) {
|
function requestNewPositionQuote(request, requestId, forceRefresh) {
|
||||||
return quoteResult
|
var result = JSON.parse(JSON.stringify(quoteResult || ({})))
|
||||||
|
result.requestId = requestId
|
||||||
|
newPositionQuoteResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshNewPositionContext(request) {
|
||||||
|
++contextRefreshCalls
|
||||||
|
lastContextRefreshRequest = request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: pageComponent
|
||||||
|
|
||||||
|
Pages.LiquidityPage {
|
||||||
|
visible: false
|
||||||
|
width: 800
|
||||||
|
height: 600
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,110 +80,48 @@ TestCase {
|
|||||||
verify(rail)
|
verify(rail)
|
||||||
verify(compactSteps)
|
verify(compactSteps)
|
||||||
verify(form)
|
verify(form)
|
||||||
|
|
||||||
compare(page.wideLayout, true)
|
compare(page.wideLayout, true)
|
||||||
verify(rail.width > 0)
|
|
||||||
verify(form.width > rail.width)
|
verify(form.width > rail.width)
|
||||||
|
|
||||||
page.width = 600
|
page.width = 600
|
||||||
wait(0)
|
wait(0)
|
||||||
|
|
||||||
compare(page.wideLayout, false)
|
compare(page.wideLayout, false)
|
||||||
verify(compactSteps.implicitHeight > 0)
|
verify(compactSteps.implicitHeight > 0)
|
||||||
verify(form.width > 0)
|
|
||||||
verify(form.width <= page.width - 32)
|
verify(form.width <= page.width - 32)
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
function test_contextAvailableWhileWalletSyncs() {
|
||||||
id: runtimeComponent
|
var backend = createTemporaryObject(backendComponent, testCase, {
|
||||||
|
"walletCanSubmit": false,
|
||||||
QtObject {
|
"walletSyncStatus": "syncing"
|
||||||
function watch(value, succeeded, failed) {
|
|
||||||
succeeded(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: pageComponent
|
|
||||||
|
|
||||||
Pages.LiquidityPage {
|
|
||||||
visible: false
|
|
||||||
width: 800
|
|
||||||
height: 600
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_contextWaitsForWalletState() {
|
|
||||||
var backend = createTemporaryObject(backendComponent, testCase)
|
|
||||||
var page = createTemporaryObject(pageComponent, testCase, {
|
|
||||||
"backend": backend
|
|
||||||
})
|
})
|
||||||
verify(backend)
|
var page = createTemporaryObject(pageComponent, testCase, { "backend": backend })
|
||||||
verify(page)
|
verify(page)
|
||||||
|
|
||||||
compare(page.flow.newPositionContext.status, "loading")
|
compare(page.flow.newPositionContext.status, "ready")
|
||||||
|
compare(page.flow.viewState.walletSyncStatus, "syncing")
|
||||||
backend.walletStateReady = true
|
verify(!page.flow.walletCanSubmit)
|
||||||
tryCompare(page.flow.newPositionContext, "status", "ready")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_contextRefreshControlsWalletScan() {
|
function test_contextRefreshControlsPublicRefresh() {
|
||||||
var backend = createTemporaryObject(backendComponent, testCase, {
|
var backend = createTemporaryObject(backendComponent, testCase)
|
||||||
"walletStateReady": true
|
var page = createTemporaryObject(pageComponent, testCase, { "backend": backend })
|
||||||
})
|
|
||||||
var page = createTemporaryObject(pageComponent, testCase, {
|
|
||||||
"backend": backend
|
|
||||||
})
|
|
||||||
verify(backend)
|
|
||||||
verify(page)
|
verify(page)
|
||||||
|
|
||||||
compare(page.flow.contextHints(false).refreshWalletAccounts, false)
|
compare(page.flow.contextHints(false).refreshWalletAccounts, false)
|
||||||
compare(page.flow.contextHints(true).refreshWalletAccounts, true)
|
compare(page.flow.contextHints(true).refreshWalletAccounts, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_staleContextCompletionCannotFinishNewerRefresh() {
|
|
||||||
var backend = createTemporaryObject(backendComponent, testCase, {
|
|
||||||
"walletStateReady": true
|
|
||||||
})
|
|
||||||
var page = createTemporaryObject(pageComponent, testCase, {
|
|
||||||
"backend": backend
|
|
||||||
})
|
|
||||||
verify(backend)
|
|
||||||
verify(page)
|
|
||||||
|
|
||||||
page.flow.contextSerial = 2
|
|
||||||
page.flow.contextLoading = true
|
|
||||||
page.flow.contextErrorCode = "newer_request_pending"
|
|
||||||
|
|
||||||
page.flow.finishContextRefresh(1, null)
|
|
||||||
page.flow.failContextRefresh(1)
|
|
||||||
|
|
||||||
compare(page.flow.contextLoading, true)
|
|
||||||
compare(page.flow.contextErrorCode, "newer_request_pending")
|
|
||||||
|
|
||||||
page.flow.finishContextRefresh(2, null)
|
|
||||||
compare(page.flow.contextLoading, false)
|
|
||||||
compare(page.flow.contextErrorCode, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_submitFailureKeepsReturnedFreshQuoteWithoutRequery() {
|
function test_submitFailureKeepsReturnedFreshQuoteWithoutRequery() {
|
||||||
var backend = createTemporaryObject(backendComponent, testCase, {
|
var backend = createTemporaryObject(backendComponent, testCase)
|
||||||
"walletStateReady": true
|
var page = createTemporaryObject(pageComponent, testCase, { "backend": backend })
|
||||||
})
|
|
||||||
var page = createTemporaryObject(pageComponent, testCase, {
|
|
||||||
"backend": backend
|
|
||||||
})
|
|
||||||
verify(backend)
|
|
||||||
verify(page)
|
|
||||||
|
|
||||||
page.flow.quoteSerial = 7
|
page.flow.quoteSerial = 7
|
||||||
page.flow.finishSubmitFailure({
|
page.flow.finishSubmitFailure({
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"code": "quote_not_submittable",
|
"code": "quote_not_submittable",
|
||||||
"quote": {
|
"quote": {
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"canSubmit": false,
|
"canSubmit": false,
|
||||||
"quoteHash": "sha256:fresh"
|
"quoteHash": "sha256:fresh"
|
||||||
@ -167,61 +133,49 @@ TestCase {
|
|||||||
compare(page.flow.quoteStale, false)
|
compare(page.flow.quoteStale, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_base58SubmittedResultEntersSuccessState() {
|
function test_submittedResultEntersSuccessState() {
|
||||||
var backend = createTemporaryObject(backendComponent, testCase, {
|
var backend = createTemporaryObject(backendComponent, testCase, {
|
||||||
"walletStateReady": true,
|
|
||||||
"submitResult": {
|
"submitResult": {
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "submitted",
|
"status": "submitted",
|
||||||
"transactionId": submittedTransactionId,
|
"transactionId": submittedTransactionId,
|
||||||
"deadlineMs": String(Date.now() + 60000)
|
"deadlineMs": String(Date.now() + 60000),
|
||||||
|
"affectedAccountIds": []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
var runtime = createTemporaryObject(runtimeComponent, testCase)
|
var page = createTemporaryObject(pageComponent, testCase, { "backend": backend })
|
||||||
var page = createTemporaryObject(pageComponent, testCase, {
|
page.flow.confirm({ "request": ({}), "quoteHash": "sha256:expected" })
|
||||||
"backend": backend,
|
wait(0)
|
||||||
"runtime": runtime
|
|
||||||
})
|
|
||||||
verify(backend)
|
|
||||||
verify(runtime)
|
|
||||||
verify(page)
|
|
||||||
|
|
||||||
page.flow.confirm({
|
|
||||||
"request": ({}),
|
|
||||||
"quoteHash": "sha256:expected"
|
|
||||||
})
|
|
||||||
|
|
||||||
compare(page.flow.transactionId, submittedTransactionId)
|
compare(page.flow.transactionId, submittedTransactionId)
|
||||||
compare(page.flow.flowErrorCode, "")
|
compare(page.flow.flowErrorCode, "")
|
||||||
compare(page.flow.submitting, false)
|
compare(page.flow.submitting, false)
|
||||||
|
compare(backend.submitCalls, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_base58MissingPoolSubmissionStartsPoolWatch() {
|
function test_missingPoolSubmissionStartsPoolProbeWithoutWalletRefresh() {
|
||||||
var backend = createTemporaryObject(backendComponent, testCase, {
|
var backend = createTemporaryObject(backendComponent, testCase, {
|
||||||
"walletStateReady": true,
|
|
||||||
"submitResult": {
|
"submitResult": {
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "submitted",
|
"status": "submitted",
|
||||||
"transactionId": submittedTransactionId,
|
"transactionId": submittedTransactionId,
|
||||||
"deadlineMs": String(Date.now() + 60000)
|
"deadlineMs": String(Date.now() + 60000)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
var runtime = createTemporaryObject(runtimeComponent, testCase)
|
var page = createTemporaryObject(pageComponent, testCase, { "backend": backend })
|
||||||
var page = createTemporaryObject(pageComponent, testCase, {
|
|
||||||
"backend": backend,
|
|
||||||
"runtime": runtime
|
|
||||||
})
|
|
||||||
verify(backend)
|
|
||||||
verify(runtime)
|
|
||||||
verify(page)
|
|
||||||
|
|
||||||
var probe = {
|
var probe = {
|
||||||
"tokenAId": "22222222222222222222222222222222",
|
"tokenAId": "22222222222222222222222222222222",
|
||||||
"tokenBId": "33333333333333333333333333333333"
|
"tokenBId": "33333333333333333333333333333333"
|
||||||
}
|
}
|
||||||
page.flow.pendingQuoteRequest = { "ok": true, "request": probe }
|
page.flow.pendingQuoteRequest = {
|
||||||
|
"ok": true,
|
||||||
|
"request": probe
|
||||||
|
}
|
||||||
page.flow.confirm({
|
page.flow.confirm({
|
||||||
|
"instruction": "NewDefinition",
|
||||||
"request": {
|
"request": {
|
||||||
|
"tokenAId": probe.tokenAId,
|
||||||
|
"tokenBId": probe.tokenBId,
|
||||||
"initialPriceRealRaw": "18446744073709551616"
|
"initialPriceRealRaw": "18446744073709551616"
|
||||||
},
|
},
|
||||||
"poolProbeRequest": probe,
|
"poolProbeRequest": probe,
|
||||||
@ -229,51 +183,51 @@ TestCase {
|
|||||||
})
|
})
|
||||||
wait(0)
|
wait(0)
|
||||||
|
|
||||||
compare(page.flow.transactionId, submittedTransactionId)
|
|
||||||
compare(page.flow.pendingPoolProbes.length, 1)
|
compare(page.flow.pendingPoolProbes.length, 1)
|
||||||
compare(page.flow.selectedPoolCreationPending(), true)
|
compare(backend.contextRefreshCalls, 0)
|
||||||
compare(page.flow.poolProbeInFlight, false)
|
verify(page.flow.selectedPoolCreationPending())
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_nativeHexSubmittedResultDoesNotEnterSuccessState() {
|
function test_activePoolSubmissionDoesNotStartPoolCreationProbe() {
|
||||||
var backend = createTemporaryObject(backendComponent, testCase, {
|
var backend = createTemporaryObject(backendComponent, testCase, {
|
||||||
"walletStateReady": true,
|
|
||||||
"submitResult": {
|
"submitResult": {
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "submitted",
|
"status": "submitted",
|
||||||
"transactionId": "000102030405060708090a0b0c0d0e0f"
|
"transactionId": submittedTransactionId,
|
||||||
+ "101112131415161718191a1b1c1d1e1f"
|
"deadlineMs": String(Date.now() + 60000)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
var runtime = createTemporaryObject(runtimeComponent, testCase)
|
var page = createTemporaryObject(pageComponent, testCase, { "backend": backend })
|
||||||
var page = createTemporaryObject(pageComponent, testCase, {
|
|
||||||
"backend": backend,
|
|
||||||
"runtime": runtime
|
|
||||||
})
|
|
||||||
verify(backend)
|
|
||||||
verify(runtime)
|
|
||||||
verify(page)
|
|
||||||
|
|
||||||
page.flow.confirm({
|
page.flow.confirm({
|
||||||
|
"instruction": "AddLiquidity",
|
||||||
"request": ({}),
|
"request": ({}),
|
||||||
|
"poolProbeRequest": {
|
||||||
|
"tokenAId": "22222222222222222222222222222222",
|
||||||
|
"tokenBId": "33333333333333333333333333333333"
|
||||||
|
},
|
||||||
"quoteHash": "sha256:expected"
|
"quoteHash": "sha256:expected"
|
||||||
})
|
})
|
||||||
|
wait(0)
|
||||||
|
|
||||||
compare(page.flow.transactionId, "")
|
compare(page.flow.pendingPoolProbes.length, 0)
|
||||||
compare(page.flow.flowErrorCode, "wallet_submission_failed")
|
|
||||||
compare(page.flow.submitting, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_poolProbeDoesNotPublishProbeAmountsAsCurrentQuote() {
|
function test_walletSyncDisablesSubmissionOnly() {
|
||||||
var backend = createTemporaryObject(backendComponent, testCase, {
|
var backend = createTemporaryObject(backendComponent, testCase, {
|
||||||
"walletStateReady": true
|
"walletCanSubmit": false,
|
||||||
|
"walletSyncStatus": "syncing"
|
||||||
})
|
})
|
||||||
var page = createTemporaryObject(pageComponent, testCase, {
|
var page = createTemporaryObject(pageComponent, testCase, { "backend": backend })
|
||||||
"backend": backend
|
page.flow.confirm({ "request": ({}), "quoteHash": "sha256:expected" })
|
||||||
})
|
|
||||||
verify(backend)
|
|
||||||
verify(page)
|
|
||||||
|
|
||||||
|
compare(backend.submitCalls, 0)
|
||||||
|
compare(page.flow.flowErrorCode, "wallet_syncing")
|
||||||
|
verify(!page.flow.submitting)
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_poolProbeDoesNotPublishProbeAsCurrentQuote() {
|
||||||
|
var backend = createTemporaryObject(backendComponent, testCase)
|
||||||
|
var page = createTemporaryObject(pageComponent, testCase, { "backend": backend })
|
||||||
var request = {
|
var request = {
|
||||||
"tokenAId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
|
"tokenAId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
|
||||||
"tokenBId": "22222222222222222222222222222222"
|
"tokenBId": "22222222222222222222222222222222"
|
||||||
@ -282,54 +236,19 @@ TestCase {
|
|||||||
page.flow.pendingQuoteRequest = { "ok": true, "request": request }
|
page.flow.pendingQuoteRequest = { "ok": true, "request": request }
|
||||||
page.flow.pendingPoolProbes = [pending]
|
page.flow.pendingPoolProbes = [pending]
|
||||||
page.flow.newPositionQuote = {
|
page.flow.newPositionQuote = {
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"poolStatus": "missing_pool",
|
"poolStatus": "missing_pool"
|
||||||
"tokenAId": request.tokenAId,
|
|
||||||
"tokenBId": request.tokenBId
|
|
||||||
}
|
}
|
||||||
page.flow.quoteStale = false
|
page.flow.quoteStale = false
|
||||||
|
|
||||||
page.flow.finishPoolProbe(pending, {
|
page.flow.finishPoolProbe(pending, {
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"poolStatus": "active_pool",
|
"poolStatus": "active_pool"
|
||||||
"tokenAId": request.tokenAId,
|
|
||||||
"tokenBId": request.tokenBId
|
|
||||||
})
|
})
|
||||||
|
|
||||||
compare(page.flow.pendingPoolProbes.length, 0)
|
|
||||||
compare(page.flow.newPositionQuote.poolStatus, "missing_pool")
|
compare(page.flow.newPositionQuote.poolStatus, "missing_pool")
|
||||||
verify(page.flow.quoteStale)
|
verify(page.flow.quoteStale)
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_poolProbeStopsBlockingAfterTransactionDeadline() {
|
|
||||||
var backend = createTemporaryObject(backendComponent, testCase, {
|
|
||||||
"walletStateReady": true
|
|
||||||
})
|
|
||||||
var page = createTemporaryObject(pageComponent, testCase, {
|
|
||||||
"backend": backend
|
|
||||||
})
|
|
||||||
verify(backend)
|
|
||||||
verify(page)
|
|
||||||
|
|
||||||
var request = {
|
|
||||||
"tokenAId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
|
|
||||||
"tokenBId": "22222222222222222222222222222222"
|
|
||||||
}
|
|
||||||
var pending = {
|
|
||||||
"key": page.flow.pairKey(request),
|
|
||||||
"request": request,
|
|
||||||
"deadlineMs": Date.now() - 1
|
|
||||||
}
|
|
||||||
page.flow.pendingQuoteRequest = { "ok": true, "request": request }
|
|
||||||
page.flow.pendingPoolProbes = [pending]
|
|
||||||
page.flow.poolProbeInFlight = true
|
|
||||||
|
|
||||||
page.flow.finishPoolProbe(pending, null)
|
|
||||||
|
|
||||||
compare(page.flow.pendingPoolProbes.length, 0)
|
|
||||||
compare(page.flow.poolProbeInFlight, false)
|
|
||||||
verify(!page.flow.selectedPoolCreationPending())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,10 @@ TestCase {
|
|||||||
"name": "Low",
|
"name": "Low",
|
||||||
"totalSupplyRaw": "1000000",
|
"totalSupplyRaw": "1000000",
|
||||||
"balanceRaw": "1000",
|
"balanceRaw": "1000",
|
||||||
|
"holdings": [{
|
||||||
|
"holdingId": "44444444444444444444444444444444",
|
||||||
|
"balanceRaw": "1000"
|
||||||
|
}],
|
||||||
"selectable": true
|
"selectable": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -53,6 +57,10 @@ TestCase {
|
|||||||
"name": "High",
|
"name": "High",
|
||||||
"totalSupplyRaw": "1000000000000",
|
"totalSupplyRaw": "1000000000000",
|
||||||
"balanceRaw": "5000000000",
|
"balanceRaw": "5000000000",
|
||||||
|
"holdings": [{
|
||||||
|
"holdingId": "55555555555555555555555555555555",
|
||||||
|
"balanceRaw": "5000000000"
|
||||||
|
}],
|
||||||
"selectable": true
|
"selectable": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -68,6 +76,10 @@ TestCase {
|
|||||||
"name": "Sir Mints-a-Lot",
|
"name": "Sir Mints-a-Lot",
|
||||||
"totalSupplyRaw": "1000000000000",
|
"totalSupplyRaw": "1000000000000",
|
||||||
"balanceRaw": "1000000000",
|
"balanceRaw": "1000000000",
|
||||||
|
"holdings": [{
|
||||||
|
"holdingId": "44444444444444444444444444444444",
|
||||||
|
"balanceRaw": "1000000000"
|
||||||
|
}],
|
||||||
"selectable": true
|
"selectable": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -75,19 +87,55 @@ TestCase {
|
|||||||
"name": "Aurora",
|
"name": "Aurora",
|
||||||
"totalSupplyRaw": "1000000000000",
|
"totalSupplyRaw": "1000000000000",
|
||||||
"balanceRaw": "1000000000",
|
"balanceRaw": "1000000000",
|
||||||
|
"holdings": [{
|
||||||
|
"holdingId": "55555555555555555555555555555555",
|
||||||
|
"balanceRaw": "1000000000"
|
||||||
|
}],
|
||||||
"selectable": true
|
"selectable": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function holdingContext() {
|
||||||
|
return {
|
||||||
|
"status": "ready",
|
||||||
|
"tokens": [
|
||||||
|
{
|
||||||
|
"definitionId": tokenLow,
|
||||||
|
"name": "Low",
|
||||||
|
"balanceRaw": "10",
|
||||||
|
"selectable": true,
|
||||||
|
"holdings": [
|
||||||
|
{ "holdingId": "44444444444444444444444444444444",
|
||||||
|
"balanceRaw": "10" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"definitionId": tokenHigh,
|
||||||
|
"name": "High",
|
||||||
|
"balanceRaw": "50",
|
||||||
|
"selectable": true,
|
||||||
|
"holdings": [
|
||||||
|
{ "holdingId": "55555555555555555555555555555555",
|
||||||
|
"balanceRaw": "20" },
|
||||||
|
{ "holdingId": "66666666666666666666666666666666",
|
||||||
|
"balanceRaw": "30" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function flowState(quote) {
|
function flowState(quote) {
|
||||||
return {
|
return {
|
||||||
"quote": quote || ({}),
|
"quote": quote || ({}),
|
||||||
"contextLoading": false,
|
"contextLoading": false,
|
||||||
"quoteLoading": false,
|
"quoteLoading": false,
|
||||||
"quoteStale": false,
|
"quoteStale": false,
|
||||||
"submitting": false
|
"submitting": false,
|
||||||
|
"walletCanSubmit": true,
|
||||||
|
"walletSyncStatus": "ready"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +166,23 @@ TestCase {
|
|||||||
compare(form.amountB, "")
|
compare(form.amountB, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function test_holdingSelectionAutoSelectsOneAndRequiresExplicitMultiple() {
|
||||||
|
var form = createForm(holdingContext())
|
||||||
|
wait(0)
|
||||||
|
compare(form.selectedHoldingAId,
|
||||||
|
"44444444444444444444444444444444")
|
||||||
|
compare(form.selectedHoldingBId, "")
|
||||||
|
verify(!form.buildQuoteRequest().ok)
|
||||||
|
|
||||||
|
form.selectHolding("B", "66666666666666666666666666666666")
|
||||||
|
var built = form.buildQuoteRequest()
|
||||||
|
verify(built.ok)
|
||||||
|
compare(built.request.holdingAId,
|
||||||
|
"66666666666666666666666666666666")
|
||||||
|
compare(built.request.holdingBId,
|
||||||
|
"44444444444444444444444444444444")
|
||||||
|
}
|
||||||
|
|
||||||
function test_displayOrderMapsToCanonicalRequestAfterSwap() {
|
function test_displayOrderMapsToCanonicalRequestAfterSwap() {
|
||||||
var form = createForm()
|
var form = createForm()
|
||||||
var built = form.buildQuoteRequest()
|
var built = form.buildQuoteRequest()
|
||||||
@ -354,7 +419,7 @@ TestCase {
|
|||||||
form.minimumAmountBRaw = "2000000"
|
form.minimumAmountBRaw = "2000000"
|
||||||
|
|
||||||
verify(form.acceptPoolActivation({
|
verify(form.acceptPoolActivation({
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"tokenAId": tokenHigh,
|
"tokenAId": tokenHigh,
|
||||||
"tokenBId": tokenLow,
|
"tokenBId": tokenLow,
|
||||||
@ -378,7 +443,7 @@ TestCase {
|
|||||||
|
|
||||||
function test_staleQuoteErrorsDoNotMarkCurrentDraft() {
|
function test_staleQuoteErrorsDoNotMarkCurrentDraft() {
|
||||||
var quote = {
|
var quote = {
|
||||||
"schema": "new-position.v1",
|
"schema": "new-position.v2",
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"tokenAId": tokenHigh,
|
"tokenAId": tokenHigh,
|
||||||
"tokenBId": tokenLow,
|
"tokenBId": tokenLow,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user