├── src ├── api │ ├── mod.rs │ └── solana_service.rs ├── model │ ├── mod.rs │ └── transaction.rs ├── util │ ├── mod.rs │ └── basic_util.rs ├── component │ ├── transaction_form │ │ ├── mod.rs │ │ └── transaction_form.rs │ ├── mod.rs │ ├── app_bar.rs │ └── app_content.rs └── main.rs ├── public ├── solana_logo.png └── solana_preview.png ├── Cargo.toml ├── .gitignore ├── Dioxus.toml └── README.md /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod solana_service; 2 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod transaction; 2 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod basic_util; 2 | -------------------------------------------------------------------------------- /src/component/transaction_form/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod transaction_form; 2 | -------------------------------------------------------------------------------- /src/component/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app_bar; 2 | pub mod app_content; 3 | pub mod transaction_form; 4 | -------------------------------------------------------------------------------- /public/solana_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miracle219/rust-solana-frontend/HEAD/public/solana_logo.png -------------------------------------------------------------------------------- /public/solana_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miracle219/rust-solana-frontend/HEAD/public/solana_preview.png -------------------------------------------------------------------------------- /src/model/transaction.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | pub struct TransactionSolPayload { 5 | pub sol_to_send: String, 6 | pub to_pubkey: String, 7 | } 8 | -------------------------------------------------------------------------------- /src/util/basic_util.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | pub fn prepare_form_values(raw_values: HashMap>) -> HashMap { 4 | let mut prepared_values: HashMap = HashMap::new(); 5 | 6 | raw_values.into_iter().for_each(|(key, value)| { 7 | prepared_values.insert(key, value.join("")); 8 | }); 9 | 10 | prepared_values 11 | } 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana-rust-frontend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | dioxus = "0.4.3" 10 | dioxus-web = "0.4.3" 11 | log = "0.4.6" 12 | wasm-logger = "0.2.0" 13 | reqwest = { version = "0.11.23", features = ["json"] } 14 | serde = "1.0.196" 15 | serde_json = "1.0.113" 16 | -------------------------------------------------------------------------------- /src/component/app_bar.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | use dioxus::prelude::*; 3 | 4 | pub fn AppBar(cx: Scope) -> Element { 5 | cx.render(rsx! { 6 | nav { 7 | class: "navbar navbar-dark bg-dark", 8 | div { 9 | class: "container-fluid py-2", 10 | img { 11 | width: "300px", 12 | alt: "solana-hero", 13 | src: "/solana_logo.png", 14 | } 15 | } 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | dist/ 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 9 | Cargo.lock 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | .env 14 | 15 | # MSVC Windows builds of rustc generate these, which store debugging information 16 | *.pdb 17 | 18 | 19 | # Added by cargo 20 | 21 | /target 22 | -------------------------------------------------------------------------------- /Dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | # App name 4 | name = "dioxus_demo" 5 | 6 | # The Dioxus platform to default to 7 | default_platform = "web" 8 | 9 | [web.app] 10 | 11 | # HTML title tag content 12 | title = "Solana | Rust" 13 | 14 | [web.watcher] 15 | 16 | # When watcher is triggered, regenerate the `index.html` 17 | reload_html = true 18 | 19 | # Which files or dirs will be monitored 20 | watch_path = ["src"] 21 | 22 | # Include style or script assets 23 | [web.resource] 24 | 25 | # CSS style file 26 | style = [ 27 | "https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css", 28 | "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css", 29 | ] 30 | 31 | # Javascript code file 32 | script = ["https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js"] 33 | 34 | [web.resource.dev] 35 | -------------------------------------------------------------------------------- /src/api/solana_service.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | const BASE_URL: &str = "http://127.0.0.1:3000/"; 4 | 5 | pub async fn get_balance() -> Result { 6 | reqwest::get(format!("{}getBalance", BASE_URL)) 7 | .await 8 | .unwrap() 9 | .text() 10 | .await 11 | } 12 | 13 | pub async fn transfer_sol(payload: HashMap) -> Result { 14 | let client = reqwest::Client::new(); 15 | 16 | client 17 | .post(format!("{}transferSols", BASE_URL)) 18 | .json(&payload) 19 | .send() 20 | .await 21 | .unwrap() 22 | .text() 23 | .await 24 | } 25 | 26 | pub async fn get_5_sols() -> Result { 27 | reqwest::get(format!("{}getSols", BASE_URL)) 28 | .await 29 | .unwrap() 30 | .text() 31 | .await 32 | } 33 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | // import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types 3 | use component::app_bar::AppBar; 4 | use component::app_content::AppContent; 5 | use dioxus::prelude::*; 6 | 7 | mod api; 8 | mod component; 9 | mod model; 10 | mod util; 11 | 12 | fn main() { 13 | wasm_logger::init(wasm_logger::Config::default()); 14 | // launch the web app 15 | dioxus_web::launch(App); 16 | } 17 | 18 | // create a component that renders a div with the text "Hello, world!" 19 | fn App(cx: Scope) -> Element { 20 | let app_style = r#" 21 | min-height: 100vh; 22 | display: flex; 23 | flex-direction: column; 24 | text-align: left; 25 | background-color: #282c34; 26 | "#; 27 | 28 | cx.render(rsx! { 29 | div { 30 | style: "{app_style}", 31 | AppBar{}, 32 | AppContent{}, 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Frontend in Rust - Smart contract 2 | This is a Smart Contract to connect with Solana using Rust, more specifically [Dioxus](https://dioxuslabs.com/). 3 | 4 | ## Prerequisites 5 | 1. This project uses [Axum](https://docs.rs/axum/latest/axum/) webserver which is a separate project written in Rust. see [here](https://github.com/bunnyBites/solana-rust-axum-backend) to know more. You might also check its [readme](https://github.com/bunnyBites/solana-rust-axum-backend/blob/main/README.md) section to know what details you need to provide to run your back-end server. 6 | 7 | 2. Phantom wallet - we will be using Devnet to test our program. You can also modify the code from the web server repo if you want to work with other environments. Check [here](https://www.soldev.app/course/interact-with-wallets) to learn more about how to configure Phantom to your browser and how to interact with it. 8 | 9 | 3. Setting Dioxus (Wasm) for our front end. Check the [official docs](https://dioxuslabs.com/learn/0.4/getting_started/wasm) to get started. 10 | 11 | ## What to expect from this project? 12 | 1. Connect with Phantom Wallet and get the balance SOL(s) of your account. 13 | 2. Transfer SOL(s) from your account to another account (you might need the public key of the receiver's account). 14 | 3. You can also Airdrop or get 5 SOL(s) to your account. 15 | 16 | ## Running the application Locally with hot-reload 17 | ```sh 18 | dx serve --hot-reload 19 | ``` 20 | 21 | ## Project Preview 22 | !["solana_ui"](public/solana_preview.png) -------------------------------------------------------------------------------- /src/component/transaction_form/transaction_form.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | use std::rc::Rc; 3 | 4 | use dioxus::prelude::*; 5 | 6 | use crate::{api::solana_service, util::basic_util}; 7 | 8 | pub fn TransactionForm(cx: Scope, set_is_loading: Rc) -> Element { 9 | cx.render(rsx!( 10 | form { 11 | class: "container", 12 | onsubmit: move |event| { 13 | let prepared_values = basic_util::prepare_form_values(event.values.clone()); 14 | set_is_loading(true); 15 | 16 | let cloned_set_is_loading = set_is_loading.clone(); 17 | async move { 18 | if let Ok(response) = solana_service::transfer_sol(prepared_values).await { 19 | log::info!("{}", response); 20 | cloned_set_is_loading(false); 21 | }else { 22 | log::info!("Failed to transfer"); 23 | cloned_set_is_loading(false); 24 | } 25 | } 26 | }, 27 | 28 | // Amount to send (SOL) input 29 | InputGroup { 30 | field_type: "number", 31 | label: "Amount(in SOL) to send", 32 | id: "sol_to_send", 33 | } 34 | 35 | // send Sol to input 36 | InputGroup { 37 | field_type: "text", 38 | label: "Send SOL to (Public key)", 39 | id: "to_pubkey", 40 | } 41 | 42 | // send button 43 | button { 44 | class: "btn btn-info btn-lg", 45 | r#type: "submit", 46 | "Send" 47 | } 48 | } 49 | )) 50 | } 51 | 52 | #[component] 53 | fn InputGroup<'a>(cx: Scope, field_type: &'a str, label: &'a str, id: &'a str) -> Element { 54 | cx.render(rsx!( 55 | div { 56 | class: "form-group my-5 text-start", 57 | label { 58 | class: "fs-3", 59 | r#for: &id[..], 60 | &label[..], 61 | } 62 | input { 63 | r#type: &field_type[..], 64 | id: &id[..], 65 | class: "form-control", 66 | name: &id[..], 67 | required: true, 68 | } 69 | } 70 | )) 71 | } 72 | -------------------------------------------------------------------------------- /src/component/app_content.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | use dioxus::prelude::*; 3 | use std::rc::Rc; 4 | 5 | use crate::{api::solana_service, component::transaction_form::transaction_form::TransactionForm}; 6 | 7 | pub fn AppContent<'a>(cx: Scope<'a>) -> Element<'a> { 8 | let is_loading: &'a UseState = use_state(cx, || false); 9 | 10 | let balance_future: &UseFuture> = use_future(cx, (), |_| { 11 | let loading = is_loading.clone(); 12 | is_loading.set(true); 13 | 14 | async move { 15 | solana_service::get_balance() 16 | .await 17 | .and_then(move |response| { 18 | loading.set(false); 19 | Ok(response) 20 | }) 21 | } 22 | }); 23 | 24 | cx.render(rsx!( 25 | div { 26 | class: "py-5 text-center text-white", 27 | // loader display 28 | LoaderDisplay{ 29 | is_loading: is_loading.get(), 30 | }, 31 | 32 | // Balance display 33 | DisplayBalance { 34 | balance_future: balance_future, 35 | set_is_loading: is_loading.setter() 36 | }, 37 | 38 | // Transaction form 39 | TransactionForm(cx, is_loading.setter()), 40 | } 41 | )) 42 | } 43 | 44 | #[component] 45 | fn LoaderDisplay<'a>(cx: Scope, is_loading: &'a bool) -> Element { 46 | cx.render(match is_loading { 47 | true => rsx!(div { 48 | class: "spinner-border text-info" 49 | }), 50 | _ => rsx!(()), 51 | }) 52 | } 53 | 54 | #[component] 55 | fn DisplayBalance<'a>( 56 | cx: Scope<'a>, 57 | balance_future: &'a UseFuture>, 58 | set_is_loading: Rc, 59 | ) -> Element<'a> { 60 | cx.render(match balance_future.value() { 61 | Some(Ok(balance)) => { 62 | rsx!( 63 | div { 64 | class: "d-flex align-items-center justify-content-center", 65 | div { 66 | class: "display-2", 67 | "Balance: {balance} SOL(s)", 68 | }, 69 | button { 70 | class: "btn btn-dark ms-3", 71 | onclick: |_| { balance_future.restart(); }, 72 | i { 73 | class: "bi bi-arrow-repeat fs-2 text-info" 74 | } 75 | } 76 | button { 77 | class: "btn btn-info btn-lg ms-3", 78 | onclick: |_| { 79 | set_is_loading(true); 80 | 81 | let cloned_set_is_loading = set_is_loading.clone(); 82 | async move { 83 | match solana_service::get_5_sols().await { 84 | Ok(response) => { 85 | log::info!("{:?}", response); 86 | cloned_set_is_loading(false); 87 | }, 88 | Err(e) => { 89 | log::info!("Failed to get solana service, {}", e); 90 | cloned_set_is_loading(false); 91 | } 92 | } 93 | } 94 | }, 95 | "Get 5 Sols" 96 | } 97 | } 98 | ) 99 | } 100 | _ => rsx!(()), 101 | }) 102 | } 103 | --------------------------------------------------------------------------------