diff --git a/Cargo.toml b/Cargo.toml index 3646926..78e9f14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.2" edition = "2021" description = "🔒 A Solana Wallet adapter for WASM frameworks." license = "MIT" -keywords = ["blockchain", "solana", "wallet", "wasm", "yew"] +keywords = ["blockchain", "solana", "wallet", "wasm", "yew", "dioxus"] categories = ["web-programming", "cryptography", "wasm"] repository = "https://github.com/gigadao/wasi-sol" documentation = "https://docs.rs/wasi-sol" @@ -28,7 +28,12 @@ web3 = { version = "0.19.0", default-features = false, features = ["eip-1193"] } wasm-bindgen = { version = "0.2.92", features = ["serde-serialize"] } solana-client-wasm = "1.18.0" emitter-rs = "0.0.5" -yew = "0.21.0" +yew = { version = "0.21.0", optional = true } +dioxus = { version = "0.5", optional = true } + +[features] +y = ["yew", ] +dio = ["dioxus", ] [package.metadata.docs.rs] all-features = true @@ -36,7 +41,7 @@ rustdoc-args = ["--cfg", "docsrs"] [profile.release] codegen-units = 1 -opt-level = "z" +opt-level = 3 lto = "thin" strip = "symbols" diff --git a/README.md b/README.md index dea43e9..198c1a4 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ A Solana Wallet adapter for WASM frameworks. | Framework | Supported | |-----------|-------------| | Yew | ✅ | -| Dioxus | ❌ | +| Dioxus | ✅ | | Leptos | ❌ | ## ⚙️ Features @@ -56,6 +56,8 @@ A Solana Wallet adapter for WASM frameworks. In addition to the [`examples`](examples) directory, you can use the following snippet of code to add `wasi-sol` wallet adapter using its built-in providers and hooks: +### YEW + ```rust , ignore use yew::prelude::*; @@ -179,6 +181,155 @@ fn main() { yew::Renderer::::new().render(); } ``` + +### Dioxus + +```rust , ignore +use dioxus::prelude::*; +use wasi_sol::core::traits::WalletAdapter; +use wasi_sol::core::wallet::BaseWalletAdapter; +use wasi_sol::provider::dioxus::connection::ConnectionProvider; +use wasi_sol::provider::dioxus::wallet::use_wallet; +use wasi_sol::provider::dioxus::wallet::WalletProvider; + +fn main() { + console_error_panic_hook::set_once(); + wasm_logger::init(wasm_logger::Config::default()); + launch(app); +} + +fn app() -> Element { + let endpoint = "https://api.mainnet-beta.solana.com"; + let wallets = vec![BaseWalletAdapter::new( + "Phantom", + "https://phantom.app", + "phantom_icon_url", + )]; + + rsx! { + ConnectionProvider { + endpoint: endpoint, + WalletProvider { + wallets: wallets, + endpoint: endpoint, + LoginPage {} + } + } + } +} + +#[component] +fn LoginPage() -> Element { + let wallet_context = use_wallet(); + let wallet_adapter = use_signal(|| wallet_context); + let mut connected = use_signal(|| false); + let wallet_info = (*wallet_adapter)().clone(); + let mut error = use_signal(|| None as Option); + + let connect_wallet = move |_| { + let mut wallet_adapter = wallet_adapter.clone(); + + spawn(async move { + let mut wallet_info = (*wallet_adapter)().clone(); + + match wallet_info.connect().await { + Ok(_) => { + wallet_adapter.set(wallet_info); + connected.set(true); + } + Err(err) => { + log::error!("Failed to connect wallet: {}", err); + } + } + }); + }; + + let disconnect_wallet = move |_| { + let mut wallet_adapter = wallet_adapter.clone(); + + spawn(async move { + let mut wallet_info = (*wallet_adapter)().clone(); + + match wallet_info.disconnect().await { + Ok(_) => { + wallet_adapter.set(wallet_info); + connected.set(false); + } + Err(err) => { + log::error!("Failed to disconnect wallet: {}", err); + error.set(Some(err.to_string())); + } + } + }); + }; + + rsx! { + div { + class: "wallet-adapter", + header { + class: "header", + img { + src: "./header.svg", + alt: "Phantom Wallet", + class: "button-icon" + }, + h1 { "Wasi Sol Dioxus Wallet Adapter" } + }, + div { + class: "content", + div { + class: "wallet-info", + if (*connected)() { + if let Some(ref key) = wallet_info.public_key() { + p { "Connected Wallet: {wallet_info.name()}" } + p { "Connected Public Key: {key}" } + } else { + p { "Connected but no wallet info available" } + } + } + }, + div { + class: "buttons", + if !(*connected)() { + button { + class: "connect-button", + onclick: connect_wallet, + img { + src: "./phantom_logo.png", + alt: "Phantom Wallet", + class: "button-icon" + }, + "Connect Wallet" + } + } else { + button { + class: "disconnect-button", + onclick: disconnect_wallet, + img { + src: "./phantom_logo.png", + alt: "Disconnect Wallet", + class: "button-icon" + }, + "Disconnect Wallet" + } + }, + if let Some(ref e) = (*error)() { + p { + style: "color: red;", + { e.clone() } + } + } + }, + }, + footer { + class: "footer", + p { "2024 GigaDAO Foundation." } + } + } + } +} +``` + ## 🎧 Event Listener ![Event Emitter Pattern](https://github.com/GigaDAO/wasi-sol/assets/62179149/65edfdc2-d86c-464a-a67f-5ef08099adc6) diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..eed0c09 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/header.svg b/assets/header.svg new file mode 100644 index 0000000..59c96f2 --- /dev/null +++ b/assets/header.svg @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/assets/main.css b/assets/main.css new file mode 100644 index 0000000..affbeb0 --- /dev/null +++ b/assets/main.css @@ -0,0 +1,40 @@ +body { + background-color: #111216; +} + +#main { + margin: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif +} + +#links { + width: 400px; + text-align: left; + font-size: x-large; + color: white; + display: flex; + flex-direction: column; +} + +#links a { + color: white; + text-decoration: none; + margin-top: 20px; + margin: 10px; + border: white 1px solid; + border-radius: 5px; + padding: 10px; +} + +#links a:hover { + background-color: #1f1f1f; + cursor: pointer; +} + +#header { + max-width: 1200px; +} diff --git a/examples/dioxus/Cargo.toml b/examples/dioxus/Cargo.toml new file mode 100644 index 0000000..d71fcb9 --- /dev/null +++ b/examples/dioxus/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wasi-sol-example" +version = "0.1.0" +edition = "2021" + +[dependencies] +console_error_panic_hook = "0.1.7" +log = "0.4.21" +wasi-sol = { path = "../../", features = ["dio"] } +wasm-logger = "0.2.0" +dioxus = { version = "0.5", features = ["web"] } diff --git a/examples/dioxus/Dioxus.toml b/examples/dioxus/Dioxus.toml new file mode 100644 index 0000000..9cb8ef8 --- /dev/null +++ b/examples/dioxus/Dioxus.toml @@ -0,0 +1,43 @@ +[application] + +# App (Project) Name +name = "wasi-sol" + +# Dioxus App Default Platform +# desktop, web +default_platform = "web" + +# `build` & `serve` dist path +out_dir = "dist" + +# resource (assets) file folder +asset_dir = "assets" + +[web.app] + +# HTML title tag content +title = "wasi-sol" + +[web.watcher] + +# when watcher trigger, regenerate the `index.html` +reload_html = true + +# which files or dirs will be watcher monitoring +watch_path = ["src", "assets"] + +# include `assets` in web platform +[web.resource] + +# CSS style file + +style = [ "main.css" ] + +# Javascript code file +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] diff --git a/examples/dioxus/README.md b/examples/dioxus/README.md new file mode 100644 index 0000000..4f2d428 --- /dev/null +++ b/examples/dioxus/README.md @@ -0,0 +1,43 @@ +# 📚 WASI SOL Dioxus Phantom Component Example + +## 🛠️ Pre-requisites: + +1. Install [`rustup`](https://www.rust-lang.org/tools/install): + + ```bash + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` + +1. Install [`Dioxus CLI`](https://dioxuslabs.com/learn/0.5/getting_started): + + ```bash + cargo install dioxus-cli + ``` + +1. Add Wasm target: + + ```bash + rustup target add wasm32-unknown-unknown + ``` + +## 🚀 Building and Running + +1. Fork/Clone the GitHub repository. + + ```bash + git clone https://github.com/gigadao/wasi-sol + ``` + +1. Navigate to the application directory. + + ```bash + cd wasi-sol/examples/dioxus + ``` + +1. Run the client: + + ```sh + dx serve --port 3000 + ``` + +Navigate to http://localhost:3000 to explore the landing page. diff --git a/examples/dioxus/assets/favicon.ico b/examples/dioxus/assets/favicon.ico new file mode 100644 index 0000000..eed0c09 Binary files /dev/null and b/examples/dioxus/assets/favicon.ico differ diff --git a/examples/dioxus/assets/header.svg b/examples/dioxus/assets/header.svg new file mode 100644 index 0000000..59c96f2 --- /dev/null +++ b/examples/dioxus/assets/header.svg @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/examples/basic/index.css b/examples/dioxus/assets/main.css similarity index 100% rename from examples/basic/index.css rename to examples/dioxus/assets/main.css diff --git a/examples/basic/images/phantom_logo.png b/examples/dioxus/assets/phantom_logo.png similarity index 100% rename from examples/basic/images/phantom_logo.png rename to examples/dioxus/assets/phantom_logo.png diff --git a/examples/dioxus/src/main.rs b/examples/dioxus/src/main.rs new file mode 100644 index 0000000..a991b07 --- /dev/null +++ b/examples/dioxus/src/main.rs @@ -0,0 +1,143 @@ +use dioxus::prelude::*; +use wasi_sol::core::traits::WalletAdapter; +use wasi_sol::core::wallet::BaseWalletAdapter; +use wasi_sol::provider::dioxus::connection::ConnectionProvider; +use wasi_sol::provider::dioxus::wallet::use_wallet; +use wasi_sol::provider::dioxus::wallet::WalletProvider; + +fn main() { + console_error_panic_hook::set_once(); + wasm_logger::init(wasm_logger::Config::default()); + launch(app); +} + +fn app() -> Element { + let endpoint = "https://api.mainnet-beta.solana.com"; + let wallets = vec![BaseWalletAdapter::new( + "Phantom", + "https://phantom.app", + "phantom_icon_url", + )]; + + rsx! { + ConnectionProvider { + endpoint: endpoint, + WalletProvider { + wallets: wallets, + endpoint: endpoint, + LoginPage {} + } + } + } +} + +#[component] +fn LoginPage() -> Element { + let wallet_context = use_wallet(); + let wallet_adapter = use_signal(|| wallet_context); + let mut connected = use_signal(|| false); + let wallet_info = (*wallet_adapter)().clone(); + let mut error = use_signal(|| None as Option); + + let connect_wallet = move |_| { + let mut wallet_adapter = wallet_adapter.clone(); + + spawn(async move { + let mut wallet_info = (*wallet_adapter)().clone(); + + match wallet_info.connect().await { + Ok(_) => { + wallet_adapter.set(wallet_info); + connected.set(true); + } + Err(err) => { + log::error!("Failed to connect wallet: {}", err); + } + } + }); + }; + + let disconnect_wallet = move |_| { + let mut wallet_adapter = wallet_adapter.clone(); + + spawn(async move { + let mut wallet_info = (*wallet_adapter)().clone(); + + match wallet_info.disconnect().await { + Ok(_) => { + wallet_adapter.set(wallet_info); + connected.set(false); + } + Err(err) => { + log::error!("Failed to disconnect wallet: {}", err); + error.set(Some(err.to_string())); + } + } + }); + }; + + rsx! { + div { + class: "wallet-adapter", + header { + class: "header", + img { + src: "./header.svg", + alt: "Phantom Wallet", + class: "button-icon" + }, + h1 { "Wasi Sol Dioxus Wallet Adapter" } + }, + div { + class: "content", + div { + class: "wallet-info", + if (*connected)() { + if let Some(ref key) = wallet_info.public_key() { + p { "Connected Wallet: {wallet_info.name()}" } + p { "Connected Public Key: {key}" } + } else { + p { "Connected but no wallet info available" } + } + } + }, + div { + class: "buttons", + if !(*connected)() { + button { + class: "connect-button", + onclick: connect_wallet, + img { + src: "./phantom_logo.png", + alt: "Phantom Wallet", + class: "button-icon" + }, + "Connect Wallet" + } + } else { + button { + class: "disconnect-button", + onclick: disconnect_wallet, + img { + src: "./phantom_logo.png", + alt: "Disconnect Wallet", + class: "button-icon" + }, + "Disconnect Wallet" + } + }, + if let Some(ref e) = (*error)() { + p { + style: "color: red;", + { e.clone() } + } + } + }, + }, + footer { + class: "footer", + p { "2024 GigaDAO Foundation." } + } + } + } +} diff --git a/examples/basic/Cargo.toml b/examples/yew/Cargo.toml similarity index 80% rename from examples/basic/Cargo.toml rename to examples/yew/Cargo.toml index 086c744..7db1916 100644 --- a/examples/basic/Cargo.toml +++ b/examples/yew/Cargo.toml @@ -6,6 +6,6 @@ edition = "2021" [dependencies] console_error_panic_hook = "0.1.7" log = "0.4.21" -wasi-sol = { path = "../../" } +wasi-sol = { path = "../../", features = ["y"] } wasm-logger = "0.2.0" yew = { version = "0.21.0", features = ["csr"] } diff --git a/examples/basic/README.md b/examples/yew/README.md similarity index 100% rename from examples/basic/README.md rename to examples/yew/README.md diff --git a/examples/yew/images/logo.jpeg b/examples/yew/images/logo.jpeg new file mode 100644 index 0000000..e79c684 Binary files /dev/null and b/examples/yew/images/logo.jpeg differ diff --git a/examples/yew/images/phantom_logo.png b/examples/yew/images/phantom_logo.png new file mode 100644 index 0000000..238fb2d Binary files /dev/null and b/examples/yew/images/phantom_logo.png differ diff --git a/examples/yew/index.css b/examples/yew/index.css new file mode 100644 index 0000000..8dea83a --- /dev/null +++ b/examples/yew/index.css @@ -0,0 +1,103 @@ +body { + background-color: #111; + color: #fff; + font-family: 'Arial', sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.wallet-adapter { + max-width: 600px; + width: 100%; + padding: 20px; + background: linear-gradient(45deg, #1a1a1a, #333); + border-radius: 10px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.6); + text-align: center; + position: relative; + overflow: hidden; +} + +.header { + margin-bottom: 20px; +} + +.wallet-info { + margin-bottom: 20px; +} + + +.buttons { + display: flex; + justify-content: center; + gap: 20px; + margin-top: 20px; +} + +.connect-button { + display: flex; + background-color: #a033ff; + align-items: center; + justify-content: center; + color: #fff; + padding: 10px 20px; + border: none; + border-radius: 20px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.3s ease, box-shadow 0.3s ease; + position: relative; + overflow: hidden; + z-index: 1; +} + +.connect-button:hover { + box-shadow: 0 0 10px rgba(160, 51, 255, 0.6); +} + +.disconnect-button { + display: flex; + background-color: #e84855; + color: #fff; + align-items: center; + justify-content: center; + padding: 10px 20px; + border: none; + border-radius: 20px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.3s ease, box-shadow 0.3s ease; + position: relative; + overflow: hidden; + z-index: 1; +} + + +.connect-button img, +.disconnect-button img { + width: 24px; + margin-right: 10px; +} + +.yew-logo { + width: 600px; +} + +.connect-button:hover, +.disconnect-button:hover { + box-shadow: 0 0 10px rgba(255, 255, 255, 0.6); +} + +.disconnect-button:hover { + box-shadow: 0 0 10px rgba(232, 72, 85, 0.6); +} + +footer { + margin-top: 20px; + padding-top: 10px; + border-top: 1px solid #666; + font-size: 14px; +} diff --git a/examples/basic/index.html b/examples/yew/index.html similarity index 100% rename from examples/basic/index.html rename to examples/yew/index.html diff --git a/examples/basic/rustfmt.toml b/examples/yew/rustfmt.toml similarity index 100% rename from examples/basic/rustfmt.toml rename to examples/yew/rustfmt.toml diff --git a/examples/basic/src/main.rs b/examples/yew/src/main.rs similarity index 93% rename from examples/basic/src/main.rs rename to examples/yew/src/main.rs index 2cbe338..b187f2e 100644 --- a/examples/basic/src/main.rs +++ b/examples/yew/src/main.rs @@ -3,12 +3,12 @@ use yew::prelude::*; use wasi_sol::{ core::traits::WalletAdapter, core::wallet::BaseWalletAdapter, - provider::{ + provider::yew::{ connection::{use_connection, ConnectionProvider}, wallet::{use_wallet, WalletProvider}, }, + pubkey::Pubkey, spawn_local, - pubkey::Pubkey }; #[function_component] @@ -52,9 +52,11 @@ pub fn LoginPage() -> Html { spawn_local(async move { let mut wallet_info = (*wallet_adapter).clone(); - wallet_info.emitter.on("connect", move |public_key: Pubkey| { - log::info!("Event Listener: Got pubkey {}", public_key); - }); + wallet_info + .emitter + .on("connect", move |public_key: Pubkey| { + log::info!("Event Listener: Got pubkey {}", public_key); + }); match wallet_info.connect().await { Ok(_) => { @@ -100,6 +102,7 @@ pub fn LoginPage() -> Html { html! {
+

{ "Wasi Sol Wallet Adapter" }

diff --git a/src/provider.rs b/src/provider.rs index 77da617..b4982da 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,3 +1,4 @@ -pub mod connection; -pub mod local_storage; -pub mod wallet; +#[cfg(feature = "dio")] +pub mod dioxus; +#[cfg(feature = "y")] +pub mod yew; diff --git a/src/provider/dioxus.rs b/src/provider/dioxus.rs new file mode 100644 index 0000000..77da617 --- /dev/null +++ b/src/provider/dioxus.rs @@ -0,0 +1,3 @@ +pub mod connection; +pub mod local_storage; +pub mod wallet; diff --git a/src/provider/dioxus/connection.rs b/src/provider/dioxus/connection.rs new file mode 100644 index 0000000..4940931 --- /dev/null +++ b/src/provider/dioxus/connection.rs @@ -0,0 +1,73 @@ +use std::{fmt, ops::Deref, rc::Rc, sync::Arc}; + +use dioxus::prelude::*; + +use solana_client_wasm::WasmClient as RpcClient; + +use solana_sdk::commitment_config::CommitmentConfig; + +#[derive(Clone)] +pub struct ConnectionContextState { + pub connection: Arc, +} + +impl PartialEq for ConnectionContextState { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.connection, &other.connection) + } +} +impl fmt::Debug for ConnectionContextState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ConnectionContextState") + .field("commitment", &self.connection.commitment()) + .finish() + } +} + +#[derive(Clone, PartialEq)] +pub struct ConnectionContext(Rc); + +impl Deref for ConnectionContext { + type Target = Rc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Debug for ConnectionContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ConnectionContext") + .field("commitment", &self.connection.commitment()) + .finish() + } +} + +#[component] +pub fn ConnectionProvider(props: ConnectionProps) -> Element { + let endpoint = use_signal(|| props.endpoint); + let connection_state = use_memo(move || { + Rc::new(ConnectionContextState { + connection: RpcClient::new_with_commitment( + (&endpoint.clone())(), + CommitmentConfig::confirmed(), + ) + .into(), + }) + }); + + use_context_provider(|| ConnectionContext(connection_state())); + + rsx! { { &props.children } } +} + +#[derive(Props, Clone, PartialEq)] +pub struct ConnectionProps { + pub children: Element, + pub endpoint: &'static str, +} + +#[component] +pub fn use_connection() -> ConnectionContext { + use_context::() +} diff --git a/src/provider/dioxus/local_storage.rs b/src/provider/dioxus/local_storage.rs new file mode 100644 index 0000000..11502d4 --- /dev/null +++ b/src/provider/dioxus/local_storage.rs @@ -0,0 +1,18 @@ +use dioxus::prelude::*; +use gloo_storage::{LocalStorage, Storage}; + +pub fn use_local_storage(key: String, initial_value: String) -> (String, Signal) { + LocalStorage::set(&key, &initial_value) + .ok() + .expect("Set LocalStorage"); + let stored_value = LocalStorage::get(&key).unwrap_or(initial_value.clone()); + let state = use_signal(|| stored_value.clone()); + + use_effect(move || { + LocalStorage::set(&key, &*state()) + .ok() + .expect("Set LocalStorage"); + }); + + (state.to_string(), state) +} diff --git a/src/provider/dioxus/wallet.rs b/src/provider/dioxus/wallet.rs new file mode 100644 index 0000000..9d4ca31 --- /dev/null +++ b/src/provider/dioxus/wallet.rs @@ -0,0 +1,41 @@ +use crate::{ + core::{traits::WalletAdapter, wallet::BaseWalletAdapter}, + provider::dioxus::local_storage::use_local_storage, +}; +use dioxus::prelude::*; + +#[derive(Props, Clone, PartialEq)] +pub struct WalletProviderProps { + pub children: Element, + pub endpoint: &'static str, + pub wallets: Vec, + #[props(default = "walletName")] + pub local_storage_key: &'static str, + #[props(default = false)] + pub auto_connect: bool, +} + +#[component] +pub fn WalletProvider(props: WalletProviderProps) -> Element { + let (wallet_name, _set_wallet_name) = + use_local_storage(props.local_storage_key.to_string(), "Phantom".to_string()); + + let wallet_context = use_memo(move || { + props + .wallets + .iter() + .find(|wallet| wallet.name() == wallet_name) + .cloned() + }); + + let context = use_signal(move || (*wallet_context)().clone().unwrap()); + + use_context_provider(|| context()); + + rsx! { { &props.children } } +} + +#[component] +pub fn use_wallet() -> BaseWalletAdapter { + use_context::() +} diff --git a/src/provider/yew.rs b/src/provider/yew.rs new file mode 100644 index 0000000..77da617 --- /dev/null +++ b/src/provider/yew.rs @@ -0,0 +1,3 @@ +pub mod connection; +pub mod local_storage; +pub mod wallet; diff --git a/src/provider/connection.rs b/src/provider/yew/connection.rs similarity index 100% rename from src/provider/connection.rs rename to src/provider/yew/connection.rs diff --git a/src/provider/local_storage.rs b/src/provider/yew/local_storage.rs similarity index 100% rename from src/provider/local_storage.rs rename to src/provider/yew/local_storage.rs diff --git a/src/provider/wallet.rs b/src/provider/yew/wallet.rs similarity index 96% rename from src/provider/wallet.rs rename to src/provider/yew/wallet.rs index 87010ac..45f6146 100644 --- a/src/provider/wallet.rs +++ b/src/provider/yew/wallet.rs @@ -1,6 +1,6 @@ use crate::{ core::{traits::WalletAdapter, wallet::BaseWalletAdapter}, - provider::local_storage::use_local_storage, + provider::yew::local_storage::use_local_storage, }; use yew::prelude::*;