From 1733c42b3235c68f19e488ff8b75f7c2908efc0b Mon Sep 17 00:00:00 2001 From: rishflab Date: Fri, 30 Jul 2021 16:48:18 +1000 Subject: [PATCH] Extract stateless functions into wallet library module This is a WIP to get feedback not really for a thorough review. The goal of this commit/branch is to extract wallet functionality into a module so it can moved into the baru library. The motivation for this refactor is so the platform team can upgrade the protocols to use PSET's. This should also take maintenance workload away from from product team. Currently there is a Wallet trait from the library that is implemented using Esplora in the application. There is a plan to remove this trait for the Esplora stuff to live in the baru library. --- Cargo.lock | 3 + extension/wallet/Cargo.toml | 1 + extension/wallet/src/cache_storage.rs | 66 --- extension/wallet/src/esplora.rs | 285 ---------- extension/wallet/src/lib.rs | 254 +++++++-- extension/wallet/src/lib_wallet.rs | 348 ++++++++++++ .../wallet/src/lib_wallet/extract_loan.rs | 79 +++ .../{wallet => lib_wallet}/extract_trade.rs | 20 +- .../wallet/src/lib_wallet/get_balances.rs | 10 + .../src/lib_wallet/get_transaction_history.rs | 13 + .../make_create_swap_payload.rs | 114 ++-- .../make_loan_request.rs | 109 ++-- .../src/{wallet => lib_wallet}/repay_loan.rs | 144 ++--- .../sign_and_send_swap_transaction.rs | 24 +- extension/wallet/src/lib_wallet/sign_loan.rs | 65 +++ .../withdraw_everything_to.rs | 44 +- extension/wallet/src/storage.rs | 3 +- extension/wallet/src/wallet.rs | 507 ++++++++---------- extension/wallet/src/{ => wallet}/assets.rs | 0 .../{create_new.rs => create_new_waves.rs} | 2 +- extension/wallet/src/wallet/extract_loan.rs | 124 ----- extension/wallet/src/wallet/get_address.rs | 12 - extension/wallet/src/wallet/get_balances.rs | 16 - .../src/wallet/get_transaction_history.rs | 19 - extension/wallet/src/wallet/sign_loan.rs | 117 ---- 25 files changed, 1107 insertions(+), 1272 deletions(-) delete mode 100644 extension/wallet/src/cache_storage.rs delete mode 100644 extension/wallet/src/esplora.rs create mode 100644 extension/wallet/src/lib_wallet.rs create mode 100644 extension/wallet/src/lib_wallet/extract_loan.rs rename extension/wallet/src/{wallet => lib_wallet}/extract_trade.rs (91%) create mode 100644 extension/wallet/src/lib_wallet/get_balances.rs create mode 100644 extension/wallet/src/lib_wallet/get_transaction_history.rs rename extension/wallet/src/{wallet => lib_wallet}/make_create_swap_payload.rs (51%) rename extension/wallet/src/{wallet => lib_wallet}/make_loan_request.rs (54%) rename extension/wallet/src/{wallet => lib_wallet}/repay_loan.rs (50%) rename extension/wallet/src/{wallet => lib_wallet}/sign_and_send_swap_transaction.rs (74%) create mode 100644 extension/wallet/src/lib_wallet/sign_loan.rs rename extension/wallet/src/{wallet => lib_wallet}/withdraw_everything_to.rs (88%) rename extension/wallet/src/{ => wallet}/assets.rs (100%) rename extension/wallet/src/wallet/{create_new.rs => create_new_waves.rs} (98%) delete mode 100644 extension/wallet/src/wallet/extract_loan.rs delete mode 100644 extension/wallet/src/wallet/get_address.rs delete mode 100644 extension/wallet/src/wallet/get_balances.rs delete mode 100644 extension/wallet/src/wallet/get_transaction_history.rs delete mode 100644 extension/wallet/src/wallet/sign_loan.rs diff --git a/Cargo.lock b/Cargo.lock index 0ca9d6108..9f59776c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aead" version = "0.3.2" @@ -2383,6 +2385,7 @@ version = "0.1.0" dependencies = [ "aes-gcm-siv", "anyhow", + "async-trait", "baru", "bdk", "coin_selection", diff --git a/extension/wallet/Cargo.toml b/extension/wallet/Cargo.toml index 79bc3ff6b..510dd15ad 100644 --- a/extension/wallet/Cargo.toml +++ b/extension/wallet/Cargo.toml @@ -13,6 +13,7 @@ default = [ "console_error_panic_hook" ] [dependencies] aes-gcm-siv = { version = "0.9", features = [ "std" ] } anyhow = "1" +async-trait = "0.1" baru = "0.1" bdk = { version = "0.4", default-features = false } coin_selection = { path = "../../coin_selection" } diff --git a/extension/wallet/src/cache_storage.rs b/extension/wallet/src/cache_storage.rs deleted file mode 100644 index 9048c3665..000000000 --- a/extension/wallet/src/cache_storage.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::storage::Storage; -use anyhow::{Context, Result}; - -/// A wrapper type around the local storage acting as cache for http requests. -pub struct CacheStorage { - inner: Storage, -} - -impl CacheStorage { - pub fn new() -> Result { - let local_storage = Storage::local_storage().with_context(|| "Could not open storage")?; - Ok(Self { - inner: local_storage, - }) - } - - /// This function will fetch the provided URL and store the response body in local storage. - /// It will fail if the response body is not a string. - async fn add(&self, url: &str) -> Result<()> { - let client = reqwest::Client::new(); - let body = client.get(url).send().await?; - let body_text = body - .text() - .await - .with_context(|| "response is not a string")?; - self.inner - .set_item(url, &body_text) - .with_context(|| format!("failed to add request for {} to storage", url))?; - - Ok(()) - } - - async fn match_with_str(&self, url: &str) -> Result> { - let maybe_response = self.inner.get_item(url)?; - match maybe_response { - None => Ok(None), - Some(response) => Ok(Some(Response { inner: response })), - } - } - - /// Convenience function that first tries to look up the value in the storage and if it is not present adds and returns it. - /// - /// This function will always return a response IF the request was successful (2xx status code). - /// Failed requests will never be added to the storage. - pub async fn match_or_add(&self, url: &str) -> Result { - Ok(match self.match_with_str(url).await? { - Some(response) => response, - None => { - self.add(url).await?; - self.match_with_str(url) - .await? - .context("no response in storage")? - } - }) - } -} - -pub struct Response { - inner: String, -} - -impl Response { - pub async fn text(&self) -> Result { - Ok(self.inner.clone()) - } -} diff --git a/extension/wallet/src/esplora.rs b/extension/wallet/src/esplora.rs deleted file mode 100644 index 4f41d496a..000000000 --- a/extension/wallet/src/esplora.rs +++ /dev/null @@ -1,285 +0,0 @@ -use crate::{cache_storage::CacheStorage, ESPLORA_API_URL}; -use anyhow::{anyhow, bail, Context, Result}; -use elements::{ - encode::{deserialize, serialize_hex}, - Address, BlockHash, Transaction, Txid, -}; -use reqwest::StatusCode; -use wasm_bindgen::UnwrapThrowExt; - -/// Fetch the UTXOs of an address. -/// -/// UTXOs change over time and as such, this function never uses a cache. -pub async fn fetch_utxos(address: &Address) -> Result> { - let esplora_url = { - let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); - guard.clone() - }; - - let path = format!("address/{}/utxo", address); - let esplora_url = esplora_url.join(path.as_str())?; - let response = reqwest::get(esplora_url.clone()) - .await - .context("failed to fetch UTXOs")?; - - if response.status() == StatusCode::NOT_FOUND { - log::debug!( - "GET {} returned 404, defaulting to empty UTXO set", - esplora_url - ); - - return Ok(Vec::new()); - } - - if !response.status().is_success() { - let error_body = response.text().await?; - return Err(anyhow!( - "failed to fetch utxos, esplora returned '{}'", - error_body - )); - } - - response - .json::>() - .await - .context("failed to deserialize response") -} - -/// Fetch transaction history for the specified address. -/// -/// Returns up to 50 mempool transactions plus the first 25 confirmed -/// transactions. See -/// https://github.com/blockstream/esplora/blob/master/API.md#get-addressaddresstxs -/// for more information. -pub async fn fetch_transaction_history(address: &Address) -> Result> { - let esplora_url = { - let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); - guard.clone() - }; - let path = format!("address/{}/txs", address); - let url = esplora_url.join(path.as_str())?; - let response = reqwest::get(url.clone()) - .await - .context("failed to fetch transaction history")?; - - if !response.status().is_success() { - let error_body = response.text().await?; - return Err(anyhow!( - "failed to fetch transaction history, esplora returned '{}' from '{}'", - error_body, - url - )); - } - - #[derive(serde::Deserialize)] - struct HistoryElement { - txid: Txid, - } - - let response = response - .json::>() - .await - .context("failed to deserialize response")?; - - Ok(response.iter().map(|elem| elem.txid).collect()) -} - -/// Fetches a transaction. -/// -/// This function makes use of the browsers local storage to avoid spamming the underlying source. -/// Transaction never change after they've been mined, hence we can cache those indefinitely. -pub async fn fetch_transaction(txid: Txid) -> Result { - let esplora_url = { - let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); - guard.clone() - }; - let cache = CacheStorage::new()?; - let body = cache - .match_or_add(&format!("{}tx/{}/hex", esplora_url, txid)) - .await? - .text() - .await?; - - Ok(deserialize(&hex::decode(body.as_bytes())?)?) -} - -pub async fn broadcast(tx: Transaction) -> Result { - let esplora_url = { - let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); - guard.clone() - }; - let esplora_url = esplora_url.join("tx")?; - let client = reqwest::Client::new(); - - let response = client - .post(esplora_url.clone()) - .body(serialize_hex(&tx)) - .send() - .await?; - - let code = response.status(); - - if !code.is_success() { - bail!("failed to successfully publish transaction"); - } - - let txid = response - .text() - .await? - .parse() - .context("failed to parse response body as txid")?; - - Ok(txid) -} - -pub async fn get_fee_estimates() -> Result { - let esplora_url = { - let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); - guard.clone() - }; - let esplora_url = esplora_url.join("fee-estimates")?; - - let fee_estimates = reqwest::get(esplora_url.clone()) - .await - .with_context(|| format!("failed to GET {}", esplora_url))? - .json() - .await - .context("failed to deserialize fee estimates")?; - - Ok(fee_estimates) -} - -/// The response object for the `/fee-estimates` endpoint. -/// -/// The key is the confirmation target (in number of blocks) and the value is the estimated feerate (in sat/vB). -/// The available confirmation targets are 1-25, 144, 504 and 1008 blocks. -#[derive(serde::Deserialize, Debug)] -pub struct FeeEstimatesResponse { - #[serde(rename = "1")] - pub b_1: Option, - #[serde(rename = "2")] - pub b_2: Option, - #[serde(rename = "3")] - pub b_3: Option, - #[serde(rename = "4")] - pub b_4: Option, - #[serde(rename = "5")] - pub b_5: Option, - #[serde(rename = "6")] - pub b_6: Option, - #[serde(rename = "7")] - pub b_7: Option, - #[serde(rename = "8")] - pub b_8: Option, - #[serde(rename = "9")] - pub b_9: Option, - #[serde(rename = "10")] - pub b_10: Option, - #[serde(rename = "11")] - pub b_11: Option, - #[serde(rename = "12")] - pub b_12: Option, - #[serde(rename = "13")] - pub b_13: Option, - #[serde(rename = "14")] - pub b_14: Option, - #[serde(rename = "15")] - pub b_15: Option, - #[serde(rename = "16")] - pub b_16: Option, - #[serde(rename = "17")] - pub b_17: Option, - #[serde(rename = "18")] - pub b_18: Option, - #[serde(rename = "19")] - pub b_19: Option, - #[serde(rename = "20")] - pub b_20: Option, - #[serde(rename = "21")] - pub b_21: Option, - #[serde(rename = "22")] - pub b_22: Option, - #[serde(rename = "23")] - pub b_23: Option, - #[serde(rename = "24")] - pub b_24: Option, - #[serde(rename = "25")] - pub b_25: Option, - #[serde(rename = "144")] - pub b_144: Option, - #[serde(rename = "504")] - pub b_504: Option, - #[serde(rename = "1008")] - pub b_1008: Option, -} - -/// Represents a UTXO as it is modeled by esplora. -/// -/// We ignore the commitments and asset IDs because we need to fetch the full transaction anyway. -/// Hence, we don't even bother with deserializing it here. -#[derive(serde::Deserialize, Debug, PartialEq, Clone, Copy)] -pub struct Utxo { - pub txid: Txid, - pub vout: u32, - pub status: UtxoStatus, -} - -#[derive(serde::Deserialize, Debug, PartialEq, Clone, Copy)] -pub struct UtxoStatus { - pub confirmed: bool, - pub block_height: Option, - pub block_hash: Option, - pub block_time: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_deserialize_confidential_utxo() { - let utxos = r#"[ - { - "txid": "26ad78aca6db29fa6ca37337fcfb23498dc1a01ee274614970097ab7ca6b6a19", - "vout": 0, - "status": { - "confirmed": true, - "block_height": 1099688, - "block_hash": "e0dd686b1a3334e941512a0e08dda69c9db71cd642d8b219f6063fb81838d86b", - "block_time": 1607042939 - }, - "valuecommitment": "0959edffa4326a255a15925a5a8eeda37e27fb80a62b1f1792dcd98bb8e29b7496", - "assetcommitment": "0b7b0f23047a44d6145fb4754f218807c1a3f0acc811221f7ba35e44dfc3a31795", - "noncecommitment": "039b1feace0413efc144298bc462a90bbf8f269cf68e3dfa65088f84f381921261" - } -] -"#; - - let utxos = serde_json::from_str::>(utxos).unwrap(); - - assert_eq!(utxos.len(), 1); - } - - #[test] - fn can_deserialize_explicit_utxo() { - let utxos = r#"[ - { - "txid": "58035633e6391fd08955f9f73b710efe3835a7975baaf1267aa4fcb3c738c1ba", - "vout": 0, - "status": { - "confirmed": true, - "block_height": 1099644, - "block_hash": "58d573591f8920b225512bb209b5d75f2ae9260f107c306b87a53c4cc4d42d7e", - "block_time": 1607040059 - }, - "value": 99958, - "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d" - } -] -"#; - - let utxos = serde_json::from_str::>(utxos).unwrap(); - - assert_eq!(utxos.len(), 1); - } -} diff --git a/extension/wallet/src/lib.rs b/extension/wallet/src/lib.rs index a6384b390..ee03e0e87 100644 --- a/extension/wallet/src/lib.rs +++ b/extension/wallet/src/lib.rs @@ -1,32 +1,32 @@ -use std::str::FromStr; - use conquer_once::Lazy; use elements::{ bitcoin::util::amount::{Amount, Denomination}, - Address, AddressParams, Txid, + Address, AddressParams, AssetId, Txid, }; use futures::lock::Mutex; use js_sys::Promise; +use reqwest::Url; +use rust_decimal::Decimal; +use std::str::FromStr; use wasm_bindgen::{prelude::*, JsCast}; use web_sys::window; +use crate::{ + lib_wallet::{LoanDetails, Wallet}, + storage::Storage, + wallet::*, +}; +use baru::loan::Borrower1; + #[macro_use] mod macros; -mod assets; -mod cache_storage; -mod esplora; +mod lib_wallet; mod logger; mod storage; mod wallet; -use crate::{storage::Storage, wallet::*}; -use reqwest::Url; - -// TODO: make this configurable through extension option UI -const DEFAULT_SAT_PER_VBYTE: u64 = 1; - -static LOADED_WALLET: Lazy>> = Lazy::new(Mutex::default); +static LOADED_WALLET: Lazy>> = Lazy::new(Mutex::default); // TODO: I was unable to use `futures::lock::Mutex` for these, but // someone else should be able to do it @@ -91,7 +91,7 @@ pub fn setup() { /// The created wallet will be automatically loaded. #[wasm_bindgen] pub async fn create_new_wallet(name: String, password: String) -> Result { - map_err_from_anyhow!(wallet::create_new(name, password, &LOADED_WALLET).await)?; + map_err_from_anyhow!(wallet::create_new_waves(name, password, &LOADED_WALLET).await)?; Ok(JsValue::null()) } @@ -131,12 +131,36 @@ pub async fn wallet_status(name: String) -> Result { /// Fails if the wallet is currently not loaded. #[wasm_bindgen] pub async fn get_address(name: String) -> Result { - let address = map_err_from_anyhow!(wallet::get_address(name, &LOADED_WALLET).await)?; + let wallet = current(&name, &LOADED_WALLET).await.unwrap(); + let address = wallet.get_address(); let address = map_err_from_anyhow!(JsValue::from_serde(&address))?; Ok(address) } +/// A single balance entry as returned by [`get_balances`]. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +pub struct BalanceEntry { + pub asset: AssetId, + pub ticker: String, + pub value: Decimal, +} + +impl BalanceEntry { + pub fn for_asset(asset: AssetId, ticker: String, value: u64, precision: u32) -> Self { + let mut decimal = Decimal::from(value); + decimal + .set_scale(precision) + .expect("precision must be < 28"); + + Self { + asset, + ticker, + value: decimal, + } + } +} + /// Get the balances of the currently loaded wallet. /// /// Returns an array of [`BalanceEntry`]s. @@ -144,7 +168,20 @@ pub async fn get_address(name: String) -> Result { /// Fails if the wallet is currently not loaded or we cannot reach the block explorer for some reason. #[wasm_bindgen] pub async fn get_balances(name: String) -> Result { - let balance_entries = map_err_from_anyhow!(wallet::get_balances(&name, &LOADED_WALLET).await)?; + let wallet = current(&name, &LOADED_WALLET).await.unwrap(); + let balance_entries = map_err_from_anyhow!(crate::lib_wallet::get_balances(&*wallet).await)?; + let balance_entries = balance_entries + .iter() + .map(|entry| { + let (ticker, precision) = lookup(entry.asset).unwrap(); + BalanceEntry::for_asset( + entry.asset, + ticker.to_string(), + entry.value, + precision as u32, + ) + }) + .collect::>(); let balance_entries = map_err_from_anyhow!(JsValue::from_serde(&balance_entries))?; Ok(balance_entries) @@ -155,9 +192,15 @@ pub async fn get_balances(name: String) -> Result { /// Returns the transaction ID of the transaction that was broadcasted. #[wasm_bindgen] pub async fn withdraw_everything_to(name: String, address: String) -> Result { + let wallet = current(&name, &LOADED_WALLET).await.unwrap(); let address = map_err_from_anyhow!(address.parse::
())?; - let txid = - map_err_from_anyhow!(wallet::withdraw_everything_to(name, &LOADED_WALLET, address).await)?; + let btc_asset_id = { + let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); + *guard + }; + let txid = map_err_from_anyhow!( + crate::lib_wallet::withdraw_everything_to(&*wallet, address, btc_asset_id).await + )?; let txid = map_err_from_anyhow!(JsValue::from_serde(&txid))?; Ok(txid) @@ -172,8 +215,23 @@ pub async fn make_buy_create_swap_payload( usdt: String, ) -> Result { let usdt = map_err_from_anyhow!(Amount::from_str_in(&usdt, Denomination::Bitcoin))?; + let btc_asset_id = { + let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); + *guard + }; + let usdt_asset_id = { + let guard = USDT_ASSET_ID.lock().expect_throw("can get lock"); + *guard + }; + let wallet = current(&wallet_name, &LOADED_WALLET).await.unwrap(); let payload = map_err_from_anyhow!( - wallet::make_buy_create_swap_payload(wallet_name, &LOADED_WALLET, usdt).await + crate::lib_wallet::make_buy_create_swap_payload( + &*wallet, + usdt, + btc_asset_id, + usdt_asset_id + ) + .await )?; let payload = map_err_from_anyhow!(JsValue::from_serde(&payload))?; @@ -189,8 +247,13 @@ pub async fn make_sell_create_swap_payload( btc: String, ) -> Result { let btc = map_err_from_anyhow!(Amount::from_str_in(&btc, Denomination::Bitcoin))?; + let btc_asset_id = { + let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); + *guard + }; + let wallet = current(&wallet_name, &LOADED_WALLET).await.unwrap(); let payload = map_err_from_anyhow!( - wallet::make_sell_create_swap_payload(wallet_name, &LOADED_WALLET, btc).await + crate::lib_wallet::make_sell_create_swap_payload(&*wallet, btc, btc_asset_id).await )?; let payload = map_err_from_anyhow!(JsValue::from_serde(&payload))?; @@ -210,9 +273,26 @@ pub async fn make_loan_request( collateral: String, ) -> Result { let collateral = map_err_from_anyhow!(Amount::from_str_in(&collateral, Denomination::Bitcoin))?; - let loan_request = map_err_from_anyhow!( - wallet::make_loan_request(wallet_name, &LOADED_WALLET, collateral).await + let wallet = current(&wallet_name, &LOADED_WALLET).await.unwrap(); + let btc_asset_id = { + let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); + *guard + }; + let usdt_asset_id = { + let guard = USDT_ASSET_ID.lock().expect_throw("can get lock"); + *guard + }; + let (borrower, loan_request) = map_err_from_anyhow!( + crate::lib_wallet::make_loan_request(&*wallet, collateral, btc_asset_id, usdt_asset_id) + .await )?; + + let storage = map_err_from_anyhow!(Storage::local_storage())?; + map_err_from_anyhow!(storage.set_item( + "borrower_state", + map_err_from_anyhow!(serde_json::to_string(&borrower))?, + ))?; + let loan_request = map_err_from_anyhow!(JsValue::from_serde(&loan_request))?; Ok(loan_request) @@ -224,7 +304,38 @@ pub async fn make_loan_request( /// Returns the signed transaction. #[wasm_bindgen] pub async fn sign_loan(wallet_name: String) -> Result { - let loan_tx = map_err_from_anyhow!(wallet::sign_loan(wallet_name, &LOADED_WALLET).await)?; + let wallet = current(&wallet_name, &LOADED_WALLET).await.unwrap(); + let storage = Storage::local_storage().unwrap(); + let borrower = storage + .get_item::("borrower_state") + .unwrap() + .unwrap(); + let (borrower1, loan_details) = + serde_json::from_str::<(Borrower1, LoanDetails)>(&borrower).unwrap(); + let loan_tx = map_err_from_anyhow!(crate::lib_wallet::sign_loan(&*wallet, borrower1).await)?; + + // We don't broadcast this transaction ourselves, but we expect + // the lender to do so very soon. We therefore save the borrower + // state so that we can later on build, sign and broadcast the + // repayment transaction + + let mut open_loans = match storage.get_item::("open_loans").unwrap() { + Some(open_loans) => serde_json::from_str(&open_loans).unwrap(), + None => Vec::::new(), + }; + + open_loans.push(loan_details); + storage + .set_item("open_loans", serde_json::to_string(&open_loans).unwrap()) + .unwrap(); + + storage + .set_item( + &format!("loan_state:{}", loan_tx.txid()), + serde_json::to_string(&borrower).unwrap(), + ) + .unwrap(); + let loan_tx = map_err_from_anyhow!(JsValue::from_serde(&Transaction::from(loan_tx)))?; Ok(loan_tx) @@ -239,9 +350,9 @@ pub async fn sign_and_send_swap_transaction( transaction: JsValue, ) -> Result { let transaction: Transaction = map_err_from_anyhow!(transaction.into_serde())?; + let wallet = current(&wallet_name, &LOADED_WALLET).await.unwrap(); let txid = map_err_from_anyhow!( - wallet::sign_and_send_swap_transaction(wallet_name, &LOADED_WALLET, transaction.into()) - .await + crate::lib_wallet::sign_and_send_swap_transaction(&*wallet, transaction.into()).await )?; let txid = map_err_from_anyhow!(JsValue::from_serde(&txid))?; @@ -257,8 +368,9 @@ pub async fn sign_and_send_swap_transaction( #[wasm_bindgen] pub async fn extract_trade(wallet_name: String, transaction: JsValue) -> Result { let transaction: Transaction = map_err_from_anyhow!(transaction.into_serde())?; + let wallet = current(&wallet_name, &LOADED_WALLET).await.unwrap(); let trade = map_err_from_anyhow!( - wallet::extract_trade(wallet_name, &LOADED_WALLET, transaction.into()).await + crate::lib_wallet::extract_trade(&*wallet, transaction.into(),).await )?; let trade = map_err_from_anyhow!(JsValue::from_serde(&trade))?; @@ -280,43 +392,101 @@ pub async fn extract_trade(wallet_name: String, transaction: JsValue) -> Result< #[wasm_bindgen] pub async fn extract_loan(wallet_name: String, loan_response: JsValue) -> Result { let loan_response = map_err_from_anyhow!(loan_response.into_serde())?; - let details = map_err_from_anyhow!( - wallet::extract_loan(wallet_name, &LOADED_WALLET, loan_response).await - )?; - let details = map_err_from_anyhow!(JsValue::from_serde(&details))?; + let wallet = current(&wallet_name, &LOADED_WALLET).await.unwrap(); - Ok(details) -} + let btc_asset_id = { + let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); + *guard + }; + let usdt_asset_id = { + let guard = USDT_ASSET_ID.lock().expect_throw("can get lock"); + *guard + }; -/// Returns all the active loans stored in the browser's local storage. -#[wasm_bindgen] -pub async fn get_open_loans() -> Result { - let storage = map_err_from_anyhow!(Storage::local_storage())?; - let loans = map_err_from_anyhow!(storage.get_open_loans().await)?; - let loans = map_err_from_anyhow!(JsValue::from_serde(&loans))?; + let storage = Storage::local_storage().unwrap(); + let borrower0 = storage + .get_item::("borrower_state") + .unwrap() + .unwrap(); + let borrower0 = serde_json::from_str(&borrower0).unwrap(); + + let (borrower1, loan_details) = map_err_from_anyhow!( + crate::lib_wallet::extract_loan( + &*wallet, + loan_response, + btc_asset_id, + usdt_asset_id, + borrower0 + ) + .await + )?; + storage + .set_item( + "borrower_state", + serde_json::to_string(&(borrower1, loan_details.clone())).unwrap(), + ) + .unwrap(); + let details = map_err_from_anyhow!(JsValue::from_serde(&loan_details))?; - Ok(loans) + Ok(details) } #[wasm_bindgen] pub async fn repay_loan(wallet_name: String, loan_txid: String) -> Result { let loan_txid = map_err_from_anyhow!(Txid::from_str(&loan_txid))?; + + let storage = Storage::local_storage().unwrap(); + + let borrower1 = storage + .get_item::(&format!("loan_state:{}", loan_txid)) + .unwrap() + .unwrap(); + let borrower1 = serde_json::from_str(&borrower1).unwrap(); + + let wallet = current(&wallet_name, &LOADED_WALLET).await.unwrap(); let txid = - map_err_from_anyhow!(wallet::repay_loan(wallet_name, &LOADED_WALLET, loan_txid).await)?; + map_err_from_anyhow!(crate::lib_wallet::repay_loan(&*wallet, loan_txid, borrower1).await)?; let txid = map_err_from_anyhow!(JsValue::from_serde(&txid))?; + // TODO: Make sure that we can safely forget this i.e. sufficient confirmations + storage + .remove_item(&format!("loan_state:{}", loan_txid)) + .unwrap(); + + let open_loans = match storage.get_item::("open_loans").unwrap() { + Some(open_loans) => serde_json::from_str(&open_loans).unwrap(), + None => Vec::::new(), + }; + let open_loans = open_loans + .iter() + .take_while(|details| loan_txid != details.txid) + .collect::>(); + storage + .set_item("open_loans", serde_json::to_string(&open_loans).unwrap()) + .unwrap(); + Ok(txid) } #[wasm_bindgen] pub async fn get_past_transactions(wallet_name: String) -> Result { - let history = - map_err_from_anyhow!(wallet::get_transaction_history(wallet_name, &LOADED_WALLET).await)?; + let wallet = current(&wallet_name, &LOADED_WALLET).await.unwrap(); + let history = map_err_from_anyhow!(crate::lib_wallet::get_transaction_history(&*wallet).await)?; let history = map_err_from_anyhow!(JsValue::from_serde(&history))?; Ok(history) } +/// Returns all the active loans stored in the browser's local storage. +#[wasm_bindgen] +pub async fn get_open_loans() -> Result { + let storage = map_err_from_anyhow!(Storage::local_storage())?; + let loans = map_err_from_anyhow!(storage.get_open_loans().await)?; + let loans = map_err_from_anyhow!(JsValue::from_serde(&loans))?; + + Ok(loans) +} + fn handle_storage_update(event: web_sys::StorageEvent) -> Promise { match (event.key().as_deref(), event.new_value().as_deref()) { (Some("CHAIN"), Some(new_value)) => { diff --git a/extension/wallet/src/lib_wallet.rs b/extension/wallet/src/lib_wallet.rs new file mode 100644 index 000000000..4f0d7a651 --- /dev/null +++ b/extension/wallet/src/lib_wallet.rs @@ -0,0 +1,348 @@ +use std::ops::{Add, Sub}; + +use anyhow::Result; +use async_trait::async_trait; +use bdk::bitcoin::secp256k1::PublicKey; +use elements::{ + bitcoin::{secp256k1::SECP256K1, Amount}, + confidential, + secp256k1_zkp::SecretKey, + Address, AssetId, BlockHash, OutPoint, Transaction, TxOut, Txid, +}; +use futures::{stream::FuturesUnordered, StreamExt, TryStreamExt}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +pub use crate::lib_wallet::{ + extract_loan::{extract_loan, Error as ExtractLoanError}, + extract_trade::extract_trade, + get_balances::get_balances, + get_transaction_history::get_transaction_history, + make_create_swap_payload::{ + make_buy_create_swap_payload, make_sell_create_swap_payload, Error as MakePayloadError, + }, + make_loan_request::{make_loan_request, Error as MakeLoanRequestError}, + repay_loan::repay_loan, + sign_and_send_swap_transaction::sign_and_send_swap_transaction, + sign_loan::sign_loan, + withdraw_everything_to::withdraw_everything_to, +}; + +mod extract_loan; +mod extract_trade; +mod get_balances; +mod get_transaction_history; +mod make_create_swap_payload; +mod make_loan_request; +mod repay_loan; +mod sign_and_send_swap_transaction; +mod sign_loan; +mod withdraw_everything_to; + +// TODO: make this configurable through extension option UI +pub const DEFAULT_SAT_PER_VBYTE: u64 = 1; + +#[async_trait] +pub trait Wallet { + async fn fetch_transaction(&self, txid: Txid) -> Result; + async fn fetch_transaction_history(&self, address: &Address) -> Result>; + async fn get_fee_estimates(&self) -> Result; + async fn broadcast(&self, tx: Transaction) -> Result; + fn get_address(&self) -> Address; + fn blinding_key(&self) -> SecretKey; + fn secret_key(&self) -> SecretKey; + fn public_key(&self) -> PublicKey { + PublicKey::from_secret_key(SECP256K1, &self.secret_key()) + } + /// A pure function to compute the balances of the wallet given a set of [`TxOut`]s. + fn compute_balances(&self, txouts: &[TxOut]) -> Vec { + let grouped_txouts = txouts + .iter() + .filter_map(|utxo| match utxo { + TxOut { + asset: confidential::Asset::Explicit(asset), + value: confidential::Value::Explicit(value), + .. + } => Some((*asset, *value)), + txout => match txout.unblind(SECP256K1, self.blinding_key()) { + Ok(unblinded_txout) => Some((unblinded_txout.asset, unblinded_txout.value)), + Err(e) => { + log::warn!("failed to unblind txout: {}", e); + None + } + }, + }) + .into_group_map(); + + grouped_txouts + .into_iter() + .map(|(asset, utxos)| { + let total_sum = utxos.into_iter().sum(); + BalanceEntry { + asset, + value: total_sum, + } + }) + .collect() + } + async fn fetch_utxos(&self) -> Result>; + async fn get_txouts Result> + Copy + Send>( + &self, + filter_map: FM, + ) -> Result> { + let utxos = self.fetch_utxos().await?; + + let txouts = utxos + .into_iter() + .map(move |utxo| async move { + let mut tx = self.fetch_transaction(utxo.txid).await?; + let txout = tx.output.remove(utxo.vout as usize); + + filter_map(utxo, txout) + }) + .collect::>() + .filter_map(|r| std::future::ready(r.transpose())) + .try_collect::>() + .await?; + + Ok(txouts) + } +} + +/// The response object for the `/fee-estimates` endpoint. +/// +/// The key is the confirmation target (in number of blocks) and the value is the estimated feerate (in sat/vB). +/// The available confirmation targets are 1-25, 144, 504 and 1008 blocks. +#[derive(serde::Deserialize, Debug)] +pub struct FeeEstimatesResponse { + #[serde(rename = "1")] + pub b_1: Option, + #[serde(rename = "2")] + pub b_2: Option, + #[serde(rename = "3")] + pub b_3: Option, + #[serde(rename = "4")] + pub b_4: Option, + #[serde(rename = "5")] + pub b_5: Option, + #[serde(rename = "6")] + pub b_6: Option, + #[serde(rename = "7")] + pub b_7: Option, + #[serde(rename = "8")] + pub b_8: Option, + #[serde(rename = "9")] + pub b_9: Option, + #[serde(rename = "10")] + pub b_10: Option, + #[serde(rename = "11")] + pub b_11: Option, + #[serde(rename = "12")] + pub b_12: Option, + #[serde(rename = "13")] + pub b_13: Option, + #[serde(rename = "14")] + pub b_14: Option, + #[serde(rename = "15")] + pub b_15: Option, + #[serde(rename = "16")] + pub b_16: Option, + #[serde(rename = "17")] + pub b_17: Option, + #[serde(rename = "18")] + pub b_18: Option, + #[serde(rename = "19")] + pub b_19: Option, + #[serde(rename = "20")] + pub b_20: Option, + #[serde(rename = "21")] + pub b_21: Option, + #[serde(rename = "22")] + pub b_22: Option, + #[serde(rename = "23")] + pub b_23: Option, + #[serde(rename = "24")] + pub b_24: Option, + #[serde(rename = "25")] + pub b_25: Option, + #[serde(rename = "144")] + pub b_144: Option, + #[serde(rename = "504")] + pub b_504: Option, + #[serde(rename = "1008")] + pub b_1008: Option, +} + +/// A single balance entry as returned by [`get_balances`]. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +pub struct BalanceEntry { + pub asset: AssetId, + pub value: u64, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct LoanDetails { + pub collateral: TradeSide, + pub principal: TradeSide, + pub principal_repayment: u64, + // TODO: Express as target date or number of days instead? + pub term: u64, + pub txid: Txid, +} + +impl LoanDetails { + #[allow(clippy::too_many_arguments)] + pub fn new( + collateral_asset: AssetId, + collateral_amount: Amount, + collateral_balance: u64, + principal_asset: AssetId, + principal_amount: Amount, + principal_balance: u64, + timelock: u64, + txid: Txid, + ) -> Result { + let collateral = TradeSide::new_sell( + collateral_asset, + collateral_amount.as_sat(), + collateral_balance, + )?; + + let principal = TradeSide::new_buy( + principal_asset, + principal_amount.as_sat(), + principal_balance, + )?; + + Ok(Self { + collateral, + principal_repayment: principal.amount, + principal, + term: timelock, + txid, + }) + } +} + +/// Represents a UTXO as it is modeled by esplora. +/// +/// We ignore the commitments and asset IDs because we need to fetch the full transaction anyway. +/// Hence, we don't even bother with deserializing it here. +// todo!(Make our own UTXO type, this is a http payload) +#[derive(serde::Deserialize, Debug, PartialEq, Clone, Copy)] +pub struct Utxo { + pub txid: Txid, + pub vout: u32, + pub status: UtxoStatus, +} + +#[derive(serde::Deserialize, Debug, PartialEq, Clone, Copy)] +pub struct UtxoStatus { + pub confirmed: bool, + pub block_height: Option, + pub block_hash: Option, + pub block_time: Option, +} + +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct TradeSide { + pub asset: AssetId, + pub amount: u64, + pub balance_before: u64, + pub balance_after: u64, +} + +impl TradeSide { + fn new_sell(asset: AssetId, amount: u64, current_balance: u64) -> Result { + Self::new(asset, amount, current_balance, u64::sub) + } + + fn new_buy(asset: AssetId, amount: u64, current_balance: u64) -> Result { + Self::new(asset, amount, current_balance, u64::add) + } + + fn new( + asset: AssetId, + amount: u64, + current_balance: u64, + balance_after: impl Fn(u64, u64) -> u64, + ) -> Result { + Ok(Self { + asset, + amount, + balance_before: current_balance, + balance_after: balance_after(current_balance, amount), + }) + } +} + +// todo!(Make our own UTXO type, this is a http payload) +/// Represents the payload for creating a swap. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct CreateSwapPayload { + pub alice_inputs: Vec, + pub address: Address, + #[serde(with = "bdk::bitcoin::util::amount::serde::as_sat")] + pub amount: bdk::bitcoin::Amount, +} + +// todo!(Make our own UTXO type, this is a http payload) +#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] +pub struct SwapUtxo { + pub outpoint: OutPoint, + pub blinding_key: SecretKey, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_deserialize_confidential_utxo() { + let utxos = r#"[ + { + "txid": "26ad78aca6db29fa6ca37337fcfb23498dc1a01ee274614970097ab7ca6b6a19", + "vout": 0, + "status": { + "confirmed": true, + "block_height": 1099688, + "block_hash": "e0dd686b1a3334e941512a0e08dda69c9db71cd642d8b219f6063fb81838d86b", + "block_time": 1607042939 + }, + "valuecommitment": "0959edffa4326a255a15925a5a8eeda37e27fb80a62b1f1792dcd98bb8e29b7496", + "assetcommitment": "0b7b0f23047a44d6145fb4754f218807c1a3f0acc811221f7ba35e44dfc3a31795", + "noncecommitment": "039b1feace0413efc144298bc462a90bbf8f269cf68e3dfa65088f84f381921261" + } +] +"#; + + let utxos = serde_json::from_str::>(utxos).unwrap(); + + assert_eq!(utxos.len(), 1); + } + + #[test] + fn can_deserialize_explicit_utxo() { + let utxos = r#"[ + { + "txid": "58035633e6391fd08955f9f73b710efe3835a7975baaf1267aa4fcb3c738c1ba", + "vout": 0, + "status": { + "confirmed": true, + "block_height": 1099644, + "block_hash": "58d573591f8920b225512bb209b5d75f2ae9260f107c306b87a53c4cc4d42d7e", + "block_time": 1607040059 + }, + "value": 99958, + "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d" + } +] +"#; + + let utxos = serde_json::from_str::>(utxos).unwrap(); + + assert_eq!(utxos.len(), 1); + } +} diff --git a/extension/wallet/src/lib_wallet/extract_loan.rs b/extension/wallet/src/lib_wallet/extract_loan.rs new file mode 100644 index 000000000..1c556129b --- /dev/null +++ b/extension/wallet/src/lib_wallet/extract_loan.rs @@ -0,0 +1,79 @@ +use crate::lib_wallet::{LoanDetails, Wallet}; +use baru::loan::{Borrower0, Borrower1, LoanResponse}; +use elements::{secp256k1_zkp::SECP256K1, AssetId}; + +pub async fn extract_loan( + wallet: &W, + loan_response: LoanResponse, + btc_asset_id: AssetId, + usdt_asset_id: AssetId, + borrower0: Borrower0, +) -> Result<(Borrower1, LoanDetails), Error> { + let txouts = wallet + .get_txouts(|utxo, txout| Ok(Some((utxo, txout)))) + .await + .map_err(Error::GetTxOuts)?; + let balances = wallet.compute_balances( + &txouts + .iter() + .map(|(_, txout)| txout) + .cloned() + .collect::>(), + ); + + let timelock = loan_response.timelock; + let borrower1 = borrower0 + .interpret(SECP256K1, loan_response) + .map_err(Error::InterpretLoanResponse)?; + + let collateral_balance = balances + .iter() + .find_map(|entry| { + if entry.asset == btc_asset_id { + Some(entry.value) + } else { + None + } + }) + .ok_or(Error::InsufficientCollateral)?; + + let principal_balance = balances + .iter() + .find_map(|entry| { + if entry.asset == usdt_asset_id { + Some(entry.value) + } else { + None + } + }) + .unwrap_or_default(); + + let loan_txid = borrower1.loan_transaction.txid(); + let loan_details = LoanDetails::new( + btc_asset_id, + borrower1.collateral_amount, + collateral_balance, + usdt_asset_id, + borrower1.principal_tx_out_amount, + principal_balance, + timelock, + loan_txid, + ) + .map_err(Error::LoanDetails)?; + + Ok((borrower1, loan_details)) +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Failed to deserialise loan response: {0}")] + LoanResponseDeserialization(#[from] serde_json::Error), + #[error("Failed to get transaction outputs: {0}")] + GetTxOuts(anyhow::Error), + #[error("Failed to interpret loan response: {0}")] + InterpretLoanResponse(anyhow::Error), + #[error("Not enough collateral to put up for loan")] + InsufficientCollateral, + #[error("Failed to build loan details: {0}")] + LoanDetails(anyhow::Error), +} diff --git a/extension/wallet/src/wallet/extract_trade.rs b/extension/wallet/src/lib_wallet/extract_trade.rs similarity index 91% rename from extension/wallet/src/wallet/extract_trade.rs rename to extension/wallet/src/lib_wallet/extract_trade.rs index 8d19ceec6..104f56c3b 100644 --- a/extension/wallet/src/wallet/extract_trade.rs +++ b/extension/wallet/src/lib_wallet/extract_trade.rs @@ -1,24 +1,18 @@ -use crate::{ - wallet::{compute_balances, current, get_txouts, Wallet}, - TradeSide, -}; +use crate::lib_wallet::{TradeSide, Wallet}; use anyhow::{bail, Context, Result}; use elements::{confidential, secp256k1_zkp::SECP256K1, Transaction, TxOut}; -use futures::lock::Mutex; use itertools::Itertools; use serde::{Deserialize, Serialize}; // TODO: Public APIs should return specific error struct/enum -pub async fn extract_trade( - name: String, - current_wallet: &Mutex>, +pub async fn extract_trade( + wallet: &W, transaction: Transaction, ) -> Result { - let wallet = current(&name, current_wallet).await?; - - let txouts = get_txouts(&wallet, |utxo, txout| Ok(Some((utxo, txout)))).await?; - let balances = compute_balances( - &wallet, + let txouts = wallet + .get_txouts(|utxo, txout| Ok(Some((utxo, txout)))) + .await?; + let balances = wallet.compute_balances( &txouts .iter() .map(|(_, txout)| txout) diff --git a/extension/wallet/src/lib_wallet/get_balances.rs b/extension/wallet/src/lib_wallet/get_balances.rs new file mode 100644 index 000000000..77c3fee08 --- /dev/null +++ b/extension/wallet/src/lib_wallet/get_balances.rs @@ -0,0 +1,10 @@ +use crate::lib_wallet::{BalanceEntry, Wallet}; +use anyhow::Result; + +pub async fn get_balances(wallet: &W) -> Result> { + let txouts = wallet.get_txouts(|_, txout| Ok(Some(txout))).await?; + + let balances = wallet.compute_balances(&txouts); + + Ok(balances) +} diff --git a/extension/wallet/src/lib_wallet/get_transaction_history.rs b/extension/wallet/src/lib_wallet/get_transaction_history.rs new file mode 100644 index 000000000..947dd8ba1 --- /dev/null +++ b/extension/wallet/src/lib_wallet/get_transaction_history.rs @@ -0,0 +1,13 @@ +use anyhow::Result; +use elements::Txid; + +use crate::lib_wallet::Wallet; + +pub async fn get_transaction_history(wallet: &W) -> Result> { + // We have a single address, so looking for the transaction + // history of said address is sufficient + let address = wallet.get_address(); + let history = wallet.fetch_transaction_history(&address).await?; + + Ok(history) +} diff --git a/extension/wallet/src/wallet/make_create_swap_payload.rs b/extension/wallet/src/lib_wallet/make_create_swap_payload.rs similarity index 51% rename from extension/wallet/src/wallet/make_create_swap_payload.rs rename to extension/wallet/src/lib_wallet/make_create_swap_payload.rs index 3d8ead3e0..646ab3f53 100644 --- a/extension/wallet/src/wallet/make_create_swap_payload.rs +++ b/extension/wallet/src/lib_wallet/make_create_swap_payload.rs @@ -1,97 +1,63 @@ -use crate::{ - wallet::{current, get_txouts, CreateSwapPayload, SwapUtxo, Wallet}, - BTC_ASSET_ID, USDT_ASSET_ID, -}; +use crate::lib_wallet::{CreateSwapPayload, SwapUtxo, Wallet}; use bdk::bitcoin::Amount; use coin_selection::{self, coin_select}; use elements::{secp256k1_zkp::SECP256K1, AssetId, OutPoint}; use estimate_transaction_size::avg_vbytes; -use futures::lock::Mutex; -use wasm_bindgen::UnwrapThrowExt; -pub async fn make_buy_create_swap_payload( - name: String, - current_wallet: &Mutex>, +pub async fn make_buy_create_swap_payload( + wallet: &W, sell_amount: Amount, + btc_asset_id: AssetId, + usdt_asset_id: AssetId, ) -> Result { - let btc_asset_id = { - let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); - *guard - }; - let usdt_asset_id = { - let guard = USDT_ASSET_ID.lock().expect_throw("can get lock"); - *guard - }; - - make_create_swap_payload( - name, - current_wallet, - sell_amount, - usdt_asset_id, - btc_asset_id, - ) - .await + make_create_swap_payload(wallet, sell_amount, usdt_asset_id, btc_asset_id).await } -pub async fn make_sell_create_swap_payload( - name: String, - current_wallet: &Mutex>, +pub async fn make_sell_create_swap_payload( + wallet: &W, sell_amount: Amount, + btc_asset_id: AssetId, ) -> Result { - let btc_asset_id = { - let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); - *guard - }; - make_create_swap_payload( - name, - current_wallet, - sell_amount, - btc_asset_id, - btc_asset_id, - ) - .await + make_create_swap_payload(wallet, sell_amount, btc_asset_id, btc_asset_id).await } -async fn make_create_swap_payload( - name: String, - current_wallet: &Mutex>, +async fn make_create_swap_payload( + wallet: &W, sell_amount: Amount, sell_asset: AssetId, fee_asset: AssetId, ) -> Result { - let wallet = current(&name, current_wallet) - .await - .map_err(Error::LoadWallet)?; let blinding_key = wallet.blinding_key(); - let utxos = get_txouts(&wallet, |utxo, txout| { - Ok({ - let unblinded_txout = txout.unblind(SECP256K1, blinding_key)?; - let outpoint = OutPoint { - txid: utxo.txid, - vout: utxo.vout, - }; - let candidate_asset = unblinded_txout.asset; + let utxos = wallet + .get_txouts(|utxo, txout| { + Ok({ + let unblinded_txout = txout.unblind(SECP256K1, blinding_key)?; + let outpoint = OutPoint { + txid: utxo.txid, + vout: utxo.vout, + }; + let candidate_asset = unblinded_txout.asset; - if candidate_asset == sell_asset { - Some(coin_selection::Utxo { - outpoint, - value: unblinded_txout.value, - script_pubkey: txout.script_pubkey, - asset: candidate_asset, - }) - } else { - log::debug!( - "utxo {} with asset id {} is not the sell asset, ignoring", - outpoint, - candidate_asset - ); - None - } + if candidate_asset == sell_asset { + Some(coin_selection::Utxo { + outpoint, + value: unblinded_txout.value, + script_pubkey: txout.script_pubkey, + asset: candidate_asset, + }) + } else { + log::debug!( + "utxo {} with asset id {} is not the sell asset, ignoring", + outpoint, + candidate_asset + ); + None + } + }) }) - }) - .await - .map_err(Error::GetTxOuts)?; + .await + .map_err(Error::GetTxOuts)?; let (bobs_fee_rate, fee_offset) = if fee_asset == sell_asset { // Bob currently hardcodes a fee-rate of 1 sat / vbyte, hence @@ -130,8 +96,6 @@ async fn make_create_swap_payload( #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Wallet is not loaded: {0}")] - LoadWallet(anyhow::Error), #[error("Coin selection: {0}")] CoinSelection(coin_selection::Error), #[error("Failed to get transaction outputs: {0}")] diff --git a/extension/wallet/src/wallet/make_loan_request.rs b/extension/wallet/src/lib_wallet/make_loan_request.rs similarity index 54% rename from extension/wallet/src/wallet/make_loan_request.rs rename to extension/wallet/src/lib_wallet/make_loan_request.rs index b586c4d55..6afef209b 100644 --- a/extension/wallet/src/wallet/make_loan_request.rs +++ b/extension/wallet/src/lib_wallet/make_loan_request.rs @@ -1,38 +1,20 @@ -use crate::{ - storage::Storage, - wallet::{current, get_txouts, Wallet}, - BTC_ASSET_ID, DEFAULT_SAT_PER_VBYTE, USDT_ASSET_ID, -}; +use crate::lib_wallet::{Wallet, DEFAULT_SAT_PER_VBYTE}; use baru::{ input::Input, loan::{Borrower0, LoanRequest}, }; use coin_selection::{self, coin_select}; -use elements::{bitcoin::util::amount::Amount, secp256k1_zkp::SECP256K1, OutPoint}; +use elements::{bitcoin::util::amount::Amount, secp256k1_zkp::SECP256K1, AssetId, OutPoint}; use estimate_transaction_size::avg_vbytes; -use futures::lock::Mutex; use rand::thread_rng; -use wasm_bindgen::UnwrapThrowExt; -pub async fn make_loan_request( - name: String, - current_wallet: &Mutex>, +pub async fn make_loan_request( + wallet: &W, collateral_amount: Amount, -) -> Result { - let btc_asset_id = { - let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); - *guard - }; - let usdt_asset_id = { - let guard = USDT_ASSET_ID.lock().expect_throw("can get lock"); - *guard - }; - + btc_asset_id: AssetId, + usdt_asset_id: AssetId, +) -> Result<(Borrower0, LoanRequest), Error> { let (address, blinding_key) = { - let wallet = current(&name, current_wallet) - .await - .map_err(Error::LoadWallet)?; - let address = wallet.get_address(); let blinding_key = wallet.blinding_key(); @@ -41,38 +23,37 @@ pub async fn make_loan_request( let coin_selector = { |amount, asset| async move { - let wallet = current(&name, current_wallet).await?; - - let utxos = get_txouts(&wallet, |utxo, txout| { - Ok({ - let unblinded_txout = txout.unblind(SECP256K1, blinding_key)?; - let outpoint = OutPoint { - txid: utxo.txid, - vout: utxo.vout, - }; - let candidate_asset = unblinded_txout.asset; - - if candidate_asset == asset { - Some(( - coin_selection::Utxo { + let utxos = wallet + .get_txouts(|utxo, txout| { + Ok({ + let unblinded_txout = txout.unblind(SECP256K1, blinding_key)?; + let outpoint = OutPoint { + txid: utxo.txid, + vout: utxo.vout, + }; + let candidate_asset = unblinded_txout.asset; + + if candidate_asset == asset { + Some(( + coin_selection::Utxo { + outpoint, + value: unblinded_txout.value, + script_pubkey: txout.script_pubkey.clone(), + asset: candidate_asset, + }, + txout, + )) + } else { + log::debug!( + "utxo {} with asset id {} is not the target asset, ignoring", outpoint, - value: unblinded_txout.value, - script_pubkey: txout.script_pubkey.clone(), - asset: candidate_asset, - }, - txout, - )) - } else { - log::debug!( - "utxo {} with asset id {} is not the target asset, ignoring", - outpoint, - candidate_asset - ); - None - } + candidate_asset + ); + None + } + }) }) - }) - .await?; + .await?; // Bob currently hardcodes a fee-rate of 1 sat / vbyte, hence // there is no need for us to perform fee estimation. Later @@ -125,29 +106,15 @@ pub async fn make_loan_request( .await .map_err(Error::BuildBorrowerState)?; - let storage = Storage::local_storage().map_err(Error::Storage)?; - storage - .set_item( - "borrower_state", - serde_json::to_string(&borrower).map_err(Error::Serialize)?, - ) - .map_err(Error::Save)?; + let loan_request = borrower.loan_request(); - Ok(borrower.loan_request()) + Ok((borrower, loan_request)) } #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Wallet is not loaded {0}")] - LoadWallet(anyhow::Error), #[error("Failed to construct borrower state: {0}")] BuildBorrowerState(anyhow::Error), - #[error("Storage error: {0}")] - Storage(anyhow::Error), - #[error("Failed to save item to storage: {0}")] - Save(anyhow::Error), - #[error("Serialization failed: {0}")] - Serialize(serde_json::Error), } /// Calculate the fee offset required for the coin selection algorithm. diff --git a/extension/wallet/src/wallet/repay_loan.rs b/extension/wallet/src/lib_wallet/repay_loan.rs similarity index 50% rename from extension/wallet/src/wallet/repay_loan.rs rename to extension/wallet/src/lib_wallet/repay_loan.rs index bf0fd0315..aef8354d8 100644 --- a/extension/wallet/src/wallet/repay_loan.rs +++ b/extension/wallet/src/lib_wallet/repay_loan.rs @@ -3,80 +3,58 @@ use coin_selection::coin_select; use elements::{ bitcoin::util::amount::Amount, secp256k1_zkp::SECP256K1, sighash::SigHashCache, OutPoint, Txid, }; -use futures::lock::Mutex; use rand::thread_rng; -use crate::{ - esplora::{broadcast, fetch_transaction}, - storage::Storage, - wallet::{current, get_txouts, LoanDetails}, - Wallet, DEFAULT_SAT_PER_VBYTE, -}; +use crate::lib_wallet::{Wallet, DEFAULT_SAT_PER_VBYTE}; // TODO: Parts of the implementation are very similar to what we do in // `sign_and_send_swap_transaction`. We could extract common // functionality into crate-local functions -pub async fn repay_loan( - name: String, - current_wallet: &Mutex>, +pub async fn repay_loan( + wallet: &W, loan_txid: Txid, + borrower1: Borrower1, ) -> Result { // TODO: Only abort early if this fails because the transaction // hasn't been mined - if fetch_transaction(loan_txid).await.is_err() { + if wallet.fetch_transaction(loan_txid).await.is_err() { return Err(Error::NoLoan); } - - let storage = Storage::local_storage().map_err(Error::Storage)?; - - let borrower = storage - .get_item::(&format!("loan_state:{}", loan_txid)) - .map_err(Error::Load)? - .ok_or(Error::EmptyState)?; - let borrower = serde_json::from_str::(&borrower).map_err(Error::Deserialize)?; - - let blinding_key = { - let wallet = current(&name, current_wallet) - .await - .map_err(Error::LoadWallet)?; - wallet.blinding_key() - }; + let blinding_key = wallet.blinding_key(); let coin_selector = { - let name = name.clone(); |amount, asset| async move { - let wallet = current(&name, current_wallet).await?; - - let utxos = get_txouts(&wallet, |utxo, txout| { - Ok({ - let unblinded_txout = txout.unblind(SECP256K1, blinding_key)?; - let outpoint = OutPoint { - txid: utxo.txid, - vout: utxo.vout, - }; - let candidate_asset = unblinded_txout.asset; - - if candidate_asset == asset { - Some(( - coin_selection::Utxo { + let utxos = wallet + .get_txouts(|utxo, txout| { + Ok({ + let unblinded_txout = txout.unblind(SECP256K1, blinding_key)?; + let outpoint = OutPoint { + txid: utxo.txid, + vout: utxo.vout, + }; + let candidate_asset = unblinded_txout.asset; + + if candidate_asset == asset { + Some(( + coin_selection::Utxo { + outpoint, + value: unblinded_txout.value, + script_pubkey: txout.script_pubkey.clone(), + asset: candidate_asset, + }, + txout, + )) + } else { + log::debug!( + "utxo {} with asset id {} is not the target asset, ignoring", outpoint, - value: unblinded_txout.value, - script_pubkey: txout.script_pubkey.clone(), - asset: candidate_asset, - }, - txout, - )) - } else { - log::debug!( - "utxo {} with asset id {} is not the target asset, ignoring", - outpoint, - candidate_asset - ); - None - } + candidate_asset + ); + None + } + }) }) - }) - .await?; + .await?; // We are selecting coins with an asset which cannot be // used to pay for fees @@ -112,8 +90,9 @@ pub async fn repay_loan( }; let signer = |mut transaction| async { - let wallet = current(&name, current_wallet).await?; - let txouts = get_txouts(&wallet, |utxo, txout| Ok(Some((utxo, txout)))).await?; + let txouts = wallet + .get_txouts(|utxo, txout| Ok(Some((utxo, txout)))) + .await?; let mut cache = SigHashCache::new(&transaction); @@ -141,7 +120,7 @@ pub async fn repay_loan( SECP256K1, &mut cache, index, - &wallet.secret_key, + &wallet.secret_key(), output.value, ); @@ -156,7 +135,7 @@ pub async fn repay_loan( Ok(transaction) }; - let loan_repayment_tx = borrower + let loan_repayment_tx = borrower1 .loan_repayment_transaction( &mut thread_rng(), SECP256K1, @@ -167,34 +146,11 @@ pub async fn repay_loan( .await .map_err(Error::BuildTransaction)?; - let repayment_txid = broadcast(loan_repayment_tx) + let repayment_txid = wallet + .broadcast(loan_repayment_tx) .await .map_err(Error::SendTransaction)?; - // TODO: Make sure that we can safely forget this i.e. sufficient - // confirmations - storage - .remove_item(&format!("loan_state:{}", loan_txid)) - .map_err(Error::Delete)?; - - let open_loans = match storage - .get_item::("open_loans") - .map_err(Error::Load)? - { - Some(open_loans) => serde_json::from_str(&open_loans).map_err(Error::Deserialize)?, - None => Vec::::new(), - }; - let open_loans = open_loans - .iter() - .take_while(|details| loan_txid != details.txid) - .collect::>(); - storage - .set_item( - "open_loans", - serde_json::to_string(&open_loans).map_err(Error::Serialize)?, - ) - .map_err(Error::Save)?; - Ok(repayment_txid) } @@ -202,22 +158,6 @@ pub async fn repay_loan( pub enum Error { #[error("Loan transaction not found in the blockchain")] NoLoan, - #[error("Storage error: {0}")] - Storage(anyhow::Error), - #[error("Failed to load item from storage: {0}")] - Load(anyhow::Error), - #[error("Deserialization failed: {0}")] - Deserialize(serde_json::Error), - #[error("Serialization failed: {0}")] - Serialize(serde_json::Error), - #[error("Failed to delete item from storage: {0}")] - Delete(anyhow::Error), - #[error("Failed to save item to storage: {0}")] - Save(anyhow::Error), - #[error("Loaded empty loan state")] - EmptyState, - #[error("Wallet is not loaded: {0}")] - LoadWallet(anyhow::Error), #[error("Failed to construct loan repayment transaction: {0}")] BuildTransaction(anyhow::Error), #[error("Failed to broadcast transaction: {0}")] diff --git a/extension/wallet/src/wallet/sign_and_send_swap_transaction.rs b/extension/wallet/src/lib_wallet/sign_and_send_swap_transaction.rs similarity index 74% rename from extension/wallet/src/wallet/sign_and_send_swap_transaction.rs rename to extension/wallet/src/lib_wallet/sign_and_send_swap_transaction.rs index d048dc7c7..41a3e52b3 100644 --- a/extension/wallet/src/wallet/sign_and_send_swap_transaction.rs +++ b/extension/wallet/src/lib_wallet/sign_and_send_swap_transaction.rs @@ -1,22 +1,14 @@ -use crate::{ - esplora::broadcast, - wallet::{current, get_txouts, Wallet}, -}; +use crate::lib_wallet::Wallet; use anyhow::Result; use baru::swap::{alice_finalize_transaction, sign_with_key}; use elements::{secp256k1_zkp::SECP256K1, sighash::SigHashCache, Transaction, Txid}; -use futures::lock::Mutex; -pub(crate) async fn sign_and_send_swap_transaction( - name: String, - current_wallet: &Mutex>, +pub async fn sign_and_send_swap_transaction( + wallet: &W, transaction: Transaction, ) -> Result { - let wallet = current(&name, current_wallet) - .await - .map_err(Error::LoadWallet)?; - - let txouts = get_txouts(&wallet, |utxo, txout| Ok(Some((utxo, txout)))) + let txouts = wallet + .get_txouts(|utxo, txout| Ok(Some((utxo, txout)))) .await .map_err(Error::GetTxOuts)?; @@ -42,7 +34,7 @@ pub(crate) async fn sign_and_send_swap_transaction( SECP256K1, &mut cache, index, - &wallet.secret_key, + &wallet.secret_key(), output.value, ); @@ -59,15 +51,13 @@ pub(crate) async fn sign_and_send_swap_transaction( .await .map_err(Error::Sign)?; - let txid = broadcast(transaction).await.map_err(Error::Send)?; + let txid = wallet.broadcast(transaction).await.map_err(Error::Send)?; Ok(txid) } #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Wallet is not loaded: {0}")] - LoadWallet(anyhow::Error), #[error("Failed to get transaction outputs: {0}")] GetTxOuts(anyhow::Error), #[error("Failed to sign transaction: {0}")] diff --git a/extension/wallet/src/lib_wallet/sign_loan.rs b/extension/wallet/src/lib_wallet/sign_loan.rs new file mode 100644 index 000000000..519b6f11b --- /dev/null +++ b/extension/wallet/src/lib_wallet/sign_loan.rs @@ -0,0 +1,65 @@ +use baru::{loan::Borrower1, swap::sign_with_key}; +use elements::{secp256k1_zkp::SECP256K1, sighash::SigHashCache, Transaction}; + +use crate::lib_wallet::Wallet; + +pub async fn sign_loan( + wallet: &W, + borrower: Borrower1, +) -> Result { + let loan_transaction = borrower + .sign(|mut transaction| async { + let txouts = wallet + .get_txouts(|utxo, txout| Ok(Some((utxo, txout)))) + .await?; + + let mut cache = SigHashCache::new(&transaction); + let witnesses = transaction + .clone() + .input + .iter() + .enumerate() + .filter_map(|(index, input)| { + txouts + .iter() + .find(|(utxo, _)| { + utxo.txid == input.previous_output.txid + && utxo.vout == input.previous_output.vout + }) + .map(|(_, txout)| (index, txout)) + }) + .map(|(index, output)| { + // TODO: It is convenient to use this import, but + // it is weird to use an API from the swap library + // here. Maybe we should move it to a common + // place, so it can be used for different + // protocols + let script_witness = sign_with_key( + SECP256K1, + &mut cache, + index, + &wallet.secret_key(), + output.value, + ); + + (index, script_witness) + }) + .collect::>(); + + for (index, witness) in witnesses { + transaction.input[index].witness.script_witness = witness + } + + Ok(transaction) + }) + .await + .map_err(Error::Sign)?; + + Ok(loan_transaction) +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Failed to sign transaction: {0}")] + Sign(anyhow::Error), +} diff --git a/extension/wallet/src/wallet/withdraw_everything_to.rs b/extension/wallet/src/lib_wallet/withdraw_everything_to.rs similarity index 88% rename from extension/wallet/src/wallet/withdraw_everything_to.rs rename to extension/wallet/src/lib_wallet/withdraw_everything_to.rs index fa2258765..557aebdec 100644 --- a/extension/wallet/src/wallet/withdraw_everything_to.rs +++ b/extension/wallet/src/lib_wallet/withdraw_everything_to.rs @@ -1,8 +1,4 @@ -use crate::{ - esplora, - wallet::{current, get_txouts, Wallet, DEFAULT_SAT_PER_VBYTE}, - BTC_ASSET_ID, -}; +use crate::lib_wallet::{Wallet, DEFAULT_SAT_PER_VBYTE}; use anyhow::{bail, Context, Result}; use elements::{ hashes::{hash160, Hash}, @@ -10,37 +6,30 @@ use elements::{ script::Builder, secp256k1_zkp::{rand, Message, SECP256K1}, sighash::SigHashCache, - Address, OutPoint, SigHashType, Transaction, TxIn, TxOut, TxOutSecrets, Txid, + Address, AssetId, OutPoint, SigHashType, Transaction, TxIn, TxOut, TxOutSecrets, Txid, }; use estimate_transaction_size::estimate_virtual_size; -use futures::lock::Mutex; use itertools::Itertools; use rand::thread_rng; use std::{collections::HashMap, iter}; -use wasm_bindgen::UnwrapThrowExt; -pub async fn withdraw_everything_to( - name: String, - current_wallet: &Mutex>, +pub async fn withdraw_everything_to( + wallet: &W, address: Address, + btc_asset_id: AssetId, ) -> Result { - let btc_asset_id = { - let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); - *guard - }; - if !address.is_blinded() { bail!("can only withdraw to blinded addresses") } - let wallet = current(&name, current_wallet).await?; let blinding_key = wallet.blinding_key(); - let txouts = get_txouts(&wallet, |utxo, txout| { - let unblinded_txout = txout.unblind(SECP256K1, blinding_key)?; - Ok(Some((utxo, txout, unblinded_txout))) - }) - .await?; + let txouts = wallet + .get_txouts(|utxo, txout| { + let unblinded_txout = txout.unblind(SECP256K1, blinding_key)?; + Ok(Some((utxo, txout, unblinded_txout))) + }) + .await?; let prevout_values = txouts .iter() @@ -55,7 +44,7 @@ pub async fn withdraw_everything_to( }) .collect::>(); - let fee_estimates = esplora::get_fee_estimates().await?; + let fee_estimates = wallet.get_fee_estimates().await?; let estimated_virtual_size = estimate_virtual_size(prevout_values.len() as u64, txouts.len() as u64); @@ -209,7 +198,7 @@ pub async fn withdraw_everything_to( for (index, input) in transaction.input.iter_mut().enumerate() { input.witness.script_witness = { - let hash = hash160::Hash::hash(&wallet.get_public_key().serialize()); + let hash = hash160::Hash::hash(&wallet.public_key().serialize()); let script = Builder::new() .push_opcode(opcodes::all::OP_DUP) .push_opcode(opcodes::all::OP_HASH160) @@ -225,19 +214,20 @@ pub async fn withdraw_everything_to( SigHashType::All, ); - let sig = SECP256K1.sign(&Message::from(sighash), &wallet.secret_key); + let sig = SECP256K1.sign(&Message::from(sighash), &wallet.secret_key()); let mut serialized_signature = sig.serialize_der().to_vec(); serialized_signature.push(SigHashType::All as u8); vec![ serialized_signature, - wallet.get_public_key().serialize().to_vec(), + wallet.public_key().serialize().to_vec(), ] } } - let txid = esplora::broadcast(transaction) + let txid = wallet + .broadcast(transaction) .await .context("failed to broadcast transaction via esplora")?; diff --git a/extension/wallet/src/storage.rs b/extension/wallet/src/storage.rs index beedd6233..a575399d3 100644 --- a/extension/wallet/src/storage.rs +++ b/extension/wallet/src/storage.rs @@ -1,9 +1,8 @@ +use crate::lib_wallet::LoanDetails; use anyhow::{Context, Result}; use std::{error::Error as StdError, str::FromStr}; use web_sys::window; -use crate::LoanDetails; - /// A wrapper type around the cache storage. pub struct Storage { inner: web_sys::Storage, diff --git a/extension/wallet/src/wallet.rs b/extension/wallet/src/wallet.rs index ee73251be..5c3c62260 100644 --- a/extension/wallet/src/wallet.rs +++ b/extension/wallet/src/wallet.rs @@ -1,102 +1,46 @@ -use crate::{ - assets::{self, lookup}, - esplora, - esplora::Utxo, - CHAIN, DEFAULT_SAT_PER_VBYTE, -}; +use std::{convert::Infallible, fmt, str}; + use aes_gcm_siv::{ aead::{Aead, NewAead}, Aes256GcmSiv, }; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; +use async_trait::async_trait; use elements::{ bitcoin::{ self, secp256k1::{SecretKey, SECP256K1}, - util::amount::Amount, }, - confidential, + encode::{deserialize, serialize_hex}, secp256k1_zkp::{rand, PublicKey}, - Address, AssetId, OutPoint, TxOut, Txid, -}; -use futures::{ - lock::{MappedMutexGuard, Mutex, MutexGuard}, - stream::FuturesUnordered, - StreamExt, TryStreamExt, + Address, Transaction, Txid, }; +use futures::lock::{MappedMutexGuard, Mutex, MutexGuard}; use hkdf::Hkdf; -use itertools::Itertools; use rand::{thread_rng, Rng}; -use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; +use reqwest::StatusCode; use sha2::{digest::generic_array::GenericArray, Sha256}; -use std::{ - convert::Infallible, - fmt, - ops::{Add, Sub}, - str, -}; use wasm_bindgen::UnwrapThrowExt; -pub use create_new::create_new; -pub use extract_loan::{extract_loan, Error as ExtractLoanError}; -pub use extract_trade::{extract_trade, Trade}; -pub use get_address::get_address; -pub use get_balances::get_balances; +use crate::{ + lib_wallet, + lib_wallet::{FeeEstimatesResponse, Utxo}, + CHAIN, ESPLORA_API_URL, +}; + +pub use assets::lookup; +pub use create_new_waves::create_new_waves; pub use get_status::{get_status, WalletStatus}; -pub use get_transaction_history::get_transaction_history; pub use load_existing::load_existing; -pub use make_create_swap_payload::{ - make_buy_create_swap_payload, make_sell_create_swap_payload, Error as MakePayloadError, -}; -pub use make_loan_request::{make_loan_request, Error as MakeLoanRequestError}; -pub use repay_loan::{repay_loan, Error as RepayLoanError}; -pub(crate) use sign_and_send_swap_transaction::sign_and_send_swap_transaction; -pub(crate) use sign_loan::sign_loan; pub use unload_current::unload_current; -pub use withdraw_everything_to::withdraw_everything_to; -mod create_new; -mod extract_loan; -mod extract_trade; -mod get_address; -mod get_balances; +mod assets; +mod create_new_waves; mod get_status; -mod get_transaction_history; mod load_existing; -mod make_create_swap_payload; -mod make_loan_request; -mod repay_loan; -mod sign_and_send_swap_transaction; -mod sign_loan; mod unload_current; -mod withdraw_everything_to; - -async fn get_txouts Result> + Copy>( - wallet: &Wallet, - filter_map: FM, -) -> Result> { - let address = wallet.get_address(); - - let utxos = esplora::fetch_utxos(&address).await?; - let txouts = utxos - .into_iter() - .map(move |utxo| async move { - let mut tx = esplora::fetch_transaction(utxo.txid).await?; - let txout = tx.output.remove(utxo.vout as usize); - - filter_map(utxo, txout) - }) - .collect::>() - .filter_map(|r| std::future::ready(r.transpose())) - .try_collect::>() - .await?; - - Ok(txouts) -} - -async fn current<'n, 'w>( +pub async fn current<'n, 'w>( name: &'n str, current_wallet: &'w Mutex>, ) -> Result, Wallet>> { @@ -115,7 +59,7 @@ pub struct Wallet { name: String, encryption_key: [u8; 32], secret_key: SecretKey, - sk_salt: [u8; 32], + pub(crate) sk_salt: [u8; 32], } const SECRET_KEY_ENCRYPTION_NONCE: &[u8; 12] = b"SECRET_KEY!!"; @@ -172,31 +116,13 @@ impl Wallet { PublicKey::from_secret_key(SECP256K1, &self.secret_key) } - pub fn get_address(&self) -> Address { - let chain = { - let guard = CHAIN.lock().expect_throw("can get lock"); - *guard - }; - let public_key = self.get_public_key(); - let blinding_key = PublicKey::from_secret_key(SECP256K1, &self.blinding_key()); - - Address::p2wpkh( - &bitcoin::PublicKey { - compressed: true, - key: public_key, - }, - Some(blinding_key), - chain.into(), - ) - } - /// Encrypts the secret key with the encryption key. /// /// # Choice of nonce /// /// We store the secret key on disk and as such have to use a constant nonce, otherwise we would not be able to decrypt it again. /// The encryption only happens once and as such, there is conceptually only one message and we are not "reusing" the nonce which would be insecure. - fn encrypted_secret_key(&self) -> Result> { + pub(crate) fn encrypted_secret_key(&self) -> Result> { let cipher = Aes256GcmSiv::new(&GenericArray::from_slice(&self.encryption_key)); let enc_sk = cipher .encrypt( @@ -208,29 +134,6 @@ impl Wallet { Ok(enc_sk) } - /// Derive the blinding key. - /// - /// # Choice of salt - /// - /// We choose to not add a salt because the ikm is already a randomly-generated, secret value with decent entropy. - /// - /// # Choice of ikm - /// - /// We derive the blinding key from the secret key to avoid having to store two secret values on disk. - /// - /// # Choice of info - /// - /// We choose to tag the derived key with `b"BLINDING_KEY"` in case we ever want to derive something else from the secret key. - fn blinding_key(&self) -> SecretKey { - let h = Hkdf::::new(None, self.secret_key.as_ref()); - - let mut bk = [0u8; 32]; - h.expand(b"BLINDING_KEY", &mut bk) - .expect("output length aligns with sha256"); - - SecretKey::from_slice(bk.as_ref()).expect("always a valid secret key") - } - /// Derive the encryption key from the wallet's password and a salt. /// /// # Choice of salt @@ -257,191 +160,229 @@ impl Wallet { } } -#[derive(Default)] -pub struct ListOfWallets(Vec); - -impl ListOfWallets { - fn has(&self, wallet: &str) -> bool { - self.0.iter().any(|w| w == wallet) - } +#[async_trait] +impl lib_wallet::Wallet for Wallet { + fn get_address(&self) -> Address { + let chain = { + let guard = CHAIN.lock().expect_throw("can get lock"); + *guard + }; + let public_key = self.get_public_key(); + let blinding_key = PublicKey::from_secret_key(SECP256K1, &self.blinding_key()); - fn add(&mut self, wallet: String) { - self.0.push(wallet); + Address::p2wpkh( + &bitcoin::PublicKey { + compressed: true, + key: public_key, + }, + Some(blinding_key), + chain.into(), + ) } -} -impl str::FromStr for ListOfWallets { - type Err = Infallible; + /// Derive the blinding key. + /// + /// # Choice of salt + /// + /// We choose to not add a salt because the ikm is already a randomly-generated, secret value with decent entropy. + /// + /// # Choice of ikm + /// + /// We derive the blinding key from the secret key to avoid having to store two secret values on disk. + /// + /// # Choice of info + /// + /// We choose to tag the derived key with `b"BLINDING_KEY"` in case we ever want to derive something else from the secret key. + fn blinding_key(&self) -> SecretKey { + let h = Hkdf::::new(None, self.secret_key.as_ref()); - fn from_str(s: &str) -> Result { - let split = s.split('\t'); + let mut bk = [0u8; 32]; + h.expand(b"BLINDING_KEY", &mut bk) + .expect("output length aligns with sha256"); - Ok(ListOfWallets(split.map(|s| s.to_owned()).collect())) + SecretKey::from_slice(bk.as_ref()).expect("always a valid secret key") } -} -impl fmt::Display for ListOfWallets { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0.join("\t")) + fn secret_key(&self) -> SecretKey { + self.secret_key } -} -/// Represents the payload for creating a swap. -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct CreateSwapPayload { - pub alice_inputs: Vec, - pub address: Address, - #[serde(with = "bdk::bitcoin::util::amount::serde::as_sat")] - pub amount: bdk::bitcoin::Amount, -} + /// Fetch the UTXOs of an address. + /// + /// UTXOs change over time and as such, this function never uses a cache. + async fn fetch_utxos(&self) -> Result> { + let address = self.get_address(); + let esplora_url = { + let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); + guard.clone() + }; -#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] -pub struct SwapUtxo { - pub outpoint: OutPoint, - pub blinding_key: SecretKey, -} + let path = format!("address/{}/utxo", address); + let esplora_url = esplora_url.join(path.as_str())?; + let response = reqwest::get(esplora_url.clone()) + .await + .context("failed to fetch UTXOs")?; -/// A single balance entry as returned by [`get_balances`]. -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] -pub struct BalanceEntry { - pub asset: AssetId, - pub ticker: String, - pub value: Decimal, -} + if response.status() == StatusCode::NOT_FOUND { + log::debug!( + "GET {} returned 404, defaulting to empty UTXO set", + esplora_url + ); -impl BalanceEntry { - pub fn for_asset(asset: AssetId, ticker: String, value: u64, precision: u32) -> Self { - let mut decimal = Decimal::from(value); - decimal - .set_scale(precision) - .expect("precision must be < 28"); - - Self { - asset, - ticker, - value: decimal, + return Ok(Vec::new()); } + + if !response.status().is_success() { + let error_body = response.text().await?; + return Err(anyhow!( + "failed to fetch utxos, esplora returned '{}'", + error_body + )); + } + + response + .json::>() + .await + .context("failed to deserialize response") } -} -/// A pure function to compute the balances of the wallet given a set of [`TxOut`]s. -fn compute_balances(wallet: &Wallet, txouts: &[TxOut]) -> Vec { - let grouped_txouts = txouts - .iter() - .filter_map(|utxo| match utxo { - TxOut { - asset: confidential::Asset::Explicit(asset), - value: confidential::Value::Explicit(value), - .. - } => Some((*asset, *value)), - txout => match txout.unblind(SECP256K1, wallet.blinding_key()) { - Ok(unblinded_txout) => Some((unblinded_txout.asset, unblinded_txout.value)), - Err(e) => { - log::warn!("failed to unblind txout: {}", e); - None - } - }, - }) - .into_group_map(); - - grouped_txouts - .into_iter() - .filter_map(|(asset, utxos)| { - let total_sum = utxos.into_iter().sum(); - let (ticker, precision) = lookup(asset)?; - - Some(BalanceEntry::for_asset( - asset, - ticker.to_owned(), - total_sum, - precision as u32, - )) - }) - .collect() -} + /// Fetches a transaction. + /// + /// This function makes use of the browsers local storage to avoid spamming the underlying source. + /// Transaction never change after they've been mined, hence we can cache those indefinitely. + async fn fetch_transaction(&self, txid: Txid) -> Result { + let esplora_url = { + let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); + guard.clone() + }; -#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct TradeSide { - pub ticker: String, - pub amount: Decimal, - pub balance_before: Decimal, - pub balance_after: Decimal, -} + //todo!(consider caching) + + let client = reqwest::Client::new(); + let body = client + .get(format!("{}tx/{}/hex", esplora_url, txid)) + .send() + .await?; + let body_text = body + .text() + .await + .with_context(|| "response is not a string")?; -impl TradeSide { - fn new_sell(asset: AssetId, amount: u64, current_balance: Decimal) -> Result { - Self::new(asset, amount, current_balance, Decimal::sub) + Ok(deserialize(&hex::decode(body_text)?)?) } - fn new_buy(asset: AssetId, amount: u64, current_balance: Decimal) -> Result { - Self::new(asset, amount, current_balance, Decimal::add) + /// Fetch transaction history for the specified address. + /// + /// Returns up to 50 mempool transactions plus the first 25 confirmed + /// transactions. See + /// https://github.com/blockstream/esplora/blob/master/API.md#get-addressaddresstxs + /// for more information. + async fn fetch_transaction_history(&self, address: &Address) -> Result> { + let esplora_url = { + let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); + guard.clone() + }; + let path = format!("address/{}/txs", address); + let url = esplora_url.join(path.as_str())?; + let response = reqwest::get(url.clone()) + .await + .context("failed to fetch transaction history")?; + + if !response.status().is_success() { + let error_body = response.text().await?; + return Err(anyhow!( + "failed to fetch transaction history, esplora returned '{}' from '{}'", + error_body, + url + )); + } + + #[derive(serde::Deserialize)] + struct HistoryElement { + txid: Txid, + } + + let response = response + .json::>() + .await + .context("failed to deserialize response")?; + + Ok(response.iter().map(|elem| elem.txid).collect()) } - fn new( - asset: AssetId, - amount: u64, - current_balance: Decimal, - balance_after: impl Fn(Decimal, Decimal) -> Decimal, - ) -> Result { - let (ticker, precision) = assets::lookup(asset).context("asset not found")?; + async fn broadcast(&self, tx: Transaction) -> Result { + let esplora_url = { + let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); + guard.clone() + }; + let esplora_url = esplora_url.join("tx")?; + let client = reqwest::Client::new(); - let mut amount = Decimal::from(amount); - amount - .set_scale(precision as u32) - .expect("precision must be < 28"); + let response = client + .post(esplora_url.clone()) + .body(serialize_hex(&tx)) + .send() + .await?; - Ok(Self { - ticker: ticker.to_owned(), - amount, - balance_before: current_balance, - balance_after: balance_after(current_balance, amount), - }) + let code = response.status(); + + if !code.is_success() { + bail!("failed to successfully publish transaction"); + } + + let txid = response + .text() + .await? + .parse() + .context("failed to parse response body as txid")?; + + Ok(txid) + } + + async fn get_fee_estimates(&self) -> Result { + let esplora_url = { + let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); + guard.clone() + }; + let esplora_url = esplora_url.join("fee-estimates")?; + + let fee_estimates = reqwest::get(esplora_url.clone()) + .await + .with_context(|| format!("failed to GET {}", esplora_url))? + .json() + .await + .context("failed to deserialize fee estimates")?; + + Ok(fee_estimates) } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct LoanDetails { - pub collateral: TradeSide, - pub principal: TradeSide, - pub principal_repayment: Decimal, - // TODO: Express as target date or number of days instead? - pub term: u64, - pub txid: Txid, +#[derive(Default)] +pub struct ListOfWallets(Vec); + +impl ListOfWallets { + pub(crate) fn has(&self, wallet: &str) -> bool { + self.0.iter().any(|w| w == wallet) + } + + pub(crate) fn add(&mut self, wallet: String) { + self.0.push(wallet); + } } -impl LoanDetails { - #[allow(clippy::too_many_arguments)] - pub fn new( - collateral_asset: AssetId, - collateral_amount: Amount, - collateral_balance: Decimal, - principal_asset: AssetId, - principal_amount: Amount, - principal_balance: Decimal, - timelock: u64, - txid: Txid, - ) -> Result { - let collateral = TradeSide::new_sell( - collateral_asset, - collateral_amount.as_sat(), - collateral_balance, - )?; - - let principal = TradeSide::new_buy( - principal_asset, - principal_amount.as_sat(), - principal_balance, - )?; +impl str::FromStr for ListOfWallets { + type Err = Infallible; - Ok(Self { - collateral, - principal_repayment: principal.amount, - principal, - term: timelock, - txid, - }) + fn from_str(s: &str) -> Result { + let split = s.split('\t'); + + Ok(ListOfWallets(split.map(|s| s.to_owned()).collect())) + } +} + +impl fmt::Display for ListOfWallets { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.join("\t")) } } @@ -480,7 +421,7 @@ mod browser_tests { set_elements_chain_in_local_storage(); let current_wallet = Mutex::default(); - create_new("wallet-1".to_owned(), "foo".to_owned(), ¤t_wallet) + create_new_waves("wallet-1".to_owned(), "foo".to_owned(), ¤t_wallet) .await .unwrap(); @@ -492,7 +433,7 @@ mod browser_tests { #[wasm_bindgen_test] pub async fn given_a_wallet_when_unloaded_cannot_get_address() { let current_wallet = Mutex::default(); - create_new("wallet-2".to_owned(), "foo".to_owned(), ¤t_wallet) + create_new_waves("wallet-2".to_owned(), "foo".to_owned(), ¤t_wallet) .await .unwrap(); @@ -511,10 +452,10 @@ mod browser_tests { pub async fn cannot_create_two_wallets_with_same_name() { let current_wallet = Mutex::default(); - create_new("wallet-3".to_owned(), "foo".to_owned(), ¤t_wallet) + create_new_waves("wallet-3".to_owned(), "foo".to_owned(), ¤t_wallet) .await .unwrap(); - let error = create_new("wallet-3".to_owned(), "foo".to_owned(), ¤t_wallet) + let error = create_new_waves("wallet-3".to_owned(), "foo".to_owned(), ¤t_wallet) .await .unwrap_err(); @@ -528,10 +469,10 @@ mod browser_tests { pub async fn cannot_load_multiple_wallets_at_the_same_time() { let current_wallet = Mutex::default(); - create_new("wallet-4".to_owned(), "foo".to_owned(), ¤t_wallet) + create_new_waves("wallet-4".to_owned(), "foo".to_owned(), ¤t_wallet) .await .unwrap(); - create_new("wallet-5".to_owned(), "foo".to_owned(), ¤t_wallet) + create_new_waves("wallet-5".to_owned(), "foo".to_owned(), ¤t_wallet) .await .unwrap(); @@ -549,7 +490,7 @@ mod browser_tests { pub async fn cannot_load_wallet_with_wrong_password() { let current_wallet = Mutex::default(); - create_new("wallet-6".to_owned(), "foo".to_owned(), ¤t_wallet) + create_new_waves("wallet-6".to_owned(), "foo".to_owned(), ¤t_wallet) .await .unwrap(); unload_current(¤t_wallet).await; @@ -576,7 +517,7 @@ mod browser_tests { pub async fn new_wallet_is_automatically_loaded() { let current_wallet = Mutex::default(); - create_new("wallet-7".to_owned(), "foo".to_owned(), ¤t_wallet) + create_new_waves("wallet-7".to_owned(), "foo".to_owned(), ¤t_wallet) .await .unwrap(); let status = get_status("wallet-7".to_owned(), ¤t_wallet) @@ -601,7 +542,7 @@ mod browser_tests { pub async fn secret_key_can_be_successfully_decrypted() { let current_wallet = Mutex::default(); - create_new("wallet-9".to_owned(), "foo".to_owned(), ¤t_wallet) + create_new_waves("wallet-9".to_owned(), "foo".to_owned(), ¤t_wallet) .await .unwrap(); let initial_sk = { diff --git a/extension/wallet/src/assets.rs b/extension/wallet/src/wallet/assets.rs similarity index 100% rename from extension/wallet/src/assets.rs rename to extension/wallet/src/wallet/assets.rs diff --git a/extension/wallet/src/wallet/create_new.rs b/extension/wallet/src/wallet/create_new_waves.rs similarity index 98% rename from extension/wallet/src/wallet/create_new.rs rename to extension/wallet/src/wallet/create_new_waves.rs index e2c71c8e9..588f20dab 100644 --- a/extension/wallet/src/wallet/create_new.rs +++ b/extension/wallet/src/wallet/create_new_waves.rs @@ -7,7 +7,7 @@ use crate::{ wallet::{ListOfWallets, Wallet}, }; -pub async fn create_new( +pub async fn create_new_waves( name: String, password: String, current_wallet: &Mutex>, diff --git a/extension/wallet/src/wallet/extract_loan.rs b/extension/wallet/src/wallet/extract_loan.rs deleted file mode 100644 index 297b50ef1..000000000 --- a/extension/wallet/src/wallet/extract_loan.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::{ - storage::Storage, - wallet::{compute_balances, current, get_txouts, Wallet}, - LoanDetails, BTC_ASSET_ID, USDT_ASSET_ID, -}; -use baru::loan::{Borrower0, LoanResponse}; -use elements::secp256k1_zkp::SECP256K1; -use futures::lock::Mutex; -use wasm_bindgen::UnwrapThrowExt; - -pub async fn extract_loan( - name: String, - current_wallet: &Mutex>, - loan_response: LoanResponse, -) -> Result { - let btc_asset_id = { - let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); - *guard - }; - let usdt_asset_id = { - let guard = USDT_ASSET_ID.lock().expect_throw("can get lock"); - *guard - }; - - let wallet = current(&name, current_wallet) - .await - .map_err(Error::LoadWallet)?; - - let txouts = get_txouts(&wallet, |utxo, txout| Ok(Some((utxo, txout)))) - .await - .map_err(Error::GetTxOuts)?; - let balances = compute_balances( - &wallet, - &txouts - .iter() - .map(|(_, txout)| txout) - .cloned() - .collect::>(), - ); - - let storage = Storage::local_storage().map_err(Error::Storage)?; - let borrower = storage - .get_item::("borrower_state") - .map_err(Error::Load)? - .ok_or(Error::EmptyState)?; - let borrower = serde_json::from_str::(&borrower).map_err(Error::Deserialize)?; - - let timelock = loan_response.timelock; - let borrower = borrower - .interpret(SECP256K1, loan_response) - .map_err(Error::InterpretLoanResponse)?; - - let collateral_balance = balances - .iter() - .find_map(|entry| { - if entry.asset == btc_asset_id { - Some(entry.value) - } else { - None - } - }) - .ok_or(Error::InsufficientCollateral)?; - - let principal_balance = balances - .iter() - .find_map(|entry| { - if entry.asset == usdt_asset_id { - Some(entry.value) - } else { - None - } - }) - .unwrap_or_default(); - - let loan_txid = borrower.loan_transaction.txid(); - let loan_details = LoanDetails::new( - btc_asset_id, - borrower.collateral_amount, - collateral_balance, - usdt_asset_id, - borrower.principal_tx_out_amount, - principal_balance, - timelock, - loan_txid, - ) - .map_err(Error::LoanDetails)?; - - storage - .set_item( - "borrower_state", - serde_json::to_string(&(borrower, loan_details.clone())).map_err(Error::Serialize)?, - ) - .map_err(Error::Save)?; - - Ok(loan_details) -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Failed to deserialise loan response: {0}")] - LoanResponseDeserialization(#[from] serde_json::Error), - #[error("Wallet is not loaded: {0}")] - LoadWallet(anyhow::Error), - #[error("Failed to get transaction outputs: {0}")] - GetTxOuts(anyhow::Error), - #[error("Storage error: {0}")] - Storage(anyhow::Error), - #[error("Failed to load item from storage: {0}")] - Load(anyhow::Error), - #[error("Failed to save item to storage: {0}")] - Save(anyhow::Error), - #[error("Loaded empty borrower state")] - EmptyState, - #[error("Deserialization failed: {0}")] - Deserialize(serde_json::Error), - #[error("Serialization failed: {0}")] - Serialize(serde_json::Error), - #[error("Failed to interpret loan response: {0}")] - InterpretLoanResponse(anyhow::Error), - #[error("Not enough collateral to put up for loan")] - InsufficientCollateral, - #[error("Failed to build loan details: {0}")] - LoanDetails(anyhow::Error), -} diff --git a/extension/wallet/src/wallet/get_address.rs b/extension/wallet/src/wallet/get_address.rs deleted file mode 100644 index d1f6b27ae..000000000 --- a/extension/wallet/src/wallet/get_address.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::wallet::{current, Wallet}; -use anyhow::Result; -use elements::Address; -use futures::lock::Mutex; - -pub async fn get_address(name: String, current_wallet: &Mutex>) -> Result
{ - let wallet = current(&name, current_wallet).await?; - - let address = wallet.get_address(); - - Ok(address) -} diff --git a/extension/wallet/src/wallet/get_balances.rs b/extension/wallet/src/wallet/get_balances.rs deleted file mode 100644 index 3629aab8f..000000000 --- a/extension/wallet/src/wallet/get_balances.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::wallet::{compute_balances, current, get_txouts, BalanceEntry, Wallet}; -use anyhow::Result; -use futures::lock::Mutex; - -pub async fn get_balances( - name: &str, - current_wallet: &Mutex>, -) -> Result> { - let wallet = current(name, current_wallet).await?; - - let txouts = get_txouts(&wallet, |_, txout| Ok(Some(txout))).await?; - - let balances = compute_balances(&wallet, &txouts); - - Ok(balances) -} diff --git a/extension/wallet/src/wallet/get_transaction_history.rs b/extension/wallet/src/wallet/get_transaction_history.rs deleted file mode 100644 index 2ad7922e4..000000000 --- a/extension/wallet/src/wallet/get_transaction_history.rs +++ /dev/null @@ -1,19 +0,0 @@ -use anyhow::Result; -use elements::Txid; -use futures::lock::Mutex; - -use crate::{esplora, wallet::current, Wallet}; - -pub async fn get_transaction_history( - name: String, - current_wallet: &Mutex>, -) -> Result> { - let wallet = current(&name, current_wallet).await?; - - // We have a single address, so looking for the transaction - // history of said address is sufficient - let address = wallet.get_address(); - let history = esplora::fetch_transaction_history(&address).await?; - - Ok(history) -} diff --git a/extension/wallet/src/wallet/sign_loan.rs b/extension/wallet/src/wallet/sign_loan.rs deleted file mode 100644 index 97bb50a75..000000000 --- a/extension/wallet/src/wallet/sign_loan.rs +++ /dev/null @@ -1,117 +0,0 @@ -use baru::{loan::Borrower1, swap::sign_with_key}; -use elements::{secp256k1_zkp::SECP256K1, sighash::SigHashCache, Transaction}; -use futures::lock::Mutex; - -use crate::{ - storage::Storage, - wallet::{current, get_txouts, LoanDetails}, - Wallet, -}; - -pub(crate) async fn sign_loan( - name: String, - current_wallet: &Mutex>, -) -> Result { - let storage = Storage::local_storage().map_err(Error::Storage)?; - let borrower = storage - .get_item::("borrower_state") - .map_err(Error::Load)? - .ok_or(Error::EmptyState)?; - let (borrower, loan_details) = - serde_json::from_str::<(Borrower1, LoanDetails)>(&borrower).map_err(Error::Deserialize)?; - - let loan_transaction = borrower - .sign(|mut transaction| async { - let wallet = current(&name, current_wallet).await?; - let txouts = get_txouts(&wallet, |utxo, txout| Ok(Some((utxo, txout)))).await?; - - let mut cache = SigHashCache::new(&transaction); - let witnesses = transaction - .clone() - .input - .iter() - .enumerate() - .filter_map(|(index, input)| { - txouts - .iter() - .find(|(utxo, _)| { - utxo.txid == input.previous_output.txid - && utxo.vout == input.previous_output.vout - }) - .map(|(_, txout)| (index, txout)) - }) - .map(|(index, output)| { - // TODO: It is convenient to use this import, but - // it is weird to use an API from the swap library - // here. Maybe we should move it to a common - // place, so it can be used for different - // protocols - let script_witness = sign_with_key( - SECP256K1, - &mut cache, - index, - &wallet.secret_key, - output.value, - ); - - (index, script_witness) - }) - .collect::>(); - - for (index, witness) in witnesses { - transaction.input[index].witness.script_witness = witness - } - - Ok(transaction) - }) - .await - .map_err(Error::Sign)?; - - // We don't broadcast this transaction ourselves, but we expect - // the lender to do so very soon. We therefore save the borrower - // state so that we can later on build, sign and broadcast the - // repayment transaction - - let mut open_loans = match storage - .get_item::("open_loans") - .map_err(Error::Load)? - { - Some(open_loans) => serde_json::from_str(&open_loans).map_err(Error::Deserialize)?, - None => Vec::::new(), - }; - - open_loans.push(loan_details); - storage - .set_item( - "open_loans", - serde_json::to_string(&open_loans).map_err(Error::Serialize)?, - ) - .map_err(Error::Save)?; - - storage - .set_item( - &format!("loan_state:{}", loan_transaction.txid()), - serde_json::to_string(&borrower).map_err(Error::Serialize)?, - ) - .map_err(Error::Save)?; - - Ok(loan_transaction) -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Storage error: {0}")] - Storage(anyhow::Error), - #[error("Failed to load item from storage: {0}")] - Load(anyhow::Error), - #[error("Loaded empty borrower state")] - EmptyState, - #[error("Failed to save item to storage: {0}")] - Save(anyhow::Error), - #[error("Deserialization failed: {0}")] - Deserialize(serde_json::Error), - #[error("Serialization failed: {0}")] - Serialize(serde_json::Error), - #[error("Failed to sign transaction: {0}")] - Sign(anyhow::Error), -}