From 3431fdb2c16a3027a89d3d54edf9a8a4efaa6e1e Mon Sep 17 00:00:00 2001 From: skutcher <59061311+skutcher@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:49:28 +0100 Subject: [PATCH 1/9] add general functions for pools and tokens --- frontend/src/pool.rs | 43 +++++ frontend/src/pool/liquidity.rs | 293 +++++++++++++++++++++++++++++++++ frontend/src/token.rs | 219 ++++++++++++++++++++++++ 3 files changed, 555 insertions(+) create mode 100644 frontend/src/pool.rs create mode 100644 frontend/src/pool/liquidity.rs create mode 100644 frontend/src/token.rs diff --git a/frontend/src/pool.rs b/frontend/src/pool.rs new file mode 100644 index 0000000..ec4e4fc --- /dev/null +++ b/frontend/src/pool.rs @@ -0,0 +1,43 @@ +use std::collections::BTreeMap; +use dioxus::prelude::Signal; +use crate::metamask::uniswap_v2::{V2PairInfo as PoolInfo}; +use crate::vanillaswap::v2::is_zero_or_empty; + +pub mod liquidity; + +pub fn use_filtered_pairs( + wrapped_native: String, + pairs: Signal>, + balances: Signal>, + show_zero_liq: Signal, + show_balanced: Signal, +) -> Vec { + let balances = balances(); + pairs() + .iter() + .filter(|p| { + // Zero liquidity filter + (show_zero_liq() + || (!is_zero_or_empty(p.reserve0.as_deref()) + && !is_zero_or_empty(p.reserve1.as_deref()))) + && + // Balanced filter + (!show_balanced() + || !is_zero_or_empty(balances.get(&p.pair_address).map(String::as_str)) + || ( + (!is_zero_or_empty(balances.get(&p.token0).map(String::as_str)) + || (p.token0.to_lowercase() == wrapped_native + && !is_zero_or_empty(balances.get("native").map(String::as_str)))) + && + (!is_zero_or_empty(balances.get(&p.token1).map(String::as_str)) + || (p.token1.to_lowercase() == wrapped_native + && !is_zero_or_empty(balances.get("native").map(String::as_str)))) + )) + }) + .cloned() + .collect::>() +} + +pub fn get_pool(address_a: &String, address_b: &String, pools: &Vec) -> Option{ + pools.iter().find(|p| (p.token0 == *address_a && p.token1 == *address_b) || (p.token1 == *address_a && p.token0 == *address_b)).cloned() +} diff --git a/frontend/src/pool/liquidity.rs b/frontend/src/pool/liquidity.rs new file mode 100644 index 0000000..ca249cf --- /dev/null +++ b/frontend/src/pool/liquidity.rs @@ -0,0 +1,293 @@ +use alloy::primitives::utils::{format_units, parse_units}; +use dioxus::prelude::*; +use wasm_bindgen_futures::spawn_local; +use crate::components::switch::{Switch, SwitchThumb}; +use crate::wallet_context::use_wallet; +use crate::token::{TokenInfo, TokenSelector, TokenSelectorAmount, TokenType}; +use crate::metamask::uniswap_v2::{V2PairInfo as PoolInfo}; +use crate::metamask::MetamaskInfo; +use crate::metamask::get_token_balance; +use crate::vanillaswap::v2::{unique_pool_tokens, get_ratio, calc_pool_share}; +use super::get_pool; + +#[derive(Clone, Copy, PartialEq)] +enum LiquidityTab { + Add, + Remove, +} + +#[component] +pub fn LiquidityPopup( + mouse_y: Signal, + show: Signal, + show_zero_liq : Signal, + show_balanced : Signal, + all_tokens : Signal, + on_close: EventHandler<()>, + info : Signal, + pool_list: Signal>, + pool_info: Signal>, + on_confirm_add_liquidity: EventHandler<(TokenInfo, TokenInfo, String, String)>, + on_confirm_rem_liquidity: EventHandler<(TokenInfo, String)>, +) -> Element { + let mut active_tab = use_signal(|| LiquidityTab::Add); + // when mounted -> visible classes, else hidden classes + let overlay_cls = if show() {"fixed inset-0 bg-black/60 flex items-center justify-center z-50 transition-opacity duration-200 opacity-100"}else{"hidden"}; + + let panel_cls = "bg-gray-900/95 border border-gray-700 rounded-2xl p-6 w-full max-w-md shadow-2xl transform transition-all duration-200 opacity-100 translate-y-0 scale-100 absolute left-1/2 -translate-x-1/2"; + + rsx! { + div { class: "{overlay_cls}", + div { class: "{panel_cls}", + style: format!("top: {}px;", mouse_y()), + button { class: "absolute top-4 right-4 text-white transition", onclick: move |_| on_close(()), "✕" } + div { class: "mt-4 flex rounded-xl bg-gray-800/60 p-1", + button { + class: format!( + "flex-1 py-2 rounded-lg text-sm font-medium transition {}", + if active_tab() == LiquidityTab::Add { + "bg-gray-900 text-white" + } else { + "text-gray-400 hover:text-white" + } + ), + onclick: move |_| active_tab.set(LiquidityTab::Add), + "Add Liquidity" + } + + button { + class: format!( + "flex-1 py-2 rounded-lg text-sm font-medium transition {}", + if active_tab() == LiquidityTab::Remove { + "bg-gray-900 text-white" + } else { + "text-gray-400 hover:text-white" + } + ), + onclick: move |_| active_tab.set(LiquidityTab::Remove), + "Remove Liquidity" + } + } + match active_tab() { + LiquidityTab::Add => rsx! { + AddLiquidityView{ + show_zero_liq, + show_balanced, + all_tokens, + info, + pool_list, + pool_info, + on_confirm: on_confirm_add_liquidity, + } + }, + LiquidityTab::Remove => rsx! { + + } + } + } + } + } +} + +pub fn RemoveLiquidityView( + info : Signal, + pool_list: Signal>, + pool_info: Signal>, + on_confirm: EventHandler<(TokenInfo, String)>, +) -> Element { + + rsx! { + } + +} + +#[component] +pub fn AddLiquidityView( + show_zero_liq : Signal, + show_balanced : Signal, + all_tokens : Signal, + info : Signal, + pool_list: Signal>, + pool_info: Signal>, + on_confirm: EventHandler<(TokenInfo, TokenInfo, String, String)>, +) -> Element { + let mut token_a = use_signal(|| None as Option); + let mut token_b = use_signal(|| None as Option); + let mut token_a_reserve = use_signal(|| "0".to_string()); + let mut amount_a = use_signal(|| "0".to_string()); + let mut amount_b = use_signal(|| "0".to_string()); + // react on pool_info change + use_effect(move || { + let pool_info = pool_info(); + spawn_local(async move { + if let Some(pool) = pool_info{ + if let Some(from_sel) = token_a() && + let Some(to_sel) = token_b() && + let Some(token_pool) = get_pool(&from_sel.address, &to_sel.address, &pool_list()) && + token_pool.pair_address == pool.pair_address + { + + }else{ + token_a.set( + Some(TokenInfo { + symbol: pool.symbol0.clone().unwrap_or("???".into()), + address: pool.token0.clone(), + decimals: pool.decimals0.unwrap_or(18), + token_type: TokenType::CAsset, + + }) + ); + token_b.set( + Some(TokenInfo { + symbol: pool.symbol1.clone().unwrap_or("???".into()), + address: pool.token1.clone(), + decimals: pool.decimals1.unwrap_or(18), + token_type: TokenType::CAsset, + + }) + ); + } + if let Some(token_a) = token_a() && token_a.address == pool.token0{ + token_a_reserve.set(pool.reserve0.clone().unwrap_or("0".to_string())); + }else if let Some(token_a) = token_a() && token_a.address == pool.token1{ + token_a_reserve.set(pool.reserve1.clone().unwrap_or("0".to_string())); + }else{ + token_a_reserve.set("0".to_string()); + } + } + }); + }); + + use_effect(move || { + let token_a = token_a(); + let token_b = token_b(); + spawn_local(async move { + if let Some(from_sel) = token_a && + let Some(to_sel) = token_b + { + let new_pool = get_pool(&from_sel.address, &to_sel.address, &pool_list()); + if new_pool.is_none() || pool_info().is_none() || new_pool.clone().unwrap() != pool_info().unwrap(){ + pool_info.set(new_pool); + } + + } + }); + }); + + // Auto-ratio sync if pool exists + let mut sync_ratio = move |changed_a: bool| { + if let Some(pool) = pool_info() { + if let Some(reserve0) = pool.reserve0 && + let Some(reserve1) = pool.reserve1 && + let Some(token_a) = token_a() && + let Some(token_b) = token_b() && + let Ok(reserve0) = parse_units(&reserve0, 0) && + let Ok(reserve1) = parse_units(&reserve1, 0) && + let Ok(amount0) = parse_units(&amount_a(), token_a.decimals as u8) && + let Ok(amount1) = parse_units(&amount_b(), token_b.decimals as u8) + { + if changed_a { + if reserve0.get_absolute() > 0 { + amount_b.set(format_units(amount0.get_absolute() * reserve1.get_absolute() / reserve0.get_absolute(), token_b.decimals as u8).unwrap_or("0".to_string())); + } + } else { + if reserve1.get_absolute() > 0 { + amount_a.set(format_units(amount1.get_absolute() * reserve0.get_absolute() / reserve1.get_absolute(), token_a.decimals as u8).unwrap_or("0".to_string())); + } + } + } + } + }; + let from_options = unique_pool_tokens(&None, &pool_list.read(), &show_zero_liq.read(), &true, &false, info().chain_id); + let to_options = if all_tokens(){ + from_options.clone() + }else{ + unique_pool_tokens(&token_a.read(), &pool_list.read(), &show_zero_liq.read(), &true, &false, info().chain_id) + }; + let (reserve0, reserve1) = if let Some(pool) = pool_info(){ + if let Some(token_a) = token_a() && token_a.address == pool.token0{ + (pool.reserve0.unwrap_or_default(),pool.reserve1.unwrap_or_default()) + }else{ + (pool.reserve1.unwrap_or_default(),pool.reserve0.unwrap_or_default()) + } + }else{ + (amount_a().clone(),amount_b().clone()) + }; + + rsx! { + // TOKEN A SELECTOR + div { class: "space-y-1 mt-3", + label { class: "text-sm text-gray-400", "Token A" } + TokenSelectorAmount { + info, + token_list: from_options, + selected: token_a, + amount : amount_a, + on_select_token: move || { + //token_a.set(Some(t)); + if let Some(from_sel) = token_a() && + let Some(to_sel) = token_b() && + let Some(pool) = get_pool(&from_sel.address, &to_sel.address, &pool_list()) + { + pool_info.set(Some(pool)); + }else{ + token_b.set(None); + } + sync_ratio(false); + }, + on_select_amount: move || {sync_ratio(true);}, + allow_manual: true + } + } + // TOKEN B SELECTOR + div { class: "space-y-1 mt-2", + div { class: "flex items-center justify-between", + label { class: "text-sm text-gray-400", "Token B" } + div { class: "flex items-center gap-2", + span { class: "text-gray-200 text-sm", "Show all tokens" } + Switch { + checked: all_tokens(), + on_checked_change: move |new_state| all_tokens.set(new_state), + SwitchThumb {} + } + } + } + TokenSelectorAmount { + info, + token_list: to_options, + selected: token_b, + amount : amount_b, + on_select_token: move || {sync_ratio(true);}, + on_select_amount: move || {sync_ratio(false);}, + allow_manual: true + } + } + + // POOL INFO + if let Some(token_a) = token_a() && let Some(token_b) = token_b() + { + div { class: "text-sm text-gray-400 bg-gray-800/40 p-3 rounded-xl border border-gray-700 mt-3 space-y-1", + p { "{token_a.symbol.clone()} per {token_b.symbol.clone()} : {get_ratio(reserve1.clone(),reserve0.clone()) }" } + p { "{token_b.symbol.clone()} per {token_a.symbol.clone()} : {get_ratio(reserve0.clone(),reserve1.clone()) }" } + p { "Share : {calc_pool_share(amount_a().clone(), token_a_reserve().clone(), token_a.decimals as u8) }%" } + } + + } + + // SUBMIT + button { + class: "w-full bg-purple-600 hover:bg-purple-500 py-3 rounded-xl text-white font-semibold mt-4 disabled:opacity-40", + disabled: token_a().is_none() || token_b().is_none() || amount_a().is_empty() || amount_b().is_empty(), + onclick: move |_| { + if let (Some(a), Some(b)) = (token_a(), token_b()) { + on_confirm((a, b, amount_a(), amount_b())); + } + }, + if pool_info().is_none(){ + "Create & Add Liquidity" + }else{ + "Add Liquidity" + } + } + } +} diff --git a/frontend/src/token.rs b/frontend/src/token.rs new file mode 100644 index 0000000..9d82757 --- /dev/null +++ b/frontend/src/token.rs @@ -0,0 +1,219 @@ +use dioxus::prelude::*; +use wasm_bindgen_futures::spawn_local; +use serde::{Serialize, Deserialize}; +use crate::metamask::{MetamaskInfo, get_token_balance}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TokenInfo { + pub symbol: String, + pub address: String, + pub decimals: u64, + pub token_type : TokenType, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum TokenType{ + Native, + DToken, + CAsset, +} + +#[component] +pub fn TokenSelectorAmount( + info : Signal, + token_list: Vec, + selected: Signal>, + amount: Signal, + on_select_token: EventHandler<()>, + on_select_amount: EventHandler<()>, + allow_manual: bool, +) -> Element { + let mut balance = use_signal(|| "0".to_string()); + + use_effect(move || { + let selected = selected(); + spawn_local(async move { + if let Some(sel) = selected + && let Ok(bal) = get_token_balance(&info().address, &sel.address, matches!(sel.token_type, TokenType::Native)).await { + log::debug!("GetTokenBalance of address {} for token address {} :{:?}",info().address, sel.address, bal); + balance.set(bal); + } + }); + }); + + rsx! { + // TOKEN SELECTOR + TokenSelector { token_list, selected, on_select: on_select_token, allow_manual} + // Amounts + if let Some(_t) = selected() { + div { class: "space-y-1 mt-3", + div { class: "flex justify-between text-xs text-gray-400", + span { "Amount" } + button { class: "px-3 py-1 bg-white/10 rounded-lg text-white", onclick: move |_| { + amount.set(balance()); + on_select_amount(()); + }, "Max" } + span { "Balance: {balance}" } + } + input { + class: "w-full bg-gray-800 border border-gray-700 rounded-xl p-3 text-white", + value: "{amount()}", + oninput: move |ev| { + amount.set(ev.value()); + on_select_amount(()); + }, + placeholder: "0.0", + } + } + } + } +} + +#[component] +pub fn TokenSelector( + token_list: Vec, + selected: Signal>, + on_select: EventHandler<()>, + allow_manual: bool, +) -> Element { + let mut query = use_signal(|| String::new()); + let mut last_query = use_signal(|| String::new()); + let mut open = use_signal(|| false); + + let filtered_tokens: Vec = token_list.iter() + .filter(|t| { + let q = query().to_lowercase(); + if q.starts_with("0x"){ + t.address.to_lowercase().contains(&q) + }else{ + t.symbol.to_lowercase().contains(&q) + } + }) + .cloned().collect(); + + use_effect(move || { + let selected = selected(); + spawn_local(async move { + log::debug!("selected changed old query {:?} - selected {:?}", query, selected); + if let Some(selected) = selected{ + query.set(selected.symbol.clone()); + }else{ + query.set(String::new()); + } + }); + }); + + let on_select_internal = move ||{ //token: TokenInfo|{ + open.set(false); + if let Some(t) = selected(){ + query.set(t.symbol.clone()); + } + on_select(()); + }; + + rsx! { + div { class: "relative", + // clicking anywhere outside closes dropdown + onclick: move |_| open.set(false), + + // INNER WRAPPER — stops propagation so clicks inside don't close it + div { class: "relative w-full", //onclick: move |ev| ev.stop_propagation(), + // INPUT + input { + class: "w-full bg-gray-800 border border-gray-700 rounded-xl p-3 text-white placeholder:text-gray-500 focus:border-purple-500 outline-none transition", + placeholder: "Search or paste address...", + value: "{query()}", + // onclick: move |_| query.set("".to_string()), + onclick: move |ev| { + ev.stop_propagation(); + if (open()){ + if query().is_empty(){ + query.set(last_query()); + } + }else{ + last_query.set(query()); + query.set("".to_string()); + } + open.set(!open()); + }, + oninput: move |ev| { + query.set(ev.value()); + open.set(true); + }, + } + + if open() { + div { + class: "absolute left-0 right-0 mt-1 z-50 rounded-xl border border-gray-700 bg-gray-900 shadow-xl", + + // prevent outside click from closing dropdown while interacting inside + onclick: move |ev| ev.stop_propagation(), + + /* FIXED HEADER */ + div { + class: "px-4 py-2 text-xs uppercase tracking-wide text-gray-500 bg-gray-800/50 border-b border-gray-700", + "Tokens" + } + + /* SCROLL AREA */ + div { + class: "max-h-72 overflow-y-auto divide-y divide-gray-800", + onclick: move |ev| ev.stop_propagation(), + /* manual address option */ + if allow_manual && !query().is_empty() { + button { + class: "w-full px-4 py-3 text-left text-sm text-gray-300 hover:bg-gray-800", + onclick: move |_| { + let address = query(); + let t = TokenInfo{ + symbol: "unknown".to_string(), + address: address, + decimals: 18, + token_type : TokenType::CAsset, + }; + selected.set(Some(t.clone())); + on_select(()); + open.set(false); + }, + + "Use address: {query()}" + } + } + + /* TOKEN LIST ITEMS */ + { filtered_tokens.iter().map(|token| {rsx! {TokenListItem {token: token.clone(), on_select: on_select_internal.clone(), selected: selected.clone()}}})} + } + } + } + } + } + } +} + +#[component] +pub fn TokenListItem( + token: TokenInfo, + on_select: EventHandler<()>, + selected: Signal>, +) -> Element { + + rsx! { + button { class: "w-full px-4 py-3 hover:bg-gray-800 transition flex items-center gap-3", + onclick: move |_| { + log::debug!("New token selected :{:?}", token); + selected.set(Some(token.clone())); + on_select(());//token.clone()); + }, + + + // Placeholder icon + div { class: "w-6 h-6 rounded-full bg-gray-700", "?"} + + + div { class: "flex flex-col text-left", + span { class: "text-white text-sm", "{token.symbol}" } + // span { class: "text-gray-400 text-xs", "{symbol}" } + } + } + } +} From 7cd5864b7b4cf5f5c2c747df5d059ab848f3750a Mon Sep 17 00:00:00 2001 From: skutcher <59061311+skutcher@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:50:32 +0100 Subject: [PATCH 2/9] add abi for v2 add and remove liquidity, add function for multi coin get balances --- frontend/src/metamask.js | 111 ++++++++++++++++++++++++++++++++++++++- frontend/src/metamask.rs | 10 ++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/frontend/src/metamask.js b/frontend/src/metamask.js index fa5c932..c789511 100644 --- a/frontend/src/metamask.js +++ b/frontend/src/metamask.js @@ -29,7 +29,11 @@ const uniswapV2RouterAbi = [ "function factory() view returns (address)", "function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) returns (uint256[])", "function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline)", - "function swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) payable" + "function swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) payable", + "function addLiquidity(address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline) external returns (uint amountA, uint amountB, uint liquidity)", + "function addLiquidityETH(address token, uint amountTokenDesired, uint amountTokenMin, uint amountETHMin, address to, uint deadline) payable returns (uint amountToken, uint amountETH, uint liquidity)", + "function removeLiquidity(address tokenA, address tokenB, uint liquidity, uint amountAMin, uint amountBMin, address to, uint deadline) external returns (uint amountA, uint amountB)", + "function removeLiquidityETH(address token, uint liquidity, uint amountTokenMin, uint amountETHMin, address to, uint deadline) external returns (uint amountToken, uint amountETH)" ]; const uniswapV2FactoryAbi = [ "function allPairsLength() view returns (uint256)", @@ -133,6 +137,40 @@ export async function js_get_token_balance(user, token, isNative) { } } +export async function js_get_tokens_balances(user, tokens) { + try { + if (!window.ethereum) throw new Error("MetaMask not installed"); + await window.ethereum.request({ method: 'eth_requestAccounts' }); + let provider = new ethers.BrowserProvider(window.ethereum); + + const calls = tokens.map(async token => { + const c = new ethers.Contract(token, erc20Abi, provider); + const bal = await c.balanceOf(user); + return [token, bal.toString()]; + }); + const results = await Promise.all(calls); + const bal = await provider.getBalance(user); + // convert to object mapping + const map = {}; + map['native'] = bal; + for (const [addr, value] of results) { + map[addr] = value; + } + + return { + ok: true, + value: safeStringify(map) + }; + } catch (err) { + console.error(err); + return { + ok: false, + value: err.reason || err.message || "Unknown error" + }; + } +} + + // FACTORY export async function js_get_all_wrappers(factoryAddress) { try { @@ -269,7 +307,7 @@ export async function js_unwrap_tokens(contractAddress, cAsset, amount, dToken) } -//UniSwap +//UniSwap V2 export async function js_get_uniswap_v2_pairs(routerAddr) { try { if (!window.ethereum) throw new Error("MetaMask not installed"); @@ -414,6 +452,75 @@ export async function js_uniswap_v2_swap_tokens(tokenIn, tokenOut, amountIn, amo } } +export async function js_uniswap_v2_add_liquidity(tokenA, tokenB, amountA, amountB, routerAddress, isNativeA, isNativeB) { + try { + if (!window.ethereum) throw new Error("MetaMask not installed"); + await window.ethereum.request({ method: 'eth_requestAccounts' }); + let provider = new ethers.BrowserProvider(window.ethereum); + let signer = await provider.getSigner(); + + const router = new ethers.Contract(routerAddress, uniswapV2RouterAbi, signer); + + if (!isNativeA){ + const tokenAContract = new ethers.Contract(tokenA, erc20Abi, signer); + // Approve + const allowance = await tokenAContract.allowance(signer, routerAddress); + if (allowance < amountA){ + const approveATx = await tokenAContract.approve(routerAddress, amountA); + await approveATx.wait(); + } + } + if (!isNativeB){ + const tokenBContract = new ethers.Contract(tokenB, erc20Abi, signer); + // Approve + const allowance = await tokenBContract.allowance(signer, routerAddress); + if (allowance < amountB){ + const approveBTx = await tokenBContract.approve(routerAddress, amountB); + await approveBTx.wait(); + } + } + + const deadline = Math.floor(Date.now() / 1000) + 60 * 10; + + let tx; + if (isNativeA || isNativeB){ + const tokenAddress = isNativeA ? tokenB : tokenA; + const tokenAmount = isNativeA ? amountB : amountA; + const nativeAmount = isNativeA ? amountA : amountB; + tx = await router.addLiquidityETH( + tokenAddress, + tokenAmount, + 0, + await signer.getAddress(), // TODO: use just signer + deadline, + { + value: nativeAmount + } + ); + }else{ + tx = await router.addLiquidity( + tokenA, + tokenB, + amountA, + amountB, + 0, + 0, + await signer.getAddress(), + deadline, + ); + } + const receipt = await tx.wait(); + + return { + ok: true, + value: JSON.stringify(`${receipt.hash}`) + }; + } catch (err) { + console.error(err); + return { ok: false, value: err?.reason || err?.message || String(err) }; + } +} + /// UniSwap V3 export async function js_get_uniswap_v3_pool_states(poolAddrs) { diff --git a/frontend/src/metamask.rs b/frontend/src/metamask.rs index 03798b5..f4a1731 100644 --- a/frontend/src/metamask.rs +++ b/frontend/src/metamask.rs @@ -1,4 +1,5 @@ use std::error::Error; +use std::collections::BTreeMap; use wasm_bindgen::prelude::*; use serde::Deserialize; use serde::de::DeserializeOwned; @@ -16,6 +17,7 @@ extern "C" { pub fn js_on_chain_changed(callback: &Closure); pub fn js_on_accounts_changed(callback: &Closure)>); async fn js_get_token_balance(user: &str, token: &str, is_native: bool) -> JsValue; + async fn js_get_tokens_balances(user: &str, token: Vec) -> JsValue; } pub fn js_parse(js: JsValue) -> Result { @@ -56,3 +58,11 @@ pub async fn connect_metamask() -> Result>{ pub async fn get_token_balance(user: &str, token: &str, is_native: bool) -> Result>{ js_try!(js_get_token_balance(user, token, is_native) => String) } + +pub async fn get_tokens_balances(user: &str, tokens: Vec<&str>) -> Result,Box>{ + let js_array: Vec = tokens.iter() + .map(|s| JsValue::from_str(s)) + .collect(); + + js_try!(js_get_tokens_balances(user, js_array) => BTreeMap) +} From 814ecffbbf8ee2efc892f6fc6cc636b7c81af25e Mon Sep 17 00:00:00 2001 From: skutcher <59061311+skutcher@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:51:00 +0100 Subject: [PATCH 3/9] add modules for token and pool --- frontend/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/main.rs b/frontend/src/main.rs index da6bbb0..826488f 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -1,6 +1,8 @@ mod app; mod config; mod components; +mod token; +mod pool; mod metamask; mod vanillaswap; mod wrapper; From cf0c71a0e72fac816da15e0a05af26d918357a22 Mon Sep 17 00:00:00 2001 From: skutcher <59061311+skutcher@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:51:35 +0100 Subject: [PATCH 4/9] route js_uniswap_v2_add_liquidity --- frontend/src/metamask/uniswap_v2.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/metamask/uniswap_v2.rs b/frontend/src/metamask/uniswap_v2.rs index a71d409..f86ec42 100644 --- a/frontend/src/metamask/uniswap_v2.rs +++ b/frontend/src/metamask/uniswap_v2.rs @@ -13,12 +13,13 @@ extern "C" { // uniswap v2 async fn js_get_uniswap_v2_pairs(router_address: &str) -> JsValue; async fn js_uniswap_v2_swap_tokens(token_in: &str, token_out: &str, amount_in: &str, - amount_out_min: &str, router_address: &str, is_native_in: bool, is_native_out: bool) -> JsValue; + amount_out_min: &str, router_address: &str, is_native_in: bool, is_native_out: bool) -> JsValue; + async fn js_uniswap_v2_add_liquidity(token_a: &str, token_b: &str, amount_a: &str, amount_b: &str, router_address: &str, is_native_a: bool, is_native_b: bool) -> JsValue; } // UniSwap -#[derive(Deserialize, Clone, Debug)] +#[derive(Deserialize, Clone, Debug, PartialEq)] pub struct V2PairInfo { pub token0: String, pub token1: String, @@ -48,3 +49,15 @@ pub async fn uniswap_v2_swap_tokens( ) -> Result { js_try!(js_uniswap_v2_swap_tokens(token_in,token_out,amount_in,amount_out_min,router_address, is_native_in, is_native_out) => String) } + +pub async fn uniswap_v2_add_liquidity( + token_a: &str, + token_b: &str, + amount_a: &str, + amount_b: &str, + router_address: &str, + is_native_a: bool, + is_native_b: bool, +)-> Result { + js_try!(js_uniswap_v2_add_liquidity(token_a,token_b,amount_a,amount_b,router_address, is_native_a, is_native_b) => String) +} From 9790aeb3eff0812a47186a37bd7d863f3609b9d1 Mon Sep 17 00:00:00 2001 From: skutcher <59061311+skutcher@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:52:24 +0100 Subject: [PATCH 5/9] use pool and token components --- frontend/src/vanillaswap/pools_v2.rs | 201 ++++++++++++++++++++------- 1 file changed, 154 insertions(+), 47 deletions(-) diff --git a/frontend/src/vanillaswap/pools_v2.rs b/frontend/src/vanillaswap/pools_v2.rs index c81856f..61cfac5 100644 --- a/frontend/src/vanillaswap/pools_v2.rs +++ b/frontend/src/vanillaswap/pools_v2.rs @@ -1,17 +1,95 @@ +use std::collections::BTreeMap; + use dioxus::prelude::*; use wasm_bindgen_futures::spawn_local; -use alloy::primitives::{utils::parse_units,U256}; +use alloy::primitives::{utils::parse_units}; use crate::components::switch::{Switch, SwitchThumb}; -use crate::metamask::uniswap_v2::{V2PairInfo, get_uniswap_v2_pairs}; +use crate::metamask::get_tokens_balances; +use crate::metamask::uniswap_v2::{V2PairInfo, uniswap_v2_add_liquidity}; +use crate::pool::use_filtered_pairs; +use crate::token::{TokenInfo, TokenType}; use crate::wallet_context::use_wallet; -use super::v2::{use_v2_pools, is_zero_or_empty}; +use super::v2::{use_v2_pools, as_decimal, unique_pool_tokens}; +use crate::pool::liquidity::LiquidityPopup; +use crate::config::{ConfigEntry, get_config_entry}; #[component] pub fn PoolV2Pairs() -> Element { - let mut show_zero_liq = use_signal(|| false); let pools = use_v2_pools(); + let mut balances = use_signal(|| BTreeMap::::new()); + let wallet = use_wallet(); + let mut show_zero_liq = use_signal(|| false); + let mut show_balanced = use_signal(|| false); + let mut all_tokens = use_signal(|| false); + let mut selected_pool = use_signal(|| None as Option); + let mut mouse_y = use_signal(|| 0.0); + let mut show_popup = use_signal(|| false); + let wrapped_native = get_config_entry((wallet.info)().chain_id, &ConfigEntry::WrappedNativeAddress).to_lowercase(); + + let on_confirm_add_liquidity = move |(token_a, token_b, amount_a, amount_b): (TokenInfo, TokenInfo, String, String)| { + spawn_local(async move { + log::debug!("{:?}-{:?}",token_a,token_b); + log::debug!("{}-{}",amount_a,amount_b); + if let Ok(amount_a) = parse_units(&amount_a, token_a.decimals as u8) && + let Ok(amount_b) = parse_units(&amount_b, token_b.decimals as u8) + { + + match uniswap_v2_add_liquidity( + &token_a.address.clone(), + &token_b.address.clone(), + &amount_a.get_absolute().to_string(), + &amount_b.get_absolute().to_string(), + &(pools.router_address)(), + matches!(token_a.token_type, TokenType::Native), + matches!(token_b.token_type, TokenType::Native), + ).await { + Ok(_receipt) => log::info!("Liquidity added!"), + Err(e) => log::error!("Failed to add liquidity: {:?}", e), + } + } + }); + }; + + let on_confirm_rem_liquidity = move |(pool, share_amount): (TokenInfo, String)|{ + + }; + + use_effect(move || { + let wallet = use_wallet(); + let pairs = pools.pairs.read().clone(); + + spawn_local(async move { + let unique_tokens = unique_pool_tokens(&None, &pairs, &show_zero_liq.read(), &true, &true, (wallet.info)().chain_id); + let unique_tokens = unique_tokens.iter().map(|t| t.address.as_str()).collect(); + if !(wallet.info)().address.is_empty(){ + match get_tokens_balances(&(wallet.info)().address, unique_tokens).await{ + Ok(bal) => { + balances.set(bal); + }, + Err(e) => log::error!("Failed getting balances: {:?}", e), + } + } + }); + }); + + let pairs_to_show = use_filtered_pairs(wrapped_native, pools.pairs, balances, show_zero_liq, show_balanced); rsx! { + + LiquidityPopup { + mouse_y: mouse_y, + show: show_popup, + show_zero_liq : show_zero_liq, + show_balanced : show_balanced, + all_tokens : all_tokens, + on_close : move |_| {show_popup.set(false)}, + info : wallet.info, + pool_list: pools.pairs, + pool_info: selected_pool, + on_confirm_add_liquidity: on_confirm_add_liquidity, + on_confirm_rem_liquidity: on_confirm_rem_liquidity, + } + div { class: "p-8 mt-12 glass w-full max-w-4xl flex flex-col gap-6 items-stretch flex-col-sm", h2 { class: "text-3xl font-bold text-center mb-6", "V2 PoolPairs" } div { class: "flex items-center gap-2", @@ -21,6 +99,25 @@ pub fn PoolV2Pairs() -> Element { on_checked_change: move |new_state| show_zero_liq.set(new_state), SwitchThumb {} } + span { class: "text-gray-200 text-sm", "Show balanced tokens only" } + Switch { + checked: show_balanced(), + on_checked_change: move |new_state| show_balanced.set(new_state), + SwitchThumb {} + } + button { + class: "ml-auto px-4 py-2 font-semibold rounded-xl btn-gradient", + disabled: *pools.is_loading.read(), + onclick: move |ev| { + let data = &*ev.data; + mouse_y.set(data.page_coordinates().y as f64); + selected_pool.set(None); + show_popup.set(true); + all_tokens.set(true); + }, + "New" + } + } if (pools.is_loading)() { div { class: "text-gray-300", "Loading..." } @@ -32,59 +129,69 @@ pub fn PoolV2Pairs() -> Element { // ---------- PAIRS LIST ---------- div { class: "flex flex-col gap-3", - for pair in pools.pairs.read().iter().filter(|p| { - show_zero_liq() || (!is_zero_or_empty(&p.reserve0) && !is_zero_or_empty(&p.reserve1)) - }) { - div { class: "p-4 bg-gray-900/60 border border-gray-800 rounded-xl shadow-md - flex flex-col gap-2 hover:bg-gray-900 transition-colors duration-200", - // row 1 - div { class: "flex items-center", - - // TOKEN 0 - div { class: "flex items-center gap-2", - div { class: "text-gray-200 font-semibold text-lg", - "{ pair.symbol0.clone().unwrap_or(\"?\".into()) }" - } - } + {pairs_to_show.iter().map(|pair| { + let pair = pair.clone(); + rsx!{ + div { + class: "p-4 bg-gray-900/60 border border-gray-800 rounded-xl shadow-md + hover:bg-gray-900 transition-colors duration-200 + grid grid-cols-[minmax(0,1fr)_auto] gap-x-4", + // LEFT SIDE + div { + class: "flex flex-col gap-1 min-w-0", - // Slash between tokens - div { class: "text-gray-500 font-bold text-xl", "/" } + // --- ROW 1 --- + div { + class: "flex flex-wrap items-center gap-1 min-w-0", - // TOKEN 1 - div { class: "flex items-center gap-2", - div { class: "text-gray-200 font-semibold text-lg", - "{ pair.symbol1.clone().unwrap_or(\"?\".into()) }" - } - } + // TOKEN 0 + div { class: "text-gray-200 font-semibold text-lg", + "{ pair.symbol0.clone().unwrap_or(\"?\".into()) }" + } - // ADDRESS RIGHT SIDE - div { class: "ml-auto text-xs text-gray-500 tracking-wide", - "{pair.pair_address}" - } - } - // ROW 2 - div { class: "flex items-center text-xs font-bold text-gray-500", - div { class: "w-1/3", - "Reserve0: { pair.reserve0.clone().unwrap_or(\"0\".into()) }" - } - div { class: "w-1/3", - "Reserve1: { pair.reserve1.clone().unwrap_or(\"0\".into()) }" + div { class: "text-gray-500 font-bold text-xl", "/" } + + // TOKEN 1 + div { class: "text-gray-200 font-semibold text-lg", + "{ pair.symbol1.clone().unwrap_or(\"?\".into()) }" + } + + // ADDRESS (wraps when needed) + div { + class: "text-xs text-gray-500 break-all flex-grow ", + "{ pair.pair_address }" + } } - } - // // ROW 3 - // div { class: "flex items-center text-xs font-bold text-gray-500", + // --- ROW 2 --- + div { + class: "flex flex-wrap items-center gap-1 text-xs font-bold text-gray-500 ", + div { "Reserve0: { as_decimal(&pair.reserve0.clone().unwrap_or(\"0\".into()), pair.decimals0.unwrap_or(0) as u8) }" } + div { "Reserve1: { as_decimal(&pair.reserve1.clone().unwrap_or(\"0\".into()), pair.decimals0.unwrap_or(0) as u8) }" } + } + } + // RIGHT SIDE — MANAGE BUTTON + div { + class: "row-span-2 flex items-center justify-center shrink-0", - // // COLUMN 3 — PRICE - // div { class: "w-1/2 text-right", - // "Price token0/token1: " - // } - // } + button { + class: "px-4 py-2 font-semibold rounded-xl btn-gradient", + disabled: *pools.is_loading.read(), + onclick: move |ev| { + let data = &*ev.data; + mouse_y.set(data.page_coordinates().y as f64); + selected_pool.set(Some(pair.clone())); + show_popup.set(true); + }, + "Manage" + } + } + } } - } + })} } } } From d2b36b31ff7025fd158d91229de367e83c57f82a Mon Sep 17 00:00:00 2001 From: skutcher <59061311+skutcher@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:53:51 +0100 Subject: [PATCH 6/9] add ability to filter for pool addresses only, add functions to calculate pool shares and ratios --- frontend/src/vanillaswap/swap_v2.rs | 9 +- frontend/src/vanillaswap/v2.rs | 172 ++++++++++++++++++++-------- 2 files changed, 132 insertions(+), 49 deletions(-) diff --git a/frontend/src/vanillaswap/swap_v2.rs b/frontend/src/vanillaswap/swap_v2.rs index 84c1992..df8a281 100644 --- a/frontend/src/vanillaswap/swap_v2.rs +++ b/frontend/src/vanillaswap/swap_v2.rs @@ -6,7 +6,7 @@ use crate::metamask::{ get_token_balance, uniswap_v2::uniswap_v2_swap_tokens }; -use crate::wrapper::{TokenInfo, TokenType}; +use crate::token::{TokenInfo, TokenType}; use crate::wallet_context::use_wallet; use super::v2::{approx_amount_out, unique_pool_tokens, use_v2_pools}; @@ -43,11 +43,12 @@ pub fn PoolV2Swap() -> Element { use_effect(move || { let from_sel = token_a().clone(); let wallet = use_wallet(); + let info = (wallet.info)(); let mut balance = balance; spawn_local(async move { if let Some(from_sel) = from_sel - && let Ok(bal) = get_token_balance(&(wallet.info)().address, &from_sel.address, matches!(from_sel.token_type, TokenType::Native)).await { + && let Ok(bal) = get_token_balance(&info.address, &from_sel.address, matches!(from_sel.token_type, TokenType::Native)).await { log::debug!("GetTokenBalance of address {} for token address {} :{:?}",(wallet.info)().address, from_sel.address, bal); balance.set(bal); } @@ -125,8 +126,8 @@ pub fn PoolV2Swap() -> Element { } }; - let from_options = unique_pool_tokens(&None, &pools.pairs.read(), &show_zero_liq.read(), (wallet.info)().chain_id); - let to_options = unique_pool_tokens(&token_a.read(), &pools.pairs.read(), &show_zero_liq.read(), (wallet.info)().chain_id); + let from_options = unique_pool_tokens(&None, &pools.pairs.read(), &show_zero_liq.read(), &true, &false, (wallet.info)().chain_id); + let to_options = unique_pool_tokens(&token_a.read(), &pools.pairs.read(), &show_zero_liq.read(), &true, &false, (wallet.info)().chain_id); let from_selected = token_a.read().as_ref().map(|t| serde_json::to_string(&t).unwrap()).unwrap_or_default(); let to_selected = token_b.read().as_ref().map(|t| serde_json::to_string(&t).unwrap()).unwrap_or_default(); // UI rendering diff --git a/frontend/src/vanillaswap/v2.rs b/frontend/src/vanillaswap/v2.rs index c58075a..d46fa7e 100644 --- a/frontend/src/vanillaswap/v2.rs +++ b/frontend/src/vanillaswap/v2.rs @@ -5,7 +5,7 @@ use crate::{ config::{get_config_entry, ConfigEntry}, metamask::uniswap_v2::{get_uniswap_v2_pairs, V2PairInfo}, wallet_context::use_wallet, - wrapper::{TokenInfo, TokenType}, + token::{TokenInfo, TokenType}, }; #[derive(Clone)] @@ -46,8 +46,8 @@ pub fn use_sync_v2_pools() { }); } -pub fn is_zero_or_empty(v: &Option) -> bool { - match v.as_deref() { +pub fn is_zero_or_empty(v: Option<&str>) -> bool { + match v { None => true, Some("") => true, Some("0") => true, @@ -63,6 +63,8 @@ pub fn unique_pool_tokens( selected_a: &Option, pairs: &Vec, zero_liquid: &bool, + include_pool_tokens : &bool, + include_pool : &bool, chain_id : u32, ) -> Vec { let address = if let Some(a) = selected_a{ @@ -77,51 +79,64 @@ pub fn unique_pool_tokens( let wrapped_native_address = get_config_entry(chain_id, &ConfigEntry::WrappedNativeAddress).to_string(); for p in pairs { - if *zero_liquid || (!is_zero_or_empty(&p.reserve0) && !is_zero_or_empty(&p.reserve1)){ - // Check if Token A is token0 - if p.token0 == address || address.is_empty() { - let token_b = TokenInfo { - symbol: p.symbol1.clone().unwrap_or("???".into()), - address: p.token1.clone(), - decimals: p.decimals1.unwrap_or(18), + if *zero_liquid || (!is_zero_or_empty(p.reserve0.as_deref()) && !is_zero_or_empty(p.reserve1.as_deref())){ + if *include_pool{ + let pool_token = TokenInfo { + symbol: format!("{}-{}", p.symbol0.clone().unwrap_or("???".into()), p.symbol1.clone().unwrap_or("???".into())), + address: p.pair_address.clone(), + decimals: 18, token_type: TokenType::CAsset, - }; - if seen.insert(token_b.address.clone()) { - if token_b.address.to_lowercase() == wrapped_native_address.to_lowercase(){ - let native = TokenInfo { - symbol: native_symbol.to_string(), - address: token_b.address.clone(), - decimals: token_b.decimals, - token_type: TokenType::Native, - }; - out.push(native); - } - out.push(token_b); - + if seen.insert(pool_token.address.clone()) { + out.push(pool_token); } } + if *include_pool_tokens{ + // Check if Token A is token0 + if p.token0 == address || address.is_empty() { + let token_b = TokenInfo { + symbol: p.symbol1.clone().unwrap_or("???".into()), + address: p.token1.clone(), + decimals: p.decimals1.unwrap_or(18), + token_type: TokenType::CAsset, - // Check if Token A is token1 - if p.token1 == address || address.is_empty(){ - let token_b = TokenInfo { - symbol: p.symbol0.clone().unwrap_or("???".into()), - address: p.token0.clone(), - decimals: p.decimals0.unwrap_or(18), - token_type: TokenType::CAsset, + }; + if seen.insert(token_b.address.clone()) { + if token_b.address.to_lowercase() == wrapped_native_address.to_lowercase(){ + let native = TokenInfo { + symbol: native_symbol.to_string(), + address: token_b.address.clone(), + decimals: token_b.decimals, + token_type: TokenType::Native, + }; + out.push(native); + } + out.push(token_b); - }; - if seen.insert(token_b.address.clone()) { - if token_b.address.to_lowercase() == wrapped_native_address.to_lowercase(){ - let native = TokenInfo { - symbol: native_symbol.to_string(), - address: token_b.address.clone(), - decimals: token_b.decimals, - token_type: TokenType::Native, - }; - out.push(native); } - out.push(token_b); + } + + // Check if Token A is token1 + if p.token1 == address || address.is_empty(){ + let token_b = TokenInfo { + symbol: p.symbol0.clone().unwrap_or("???".into()), + address: p.token0.clone(), + decimals: p.decimals0.unwrap_or(18), + token_type: TokenType::CAsset, + + }; + if seen.insert(token_b.address.clone()) { + if token_b.address.to_lowercase() == wrapped_native_address.to_lowercase(){ + let native = TokenInfo { + symbol: native_symbol.to_string(), + address: token_b.address.clone(), + decimals: token_b.decimals, + token_type: TokenType::Native, + }; + out.push(native); + } + out.push(token_b); + } } } } @@ -158,10 +173,19 @@ pub fn approx_amount_out( format_units(amount_o, decimals_o as u8).unwrap_or("0".to_string()) } -// /// Displays the token amount in a human-readable format -// pub fn display_token(value: U256) -> String { -// format!("{:.18}", value.low_u128() as f64 / 1_000_000_000_000_000_000.0) -// } +pub fn as_decimal( + amount_in: &String, + decimals: u8, +) -> String{ + + if let Ok(amount_u256) = parse_units(amount_in,0) && + let Ok(amount_decimal) = format_units(amount_u256, decimals) + { + return amount_decimal; + } + + "0".to_string() +} // https://github.com/alloy-rs/examples/blob/main/examples/advanced/examples/uniswap_u256/helpers/alloy.rs @@ -256,3 +280,61 @@ fn get_denominator( fn get_uniswappy_fee() -> U256 { U256::from(997) } + +pub fn get_ratio( + reserves0: String, + reserves1: String +) -> String { + if let Ok(reserve0) = parse_units(&reserves0, 0) && + let Ok(reserve1) = parse_units(&reserves1, 0) + { + if reserve0.get_absolute() > 0{ + return format_units(U256::from(100_000_000) * reserve1.get_absolute() / reserve0.get_absolute(), 8).unwrap_or("0".to_string()) + } + } + "0".to_string() +} + +pub fn calc_pool_share( + amount_a: String, + reserves0: String, + decimals: u8 +) -> String { + log::debug!("Triggered calc pool share"); + if let Ok(reserve0) = parse_units(&reserves0, 0) && + let Ok(amount_a) = parse_units(&amount_a, decimals) + { + log::debug!("Amount a calc pool share :{}",amount_a); + log::debug!("Reserve a calc pool share :{}",reserve0); + let share = if reserve0.get_absolute() > 0 || amount_a.get_absolute() > 0{ + (amount_a.get_absolute() * U256::from(1_000_000)) / (reserve0.get_absolute() + amount_a.get_absolute()) + }else{ + U256::from(0) + }; + log::debug!("Triggered calc pool share :{}",share); + return format_units(share, 4).unwrap_or("0".to_string()) + } + "0".to_string() +} + + +pub fn calc_price_impact( + amount_a: f64, + amount_b: f64, + r0: f64, + r1: f64, +) -> f64 { + if r0 == 0.0 || r1 == 0.0 { return 0.0; } + let price_before = r0 / r1; + let price_after = (r0 + amount_a) / (r1 + amount_b); + ((price_after - price_before) / price_before) * 100.0 +} + + +// pub fn calc_pool_share( +// amount_a: f64, +// r0: f64, +// ) -> f64 { +// if r0 == 0.0 { return 100.0; } +// (amount_a / (r0 + amount_a)) * 100.0 +// } From 1d964842d49593ef6bd74c072d7fb90065c597f3 Mon Sep 17 00:00:00 2001 From: skutcher <59061311+skutcher@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:55:03 +0100 Subject: [PATCH 7/9] move signal read outside the if condition (bug fix) --- frontend/src/vanillaswap/swap_v3.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/vanillaswap/swap_v3.rs b/frontend/src/vanillaswap/swap_v3.rs index fff1a8a..4b9f770 100644 --- a/frontend/src/vanillaswap/swap_v3.rs +++ b/frontend/src/vanillaswap/swap_v3.rs @@ -6,7 +6,7 @@ use crate::metamask::{ get_token_balance, uniswap_v3::uniswap_v3_swap_tokens }; -use crate::wrapper::{TokenInfo, TokenType}; +use crate::token::{TokenInfo, TokenType}; use crate::wallet_context::use_wallet; use super::v3::{approx_amount_out, unique_pool_tokens, use_v3_pools}; @@ -44,11 +44,12 @@ pub fn PoolV3Swap() -> Element { use_effect(move || { let from_sel = token_a().clone(); let wallet = use_wallet(); + let info = (wallet.info)(); let mut balance = balance; spawn_local(async move { if let Some(from_sel) = from_sel - && let Ok(bal) = get_token_balance(&(wallet.info)().address, &from_sel.address, matches!(from_sel.token_type, TokenType::Native)).await { + && let Ok(bal) = get_token_balance(&info.address, &from_sel.address, matches!(from_sel.token_type, TokenType::Native)).await { log::debug!("GetTokenBalance of address {} for token address {} :{:?}",(wallet.info)().address, from_sel.address, bal); balance.set(bal); } From 3af3cd7d54ecf9341f02d575d5a46e1d67441108 Mon Sep 17 00:00:00 2001 From: skutcher <59061311+skutcher@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:55:21 +0100 Subject: [PATCH 8/9] use the general token info --- frontend/src/vanillaswap/v3.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/vanillaswap/v3.rs b/frontend/src/vanillaswap/v3.rs index 141ce76..ad244c5 100644 --- a/frontend/src/vanillaswap/v3.rs +++ b/frontend/src/vanillaswap/v3.rs @@ -6,7 +6,7 @@ use crate::{ config::{get_config_entry, ConfigEntry}, metamask::uniswap_v3::{V3PoolState, get_uniswap_v3_pool_states}, wallet_context::use_wallet, - wrapper::{TokenInfo, TokenType}, + token::{TokenInfo, TokenType}, }; From 50ff6584bef776c2f0c00bce7341fbc63ef17990 Mon Sep 17 00:00:00 2001 From: skutcher <59061311+skutcher@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:56:02 +0100 Subject: [PATCH 9/9] delete token structs which are moved into the token module --- frontend/src/wrapper.rs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/frontend/src/wrapper.rs b/frontend/src/wrapper.rs index ee26a2a..2a53c51 100644 --- a/frontend/src/wrapper.rs +++ b/frontend/src/wrapper.rs @@ -8,25 +8,10 @@ use crate::{ get_token_balance, wrapper::{TokenWrapperInfo, get_all_wrappers, wrap_tokens, unwrap_tokens} }, + token::{TokenInfo, TokenType}, wallet_context::use_wallet, }; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct TokenInfo { - pub symbol: String, - pub address: String, - pub decimals: u64, - pub token_type : TokenType, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub enum TokenType{ - Native, - DToken, - CAsset, -} - fn update_pair(from: &String, wrappers: &[TokenWrapperInfo]) -> (Option, Option){ if let Some(tok) = wrappers.iter().find(|t| t.d_token_address == *from || t.c_asset_address == *from) {