iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }