Skip to content

Commit

Permalink
UI plugged into loan offer from Bobtimus
Browse files Browse the repository at this point in the history
The UI now properly display the values given by Bobtimus.
When taking a loan the UI values are not sent yet though, it still uses hardcoded values from within the extension wallet.
  • Loading branch information
da-kami committed Jul 29, 2021
1 parent e01e6d9 commit 892322e
Show file tree
Hide file tree
Showing 11 changed files with 291 additions and 40 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion bobtimus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ log = "0.4"
mime_guess = "2.0.3"
reqwest = "0.11"
rust-embed = "5.7.0"
rust_decimal = "1.8"
rust_decimal = { version = "1.15", features = [ "serde-float" ] }
rust_decimal_macros = "1.15"
serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
sha2 = "0.9"
Expand Down
2 changes: 1 addition & 1 deletion bobtimus/src/amounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl LiquidUsdt {
Ok(Self(amount))
}

fn serialize_to_nominal<S>(amount: &LiquidUsdt, serializer: S) -> Result<S::Ok, S::Error>
pub fn serialize_to_nominal<S>(amount: &LiquidUsdt, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Expand Down
36 changes: 32 additions & 4 deletions bobtimus/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,20 @@ where
}
});

let create_loan = warp::post()
let offer_loan = warp::get()
.and(warp::path!("api" / "loan" / "lbtc-lusdt"))
.and_then({
let bobtimus = bobtimus.clone();
move || {
let bobtimus = bobtimus.clone();
async move {
let mut bobtimus = bobtimus.lock().await;
offer_loan(&mut bobtimus).await
}
}
});

let take_loan = warp::post()
.and(warp::path!("api" / "loan" / "lbtc-lusdt"))
.and(warp::body::json())
.and_then({
Expand All @@ -81,7 +94,7 @@ where
let bobtimus = bobtimus.clone();
async move {
let mut bobtimus = bobtimus.lock().await;
create_loan(&mut bobtimus, payload).await
take_loan(&mut bobtimus, payload).await
}
}
});
Expand All @@ -104,7 +117,8 @@ where
latest_rate
.or(create_sell_swap)
.or(create_buy_swap)
.or(create_loan)
.or(offer_loan)
.or(take_loan)
.or(finalize_loan)
.or(waves_resources)
.or(index_html)
Expand Down Expand Up @@ -158,7 +172,21 @@ where
.map_err(warp::reject::custom)
}

async fn create_loan<R, RS>(
async fn offer_loan<R, RS>(bobtimus: &mut Bobtimus<R, RS>) -> Result<impl Reply, Rejection>
where
R: RngCore + CryptoRng,
RS: LatestRate,
{
bobtimus
.handle_loan_offer_request()
.await
.map(|loan_offer| warp::reply::json(&loan_offer))
.map_err(anyhow::Error::from)
.map_err(problem::from_anyhow)
.map_err(warp::reject::custom)
}

async fn take_loan<R, RS>(
bobtimus: &mut Bobtimus<R, RS>,
payload: serde_json::Value,
) -> Result<impl Reply, Rejection>
Expand Down
38 changes: 34 additions & 4 deletions bobtimus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ pub mod elements_rpc;
pub mod fixed_rate;
pub mod http;
pub mod kraken;
pub mod loan;
pub mod problem;
pub mod schema;

use crate::loan::{Interest, LoanOffer};
pub use amounts::*;
use rust_decimal_macros::dec;

pub const USDT_ASSET_ID: &str = "ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2";

Expand Down Expand Up @@ -257,10 +260,37 @@ where
Ok(transaction)
}

/// Handle Alice's loan request in which she puts up L-BTC as
/// collateral and we give lend her L-USDt which she will have to
/// Handle the borrower's loan offer request
///
/// 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<LoanOffer> {
Ok(LoanOffer {
rate: self.rate_service.latest_rate(),
// TODO: Dynamic fee estimation
fee_sats_per_vbyte: Amount::from_sat(1),
min_principal: LiquidUsdt::from_str_in_dollar("100")
.expect("static value to be convertible"),
max_principal: LiquidUsdt::from_str_in_dollar("10000")
.expect("static value to be convertible"),
max_ltv: dec!(0.8),
// TODO: Dynamic interest based on current market values
interest: vec![Interest {
// Assuming 1 min block interval, 43200 mins = 30 days
timelock: 43200,
interest_rate: dec!(0.15),
}],
})
}

/// Handle the borrower's loan request in which she puts up L-BTC as
/// collateral and we lend L-USDt to her which she will have to
/// repay in the future.
pub async fn handle_loan_request(&mut self, payload: LoanRequest) -> Result<LoanResponse> {
// TODO: Ensure that the loan requested is within what we "currently" accept.
// 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 lender_address = self
.elementsd
.get_new_segwit_confidential_address()
Expand Down Expand Up @@ -299,9 +329,9 @@ where
Ok(loan_response)
}

/// Handle Alice's request to finalize a loan.
/// Handle the borrower's request to finalize a loan.
///
/// If we still agree with the loan transaction sent by Alice, we
/// If we still agree with the loan transaction sent by the borrower, we
/// will sign and broadcast it.
///
/// Additionally, we save the signed liquidation transaction so
Expand Down
52 changes: 52 additions & 0 deletions bobtimus/src/loan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::{LiquidUsdt, Rate};
use elements::bitcoin::Amount;
use rust_decimal::Decimal;

#[derive(Debug, Clone, serde::Serialize)]
pub struct LoanOffer {
// TODO: Potentially add an id if we want to track offers - for now we just check if an incoming request is acceptable
pub rate: Rate,

#[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")]
pub fee_sats_per_vbyte: Amount,

#[serde(serialize_with = "LiquidUsdt::serialize_to_nominal")]
pub min_principal: LiquidUsdt,
#[serde(serialize_with = "LiquidUsdt::serialize_to_nominal")]
pub max_principal: LiquidUsdt,

/// The maximum LTV that defines at what point the lender liquidates
///
/// LTV ... loan to value
/// LTV = principal_amount/loan_value
/// where:
/// principal_amount: the amount lent out
/// loan_value: the amount of collateral
///
/// Simple Example (interest / fees not taken into account):
///
/// The borrower takes out a loan at:
/// max_ltv = 0.7 (70%)
/// rate: 1 BTC = $100
/// principal_amount: $100
/// collateral: 2 BTC = $200 (over-collateralized by 200%)
/// current LTV = 100 / 200 = 0.5 (50%)
/// Since the actual LTV 0.5 < 0.7, so all is good.
///
/// Let's say Bitcoin value falls to $70:
/// LTV = 100 / 2 * 70 => 100 / 140 = 0.71
/// The actual LTV 0.71 > 0.7 so the lender liquidates.
///
/// The max_ltv protects the lender from Bitcoin falling too much.
pub max_ltv: Decimal,

pub interest: Vec<Interest>,
}

#[derive(Debug, Clone, serde::Serialize)]
pub struct Interest {
/// Timelock in blocks
pub timelock: u64,
/// Interest rate in percent
pub interest_rate: Decimal,
}
5 changes: 5 additions & 0 deletions extension/wallet/src/wallet/make_loan_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ use futures::lock::Mutex;
use rand::thread_rng;
use wasm_bindgen::UnwrapThrowExt;

// TODO: The return type will have to change to a type specific to this wallet.
// The type "LoanRequest" is from baru and should not be used here.
// Once we take a loan with Bobtimus, Bobtimus will create what is needed for baru (i.e. what is called "LoanRequest" atm).
// We might have to pass more into here / add to what is returned on the outside the wallet before taking a loan on Bobtimus.
// e.g. timelock should not be decided in here, it depends on what the user chooses...
pub async fn make_loan_request(
name: String,
current_wallet: &Mutex<Option<Wallet>>,
Expand Down
38 changes: 38 additions & 0 deletions waves/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,24 @@ import React from "react";
import { Listener, Source, SSEProvider } from "react-hooks-sse";
import { BrowserRouter } from "react-router-dom";
import App, { Asset, reducer } from "./App";
import { Interest, Rate } from "./Bobtimus";
import calculateBetaAmount from "./calculateBetaAmount";

const defaultLoanOffer = {
rate: {
ask: 20000,
bid: 20000,
},
fee_sats_per_vbyte: 1,
min_principal: 100,
max_principal: 10000,
max_ltv: 0.8,
interest: [{
timelock: 43200,
interest_rate: 0.15,
}],
};

const defaultState = {
trade: {
alpha: {
Expand All @@ -17,6 +33,7 @@ const defaultState = {
borrow: {
principalAmount: "1000",
loanTerm: 30,
loanOffer: defaultLoanOffer,
},
wallet: {
balance: {
Expand Down Expand Up @@ -185,6 +202,7 @@ test("update principal amount logic", () => {
borrow: {
loanTerm: 30,
principalAmount: "10000",
loanOffer: null,
},
};

Expand All @@ -196,3 +214,23 @@ test("update principal amount logic", () => {
}).borrow.principalAmount,
).toBe(newValue);
});

test("update loan offer logic", () => {
const initialState = {
...defaultState,
borrow: {
loanTerm: 0,
principalAmount: "0",
loanOffer: null,
},
};

let newState = reducer(initialState, {
type: "UpdateLoanOffer",
value: defaultLoanOffer,
});

expect(newState.borrow.loanOffer).toBe(defaultLoanOffer);
expect(newState.borrow.principalAmount).toBe("100");
expect(newState.borrow.loanTerm).toBe(30);
});
40 changes: 35 additions & 5 deletions waves/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useAsync } from "react-async";
import { useSSE } from "react-hooks-sse";
import { Link as RouteLink, Redirect, Route, Switch, useHistory } from "react-router-dom";
import "./App.css";
import { fundAddress } from "./Bobtimus";
import { fundAddress, Interest, LoanOffer } from "./Bobtimus";
import Borrow from "./Borrow";
import COMIT from "./components/comit_logo_spellout_opacity_50.svg";
import Trade from "./Trade";
Expand All @@ -23,7 +23,6 @@ export type AssetSide = "Alpha" | "Beta";

export type Action =
| { type: "UpdateAlphaAmount"; value: string }
| { type: "UpdatePrincipalAmount"; value: string }
| { type: "UpdateAlphaAssetType"; value: Asset }
| { type: "UpdateBetaAssetType"; value: Asset }
| {
Expand All @@ -34,7 +33,10 @@ export type Action =
}
| { type: "PublishTransaction"; value: string }
| { type: "UpdateWalletStatus"; value: WalletStatus }
| { type: "UpdateBalance"; value: Balances };
| { type: "UpdateBalance"; value: Balances }
| { type: "UpdatePrincipalAmount"; value: string }
| { type: "UpdateLoanTerm"; value: number }
| { type: "UpdateLoanOffer"; value: LoanOffer };

export interface TradeState {
alpha: AssetState;
Expand All @@ -43,8 +45,12 @@ export interface TradeState {
}

export interface BorrowState {
// user can select
loanTerm: number;
principalAmount: string;

// from Bobtimus
loanOffer: LoanOffer | null;
}

export interface State {
Expand Down Expand Up @@ -93,8 +99,9 @@ const initialState = {
txId: "",
},
borrow: {
principalAmount: "20000.0",
loanTerm: 30,
principalAmount: "0.0",
loanTerm: 0,
loanOffer: null,
},
wallet: {
balance: {
Expand Down Expand Up @@ -198,6 +205,29 @@ export function reducer(state: State = initialState, action: Action) {
principalAmount: action.value,
},
};
case "UpdateLoanTerm":
return {
...state,
borrow: {
...state.borrow,
loanTerm: action.value,
},
};
case "UpdateLoanOffer":
// 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 loanTerm = action.value.interest[0].timelock / 60 / 24;

return {
...state,
borrow: {
...state.borrow,
principalAmount,
loanTerm,
loanOffer: action.value,
},
};
default:
throw new Error("Unknown update action received");
}
Expand Down
Loading

0 comments on commit 892322e

Please sign in to comment.