Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loan offer #194

Merged
merged 7 commits into from
Aug 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 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
43 changes: 38 additions & 5 deletions bobtimus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ pub mod elements_rpc;
pub mod fixed_rate;
pub mod http;
pub mod kraken;
pub mod models;
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 @@ -258,10 +260,41 @@ 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> {
let current_height = self.elementsd.get_blockcount().await?;

Ok(LoanOffer {
rate: self.rate_service.latest_rate(),
// TODO: Dynamic fee estimation
fee_sats_per_vbyte: Amount::from_sat(1),
// TODO: Send sats over the wire and refactor waves reducer to use amount classes
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 {
// Absolute timelock calculated from current block height
// Assuming 1 min block interval, 43200 mins = 30 days
timelock: current_height + 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 @@ -300,9 +333,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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% correct would be
principal_amount+interest_rate/loan_value
but let's ignore that for now.

/// 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: u32,
/// Interest rate in percent
pub interest_rate: Decimal,
}
1 change: 0 additions & 1 deletion bobtimus/src/models.rs

This file was deleted.

File renamed without changes.
5 changes: 5 additions & 0 deletions extension/src/background-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,8 @@ export async function getPastTransactions(): Promise<Txid[]> {
// @ts-ignore
return proxy.getPastTransactions();
}

export async function getBlockHeight(): Promise<number> {
// @ts-ignore
return proxy.getBlockHeight();
}
14 changes: 13 additions & 1 deletion extension/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
extractTrade,
getAddress,
getBalances,
getBlockHeight,
getOpenLoans,
getPastTransactions,
makeBuyCreateSwapPayload,
Expand Down Expand Up @@ -67,7 +68,13 @@ browser.runtime.onMessage.addListener(async (msg: Message<any>, sender) => {
break;
case MessageKind.LoanRequest:
message = await call_wallet(
async () => await makeLoanRequestPayload(walletName, msg.payload),
async () =>
await makeLoanRequestPayload(
walletName,
msg.payload.collateral,
msg.payload.fee_rate,
msg.payload.timeout,
),
MessageKind.LoanResponse,
);
break;
Expand Down Expand Up @@ -208,6 +215,11 @@ window.createWalletFromBip39 = async (seed_words: string, password: string) => {
return createNewBip39Wallet(walletName, seed_words, password);
};

// @ts-ignore
window.getBlockHeight = async () => {
return getBlockHeight();
};

function updateBadge() {
let count = 0;
if (loanToSign) count++;
Expand Down
26 changes: 23 additions & 3 deletions extension/src/components/ConfirmLoan.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Box, Button, Flex, Heading, Image, Spacer, Text } from "@chakra-ui/react";
import { Box, Button, Flex, Heading, Image, Spacer, Text, useInterval } from "@chakra-ui/react";
import Debug from "debug";
import moment from "moment";
import React from "react";
import { useAsync } from "react-async";
import { signLoan } from "../background-proxy";
import { getBlockHeight, signLoan } from "../background-proxy";
import { LoanToSign, USDT_TICKER } from "../models";
import YouSwapItem from "./SwapItem";
import Usdt from "./tether.svg";

const debug = Debug("confirmloan:error");

interface ConfirmLoanProps {
onCancel: (tabId: number) => void;
onSuccess: () => void;
Expand All @@ -24,6 +28,22 @@ export default function ConfirmLoan(

let { details: { collateral, principal, principalRepayment, term } } = loanToSign;

const blockHeightHook = useAsync({
promiseFn: getBlockHeight,
onReject: (e) => debug("Failed to fetch block height %s", e),
});
let { data: blockHeight, reload: reloadBlockHeight } = blockHeightHook;

useInterval(() => {
reloadBlockHeight();
}, 6000); // 1 min

// format the time nicely into something like : `in 13 hours` or `in 1 month`.
// block-height and loan-term are in "blocktime" ^= minutes
const deadline = blockHeight && term
? moment().add(Math.abs(blockHeight - term), "minutes").fromNow()
: null;

return (<Box>
<form
onSubmit={async e => {
Expand Down Expand Up @@ -64,7 +84,7 @@ export default function ConfirmLoan(
<Box w="100%">
<Flex>
<Box h="40px" p="1">
<Text>Loan term: {term}</Text>
<Text>Loan term: {term} block-height {deadline ? "(due " + deadline + ")" : ""}</Text>
</Box>
</Flex>
</Box>
Expand Down
4 changes: 2 additions & 2 deletions extension/src/components/CreateWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ function CreateWallet({ onUnlock }: CreateWalletProps) {
await createWalletFromBip39(backedUpSeedWords, password);
onUnlock();
},
onReject: (e) => debug("Failed to unlock wallet: %s", e),
onReject: (e) => debug("Failed to create wallet: %s", e),
});
let { run: newSeedWords, isPending: isGeneratingSeedWords, isRejected: generatingSeedWordsFailed } = useAsync({
deferFn: async () => {
let words = await bip39SeedWords();
setSeedWords(words);
},
onReject: (e) => debug("Failed to unlock wallet: %s", e),
onReject: (e) => debug("Failed to generate seed words: %s", e),
});

return (
Expand Down
Loading