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