wallet.rs (7314B)
1 use anyhow::{Context, Result, bail}; 2 use axum::{Json, extract::State, http::HeaderMap}; 3 4 use crate::{ 5 adapters::wallet_store, 6 app::FeeEstimate, 7 domain::{Amount, MINE_FINALIZER_FEE, OutPoint}, 8 }; 9 10 use super::{ 11 HttpState, 12 types::{BurnSettingsForm, FeeEstimateResponse, TransferForm, WalletSetupResponse}, 13 wallet_password_for_request, 14 }; 15 16 pub(super) async fn api_wallet_setup( 17 State(state): State<HttpState>, 18 headers: HeaderMap, 19 ) -> Json<WalletSetupResponse> { 20 wallet_setup_json(wallet_setup_response(&state, &headers).await) 21 } 22 23 pub(super) async fn wallet_setup_response( 24 state: &HttpState, 25 headers: &HeaderMap, 26 ) -> Result<WalletSetupResponse> { 27 let setup_complete = state.ui_config.lock().await.setup_complete; 28 let password = wallet_password_for_request(state, headers).await; 29 let seed_phrase = if setup_complete { 30 None 31 } else { 32 wallet_store::setup_seed_phrase_with_password(&state.wallet_path, password.as_deref())? 33 }; 34 let address = state.node.lock().await.wallet_address().to_string(); 35 Ok(WalletSetupResponse { 36 ok: true, 37 error: None, 38 address: Some(address), 39 seed_phrase, 40 dev_verify_bypass: dev_seed_verify_bypass_enabled(), 41 requires_peer: setup_requires_peer(state).await, 42 }) 43 } 44 45 pub(super) async fn setup_requires_peer(state: &HttpState) -> bool { 46 !state.node.lock().await.has_real_chain() 47 } 48 49 pub(super) async fn replace_setup_wallet_with_generated_seed( 50 state: &HttpState, 51 headers: &HeaderMap, 52 ) -> Result<WalletSetupResponse> { 53 ensure_wallet_setup_open(state).await?; 54 let password = wallet_password_for_request(state, headers) 55 .await 56 .context("wallet password session is required")?; 57 let (wallet, seed_phrase) = 58 wallet_store::replace_with_generated_seed_phrase_encrypted(&state.wallet_path, &password)?; 59 let address = wallet.address().to_string(); 60 state.node.lock().await.replace_wallet(wallet); 61 Ok(WalletSetupResponse { 62 ok: true, 63 error: None, 64 address: Some(address), 65 seed_phrase: Some(seed_phrase), 66 dev_verify_bypass: dev_seed_verify_bypass_enabled(), 67 requires_peer: setup_requires_peer(state).await, 68 }) 69 } 70 71 pub(super) async fn import_setup_wallet_seed( 72 state: &HttpState, 73 headers: &HeaderMap, 74 seed_phrase: &str, 75 ) -> Result<WalletSetupResponse> { 76 ensure_wallet_setup_open(state).await?; 77 let password = wallet_password_for_request(state, headers) 78 .await 79 .context("wallet password session is required")?; 80 let wallet = wallet_store::replace_with_imported_seed_phrase_encrypted( 81 &state.wallet_path, 82 seed_phrase, 83 &password, 84 )?; 85 let address = wallet.address().to_string(); 86 state.node.lock().await.replace_wallet(wallet); 87 Ok(WalletSetupResponse { 88 ok: true, 89 error: None, 90 address: Some(address), 91 seed_phrase: None, 92 dev_verify_bypass: dev_seed_verify_bypass_enabled(), 93 requires_peer: setup_requires_peer(state).await, 94 }) 95 } 96 97 async fn ensure_wallet_setup_open(state: &HttpState) -> Result<()> { 98 let setup_complete = state.ui_config.lock().await.setup_complete; 99 if setup_complete { 100 bail!("wallet setup is already complete"); 101 } 102 Ok(()) 103 } 104 105 pub(super) fn wallet_setup_json(result: Result<WalletSetupResponse>) -> Json<WalletSetupResponse> { 106 match result { 107 Ok(response) => Json(response), 108 Err(error) => Json(WalletSetupResponse { 109 ok: false, 110 error: Some(format!("{error:#}")), 111 address: None, 112 seed_phrase: None, 113 dev_verify_bypass: dev_seed_verify_bypass_enabled(), 114 requires_peer: false, 115 }), 116 } 117 } 118 119 fn dev_seed_verify_bypass_enabled() -> bool { 120 dev_seed_verify_bypass_allowed(std::env::var_os("IUNA_DEV_SKIP_SEED_VERIFY").is_some()) 121 } 122 123 pub(super) fn dev_seed_verify_bypass_allowed(env_present: bool) -> bool { 124 env_present 125 } 126 127 pub(super) async fn transfer(state: &HttpState, form: TransferForm) -> Result<()> { 128 let (to, amount, fee_per_byte, selected_utxos) = validate_transfer_form(form)?; 129 130 let result = { 131 let mut node = state.node.lock().await; 132 let result = node.transfer_with_fee_rate(to, amount, fee_per_byte, &selected_utxos); 133 let outbox = node.drain_outbox(); 134 (result, outbox) 135 }; 136 137 match result.0 { 138 Ok(_) => state.gossip.broadcast(result.1).await, 139 Err(error) => Err(error), 140 } 141 } 142 143 pub(super) fn validate_transfer_form( 144 form: TransferForm, 145 ) -> Result<(String, Amount, Amount, Vec<OutPoint>)> { 146 let to = form.to.trim(); 147 if to.is_empty() { 148 bail!("recipient is required"); 149 } 150 if form.amount == 0 { 151 bail!("amount must be greater than zero"); 152 } 153 let fee = required_fee_per_byte_transfer(&form)?; 154 let selected_utxos = form 155 .utxos 156 .lines() 157 .flat_map(|line| line.split(',')) 158 .map(str::trim) 159 .filter(|value| !value.trim().is_empty()) 160 .map(parse_outpoint) 161 .collect::<Result<Vec<_>>>()?; 162 Ok((to.to_string(), form.amount, fee, selected_utxos)) 163 } 164 165 pub(super) async fn estimate_transfer_fee( 166 state: &HttpState, 167 form: TransferForm, 168 ) -> Result<FeeEstimate> { 169 let (to, amount, fee_per_byte, selected_utxos) = validate_transfer_form(form)?; 170 state 171 .node 172 .lock() 173 .await 174 .estimate_transfer_fee(to, amount, fee_per_byte, &selected_utxos) 175 } 176 177 pub(super) async fn estimate_burn_fee( 178 state: &HttpState, 179 form: BurnSettingsForm, 180 ) -> Result<FeeEstimate> { 181 let fee_per_byte = required_fee_per_byte_burn(&form)?; 182 if form.amount == 0 { 183 bail!("amount must be greater than zero"); 184 } 185 state 186 .node 187 .lock() 188 .await 189 .estimate_burn_fee(form.amount, fee_per_byte) 190 } 191 192 pub(super) async fn estimate_mine_fee(state: &HttpState) -> Result<FeeEstimate> { 193 state 194 .node 195 .lock() 196 .await 197 .estimate_mine_fee(MINE_FINALIZER_FEE) 198 } 199 200 fn required_fee_per_byte_transfer(form: &TransferForm) -> Result<Amount> { 201 form.fee_per_byte.context("fee per byte is required") 202 } 203 204 pub(super) fn required_fee_per_byte_burn(form: &BurnSettingsForm) -> Result<Amount> { 205 form.fee_per_byte.context("fee per byte is required") 206 } 207 208 pub(super) fn fee_estimate_json(result: Result<FeeEstimate>) -> Json<FeeEstimateResponse> { 209 match result { 210 Ok(estimate) => Json(FeeEstimateResponse { 211 ok: true, 212 error: None, 213 bytes: Some(estimate.bytes), 214 fee: Some(estimate.fee), 215 }), 216 Err(error) => Json(FeeEstimateResponse { 217 ok: false, 218 error: Some(format!("{error:#}")), 219 bytes: None, 220 fee: None, 221 }), 222 } 223 } 224 225 fn parse_outpoint(value: &str) -> Result<OutPoint> { 226 let (txid, index) = value 227 .rsplit_once(':') 228 .with_context(|| format!("invalid UTXO reference {value}"))?; 229 if txid.is_empty() { 230 bail!("invalid UTXO reference {value}"); 231 } 232 Ok(OutPoint { 233 txid: txid.to_string(), 234 index: index 235 .parse::<u32>() 236 .with_context(|| format!("invalid UTXO reference {value}"))?, 237 }) 238 }