diff --git a/Cargo.lock b/Cargo.lock index e2b76d508..b959f3dff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,17 +123,19 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "baru" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a925d16d7755d1f745724754b5812644fc6bcf9f2936de18c9710d707c2f87" +version = "0.2.0" +source = "git+https://github.com/da-kami/baru?branch=lender-decides-timelock#9bb512a0037b888659a8f66e6f1f5b682da1bd3b" dependencies = [ "anyhow", "bitcoin_hashes 0.9.6", + "conquer-once", "elements", + "elements-miniscript", "env_logger", "hex", "hmac 0.10.1", "log", + "rand 0.6.5", "rust_decimal", "secp256k1", "secp256k1-zkp", @@ -156,10 +158,10 @@ checksum = "daeccaea73c9fc27e218e2a4402070707fb8354afd30fecd4a1c9a0bea8b79c4" dependencies = [ "async-trait", "bdk-macros", - "bitcoin", + "bitcoin 0.26.0", "js-sys", "log", - "miniscript", + "miniscript 5.1.0", "rand 0.7.3", "serde 1.0.126", "serde_json 1.0.64", @@ -183,6 +185,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dabbe35f96fb9507f7330793dc490461b2962659ac5d427181e451a623751d1" +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + [[package]] name = "bip32" version = "0.2.1" @@ -199,18 +207,45 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitcoin" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec5f88a446d66e7474a3b8fa2e348320b574463fb78d799d90ba68f79f48e0e" dependencies = [ - "bech32", + "bech32 0.7.3", "bitcoin_hashes 0.9.6", "secp256k1", "serde 1.0.126", ] +[[package]] +name = "bitcoin" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a427b27dae305157520d86673f2393b3eb08d880609abfcffc6e3c3c820e764" +dependencies = [ + "bech32 0.8.1", + "bitcoin_hashes 0.10.0", + "secp256k1", + "serde 1.0.126", +] + [[package]] name = "bitcoin_hashes" version = "0.7.6" @@ -226,6 +261,15 @@ dependencies = [ "serde 1.0.126", ] +[[package]] +name = "bitcoin_hashes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" +dependencies = [ + "serde 1.0.126", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -261,6 +305,7 @@ dependencies = [ "jsonrpc_client", "log", "mime_guess", + "proptest", "reqwest", "rust-embed", "rust_decimal", @@ -526,12 +571,12 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "elements" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bc8e918eb82346053fc6dcac7281066778164bced4cfa40e4b2b2e4b219c8b" +checksum = "aa776445bd0ef9b30eab2e7c09cacc3a545e0502c178fdca9c4c6d80f738d519" dependencies = [ - "bitcoin", - "bitcoin_hashes 0.9.6", + "bitcoin 0.27.0", + "bitcoin_hashes 0.10.0", "secp256k1-zkp", "serde 1.0.126", "serde_json 0.9.10", @@ -554,6 +599,18 @@ dependencies = [ "url", ] +[[package]] +name = "elements-miniscript" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc1fe1025a4d64d76bcc479685610a2d9c4903fdb1eb72c8bf4c2d1b3dbdc1a" +dependencies = [ + "bitcoin 0.27.0", + "elements", + "miniscript 6.0.0", + "serde 1.0.126", +] + [[package]] name = "encoding_rs" version = "0.8.28" @@ -1145,7 +1202,16 @@ version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71f455be59a359d50370c4f587afbc5739c862e684c5afecae80ab93e7474b4e" dependencies = [ - "bitcoin", + "bitcoin 0.26.0", +] + +[[package]] +name = "miniscript" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f88ac7d08e876153a4a81e851693db71f325b382baa77723f63210451e2414f" +dependencies = [ + "bitcoin 0.27.0", ] [[package]] @@ -1394,6 +1460,38 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "proptest" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits 0.2.14", + "quick-error 2.0.1", + "rand 0.8.3", + "rand_chacha 0.3.0", + "rand_xorshift 0.3.0", + "regex-syntax", + "rusty-fork", + "tempfile", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.9" @@ -1418,7 +1516,7 @@ dependencies = [ "rand_jitter", "rand_os", "rand_pcg", - "rand_xorshift", + "rand_xorshift 0.1.1", "winapi", ] @@ -1591,6 +1689,15 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.2", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -1783,6 +1890,18 @@ dependencies = [ "webpki", ] +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.5" @@ -2481,6 +2600,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.3.2" @@ -2499,7 +2627,6 @@ dependencies = [ "aes-gcm-siv", "anyhow", "baru", - "bdk", "bip32", "coin_selection", "conquer-once", diff --git a/bobtimus/Cargo.toml b/bobtimus/Cargo.toml index 01af76347..9d2f0a5f3 100644 --- a/bobtimus/Cargo.toml +++ b/bobtimus/Cargo.toml @@ -7,12 +7,12 @@ edition = "2018" [dependencies] anyhow = "1" async-trait = "0.1" -baru = "0.1" +baru = { git = "https://github.com/da-kami/baru", branch = "lender-decides-timelock" } bitcoin_hashes = "0.9.0" diesel = { version = "1.4", features = ["sqlite"] } diesel_migrations = "1.4" directories = "3.0" -elements = { version = "0.17", features = ["serde-feature"] } +elements = { version = "0.18", features = ["serde-feature"] } elements-harness = { git = "https://github.com/comit-network/elements-harness" } futures = { version = "0.3", default-features = false } hex = "0.4" @@ -21,6 +21,7 @@ http-api-problem = { version = "0.21", features = ["warp"] } jsonrpc_client = { version = "0.6", features = ["reqwest"] } log = "0.4" mime_guess = "2.0.3" +proptest = "1" reqwest = "0.11" rust-embed = "5.7.0" rust_decimal = { version = "1.15", features = ["serde-float"] } diff --git a/bobtimus/proptest-regressions/lib.txt b/bobtimus/proptest-regressions/lib.txt new file mode 100644 index 000000000..d958802b1 --- /dev/null +++ b/bobtimus/proptest-regressions/lib.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 9bb7f1b54a4968641948d278e51a05ed2930034b95de73641862e33bfcdcea4d # shrinks to term_in_days = 30868 diff --git a/bobtimus/src/database.rs b/bobtimus/src/database.rs index 3151cf878..6559bb96f 100644 --- a/bobtimus/src/database.rs +++ b/bobtimus/src/database.rs @@ -111,10 +111,10 @@ pub mod queries { pub fn get_publishable_liquidations_txs( conn: &SqliteConnection, - blockcount: u32, + secs_since_epoch: u64, ) -> Result> { let txs = liquidations::table - .filter(liquidations::locktime.le(blockcount as i64)) + .filter(liquidations::locktime.le(secs_since_epoch as i64)) .get_results::(conn)?; let txs = txs diff --git a/bobtimus/src/elements_rpc.rs b/bobtimus/src/elements_rpc.rs index 186141518..220e7f1c2 100644 --- a/bobtimus/src/elements_rpc.rs +++ b/bobtimus/src/elements_rpc.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Context, Result}; use bitcoin_hashes::hex::FromHex; use elements::{ - bitcoin::{Amount, PrivateKey}, + bitcoin::Amount, confidential::{Asset, Nonce, Value}, encode::serialize_hex, secp256k1_zkp::{SecretKey, Signature}, @@ -338,18 +338,16 @@ impl Client { Ok(sig) } - pub async fn dump_private_key(&self, address: &Address) -> Result { - let privkey = self.dumpprivkey(address).await?; - let privkey = PrivateKey::from_wif(&privkey)?; - - Ok(privkey.key) - } - pub async fn get_blockcount(&self) -> Result { let blockcount = self.getblockcount().await?; Ok(blockcount) } + + pub async fn get_address_blinding_key(&self, address: &Address) -> Result { + let key = self.dumpblindingkey(address).await?; + Ok(key) + } } #[derive(Debug, Deserialize)] diff --git a/bobtimus/src/lib.rs b/bobtimus/src/lib.rs index 0562cc2ee..cd305c54d 100644 --- a/bobtimus/src/lib.rs +++ b/bobtimus/src/lib.rs @@ -3,7 +3,7 @@ extern crate diesel; #[macro_use] extern crate diesel_migrations; -use std::{collections::HashMap, convert::TryInto}; +use std::collections::HashMap; use crate::{ database::{queries, Sqlite}, @@ -12,7 +12,7 @@ use crate::{ use anyhow::{Context, Result}; use baru::{ input::Input, - loan::{Lender0, Lender1, LoanRequest, LoanResponse}, + loan::{Lender0, Lender1, LoanResponse}, swap, }; use database::LiquidationForm; @@ -43,9 +43,14 @@ pub mod loan; pub mod problem; pub mod schema; -use crate::loan::{Interest, LoanOffer}; +use crate::loan::{Interest, LoanOffer, LoanRequest}; pub use amounts::*; +use elements::bitcoin::PublicKey; use rust_decimal_macros::dec; +use std::{ + convert::TryFrom, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; pub const USDT_ASSET_ID: &str = "ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"; @@ -265,8 +270,6 @@ where /// We return the range of possible loan terms to the borrower. /// The borrower can then request a loan using parameters that are within our terms. pub async fn handle_loan_offer_request(&mut self) -> Result { - let current_height = self.elementsd.get_blockcount().await?; - Ok(LoanOffer { rate: self.rate_service.latest_rate(), // TODO: Dynamic fee estimation @@ -279,10 +282,9 @@ where max_ltv: dec!(0.8), // TODO: Dynamic interest based on current market values interest: vec![Interest { - // Absolute timelock calculated from current block height - // Assuming 1 min block interval, 43200 mins = 30 days - timelock: current_height + 43200, + term: 30, interest_rate: dec!(0.15), + collateralization: dec!(1.5), }], }) } @@ -295,17 +297,33 @@ where // Currently there is no ID for incoming loan requests, that would associate them with an offer, // so we just have to ensure that the loan is still "acceptable" in the current state of Bobtimus. + let oracle_secret_key = elements::secp256k1_zkp::key::ONE_KEY; + let oralce_priv_key = elements::bitcoin::PrivateKey::new( + oracle_secret_key, + elements::bitcoin::Network::Regtest, + ); + let oracle_pk = PublicKey::from_private_key(&self.secp, &oralce_priv_key); + + let timelock = days_to_unix_timestamp_timelock(payload.term, SystemTime::now())?; + let lender_address = self .elementsd .get_new_segwit_confidential_address() .await .context("failed to get lender address")?; + let address_blinder = self + .elementsd + .get_address_blinding_key(&lender_address) + .await?; + let lender0 = Lender0::new( &mut self.rng, self.btc_asset_id, self.usdt_asset_id, lender_address, + address_blinder, + oracle_pk, ) .unwrap(); @@ -319,8 +337,9 @@ where Self::find_inputs(&elementsd_client, asset, amount).await } }, - payload, + payload.into(), self.rate_service.latest_rate().bid.as_satodollar(), + timelock, ) .await .unwrap(); @@ -328,7 +347,7 @@ where let loan_response = lender1.loan_response(); self.lender_states - .insert(loan_response.transaction.txid(), lender1); + .insert(loan_response.transaction().txid(), lender1); Ok(loan_response) } @@ -360,16 +379,14 @@ where let txid = self.elementsd.send_raw_transaction(&transaction).await?; - let liquidation_tx = - lender.liquidation_transaction(&mut self.rng, &self.secp, Amount::ONE_SAT)?; - let locktime = lender - .timelock - .try_into() - .expect("TODO: locktimes should be modelled as u32"); + let liquidation_tx = lender + .liquidation_transaction(&mut self.rng, &self.secp, Amount::ONE_SAT) + .await?; + let locktime = lender.collateral_contract().timelock(); self.db .do_in_transaction(|conn| { - LiquidationForm::new(txid, &liquidation_tx, locktime).insert(conn)?; + LiquidationForm::new(txid, &liquidation_tx, *locktime).insert(conn)?; Ok(()) }) @@ -410,10 +427,14 @@ impl RateSubscription { } pub async fn liquidate_loans(elementsd: &Client, db: Sqlite) -> Result<()> { - let blockcount = elementsd.get_blockcount().await?; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + let secs_since_epoch = now.as_secs(); + let liquidation_txs = db .do_in_transaction(|conn| { - let txs = queries::get_publishable_liquidations_txs(conn, blockcount)?; + let txs = queries::get_publishable_liquidations_txs(conn, secs_since_epoch)?; Ok(txs) }) .await?; @@ -427,6 +448,21 @@ pub async fn liquidate_loans(elementsd: &Client, db: Sqlite) -> Result<()> { Ok(()) } +/// Calculates the absolute timelock from the loan term in days +/// +/// The timelock is represented as Unix timestamp (seconds since the epoch). +/// Note: Miniscript uses u32 for representing the timestamp so we return a u32. +fn days_to_unix_timestamp_timelock(term_in_days: u32, now: SystemTime) -> Result { + let since_the_epoch = now.duration_since(UNIX_EPOCH).expect("Time went backwards"); + + let term = Duration::from_secs((term_in_days * 24 * 60 * 60) as u64); + + let timelock = (since_the_epoch + term).as_secs(); + let timelock = u32::try_from(timelock) + .context("Overflow, the given timestamp appears to be too far in the future")?; + + Ok(timelock) +} #[cfg(test)] mod tests { @@ -444,8 +480,38 @@ mod tests { Address, AddressParams, OutPoint, Transaction, TxOut, }; use elements_harness::Elementsd; + use proptest::proptest; use testcontainers::clients::Cli; + // This test ensures that this function will not panic on different systems now and in the future. + // At the point of writing 30868 days were supported, equivalent to 84.569863 calendar years. + // We allow a maximum of 18250 days = 50 years for loan terms. + // This test will pass for the next ~34.5 years given a correct system time. + proptest! { + #[test] + fn timelock_calculation_does_not_panic_between_1_day_and_100_years( + term_in_days in 1u32..18250, // 18250 days = 50 years + ) { + let now = SystemTime::now(); + let _ = days_to_unix_timestamp_timelock(term_in_days, now).unwrap(); + } + } + + #[test] + fn timelock_calculation_30_days() { + let term_in_days = 30; + let now = SystemTime::now(); + + let since_epoch = u32::try_from(now.duration_since(UNIX_EPOCH).unwrap().as_secs()).unwrap(); + + let timelock = days_to_unix_timestamp_timelock(term_in_days, now).unwrap(); + + let difference = timelock - since_epoch; + + // 2_592_000 = 30 days in secs + assert_eq!(difference, 2_592_000) + } + #[tokio::test] async fn test_handle_btc_sell_swap_request() { let db = Sqlite::new_ephemeral_db().expect("A ephemeral db"); diff --git a/bobtimus/src/loan.rs b/bobtimus/src/loan.rs index e2a6875e8..1b3842894 100644 --- a/bobtimus/src/loan.rs +++ b/bobtimus/src/loan.rs @@ -1,5 +1,9 @@ use crate::{LiquidUsdt, Rate}; -use elements::bitcoin::Amount; +use baru::input::Input; +use elements::{ + bitcoin::{Amount, PublicKey}, + Address, +}; use rust_decimal::Decimal; #[derive(Debug, Clone, serde::Serialize)] @@ -45,8 +49,37 @@ pub struct LoanOffer { #[derive(Debug, Clone, serde::Serialize)] pub struct Interest { - /// Timelock in blocks - pub timelock: u32, + /// Loan term in days + pub term: u32, + /// Collateralization in percent + /// + /// Rational: If a borrower over-collateralizes with e.g. 150% -> better rate than at 140% + pub collateralization: Decimal, /// Interest rate in percent pub interest_rate: Decimal, } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct LoanRequest { + #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] + pub collateral_amount: Amount, + collateral_inputs: Vec, + #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] + fee_sats_per_vbyte: Amount, + borrower_pk: PublicKey, + /// Loan term in days + pub term: u32, + borrower_address: Address, +} + +impl From for baru::loan::LoanRequest { + fn from(loan_request: LoanRequest) -> Self { + baru::loan::LoanRequest::new( + loan_request.collateral_amount, + loan_request.collateral_inputs, + loan_request.fee_sats_per_vbyte, + loan_request.borrower_pk, + loan_request.borrower_address, + ) + } +} diff --git a/coin_selection/Cargo.toml b/coin_selection/Cargo.toml index 3d75252d1..e6c8f9e42 100644 --- a/coin_selection/Cargo.toml +++ b/coin_selection/Cargo.toml @@ -6,6 +6,6 @@ edition = "2018" [dependencies] bdk = { version = "0.4", default-features = false } -elements = "0.17" +elements = "0.18" estimate_transaction_size = { path = "../estimate_transaction_size" } thiserror = "1" diff --git a/coin_selection/src/lib.rs b/coin_selection/src/lib.rs index 9c1df2f1f..4699a5f2b 100644 --- a/coin_selection/src/lib.rs +++ b/coin_selection/src/lib.rs @@ -1,11 +1,13 @@ use bdk::{ - bitcoin::{Amount, Denomination}, database::{BatchOperations, Database}, wallet::coin_selection::{ BranchAndBoundCoinSelection, CoinSelectionAlgorithm, CoinSelectionResult, }, }; -use elements::{AssetId, OutPoint, Script}; +use elements::{ + bitcoin::{Amount, Denomination}, + AssetId, OutPoint, Script, +}; use estimate_transaction_size::avg_vbytes; /// Select a subset of `utxos` to cover the `target` amount. @@ -70,7 +72,8 @@ pub fn coin_select( utxos .iter() .find(|utxo| { - bdk_utxo.outpoint.txid.as_hash() == utxo.outpoint.txid.as_hash() + format!("{}", bdk_utxo.outpoint.txid) + == format!("{}", utxo.outpoint.txid.as_hash()) && bdk_utxo.outpoint.vout == utxo.outpoint.vout }) .expect("same source of utxos") @@ -100,7 +103,7 @@ pub enum Error { #[error("All UTXOs must have the same asset ID")] HeterogeneousUtxos, #[error("Failed to parse recommended fee: {0}")] - ParseFee(#[from] bdk::bitcoin::util::amount::ParseAmountError), + ParseFee(#[from] elements::bitcoin::util::amount::ParseAmountError), #[error("Error from bdk: {0}")] Bdk(#[from] bdk::Error), } @@ -121,7 +124,9 @@ impl From for bdk::UTXO { Self { outpoint: bdk::bitcoin::OutPoint { - txid: bdk::bitcoin::Txid::from_hash(utxo.outpoint.txid.as_hash()), + txid: format!("{}", utxo.outpoint.txid) + .parse() + .expect("txid to be a txid"), vout: utxo.outpoint.vout, }, txout: bdk::bitcoin::TxOut { diff --git a/extension/src/background/index.ts b/extension/src/background/index.ts index 2f5efde6e..ddf120c96 100644 --- a/extension/src/background/index.ts +++ b/extension/src/background/index.ts @@ -73,7 +73,6 @@ browser.runtime.onMessage.addListener(async (msg: Message, sender) => { walletName, msg.payload.collateral, msg.payload.fee_rate, - msg.payload.timeout, ), MessageKind.LoanResponse, ); diff --git a/extension/src/wasmProxy.ts b/extension/src/wasmProxy.ts index ed726fa32..621d33be3 100644 --- a/extension/src/wasmProxy.ts +++ b/extension/src/wasmProxy.ts @@ -59,12 +59,11 @@ export async function makeLoanRequestPayload( name: string, collateral: string, fee_rate: string, - timelock: string, ): Promise { const { make_loan_request } = await import("./wallet"); debug("makeLoanRequestPayload"); - return make_loan_request(name, collateral, fee_rate, timelock); + return make_loan_request(name, collateral, fee_rate); } export async function signAndSendSwap(name: string, hex: string): Promise { diff --git a/extension/wallet/Cargo.toml b/extension/wallet/Cargo.toml index 897608d4d..43acdbfc7 100644 --- a/extension/wallet/Cargo.toml +++ b/extension/wallet/Cargo.toml @@ -13,13 +13,12 @@ default = ["console_error_panic_hook"] [dependencies] aes-gcm-siv = { version = "0.9", features = ["std"] } anyhow = "1" -baru = "0.1" -bdk = { version = "0.4", default-features = false } +baru = { git = "https://github.com/da-kami/baru", branch = "lender-decides-timelock" } bip32 = { version = "0.2", features = ["secp256k1-ffi", "bip39"], default-features = false } coin_selection = { path = "../../coin_selection" } conquer-once = "0.3" console_error_panic_hook = { version = "0.1.6", optional = true } -elements = { version = "0.17", features = ["serde-feature"] } +elements = { version = "0.18", features = ["serde-feature"] } estimate_transaction_size = { path = "../../estimate_transaction_size" } futures = "0.3" getrandom = { version = "0.2", features = ["wasm-bindgen", "js"] } @@ -48,7 +47,7 @@ wasm-bindgen-test = "0.3.13" [build-dependencies] anyhow = "1" conquer-once = "0.3" -elements = { version = "0.17" } +elements = { version = "0.18" } # By default wasm-opt is true which makes the build fail. [package.metadata.wasm-pack.profile.release] diff --git a/extension/wallet/src/esplora.rs b/extension/wallet/src/esplora.rs index 9309f4812..52befd98c 100644 --- a/extension/wallet/src/esplora.rs +++ b/extension/wallet/src/esplora.rs @@ -39,10 +39,16 @@ pub async fn fetch_utxos(address: &Address) -> Result> { )); } - response + let mut utxos = response .json::>() .await - .context("failed to deserialize response") + .context("failed to deserialize response")?; + + // Sort UTXOs to have more deterministic output in case something goes wrong. + // Note that the order of these UTXOs does not have to be strictly assured. + utxos.sort_by(|l, r| l.txid.cmp(&r.txid).then(l.vout.cmp(&r.vout))); + + Ok(utxos) } /// Fetch transaction history for the specified address. diff --git a/extension/wallet/src/lib.rs b/extension/wallet/src/lib.rs index 087e01cc1..90bae89bf 100644 --- a/extension/wallet/src/lib.rs +++ b/extension/wallet/src/lib.rs @@ -235,19 +235,16 @@ pub async fn make_loan_request( wallet_name: String, collateral: String, fee_rate: String, - timeout: String, ) -> Result { // TODO: Change the UI to handle SATs not BTC let collateral_in_btc = map_err_from_anyhow!(parse_to_bitcoin_amount(collateral))?; let fee_rate_in_sat = Amount::from_sat(map_err_from_anyhow!(u64::from_str(fee_rate.as_str()))?); - let timeout = map_err_from_anyhow!(u64::from_str(timeout.as_str()))?; let loan_request = map_err_from_anyhow!( wallet::make_loan_request( wallet_name, &LOADED_WALLET, collateral_in_btc, fee_rate_in_sat, - timeout ) .await )?; diff --git a/extension/wallet/src/wallet.rs b/extension/wallet/src/wallet.rs index 0f8263b0f..4738f1ef6 100644 --- a/extension/wallet/src/wallet.rs +++ b/extension/wallet/src/wallet.rs @@ -310,8 +310,8 @@ impl fmt::Display for ListOfWallets { pub struct CreateSwapPayload { pub alice_inputs: Vec, pub address: Address, - #[serde(with = "bdk::bitcoin::util::amount::serde::as_sat")] - pub amount: bdk::bitcoin::Amount, + #[serde(with = "elements::bitcoin::util::amount::serde::as_sat")] + pub amount: elements::bitcoin::Amount, } #[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] @@ -426,7 +426,7 @@ pub struct LoanDetails { pub principal: TradeSide, pub principal_repayment: Decimal, // TODO: Express as target date or number of days instead? - pub term: u64, + pub term: u32, pub txid: Txid, } @@ -439,7 +439,7 @@ impl LoanDetails { principal_asset: AssetId, principal_amount: Amount, principal_balance: Decimal, - timelock: u64, + timelock: u32, txid: Txid, ) -> Result { let collateral = TradeSide::new_sell( diff --git a/extension/wallet/src/wallet/extract_loan.rs b/extension/wallet/src/wallet/extract_loan.rs index 297b50ef1..03a01008e 100644 --- a/extension/wallet/src/wallet/extract_loan.rs +++ b/extension/wallet/src/wallet/extract_loan.rs @@ -45,10 +45,10 @@ pub async fn extract_loan( .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 timelock = borrower.collateral_contract().timelock(); let collateral_balance = balances .iter() @@ -72,15 +72,15 @@ pub async fn extract_loan( }) .unwrap_or_default(); - let loan_txid = borrower.loan_transaction.txid(); + let loan_txid = borrower.loan_transaction().txid(); let loan_details = LoanDetails::new( btc_asset_id, - borrower.collateral_amount, + borrower.collateral_amount(), collateral_balance, usdt_asset_id, - borrower.principal_tx_out_amount, + borrower.principal_amount(), principal_balance, - timelock, + *timelock, loan_txid, ) .map_err(Error::LoanDetails)?; diff --git a/extension/wallet/src/wallet/make_create_swap_payload.rs b/extension/wallet/src/wallet/make_create_swap_payload.rs index 3d8ead3e0..092f35162 100644 --- a/extension/wallet/src/wallet/make_create_swap_payload.rs +++ b/extension/wallet/src/wallet/make_create_swap_payload.rs @@ -2,9 +2,8 @@ use crate::{ wallet::{current, get_txouts, CreateSwapPayload, SwapUtxo, Wallet}, BTC_ASSET_ID, USDT_ASSET_ID, }; -use bdk::bitcoin::Amount; use coin_selection::{self, coin_select}; -use elements::{secp256k1_zkp::SECP256K1, AssetId, OutPoint}; +use elements::{bitcoin::Amount, secp256k1_zkp::SECP256K1, AssetId, OutPoint}; use estimate_transaction_size::avg_vbytes; use futures::lock::Mutex; use wasm_bindgen::UnwrapThrowExt; diff --git a/extension/wallet/src/wallet/make_loan_request.rs b/extension/wallet/src/wallet/make_loan_request.rs index 5da12ce65..4cb898658 100644 --- a/extension/wallet/src/wallet/make_loan_request.rs +++ b/extension/wallet/src/wallet/make_loan_request.rs @@ -19,7 +19,6 @@ pub async fn make_loan_request( current_wallet: &Mutex>, collateral_amount: Amount, fee_rate: Amount, - timelock: u64, ) -> Result { let btc_asset_id = { let guard = BTC_ASSET_ID.lock().expect_throw("can get lock"); @@ -113,7 +112,6 @@ pub async fn make_loan_request( blinding_key, collateral_amount, fee_rate, - timelock, btc_asset_id, usdt_asset_id, ) diff --git a/waves/src/App.test.tsx b/waves/src/App.test.tsx index 73d63de5c..d4423c226 100644 --- a/waves/src/App.test.tsx +++ b/waves/src/App.test.tsx @@ -1,9 +1,5 @@ -import { act, render, screen } from "@testing-library/react"; import React from "react"; -import { Listener, Source, SSEProvider } from "react-hooks-sse"; -import { BrowserRouter } from "react-router-dom"; -import App, { Asset, reducer, State } from "./App"; -import { Interest, Rate } from "./Bobtimus"; +import { Asset, reducer, State } from "./App"; import calculateBetaAmount from "./calculateBetaAmount"; const defaultLoanOffer = { @@ -16,8 +12,9 @@ const defaultLoanOffer = { max_principal: 10000, max_ltv: 0.8, interest: [{ - timelock: 43200, + term: 30, interest_rate: 0.15, + collateralization: 1.5, }], }; diff --git a/waves/src/App.tsx b/waves/src/App.tsx index 95869dd99..1c5bb87bc 100644 --- a/waves/src/App.tsx +++ b/waves/src/App.tsx @@ -224,7 +224,7 @@ export function reducer(state: State = initialState, action: Action) { // TODO: We currently always overwrite upon a new loan offer // This will have to be adapted once we refresh loan offers. const principalAmount = action.value.min_principal.toString(); - const loanTermInDays = action.value.interest[0].timelock / 60 / 24; + const loanTermInDays = action.value.interest[0].term; return { ...state, diff --git a/waves/src/Bobtimus.tsx b/waves/src/Bobtimus.tsx index 79f11faa0..0ce56e214 100644 --- a/waves/src/Bobtimus.tsx +++ b/waves/src/Bobtimus.tsx @@ -1,7 +1,7 @@ import Debug from "debug"; import React, { ReactElement } from "react"; import { SSEProvider } from "react-hooks-sse"; -import { CreateSwapPayload, LoanRequestPayload } from "./waves-provider/wavesProvider"; +import { CreateSwapPayload, LoanRequestPayload, OutPoint } from "./waves-provider/wavesProvider"; const debug = Debug("bobtimus"); @@ -25,8 +25,9 @@ export interface Rate { } export interface Interest { - timelock: number; + term: number; interest_rate: number; // percentage, decimal represented as float + collateralization: number; // percentage, decimal represented as float } export interface LoanOffer { @@ -38,6 +39,17 @@ export interface LoanOffer { interest: Interest[]; } +export interface LoanRequest { + collateral_amount: number; + collateral_inputs: { txin: OutPoint; original_txout: any; blinding_key: string }[]; + fee_sats_per_vbyte: number; + borrower_pk: string; + borrower_address: string; + + /// Loan term in days + term: number; +} + export async function getLoanOffer(): Promise { let res = await fetch(`/api/loan/lbtc-lusdt`, { method: "GET", @@ -55,14 +67,23 @@ export async function getLoanOffer(): Promise { return await res.json(); } -export async function postLoanRequest(payload: LoanRequestPayload) { +export async function postLoanRequest(walletParams: LoanRequestPayload, termInDays: number) { + let loanRequest: LoanRequest = { + borrower_address: walletParams.borrower_address, + borrower_pk: walletParams.borrower_pk, + collateral_amount: walletParams.collateral_amount, + collateral_inputs: walletParams.collateral_inputs, + fee_sats_per_vbyte: walletParams.fee_sats_per_vbyte, + term: termInDays, + }; + let res = await fetch(`/api/loan/lbtc-lusdt`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify(payload), + body: JSON.stringify(loanRequest), }); if (res.status !== 200) { diff --git a/waves/src/Borrow.tsx b/waves/src/Borrow.tsx index 48c3573de..e4a5dd5d1 100644 --- a/waves/src/Borrow.tsx +++ b/waves/src/Borrow.tsx @@ -72,17 +72,13 @@ function Borrow({ dispatch, state, rate, wavesProvider, walletStatusAsyncState } try { const feeRate = state.loanOffer!.fee_sats_per_vbyte; - // Liquid has 1 minute blocktime - const loanTermInBlocks = state.loanTermInDays * 24 * 60; - debug("loan term in blocks: " + loanTermInBlocks); - - let loanRequest = await wavesProvider.makeLoanRequestPayload( + let loanRequestWalletParams = await wavesProvider.makeLoanRequestPayload( collateralAmount.toString(), feeRate.toString(), - loanTermInBlocks.toString(), ); - let loanResponse = await postLoanRequest(loanRequest); + + let loanResponse = await postLoanRequest(loanRequestWalletParams, state.loanTermInDays); debug(JSON.stringify(loanResponse)); let loanTransaction = await wavesProvider.signLoan(loanResponse); diff --git a/waves/src/waves-provider/index.d.ts b/waves/src/waves-provider/index.d.ts index a31f8a88e..c711df07c 100644 --- a/waves/src/waves-provider/index.d.ts +++ b/waves/src/waves-provider/index.d.ts @@ -18,7 +18,6 @@ export default class WavesProvider { public async makeLoanRequestPayload( collateral: string, fee_rate: string, - timeout: string, ): Promise; public async signAndSendSwap(tx_hex: string): Promise; diff --git a/waves/src/waves-provider/wavesProvider.ts b/waves/src/waves-provider/wavesProvider.ts index 7dd0c27a9..9acbb2382 100644 --- a/waves/src/waves-provider/wavesProvider.ts +++ b/waves/src/waves-provider/wavesProvider.ts @@ -35,7 +35,6 @@ export interface LoanRequestPayload { collateral_inputs: { txin: OutPoint; original_txout: any; blinding_key: string }[]; fee_sats_per_vbyte: number; borrower_pk: string; - timelock: number; borrower_address: string; }