iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }