diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d669c3f..db4dde4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,13 +24,14 @@ jobs: - name: Install Dioxus CLI run: cargo install dioxus-cli --locked - - name: Build Dioxus Web App - run: dx build --release + - name: Build Dioxus Web App (in frontend/) + working-directory: frontend + run: dx build --platform web - - name: Upload artifact + - name: Upload GitHub Pages artifact uses: actions/upload-pages-artifact@v3 with: - path: target/dioxus/release/web + path: frontend/target/dx/dioxus_meta_app/debug/web/public deploy: runs-on: ubuntu-latest diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 6e6fc9e..829f485 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -4,14 +4,13 @@ version = "0.1.0" edition = "2024" [dependencies] -dioxus = { version = "0.7", features = ["macro","web"] } -dioxus-web = "0.7" -wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" -js-sys = "0.3" -web-sys = { version = "0.3", features = ["Window", "console", "HtmlInputElement"] } +dioxus = { version = "0.7.1", features = ["asset", "devtools", "document", "hooks", "html", "launch", "macro", "signals"], default-features = false } +wasm-bindgen = { version = "0.2", default-features = false } +wasm-bindgen-futures = { version = "0.4", default-features = false } +js-sys = { version = "0.3", default-features = false } +web-sys = { version = "0.3", default-features = false } serde-wasm-bindgen = "0.6" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", default-features = false } serde_json = "1.0" log = "0.4" console_log = "1.0" @@ -19,6 +18,9 @@ alloy = {version = "1.1", default-features = false } [profile] +# [profile.release] +# strip = false + [profile.wasm-dev] inherits = "dev" opt-level = 1 diff --git a/frontend/Dioxus.toml b/frontend/Dioxus.toml new file mode 100644 index 0000000..4e5ceb4 --- /dev/null +++ b/frontend/Dioxus.toml @@ -0,0 +1,15 @@ +[application] +name = "cAssets_dToken_Wrapper" +default_platform = "web" + +[web.app] +title = "cAssets_dToken_Wrapper" +base_path = "cAssets_dToken_wrapper" +## keeps CSP happy +csp_hash = true +## avoids SWC minifier errors +minify_js = false + +[web.wasm] +## ensures WASM + JS are together +mode = "bundled" diff --git a/frontend/src/app.rs b/frontend/src/app.rs index f2c8d59..94ece3a 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -2,7 +2,7 @@ use alloy::primitives::{utils::{format_units, parse_units},U256}; use dioxus::prelude::*; use wasm_bindgen_futures::spawn_local; use serde_wasm_bindgen::from_value; -use crate::metamask::{TokenWrapperInfo, connect_metamask, get_token_balance, fetch_wrappers, wrap_tokens, unwrap_tokens}; +use crate::metamask::{TokenWrapperInfo, connect_metamask, get_token_balance, get_all_wrappers, wrap_tokens, unwrap_tokens}; #[derive(Clone, Debug)] struct TokenInfo { @@ -84,23 +84,27 @@ pub fn App() -> Element { let mut to_selected = to_selected; async move { - let addr = connect_metamask().await; - address.set(addr.as_string().unwrap_or_default()); - let addr = address.read().clone(); - match fetch_wrappers(factory_address).await { - Ok(list) => { - if let Some(first) = list.first() { - let (from,to) = update_pair(&first.d_token_symbol, &list); - to_selected.set(to); - from_selected.set(from); - if let Ok(bal) = from_value::(get_token_balance(&addr, &first.d_token_address).await) { - log::debug!("GetTokenBalance of address {} for token address {} :{:?}",addr, first.d_token_address, bal); - balance.set(bal); - } + match connect_metamask().await{ + Ok(addr) => { + address.set(addr); + let addr = address.read().clone(); + match get_all_wrappers(factory_address).await { + Ok(list) => { + if let Some(first) = list.first() { + let (from,to) = update_pair(&first.d_token_symbol, &list); + to_selected.set(to); + from_selected.set(from); + if let Ok(bal) = get_token_balance(&addr, &first.d_token_address).await { + log::debug!("GetTokenBalance of address {} for token address {} :{:?}",addr, first.d_token_address, bal); + balance.set(bal); + } + } + wrappers.set(list); + }, + Err(e) => log::error!("Error fetching wrappers: {:?}", e) } - wrappers.set(list); }, - Err(e) => log::error!("Error fetching wrappers: {:?}", e) + Err(e) => log::error!("Error connecting metamask: {:?}", e) } } }); @@ -114,7 +118,7 @@ pub fn App() -> Element { spawn_local(async move { if let Some(from_sel) = from_sel - && let Ok(bal) = from_value::(get_token_balance(&addr, &from_sel.address).await) { + && let Ok(bal) = get_token_balance(&addr, &from_sel.address).await { log::debug!("GetTokenBalance of address {} for token address {} :{:?}",addr, from_sel.address, bal); balance.set(bal); } @@ -285,8 +289,8 @@ pub fn App() -> Element { } else { unwrap_tokens(router_address, &from_selected.address.to_string(), &amount.read(), &to_selected.address.to_string()).await }; - tx_status.set(format!("{:?}", serde_wasm_bindgen::from_value::(res))); - if let Ok(bal) = from_value::(get_token_balance(&address(), &from_selected.address.to_string()).await) { + tx_status.set(format!("{:?}", res)); + if let Ok(bal) = get_token_balance(&address(), &from_selected.address.to_string()).await { log::debug!("TokenBalance {:?}",bal); balance.set(bal); } diff --git a/frontend/src/metamask.js b/frontend/src/metamask.js index 6e05863..2c285c0 100644 --- a/frontend/src/metamask.js +++ b/frontend/src/metamask.js @@ -5,92 +5,123 @@ let provider; let signer; // METAMASK -export async function connect_metamask() { - if (!window.ethereum) throw new Error("MetaMask not installed"); - await window.ethereum.request({ method: 'eth_requestAccounts' }); - provider = new ethers.BrowserProvider(window.ethereum); - signer = await provider.getSigner(); - return await signer.getAddress(); +export async function js_connect_metamask() { + try { + if (!window.ethereum) throw new Error("MetaMask not installed"); + await window.ethereum.request({ method: 'eth_requestAccounts' }); + provider = new ethers.BrowserProvider(window.ethereum); + signer = await provider.getSigner(); + const addr = await signer.getAddress(); + return { + ok: true, + value: JSON.stringify(addr) + }; + }catch (err) { + console.error(err); + return { + ok: false, + value: err.reason || err.message || "Unknown error" + }; + } } // ERC20 -export async function get_token_balance(user, token) { +export async function js_get_token_balance(user, token) { const abi = ["function balanceOf(address) view returns (uint256)", "function decimals() view returns (uint8)"]; try { const erc20 = new ethers.Contract(token, abi, provider); const [bal, decimals] = await Promise.all([erc20.balanceOf(user), erc20.decimals()]); - return ethers.formatUnits(bal, decimals); + return { + ok: true, + value: JSON.stringify(ethers.formatUnits(bal, decimals)) + }; } catch (err) { - // console.error("ERC20 get balance error: ",err); - return `Error: ${err.reason || err.message}`; + console.error(err); + return { + ok: false, + value: err.reason || err.message || "Unknown error" + }; } } // FACTORY -export async function get_all_wrappers(factoryAddress) { +export async function js_get_all_wrappers(factoryAddress) { // Factory ABI console.log("GetAllWrappers called"); - const factoryAbi = ["function getAllWraps() view returns (address[])"]; - const factory = new ethers.Contract(factoryAddress, factoryAbi, provider); - // Wrapper ABI - const wrapperAbi = ["function info() view returns (tuple(address dTokenAddress, address cAssetAddress, uint8 dTokenDecimals, uint8 cAssetDecimals, uint256 dTokenInFeeBps, uint256 dTokenOutFeeBps, address dTokenTreasury, address cAssetTreasury))"]; - const wrapAddresses = await factory.getAllWraps(); - // console.log("WrapAddresses:", wrapAddresses); - const tokenList = []; - - for (const wrapperAddr of wrapAddresses) { - console.log("WrapperAddress:", wrapperAddr); - try { - const wrapper = new ethers.Contract(wrapperAddr, wrapperAbi, provider); - const info = await wrapper.info(); - - const dTokenContract = new ethers.Contract( - info.dTokenAddress, - ["function symbol() view returns (string)", "function decimals() view returns (uint8)"], - provider - ); - - const [dtoken_symbol, dtoken_decimals] = await Promise.all([ - dTokenContract.symbol(), - dTokenContract.decimals() - ]); - - const cAssetContract = new ethers.Contract( - info.cAssetAddress, - ["function symbol() view returns (string)", "function decimals() view returns (uint8)"], - provider - ); - - const [casset_symbol, casset_decimals] = await Promise.all([ - cAssetContract.symbol(), - cAssetContract.decimals() - ]); - - - tokenList.push({ - wrapper: wrapperAddr, - dTokenSymbol: dtoken_symbol, - dTokenAddress: info.dTokenAddress, - dTokenDecimals: info.dTokenDecimals, - cAssetSymbol: casset_symbol, - cAssetAddress: info.cAssetAddress, - cAssetDecimals: info.cAssetDecimals, - fees: { - inBps: info.dTokenInFeeBps, - outBps: info.dTokenOutFeeBps - } - }); - } catch (err) { - console.warn("Skipping wrapper", wrapperAddr, err); + try { + const factoryAbi = ["function getAllWraps() view returns (address[])"]; + const factory = new ethers.Contract(factoryAddress, factoryAbi, provider); + // Wrapper ABI + const wrapperAbi = ["function info() view returns (tuple(address dTokenAddress, address cAssetAddress, uint8 dTokenDecimals, uint8 cAssetDecimals, uint256 dTokenInFeeBps, uint256 dTokenOutFeeBps))"]; + const wrapAddresses = await factory.getAllWraps(); + // console.log("WrapAddresses:", wrapAddresses); + const tokenList = []; + + for (const wrapperAddr of wrapAddresses) { + console.log("WrapperAddress:", wrapperAddr); + try { + const wrapper = new ethers.Contract(wrapperAddr, wrapperAbi, provider); + const info = await wrapper.info(); + + const dTokenContract = new ethers.Contract( + info.dTokenAddress, + ["function symbol() view returns (string)", "function decimals() view returns (uint8)"], + provider + ); + + const [dtoken_symbol, dtoken_decimals] = await Promise.all([ + dTokenContract.symbol(), + dTokenContract.decimals() + ]); + + const cAssetContract = new ethers.Contract( + info.cAssetAddress, + ["function symbol() view returns (string)", "function decimals() view returns (uint8)"], + provider + ); + + const [casset_symbol, casset_decimals] = await Promise.all([ + cAssetContract.symbol(), + cAssetContract.decimals() + ]); + + + tokenList.push({ + wrapper: wrapperAddr, + dTokenSymbol: dtoken_symbol, + dTokenAddress: info.dTokenAddress, + dTokenDecimals: info.dTokenDecimals, + cAssetSymbol: casset_symbol, + cAssetAddress: info.cAssetAddress, + cAssetDecimals: info.cAssetDecimals, + fees: { + inBps: info.dTokenInFeeBps, + outBps: info.dTokenOutFeeBps + } + }); + } catch (err) { + console.warn("Skipping wrapper", wrapperAddr, err); + } } + return { + ok: true, + value: JSON.stringify(tokenList , (key, value) => + typeof value === "bigint" ? value.toString() : value + ) + }; + } catch (err) { + return { + ok: false, + value: err.reason || err.message || "Unknown error" + }; + } - return tokenList; // Array of objects with wrapper + cAsset info } // ROUTER -export async function wrap_tokens(contractAddress, dToken, amount, cAsset) { +export async function js_wrap_tokens(contractAddress, dToken, amount, cAsset) { try { const abi = ["function wrap(address dTokent, uint256 amount, address cAsset) external"]; const approveAbi = ["function approve(address spender, uint256 amount) external returns (bool)", @@ -106,15 +137,21 @@ export async function wrap_tokens(contractAddress, dToken, amount, cAsset) { const connected = contract.connect(signer); const tx = await connected.wrap(dToken, amount_u256, cAsset); const receipt = await tx.wait(); - return "Wrap successful: ", receipt.hash; + return { + ok: true, + value: JSON.stringify(`${receipt.hash}`) + }; } catch (err) { console.error(err); - return `Error: ${err.reason || err.message}`; + return { + ok: false, + value: err.reason || err.message || "Unknown error" + }; } } -export async function unwrap_tokens(contractAddress, cAsset, amount, dToken) { +export async function js_unwrap_tokens(contractAddress, cAsset, amount, dToken) { try { const abi = ["function unwrap(address cAsset, uint256 amount, address dToken) external"]; const approveAbi = ["function approve(address spender, uint256 amount) external returns (bool)", @@ -130,9 +167,15 @@ export async function unwrap_tokens(contractAddress, cAsset, amount, dToken) { const connected = contract.connect(signer); const tx = await connected.unwrap(cAsset, amount_u256, dToken); const receipt = await tx.wait(); - return "Unwrap successful: ", receipt.hash; + return { + ok: true, + value: JSON.stringify(`${receipt.hash}`) + }; } catch (err) { console.error(err); - return `Error: ${err.reason || err.message}`; + return { + ok: false, + value: err.reason || err.message || "Unknown error" + }; } } diff --git a/frontend/src/metamask.rs b/frontend/src/metamask.rs index 8bef9e9..394ef31 100644 --- a/frontend/src/metamask.rs +++ b/frontend/src/metamask.rs @@ -1,7 +1,48 @@ +use std::error::Error; use wasm_bindgen::prelude::*; use serde::Deserialize; +use serde::de::DeserializeOwned; use serde_wasm_bindgen::from_value; +// Bind JS functions in metamask.js +#[wasm_bindgen(module = "/src/metamask.js")] +extern "C" { + pub async fn js_connect_metamask() -> JsValue; + pub async fn js_get_token_balance(user: &str, token: &str) -> JsValue; + pub async fn js_get_all_wrappers(factory_address: &str) -> JsValue; + pub async fn js_wrap_tokens(contract: &str, dToken: &str, amount: &str, cAsset: &str,) -> JsValue; + pub async fn js_unwrap_tokens(contract: &str, cAsset: &str, amount: &str, dToken: &str) -> JsValue; +} + +pub fn js_parse(js: JsValue) -> Result { + // Parse the wrapper + let wrapper: JsReturn = + serde_wasm_bindgen::from_value(js).map_err(|e| format!("{:?}", e))?; + + if !wrapper.ok { + return Err(wrapper.value); // This is already a string + } + + // Now parse the inner JSON + serde_json::from_str(&wrapper.value).map_err(|e| format!("{:?}", e)) +} + +#[macro_export] +macro_rules! js_try { + ($expr:expr => $ty:ty) => {{ + let js_val = $expr.await; + Ok(js_parse::<$ty>(js_val)?) + }}; +} + +fn from_str_to_u64<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s: &str = Deserialize::deserialize(deserializer)?; + s.parse::().map_err(serde::de::Error::custom) +} + #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct TokenWrapperInfo { @@ -9,9 +50,11 @@ pub struct TokenWrapperInfo { pub wrapper: String, pub d_token_symbol: String, pub d_token_address: String, + #[serde(deserialize_with = "from_str_to_u64")] pub d_token_decimals: u64, pub c_asset_symbol: String, pub c_asset_address: String, + #[serde(deserialize_with = "from_str_to_u64")] pub c_asset_decimals: u64, pub fees: Fees, } @@ -19,22 +62,37 @@ pub struct TokenWrapperInfo { #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct Fees { + #[serde(deserialize_with = "from_str_to_u64")] pub in_bps: u64, + #[serde(deserialize_with = "from_str_to_u64")] pub out_bps: u64, } -// Bind JS functions in metamask.js -#[wasm_bindgen(module = "/src/metamask.js")] -extern "C" { - pub async fn connect_metamask() -> JsValue; - pub async fn get_token_balance(user: &str, token: &str) -> JsValue; - pub async fn get_all_wrappers(factory_address: &str) -> JsValue; - pub async fn wrap_tokens(contract: &str, dToken: &str, amount: &str, cAsset: &str,) -> JsValue; - pub async fn unwrap_tokens(contract: &str, cAsset: &str, amount: &str, dToken: &str) -> JsValue; + +#[derive(Deserialize)] +struct JsReturn { + ok: bool, + value: String, +} + + + +pub async fn get_all_wrappers(factory_address: &str) -> Result, Box> { + js_try!(js_get_all_wrappers(factory_address) => Vec) +} + +pub async fn connect_metamask() -> Result>{ + js_try!(js_connect_metamask() => String) +} + +pub async fn get_token_balance(user: &str, token: &str) -> Result>{ + js_try!(js_get_token_balance(user, token) => String) +} + +pub async fn wrap_tokens(contract: &str, dToken: &str, amount: &str, cAsset: &str,) -> Result>{ + js_try!(js_wrap_tokens(contract, dToken, amount, cAsset) => String) } -pub async fn fetch_wrappers(factory_address: &str) -> Result, JsValue> { - let js_val = get_all_wrappers(factory_address).await; - let parsed: Vec = from_value(js_val)?; // <-- use serde_wasm_bindgen - Ok(parsed) +pub async fn unwrap_tokens(contract: &str, cAsset: &str, amount: &str, dToken: &str,) -> Result>{ + js_try!(js_unwrap_tokens(contract, cAsset, amount, dToken) => String) }