mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
fix(tinyplace): read trading balance from tiny.place settlement RPC (#4218)
Co-authored-by: M3gA-Mind <elvin@mahadao.com>
This commit is contained in:
@@ -659,38 +659,68 @@ pub(crate) fn handle_tinyplace_registry_register(params: Map<String, Value>) ->
|
||||
/// render (it falls back to letting the backend reject an underfunded payment).
|
||||
async fn wallet_usdc_balance(address: &str) -> (Option<Value>, String) {
|
||||
// The wallet's `balances()` only reports NATIVE assets (SOL/ETH/…), never SPL
|
||||
// tokens — so query the SPL USDC balance directly from the configured Solana
|
||||
// cluster's RPC (getTokenAccountsByOwner for the cluster's USDC mint). RPC
|
||||
// failure → None (UI shows "Unknown"); RPC ok but no token account → 0.
|
||||
let cluster = crate::openhuman::wallet::solana_cluster();
|
||||
let mint = cluster.usdc_mint();
|
||||
let rpc_url = cluster.rpc_url();
|
||||
// tokens — so query the SPL USDC balance directly via
|
||||
// `getTokenAccountsByOwner` for the cluster's USDC mint. RPC failure → None
|
||||
// (UI shows "Unknown"); RPC ok but no token account → 0.
|
||||
//
|
||||
// TAURI-RUST / #4202: query tiny.place's own Solana settlement RPC FIRST, not
|
||||
// the bare public cluster RPC. tiny.place settles on its own RPC — the chain
|
||||
// where agent token accounts are actually funded — so reading the balance
|
||||
// from the public `api.mainnet-beta.solana.com` endpoint (which also rate-
|
||||
// limits / restricts `getTokenAccountsByOwner`) returned None and the confirm
|
||||
// card showed "Unknown" even with a funded, connected wallet. Use the same
|
||||
// ordered endpoints the payment path broadcasts through (tiny.place RPC →
|
||||
// public cluster fallback), so the displayed balance matches the chain the
|
||||
// payment will actually settle on.
|
||||
let mint = crate::openhuman::wallet::solana_cluster().usdc_mint();
|
||||
let endpoints = crate::openhuman::wallet::tinyplace_solana_rpc_endpoints();
|
||||
let body = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "getTokenAccountsByOwner",
|
||||
"params": [address, { "mint": mint }, { "encoding": "jsonParsed" }],
|
||||
});
|
||||
let json: Value = match reqwest::Client::new()
|
||||
.post(rpc_url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) => match resp.json().await {
|
||||
Ok(j) => j,
|
||||
let client = reqwest::Client::new();
|
||||
// Try each endpoint in priority order; only advance to the next one on a
|
||||
// transport / parse / malformed-shape failure (the endpoint couldn't give an
|
||||
// authoritative answer). A well-formed `/result/value` — including an empty
|
||||
// array (no token account = a real zero balance) — is authoritative and
|
||||
// returns immediately.
|
||||
let mut accounts: Option<Vec<Value>> = None;
|
||||
for rpc_url in &endpoints {
|
||||
// A settlement endpoint may embed a private provider token in its
|
||||
// path/query; redact to scheme+host before it reaches the logs.
|
||||
let safe_url = crate::openhuman::wallet::redact_rpc_url(rpc_url);
|
||||
let json: Value = match client.post(rpc_url).json(&body).send().await {
|
||||
Ok(resp) => match resp.json().await {
|
||||
Ok(j) => j,
|
||||
Err(e) => {
|
||||
log::warn!("{LOG_PREFIX} usdc balance: rpc parse failed at {safe_url}: {e}");
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("{LOG_PREFIX} usdc balance: rpc parse failed: {e}");
|
||||
return (None, address.to_string());
|
||||
log::warn!("{LOG_PREFIX} usdc balance: rpc send failed at {safe_url}: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match json.pointer("/result/value").and_then(Value::as_array) {
|
||||
Some(value) => {
|
||||
accounts = Some(value.clone());
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
log::warn!("{LOG_PREFIX} usdc balance: unexpected rpc shape at {safe_url}");
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("{LOG_PREFIX} usdc balance: rpc send failed: {e}");
|
||||
return (None, address.to_string());
|
||||
}
|
||||
};
|
||||
let Some(accounts) = json.pointer("/result/value").and_then(Value::as_array) else {
|
||||
log::warn!("{LOG_PREFIX} usdc balance: unexpected rpc shape");
|
||||
}
|
||||
let Some(accounts) = accounts else {
|
||||
log::warn!(
|
||||
"{LOG_PREFIX} usdc balance: no endpoint returned a usable response \
|
||||
(tried {} endpoint(s))",
|
||||
endpoints.len()
|
||||
);
|
||||
return (None, address.to_string());
|
||||
};
|
||||
// No token account = the ATA was never created = a real zero balance.
|
||||
@@ -711,7 +741,7 @@ async fn wallet_usdc_balance(address: &str) -> (Option<Value>, String) {
|
||||
),
|
||||
None => ("0".to_string(), "0".to_string(), 6),
|
||||
};
|
||||
log::debug!("{LOG_PREFIX} usdc balance for {address}: {formatted} (cluster={cluster:?})");
|
||||
log::debug!("{LOG_PREFIX} usdc balance for {address}: {formatted} USDC (mint={mint})");
|
||||
(
|
||||
Some(serde_json::json!({
|
||||
"raw": raw,
|
||||
@@ -5035,6 +5065,65 @@ mod tests {
|
||||
assert_eq!(price_from_params(&p).unwrap().asset, "SOL");
|
||||
}
|
||||
|
||||
/// #4202: the confirm-card balance must be read from tiny.place's own Solana
|
||||
/// settlement RPC (the chain where agent token accounts are funded), not the
|
||||
/// bare public cluster RPC. We point `TINYPLACE_SOLANA_RPC_URL` at a mock
|
||||
/// that answers `getTokenAccountsByOwner`; the balance must come back as
|
||||
/// `Some` with the parsed USDC amount. Before the fix `wallet_usdc_balance`
|
||||
/// queried the public cluster directly and ignored this endpoint, so the
|
||||
/// confirm card showed "Unknown".
|
||||
#[tokio::test]
|
||||
async fn wallet_usdc_balance_reads_from_tinyplace_settlement_rpc() {
|
||||
let _lock = crate::openhuman::config::TEST_ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
let app = axum::Router::new().route(
|
||||
"/",
|
||||
axum::routing::post(|| async {
|
||||
axum::Json(serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"context": { "slot": 1 },
|
||||
"value": [{
|
||||
"account": { "data": { "parsed": { "info": { "tokenAmount": {
|
||||
"amount": "12500000",
|
||||
"decimals": 6,
|
||||
"uiAmount": 12.5,
|
||||
"uiAmountString": "12.5"
|
||||
}}}}}
|
||||
}]
|
||||
}
|
||||
}))
|
||||
}),
|
||||
);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
|
||||
let url = format!("http://127.0.0.1:{}/", addr.port());
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("TINYPLACE_SOLANA_RPC_URL", &url);
|
||||
}
|
||||
let (balance, address) = wallet_usdc_balance("OwnerAddr1111111111").await;
|
||||
unsafe {
|
||||
std::env::remove_var("TINYPLACE_SOLANA_RPC_URL");
|
||||
}
|
||||
|
||||
assert_eq!(address, "OwnerAddr1111111111");
|
||||
let balance = balance.expect("balance must resolve from the tiny.place settlement RPC");
|
||||
assert_eq!(
|
||||
balance.get("formatted").and_then(Value::as_str),
|
||||
Some("12.5")
|
||||
);
|
||||
assert_eq!(
|
||||
balance.get("assetSymbol").and_then(Value::as_str),
|
||||
Some("USDC")
|
||||
);
|
||||
assert_eq!(balance.get("decimals").and_then(Value::as_u64), Some(6));
|
||||
}
|
||||
|
||||
// ── GraphQL Feed handler param validation ────────────────────────────────
|
||||
|
||||
/// graphql_posts requires `handle`.
|
||||
|
||||
@@ -40,6 +40,9 @@ pub use ops::{
|
||||
reveal_recovery_phrase, setup, status, RevealRecoveryPhraseResult, WalletAccount, WalletChain,
|
||||
WalletSetupParams, WalletSetupSource, WalletStatus, WALLET_NOT_CONFIGURED_MESSAGE,
|
||||
};
|
||||
/// Reduce an RPC URL to `scheme://host` for logging so private provider tokens
|
||||
/// embedded in the path/query never reach the logs.
|
||||
pub(crate) use rpc::redact_rpc_url;
|
||||
pub use schemas::{
|
||||
all_controller_schemas, all_registered_controllers, all_wallet_controller_schemas,
|
||||
all_wallet_registered_controllers, schemas, wallet_schemas,
|
||||
|
||||
@@ -26,7 +26,7 @@ static SHARED_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
||||
.expect("wallet RPC client builder must succeed with default settings")
|
||||
});
|
||||
|
||||
fn redact_rpc_url(raw: &str) -> String {
|
||||
pub(crate) fn redact_rpc_url(raw: &str) -> String {
|
||||
match reqwest::Url::parse(raw) {
|
||||
Ok(url) => match url.host_str() {
|
||||
Some(host) => format!("{}://{}", url.scheme(), host),
|
||||
|
||||
Reference in New Issue
Block a user