Browse Source

Merge pull request #56 from comit-network/small-fixes

Small fixes (and rename offer to order...)
no-contract-setup-message
Daniel Karzel 4 years ago
committed by GitHub
parent
commit
99705d75d2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 2
      cfd_protocol/src/lib.rs
  3. 16
      daemon/migrations/20210903050345_create_cfd_and_order_tables.sql
  4. 70
      daemon/sqlx-data.json
  5. 136
      daemon/src/db.rs
  6. 12
      daemon/src/maker.rs
  7. 86
      daemon/src/maker_cfd_actor.rs
  8. 42
      daemon/src/maker_inc_connections_actor.rs
  9. 36
      daemon/src/model/cfd.rs
  10. 50
      daemon/src/routes_maker.rs
  11. 22
      daemon/src/routes_taker.rs
  12. 8
      daemon/src/taker.rs
  13. 58
      daemon/src/taker_cfd_actor.rs
  14. 10
      daemon/src/taker_inc_message_actor.rs
  15. 38
      daemon/src/to_sse_event.rs
  16. 16
      daemon/src/wire.rs
  17. 6
      docs/asset/mvp_maker_taker_db.puml
  18. 34
      docs/asset/mvp_maker_taker_messaging.puml
  19. 42
      frontend/src/Maker.tsx
  20. 24
      frontend/src/Taker.tsx
  21. 30
      frontend/src/components/OrderTile.tsx
  22. 4
      frontend/src/components/Types.tsx

2
README.md

@ -20,7 +20,7 @@ Once the maker is started you can start the taker:
cargo run --bin taker
```
Upon startup the taker daemon will connect to the (hardcoded) maker and retrieve the current offer.
Upon startup the taker daemon will connect to the (hardcoded) maker and retrieve the current order.
Note: The sqlite databases for maker and taker are currently created in the project root.

2
cfd_protocol/src/lib.rs

@ -409,7 +409,7 @@ pub fn punish_transaction(
Ok(punish_tx)
}
// NOTE: We have decided to not offer any verification utility because
// NOTE: We have decided to not order any verification utility because
// the APIs would be incredibly thin
#[derive(Clone)]

16
daemon/migrations/20210903050345_create_cfd_and_offer_tables.sql → daemon/migrations/20210903050345_create_cfd_and_order_tables.sql

@ -1,5 +1,5 @@
-- todo: Decimal is had to deserialize as number so we use text
create table if not exists offers
create table if not exists orders
(
id integer primary key autoincrement,
uuid text unique not null,
@ -15,21 +15,21 @@ create table if not exists offers
origin text not null
);
create unique index if not exists offers_uuid
on offers (uuid);
create unique index if not exists orders_uuid
on orders (uuid);
create table if not exists cfds
(
id integer primary key autoincrement,
offer_id integer unique not null,
offer_uuid text unique not null,
order_id integer unique not null,
order_uuid text unique not null,
quantity_usd text not null,
foreign key (offer_id) references offers (id)
foreign key (order_id) references orders (id)
);
create unique index if not exists cfd_offer_uuid
on cfds (offer_uuid);
create unique index if not exists cfd_order_uuid
on cfds (order_uuid);
create table if not exists cfd_states
(

70
daemon/sqlx-data.json

@ -1,25 +1,35 @@
{
"db": "SQLite",
"1bd5f2355d2e9351a443ec10b2533ca9326bb2a27b9f049d60759ac5a9eba758": {
"query": "\n select\n id\n from cfds\n where offer_uuid = ?;\n ",
"3ec696a1077ae52f21230ac33b9083ae3420698ff4d6bb1bba2d71ad518daf85": {
"query": "\n insert into orders (\n uuid,\n trading_pair,\n position,\n initial_price,\n min_quantity,\n max_quantity,\n leverage,\n liquidation_price,\n creation_timestamp,\n term,\n origin\n ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 11
},
"nullable": []
}
},
"50abbb297394739ec9d85917f8c32aa8bcfa0bfe140b24e9eeda4ce8d30d4f8d": {
"query": "\n select\n state\n from cfd_states\n where cfd_id = ?\n order by id desc\n limit 1;\n ",
"describe": {
"columns": [
{
"name": "id",
"name": "state",
"ordinal": 0,
"type_info": "Int64"
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true
false
]
}
},
"2bfe23378b852e9c98f1db3e7c7694e1a7037183eb6fa8d81916cdb46c760547": {
"query": "\n select\n cfds.id as cfd_id,\n offers.uuid as offer_id,\n offers.initial_price as initial_price,\n offers.leverage as leverage,\n offers.trading_pair as trading_pair,\n offers.position as position,\n offers.origin as origin,\n offers.liquidation_price as liquidation_price,\n cfds.quantity_usd as quantity_usd,\n cfd_states.state as state\n from cfds as cfds\n inner join offers as offers on cfds.offer_id = offers.id\n inner join cfd_states as cfd_states on cfd_states.cfd_id = cfds.id\n where cfd_states.state in (\n select\n state\n from cfd_states\n where cfd_id = cfds.id\n order by id desc\n limit 1\n )\n ",
"7941da81b43abcaa82d51bbfecaed51f13fc637dc611cebb87eea638ba055a42": {
"query": "\n select\n cfds.id as cfd_id,\n orders.uuid as order_id,\n orders.initial_price as initial_price,\n orders.leverage as leverage,\n orders.trading_pair as trading_pair,\n orders.position as position,\n orders.origin as origin,\n orders.liquidation_price as liquidation_price,\n cfds.quantity_usd as quantity_usd,\n cfd_states.state as state\n from cfds as cfds\n inner join orders as orders on cfds.order_id = orders.id\n inner join cfd_states as cfd_states on cfd_states.cfd_id = cfds.id\n where cfd_states.state in (\n select\n state\n from cfd_states\n where cfd_id = cfds.id\n order by id desc\n limit 1\n )\n ",
"describe": {
"columns": [
{
@ -28,7 +38,7 @@
"type_info": "Int64"
},
{
"name": "offer_id",
"name": "order_id",
"ordinal": 1,
"type_info": "Text"
},
@ -90,44 +100,24 @@
]
}
},
"4a8db91aaef56a804d0151460297175355c9adee100b87fa9052245cdd18d7e9": {
"query": "\n insert into offers (\n uuid,\n trading_pair,\n position,\n initial_price,\n min_quantity,\n max_quantity,\n leverage,\n liquidation_price,\n creation_timestamp,\n term,\n origin\n ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 11
},
"nullable": []
}
},
"50abbb297394739ec9d85917f8c32aa8bcfa0bfe140b24e9eeda4ce8d30d4f8d": {
"query": "\n select\n state\n from cfd_states\n where cfd_id = ?\n order by id desc\n limit 1;\n ",
"8e7571250da58b12f5884f17656e5966957c7798ea029c701a4fc43fd613f015": {
"query": "\n select\n id\n from cfds\n where order_uuid = ?;\n ",
"describe": {
"columns": [
{
"name": "state",
"name": "id",
"ordinal": 0,
"type_info": "Text"
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
true
]
}
},
"79162c94809f9fac4850236d06a76206d0914285d462c04e30ad7af222092675": {
"query": "\n insert into cfds (\n offer_id,\n offer_uuid,\n quantity_usd\n ) values (?, ?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
}
},
"a464a1feb12abadff8bfd5b2b3b7362f3846869c0702944b21737eff8f420be5": {
"query": "\n insert into cfd_states (\n cfd_id,\n state\n ) values (?, ?);\n ",
"describe": {
@ -138,8 +128,8 @@
"nullable": []
}
},
"d380cac37744c2169d4e0a8f13d223cbd37ca5034e461a583b18cf7566a9c5c6": {
"query": "\n select * from offers where uuid = ?;\n ",
"f3dce76f316212c91cb3402b0cef00f1c9adbef8519c54e9bdbd373aab946209": {
"query": "\n select * from orders where uuid = ?;\n ",
"describe": {
"columns": [
{
@ -231,5 +221,15 @@
},
"nullable": []
}
},
"fad32c267100e26f4fba80e4feb5ff45ee29c3a67bd378f6627b1f13ee45c573": {
"query": "\n insert into cfds (\n order_id,\n order_uuid,\n quantity_usd\n ) values (?, ?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
}
}
}

136
daemon/src/db.rs

@ -1,4 +1,4 @@
use crate::model::cfd::{Cfd, CfdOffer, CfdOfferId, CfdState};
use crate::model::cfd::{Cfd, CfdState, Order, OrderId};
use crate::model::{Leverage, Position};
use anyhow::Context;
use rocket_db_pools::sqlx;
@ -9,9 +9,9 @@ use std::convert::TryInto;
use std::mem;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum OfferOrigin {
Mine,
Others,
pub enum Origin {
Ours,
Theirs,
}
pub async fn run_migrations(pool: &SqlitePool) -> anyhow::Result<()> {
@ -19,26 +19,26 @@ pub async fn run_migrations(pool: &SqlitePool) -> anyhow::Result<()> {
Ok(())
}
pub async fn insert_cfd_offer(
cfd_offer: &CfdOffer,
pub async fn insert_order(
order: &Order,
conn: &mut PoolConnection<Sqlite>,
origin: OfferOrigin,
origin: Origin,
) -> anyhow::Result<()> {
let uuid = serde_json::to_string(&cfd_offer.id).unwrap();
let trading_pair = serde_json::to_string(&cfd_offer.trading_pair).unwrap();
let position = serde_json::to_string(&cfd_offer.position).unwrap();
let initial_price = serde_json::to_string(&cfd_offer.price).unwrap();
let min_quantity = serde_json::to_string(&cfd_offer.min_quantity).unwrap();
let max_quantity = serde_json::to_string(&cfd_offer.max_quantity).unwrap();
let leverage = cfd_offer.leverage.0;
let liquidation_price = serde_json::to_string(&cfd_offer.liquidation_price).unwrap();
let creation_timestamp = serde_json::to_string(&cfd_offer.creation_timestamp).unwrap();
let term = serde_json::to_string(&cfd_offer.term).unwrap();
let uuid = serde_json::to_string(&order.id).unwrap();
let trading_pair = serde_json::to_string(&order.trading_pair).unwrap();
let position = serde_json::to_string(&order.position).unwrap();
let initial_price = serde_json::to_string(&order.price).unwrap();
let min_quantity = serde_json::to_string(&order.min_quantity).unwrap();
let max_quantity = serde_json::to_string(&order.max_quantity).unwrap();
let leverage = order.leverage.0;
let liquidation_price = serde_json::to_string(&order.liquidation_price).unwrap();
let creation_timestamp = serde_json::to_string(&order.creation_timestamp).unwrap();
let term = serde_json::to_string(&order.term).unwrap();
let origin = serde_json::to_string(&origin).unwrap();
sqlx::query!(
r#"
insert into offers (
insert into orders (
uuid,
trading_pair,
position,
@ -70,15 +70,15 @@ pub async fn insert_cfd_offer(
Ok(())
}
pub async fn load_offer_by_id(
id: CfdOfferId,
pub async fn load_order_by_id(
id: OrderId,
conn: &mut PoolConnection<Sqlite>,
) -> anyhow::Result<CfdOffer> {
) -> anyhow::Result<Order> {
let uuid = serde_json::to_string(&id).unwrap();
let row = sqlx::query!(
r#"
select * from offers where uuid = ?;
select * from orders where uuid = ?;
"#,
uuid
)
@ -96,7 +96,7 @@ pub async fn load_offer_by_id(
let creation_timestamp = serde_json::from_str(row.creation_timestamp.as_str()).unwrap();
let term = serde_json::from_str(row.term.as_str()).unwrap();
Ok(CfdOffer {
Ok(Order {
id: uuid,
trading_pair,
position,
@ -113,17 +113,17 @@ pub async fn load_offer_by_id(
pub async fn insert_cfd(cfd: Cfd, conn: &mut PoolConnection<Sqlite>) -> anyhow::Result<()> {
let mut tx = conn.begin().await?;
let offer_uuid = serde_json::to_string(&cfd.offer_id)?;
let offer_row = sqlx::query!(
let order_uuid = serde_json::to_string(&cfd.order_id)?;
let order_row = sqlx::query!(
r#"
select * from offers where uuid = ?;
select * from orders where uuid = ?;
"#,
offer_uuid
order_uuid
)
.fetch_one(&mut tx)
.await?;
let offer_id = offer_row.id;
let order_id = order_row.id;
let quantity_usd = serde_json::to_string(&cfd.quantity_usd)?;
let cfd_state = serde_json::to_string(&cfd.state)?;
@ -134,13 +134,13 @@ pub async fn insert_cfd(cfd: Cfd, conn: &mut PoolConnection<Sqlite>) -> anyhow::
let cfd_id = sqlx::query!(
r#"
insert into cfds (
offer_id,
offer_uuid,
order_id,
order_uuid,
quantity_usd
) values (?, ?, ?);
"#,
offer_id,
offer_uuid,
order_id,
order_uuid,
quantity_usd,
)
.execute(&mut tx)
@ -166,12 +166,12 @@ pub async fn insert_cfd(cfd: Cfd, conn: &mut PoolConnection<Sqlite>) -> anyhow::
}
#[allow(dead_code)]
pub async fn insert_new_cfd_state_by_offer_id(
offer_id: CfdOfferId,
pub async fn insert_new_cfd_state_by_order_id(
order_id: OrderId,
new_state: CfdState,
conn: &mut PoolConnection<Sqlite>,
) -> anyhow::Result<()> {
let cfd_id = load_cfd_id_by_offer_uuid(offer_id, conn).await?;
let cfd_id = load_cfd_id_by_order_uuid(order_id, conn).await?;
let latest_cfd_state_in_db = load_latest_cfd_state(cfd_id, conn)
.await
.context("loading latest state failed")?;
@ -179,7 +179,7 @@ pub async fn insert_new_cfd_state_by_offer_id(
// make sure that the new state is different than the current one to avoid that we save the same
// state twice
if mem::discriminant(&latest_cfd_state_in_db) == mem::discriminant(&new_state) {
anyhow::bail!("Cannot insert new state {} for cfd with order_id {} because it currently already is in state {}", new_state, offer_id, latest_cfd_state_in_db);
anyhow::bail!("Cannot insert new state {} for cfd with order_id {} because it currently already is in state {}", new_state, order_id, latest_cfd_state_in_db);
}
let cfd_state = serde_json::to_string(&new_state)?;
@ -201,20 +201,20 @@ pub async fn insert_new_cfd_state_by_offer_id(
}
#[allow(dead_code)]
async fn load_cfd_id_by_offer_uuid(
offer_uuid: CfdOfferId,
async fn load_cfd_id_by_order_uuid(
order_uuid: OrderId,
conn: &mut PoolConnection<Sqlite>,
) -> anyhow::Result<i64> {
let offer_uuid = serde_json::to_string(&offer_uuid)?;
let order_uuid = serde_json::to_string(&order_uuid)?;
let cfd_id = sqlx::query!(
r#"
select
id
from cfds
where offer_uuid = ?;
where order_uuid = ?;
"#,
offer_uuid
order_uuid
)
.fetch_one(conn)
.await?;
@ -257,17 +257,17 @@ pub async fn load_all_cfds(conn: &mut PoolConnection<Sqlite>) -> anyhow::Result<
r#"
select
cfds.id as cfd_id,
offers.uuid as offer_id,
offers.initial_price as initial_price,
offers.leverage as leverage,
offers.trading_pair as trading_pair,
offers.position as position,
offers.origin as origin,
offers.liquidation_price as liquidation_price,
orders.uuid as order_id,
orders.initial_price as initial_price,
orders.leverage as leverage,
orders.trading_pair as trading_pair,
orders.position as position,
orders.origin as origin,
orders.liquidation_price as liquidation_price,
cfds.quantity_usd as quantity_usd,
cfd_states.state as state
from cfds as cfds
inner join offers as offers on cfds.offer_id = offers.id
inner join orders as orders on cfds.order_id = orders.id
inner join cfd_states as cfd_states on cfd_states.cfd_id = cfds.id
where cfd_states.state in (
select
@ -285,7 +285,7 @@ pub async fn load_all_cfds(conn: &mut PoolConnection<Sqlite>) -> anyhow::Result<
let cfds = rows
.iter()
.map(|row| {
let offer_id = serde_json::from_str(row.offer_id.as_str()).unwrap();
let order_id = serde_json::from_str(row.order_id.as_str()).unwrap();
let initial_price = serde_json::from_str(row.initial_price.as_str()).unwrap();
let leverage = Leverage(row.leverage.try_into().unwrap());
let trading_pair = serde_json::from_str(row.trading_pair.as_str()).unwrap();
@ -293,16 +293,16 @@ pub async fn load_all_cfds(conn: &mut PoolConnection<Sqlite>) -> anyhow::Result<
let quantity = serde_json::from_str(row.quantity_usd.as_str()).unwrap();
let latest_state = serde_json::from_str(row.state.as_str()).unwrap();
let origin: OfferOrigin = serde_json::from_str(row.origin.as_str()).unwrap();
let origin: Origin = serde_json::from_str(row.origin.as_str()).unwrap();
let position: Position = serde_json::from_str(row.position.as_str()).unwrap();
let position = match origin {
OfferOrigin::Mine => position,
OfferOrigin::Others => position.counter_position(),
Origin::Ours => position,
Origin::Theirs => position.counter_position(),
};
Cfd {
offer_id,
order_id,
initial_price,
leverage,
trading_pair,
@ -326,25 +326,25 @@ mod tests {
use sqlx::SqlitePool;
use tempfile::tempdir;
use crate::db::insert_cfd_offer;
use crate::model::cfd::{Cfd, CfdOffer, CfdState, CfdStateCommon};
use crate::db::insert_order;
use crate::model::cfd::{Cfd, CfdState, CfdStateCommon, Order};
use crate::model::Usd;
use super::*;
#[tokio::test]
async fn test_insert_and_load_offer() {
async fn test_insert_and_load_order() {
let pool = setup_test_db().await;
let mut conn = pool.acquire().await.unwrap();
let cfd_offer = CfdOffer::from_default_with_price(Usd(dec!(10000))).unwrap();
insert_cfd_offer(&cfd_offer, &mut conn, OfferOrigin::Others)
let order = Order::from_default_with_price(Usd(dec!(10000))).unwrap();
insert_order(&order, &mut conn, Origin::Theirs)
.await
.unwrap();
let cfd_offer_loaded = load_offer_by_id(cfd_offer.id, &mut conn).await.unwrap();
let order_loaded = load_order_by_id(order.id, &mut conn).await.unwrap();
assert_eq!(cfd_offer, cfd_offer_loaded);
assert_eq!(order, order_loaded);
}
#[tokio::test]
@ -352,9 +352,9 @@ mod tests {
let pool = setup_test_db().await;
let mut conn = pool.acquire().await.unwrap();
let cfd_offer = CfdOffer::from_default_with_price(Usd(dec!(10000))).unwrap();
let order = Order::from_default_with_price(Usd(dec!(10000))).unwrap();
let cfd = Cfd::new(
cfd_offer.clone(),
order.clone(),
Usd(dec!(1000)),
CfdState::PendingTakeRequest {
common: CfdStateCommon {
@ -365,7 +365,7 @@ mod tests {
);
// the order ahs to exist in the db in order to be able to insert the cfd
insert_cfd_offer(&cfd_offer, &mut conn, OfferOrigin::Others)
insert_order(&order, &mut conn, Origin::Theirs)
.await
.unwrap();
insert_cfd(cfd.clone(), &mut conn).await.unwrap();
@ -380,9 +380,9 @@ mod tests {
let pool = setup_test_db().await;
let mut conn = pool.acquire().await.unwrap();
let cfd_offer = CfdOffer::from_default_with_price(Usd(dec!(10000))).unwrap();
let order = Order::from_default_with_price(Usd(dec!(10000))).unwrap();
let mut cfd = Cfd::new(
cfd_offer.clone(),
order.clone(),
Usd(dec!(1000)),
CfdState::PendingTakeRequest {
common: CfdStateCommon {
@ -393,7 +393,7 @@ mod tests {
);
// the order ahs to exist in the db in order to be able to insert the cfd
insert_cfd_offer(&cfd_offer, &mut conn, OfferOrigin::Others)
insert_order(&order, &mut conn, Origin::Theirs)
.await
.unwrap();
insert_cfd(cfd.clone(), &mut conn).await.unwrap();
@ -403,7 +403,7 @@ mod tests {
transition_timestamp: SystemTime::now(),
},
};
insert_new_cfd_state_by_offer_id(cfd.offer_id, cfd.state, &mut conn)
insert_new_cfd_state_by_order_id(cfd.order_id, cfd.state, &mut conn)
.await
.unwrap();

12
daemon/src/maker.rs

@ -2,7 +2,7 @@ use anyhow::Result;
use bdk::bitcoin::secp256k1::{schnorrsig, SECP256K1};
use bdk::bitcoin::{self, Amount};
use bdk::blockchain::{ElectrumBlockchain, NoopProgress};
use model::cfd::{Cfd, CfdOffer};
use model::cfd::{Cfd, Order};
use rocket::fairing::AdHoc;
use rocket::figment::util::map;
use rocket::figment::value::{Map, Value};
@ -45,7 +45,7 @@ async fn main() -> Result<()> {
let oracle = schnorrsig::KeyPair::new(SECP256K1, &mut rand::thread_rng()); // TODO: Fetch oracle public key from oracle.
let (cfd_feed_sender, cfd_feed_receiver) = watch::channel::<Vec<Cfd>>(vec![]);
let (offer_feed_sender, offer_feed_receiver) = watch::channel::<Option<CfdOffer>>(None);
let (order_feed_sender, order_feed_receiver) = watch::channel::<Option<Order>>(None);
let (_balance_feed_sender, balance_feed_receiver) = watch::channel::<Amount>(Amount::ZERO);
let db: Map<_, Value> = map! {
@ -63,7 +63,7 @@ async fn main() -> Result<()> {
rocket::custom(figment)
.manage(cfd_feed_receiver)
.manage(offer_feed_receiver)
.manage(order_feed_receiver)
.manage(balance_feed_receiver)
.attach(Db::init())
.attach(AdHoc::try_on_ignite(
@ -95,7 +95,7 @@ async fn main() -> Result<()> {
schnorrsig::PublicKey::from_keypair(SECP256K1, &oracle),
connections_actor_inbox_sender,
cfd_feed_sender,
offer_feed_sender,
order_feed_sender,
);
let connections_actor = maker_inc_connections_actor::new(
listener,
@ -113,8 +113,8 @@ async fn main() -> Result<()> {
"/",
rocket::routes![
routes_maker::maker_feed,
routes_maker::post_sell_offer,
// routes_maker::post_confirm_offer,
routes_maker::post_sell_order,
// routes_maker::post_confirm_order,
routes_maker::get_health_check
],
)

86
daemon/src/maker_cfd_actor.rs

@ -1,8 +1,8 @@
use std::collections::HashMap;
use std::time::SystemTime;
use crate::db::{insert_cfd, insert_cfd_offer, load_all_cfds, load_offer_by_id, OfferOrigin};
use crate::model::cfd::{Cfd, CfdOffer, CfdOfferId, CfdState, CfdStateCommon, FinalizedCfd};
use crate::db::{insert_cfd, insert_order, load_all_cfds, load_order_by_id, Origin};
use crate::model::cfd::{Cfd, CfdState, CfdStateCommon, FinalizedCfd, Order, OrderId};
use crate::model::{TakerId, Usd};
use crate::wire::{Msg0, Msg1, SetupMsg};
use crate::{maker_cfd_actor, maker_inc_connections_actor};
@ -19,15 +19,15 @@ use tokio::sync::{mpsc, watch};
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Command {
TakeOffer {
TakeOrder {
taker_id: TakerId,
offer_id: CfdOfferId,
order_id: OrderId,
quantity: Usd,
},
NewOffer(CfdOffer),
NewOrder(Order),
StartContractSetup {
taker_id: TakerId,
offer_id: CfdOfferId,
order_id: OrderId,
},
NewTakerOnline {
id: TakerId,
@ -42,7 +42,7 @@ pub fn new<B, D>(
oracle_pk: schnorrsig::PublicKey,
takers: mpsc::UnboundedSender<maker_inc_connections_actor::Command>,
cfd_feed_actor_inbox: watch::Sender<Vec<Cfd>>,
offer_feed_sender: watch::Sender<Option<CfdOffer>>,
order_feed_sender: watch::Sender<Option<Order>>,
) -> (
impl Future<Output = ()>,
mpsc::UnboundedSender<maker_cfd_actor::Command>,
@ -53,7 +53,7 @@ where
let (sender, mut receiver) = mpsc::unbounded_channel();
let mut current_contract_setup = None;
let mut current_offer_id = None;
let mut current_order_id = None;
let actor = {
let sender = sender.clone();
@ -67,27 +67,27 @@ where
while let Some(message) = receiver.recv().await {
match message {
maker_cfd_actor::Command::TakeOffer {
maker_cfd_actor::Command::TakeOrder {
taker_id,
offer_id,
order_id,
quantity,
} => {
println!(
"Taker {} wants to take {} of offer {}",
taker_id, quantity, offer_id
"Taker {} wants to take {} of order {}",
taker_id, quantity, order_id
);
let mut conn = db.acquire().await.unwrap();
// 1. Validate if offer is still valid
let current_offer = match current_offer_id {
Some(current_offer_id) if current_offer_id == offer_id => {
load_offer_by_id(current_offer_id, &mut conn).await.unwrap()
// 1. Validate if order is still valid
let current_order = match current_order_id {
Some(current_order_id) if current_order_id == order_id => {
load_order_by_id(current_order_id, &mut conn).await.unwrap()
}
_ => {
takers
.send(maker_inc_connections_actor::Command::NotifyInvalidOfferId {
id: offer_id,
.send(maker_inc_connections_actor::Command::NotifyInvalidOrderId {
id: order_id,
taker_id,
})
.unwrap();
@ -98,20 +98,20 @@ where
// 2. Insert CFD in DB
// TODO: Don't auto-accept, present to user in UI instead
let cfd = Cfd::new(
current_offer.clone(),
current_order.clone(),
quantity,
CfdState::Accepted {
common: CfdStateCommon {
transition_timestamp: SystemTime::now(),
},
},
current_offer.position,
current_order.position,
);
insert_cfd(cfd, &mut conn).await.unwrap();
takers
.send(maker_inc_connections_actor::Command::NotifyOfferAccepted {
id: offer_id,
.send(maker_inc_connections_actor::Command::NotifyOrderAccepted {
id: order_id,
taker_id,
})
.unwrap();
@ -119,60 +119,56 @@ where
.send(load_all_cfds(&mut conn).await.unwrap())
.unwrap();
// 3. Remove current offer
current_offer_id = None;
// 3. Remove current order
current_order_id = None;
takers
.send(maker_inc_connections_actor::Command::BroadcastCurrentOffer(
None,
))
.send(maker_inc_connections_actor::Command::BroadcastOrder(None))
.unwrap();
offer_feed_sender.send(None).unwrap();
order_feed_sender.send(None).unwrap();
}
maker_cfd_actor::Command::NewOffer(offer) => {
maker_cfd_actor::Command::NewOrder(order) => {
// 1. Save to DB
let mut conn = db.acquire().await.unwrap();
insert_cfd_offer(&offer, &mut conn, OfferOrigin::Mine)
.await
.unwrap();
insert_order(&order, &mut conn, Origin::Ours).await.unwrap();
// 2. Update actor state to current offer
current_offer_id.replace(offer.id);
// 2. Update actor state to current order
current_order_id.replace(order.id);
// 3. Notify UI via feed
offer_feed_sender.send(Some(offer.clone())).unwrap();
order_feed_sender.send(Some(order.clone())).unwrap();
// 4. Inform connected takers
takers
.send(maker_inc_connections_actor::Command::BroadcastCurrentOffer(
Some(offer),
))
.send(maker_inc_connections_actor::Command::BroadcastOrder(Some(
order,
)))
.unwrap();
}
maker_cfd_actor::Command::NewTakerOnline { id: taker_id } => {
let mut conn = db.acquire().await.unwrap();
let current_offer = match current_offer_id {
Some(current_offer_id) => {
Some(load_offer_by_id(current_offer_id, &mut conn).await.unwrap())
let current_order = match current_order_id {
Some(current_order_id) => {
Some(load_order_by_id(current_order_id, &mut conn).await.unwrap())
}
None => None,
};
takers
.send(maker_inc_connections_actor::Command::SendCurrentOffer {
offer: current_offer,
.send(maker_inc_connections_actor::Command::SendOrder {
order: current_order,
taker_id,
})
.unwrap();
}
maker_cfd_actor::Command::StartContractSetup {
taker_id,
offer_id: _offer_id,
order_id: _order_id,
} => {
// Kick-off the CFD protocol
let (sk, pk) = crate::keypair::new(&mut rand::thread_rng());
// TODO: Load correct quantity from DB with offer_id
// TODO: Load correct quantity from DB with order_id
let maker_params = wallet
.build_party_params(bitcoin::Amount::ZERO, pk)
.unwrap();

42
daemon/src/maker_inc_connections_actor.rs

@ -1,4 +1,4 @@
use crate::model::cfd::{CfdOffer, CfdOfferId};
use crate::model::cfd::{Order, OrderId};
use crate::model::TakerId;
use crate::wire::SetupMsg;
use crate::{maker_cfd_actor, maker_inc_connections_actor, send_wire_message_actor, wire};
@ -12,17 +12,17 @@ use tokio_util::codec::{FramedRead, LengthDelimitedCodec};
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Command {
BroadcastCurrentOffer(Option<CfdOffer>),
SendCurrentOffer {
offer: Option<CfdOffer>,
BroadcastOrder(Option<Order>),
SendOrder {
order: Option<Order>,
taker_id: TakerId,
},
NotifyInvalidOfferId {
id: CfdOfferId,
NotifyInvalidOrderId {
id: OrderId,
taker_id: TakerId,
},
NotifyOfferAccepted {
id: CfdOfferId,
NotifyOrderAccepted {
id: OrderId,
taker_id: TakerId,
},
OutProtocolMsg {
@ -59,22 +59,22 @@ pub fn new(
},
Some(message) = our_inbox.recv() => {
match message {
maker_inc_connections_actor::Command::NotifyInvalidOfferId { id, taker_id } => {
maker_inc_connections_actor::Command::NotifyInvalidOrderId { id, taker_id } => {
let conn = write_connections.get(&taker_id).expect("no connection to taker_id");
conn.send(wire::MakerToTaker::InvalidOfferId(id)).unwrap();
conn.send(wire::MakerToTaker::InvalidOrderId(id)).unwrap();
},
maker_inc_connections_actor::Command::BroadcastCurrentOffer(offer) => {
maker_inc_connections_actor::Command::BroadcastOrder(order) => {
for conn in write_connections.values() {
conn.send(wire::MakerToTaker::CurrentOffer(offer.clone())).unwrap();
conn.send(wire::MakerToTaker::CurrentOrder(order.clone())).unwrap();
}
},
maker_inc_connections_actor::Command::SendCurrentOffer {offer, taker_id} => {
maker_inc_connections_actor::Command::SendOrder {order, taker_id} => {
let conn = write_connections.get(&taker_id).expect("no connection to taker_id");
conn.send(wire::MakerToTaker::CurrentOffer(offer)).unwrap();
conn.send(wire::MakerToTaker::CurrentOrder(order)).unwrap();
},
maker_inc_connections_actor::Command::NotifyOfferAccepted { id, taker_id } => {
maker_inc_connections_actor::Command::NotifyOrderAccepted { id, taker_id } => {
let conn = write_connections.get(&taker_id).expect("no connection to taker_id");
conn.send(wire::MakerToTaker::ConfirmTakeOffer(id)).unwrap();
conn.send(wire::MakerToTaker::ConfirmTakeOrder(id)).unwrap();
},
maker_inc_connections_actor::Command::OutProtocolMsg { taker_id, msg } => {
let conn = write_connections.get(&taker_id).expect("no connection to taker_id");
@ -100,15 +100,15 @@ fn in_taker_messages(
async move {
while let Some(message) = messages.next().await {
match message {
Ok(wire::TakerToMaker::TakeOffer { offer_id, quantity }) => cfd_actor_inbox
.send(maker_cfd_actor::Command::TakeOffer {
Ok(wire::TakerToMaker::TakeOrder { order_id, quantity }) => cfd_actor_inbox
.send(maker_cfd_actor::Command::TakeOrder {
taker_id,
offer_id,
order_id,
quantity,
})
.unwrap(),
Ok(wire::TakerToMaker::StartContractSetup(offer_id)) => cfd_actor_inbox
.send(maker_cfd_actor::Command::StartContractSetup { taker_id, offer_id })
Ok(wire::TakerToMaker::StartContractSetup(order_id)) => cfd_actor_inbox
.send(maker_cfd_actor::Command::StartContractSetup { taker_id, order_id })
.unwrap(),
Ok(wire::TakerToMaker::Protocol(msg)) => cfd_actor_inbox
.send(maker_cfd_actor::Command::IncProtocolMsg(msg))

36
daemon/src/model/cfd.rs

@ -12,24 +12,24 @@ use std::time::{Duration, SystemTime};
use uuid::Uuid;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct CfdOfferId(Uuid);
pub struct OrderId(Uuid);
impl Default for CfdOfferId {
impl Default for OrderId {
fn default() -> Self {
Self(Uuid::new_v4())
}
}
impl Display for CfdOfferId {
impl Display for OrderId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
/// A concrete offer created by a maker for a taker
/// A concrete order created by a maker for a taker
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CfdOffer {
pub id: CfdOfferId,
pub struct Order {
pub id: OrderId,
pub trading_pair: TradingPair,
pub position: Position,
@ -52,15 +52,15 @@ pub struct CfdOffer {
}
#[allow(dead_code)] // Only one binary and the tests use this.
impl CfdOffer {
impl Order {
pub fn from_default_with_price(price: Usd) -> Result<Self> {
let leverage = Leverage(5);
let maintenance_margin_rate = dec!(0.005);
let liquidation_price =
calculate_liquidation_price(&leverage, &price, &maintenance_margin_rate)?;
Ok(CfdOffer {
id: CfdOfferId::default(),
Ok(Order {
id: OrderId::default(),
price,
min_quantity: Usd(dec!(1000)),
max_quantity: Usd(dec!(10000)),
@ -72,12 +72,12 @@ impl CfdOffer {
term: Duration::from_secs(60 * 60 * 8), // 8 hours
})
}
pub fn with_min_quantity(mut self, min_quantity: Usd) -> CfdOffer {
pub fn with_min_quantity(mut self, min_quantity: Usd) -> Order {
self.min_quantity = min_quantity;
self
}
pub fn with_max_quantity(mut self, max_quantity: Usd) -> CfdOffer {
pub fn with_max_quantity(mut self, max_quantity: Usd) -> Order {
self.max_quantity = max_quantity;
self
}
@ -240,7 +240,7 @@ impl Display for CfdState {
/// Represents a cfd (including state)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Cfd {
pub offer_id: CfdOfferId,
pub order_id: OrderId,
pub initial_price: Usd,
pub leverage: Leverage,
@ -254,14 +254,14 @@ pub struct Cfd {
}
impl Cfd {
pub fn new(cfd_offer: CfdOffer, quantity: Usd, state: CfdState, position: Position) -> Self {
pub fn new(cfd_order: Order, quantity: Usd, state: CfdState, position: Position) -> Self {
Cfd {
offer_id: cfd_offer.id,
initial_price: cfd_offer.price,
leverage: cfd_offer.leverage,
trading_pair: cfd_offer.trading_pair,
order_id: cfd_order.id,
initial_price: cfd_order.price,
leverage: cfd_order.leverage,
trading_pair: cfd_order.trading_pair,
position,
liquidation_price: cfd_offer.liquidation_price,
liquidation_price: cfd_order.liquidation_price,
quantity_usd: quantity,
state,
}

50
daemon/src/routes_maker.rs

@ -1,5 +1,5 @@
use crate::maker_cfd_actor;
use crate::model::cfd::{Cfd, CfdOffer};
use crate::model::cfd::{Cfd, Order};
use crate::model::Usd;
use crate::to_sse_event::ToSseEvent;
use anyhow::Result;
@ -15,19 +15,19 @@ use tokio::sync::{mpsc, watch};
#[rocket::get("/maker-feed")]
pub async fn maker_feed(
rx_cfds: &State<watch::Receiver<Vec<Cfd>>>,
rx_offer: &State<watch::Receiver<Option<CfdOffer>>>,
rx_order: &State<watch::Receiver<Option<Order>>>,
rx_balance: &State<watch::Receiver<Amount>>,
) -> EventStream![] {
let mut rx_cfds = rx_cfds.inner().clone();
let mut rx_offer = rx_offer.inner().clone();
let mut rx_order = rx_order.inner().clone();
let mut rx_balance = rx_balance.inner().clone();
EventStream! {
let balance = rx_balance.borrow().clone();
yield balance.to_sse_event();
let offer = rx_offer.borrow().clone();
yield offer.to_sse_event();
let order = rx_order.borrow().clone();
yield order.to_sse_event();
let cfds = rx_cfds.borrow().clone();
yield cfds.to_sse_event();
@ -38,9 +38,9 @@ pub async fn maker_feed(
let balance = rx_balance.borrow().clone();
yield balance.to_sse_event();
},
Ok(()) = rx_offer.changed() => {
let offer = rx_offer.borrow().clone();
yield offer.to_sse_event();
Ok(()) = rx_order.changed() => {
let order = rx_order.borrow().clone();
yield order.to_sse_event();
}
Ok(()) = rx_cfds.changed() => {
let cfds = rx_cfds.borrow().clone();
@ -51,10 +51,10 @@ pub async fn maker_feed(
}
}
/// The maker POSTs this to create a new CfdOffer
/// The maker POSTs this to create a new CfdOrder
// TODO: Use Rocket form?
#[derive(Debug, Clone, Deserialize)]
pub struct CfdNewOfferRequest {
pub struct CfdNewOrderRequest {
pub price: Usd,
// TODO: [post-MVP] Representation of the contract size; at the moment the contract size is
// always 1 USD
@ -62,18 +62,18 @@ pub struct CfdNewOfferRequest {
pub max_quantity: Usd,
}
#[rocket::post("/offer/sell", data = "<offer>")]
pub async fn post_sell_offer(
offer: Json<CfdNewOfferRequest>,
#[rocket::post("/order/sell", data = "<order>")]
pub async fn post_sell_order(
order: Json<CfdNewOrderRequest>,
cfd_actor_inbox: &State<mpsc::UnboundedSender<maker_cfd_actor::Command>>,
) -> Result<status::Accepted<()>, status::BadRequest<String>> {
let offer = CfdOffer::from_default_with_price(offer.price)
let order = Order::from_default_with_price(order.price)
.map_err(|e| status::BadRequest(Some(e.to_string())))?
.with_min_quantity(offer.min_quantity)
.with_max_quantity(offer.max_quantity);
.with_min_quantity(order.min_quantity)
.with_max_quantity(order.max_quantity);
cfd_actor_inbox
.send(maker_cfd_actor::Command::NewOffer(offer))
.send(maker_cfd_actor::Command::NewOrder(order))
.expect("actor to always be available");
Ok(status::Accepted(None))
@ -81,20 +81,20 @@ pub async fn post_sell_offer(
// // TODO: Shall we use a simpler struct for verification? AFAICT quantity is not
// // needed, no need to send the whole CFD either as the other fields can be generated from the
// offer #[rocket::post("/offer/confirm", data = "<cfd_confirm_offer_request>")]
// pub async fn post_confirm_offer(
// cfd_confirm_offer_request: Json<CfdTakeRequest>,
// queue: &State<mpsc::Sender<CfdOffer>>,
// order #[rocket::post("/order/confirm", data = "<cfd_confirm_order_request>")]
// pub async fn post_confirm_order(
// cfd_confirm_order_request: Json<CfdTakeRequest>,
// queue: &State<mpsc::Sender<CfdOrder>>,
// mut conn: Connection<Db>,
// ) -> Result<status::Accepted<()>, status::BadRequest<String>> {
// dbg!(&cfd_confirm_offer_request);
// dbg!(&cfd_confirm_order_request);
// let offer = db::load_offer_by_id_from_conn(cfd_confirm_offer_request.offer_id, &mut conn)
// let order = db::load_order_by_id_from_conn(cfd_confirm_order_request.order_id, &mut conn)
// .await
// .map_err(|e| status::BadRequest(Some(e.to_string())))?;
// let _res = queue
// .send(offer)
// .send(order)
// .await
// .map_err(|_| status::BadRequest(Some("internal server error".to_string())))?;
@ -105,4 +105,4 @@ pub async fn post_sell_offer(
pub fn get_health_check() {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct RetrieveCurrentOffer;
struct RetrieveCurrentOrder;

22
daemon/src/routes_taker.rs

@ -1,4 +1,4 @@
use crate::model::cfd::{calculate_buy_margin, Cfd, CfdOffer, CfdOfferId};
use crate::model::cfd::{calculate_buy_margin, Cfd, Order, OrderId};
use crate::model::{Leverage, Usd};
use crate::taker_cfd_actor;
use crate::to_sse_event::ToSseEvent;
@ -14,19 +14,19 @@ use tokio::sync::{mpsc, watch};
#[rocket::get("/feed")]
pub async fn feed(
rx_cfds: &State<watch::Receiver<Vec<Cfd>>>,
rx_offer: &State<watch::Receiver<Option<CfdOffer>>>,
rx_order: &State<watch::Receiver<Option<Order>>>,
rx_balance: &State<watch::Receiver<Amount>>,
) -> EventStream![] {
let mut rx_cfds = rx_cfds.inner().clone();
let mut rx_offer = rx_offer.inner().clone();
let mut rx_order = rx_order.inner().clone();
let mut rx_balance = rx_balance.inner().clone();
EventStream! {
let balance = rx_balance.borrow().clone();
yield balance.to_sse_event();
let offer = rx_offer.borrow().clone();
yield offer.to_sse_event();
let order = rx_order.borrow().clone();
yield order.to_sse_event();
let cfds = rx_cfds.borrow().clone();
yield cfds.to_sse_event();
@ -37,9 +37,9 @@ pub async fn feed(
let balance = rx_balance.borrow().clone();
yield balance.to_sse_event();
},
Ok(()) = rx_offer.changed() => {
let offer = rx_offer.borrow().clone();
yield offer.to_sse_event();
Ok(()) = rx_order.changed() => {
let order = rx_order.borrow().clone();
yield order.to_sse_event();
}
Ok(()) = rx_cfds.changed() => {
let cfds = rx_cfds.borrow().clone();
@ -52,7 +52,7 @@ pub async fn feed(
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CfdTakeRequest {
pub offer_id: CfdOfferId,
pub order_id: OrderId,
pub quantity: Usd,
}
@ -62,8 +62,8 @@ pub async fn post_cfd(
cfd_actor_inbox: &State<mpsc::UnboundedSender<taker_cfd_actor::Command>>,
) {
cfd_actor_inbox
.send(taker_cfd_actor::Command::TakeOffer {
offer_id: cfd_take_request.offer_id,
.send(taker_cfd_actor::Command::TakeOrder {
order_id: cfd_take_request.order_id,
quantity: cfd_take_request.quantity,
})
.expect("actor to never disappear");

8
daemon/src/taker.rs

@ -2,7 +2,7 @@ use anyhow::Result;
use bdk::bitcoin::secp256k1::{schnorrsig, SECP256K1};
use bdk::bitcoin::{self, Amount};
use bdk::blockchain::{ElectrumBlockchain, NoopProgress};
use model::cfd::{Cfd, CfdOffer};
use model::cfd::{Cfd, Order};
use rocket::fairing::AdHoc;
use rocket::figment::util::map;
use rocket::figment::value::{Map, Value};
@ -45,7 +45,7 @@ async fn main() -> Result<()> {
let oracle = schnorrsig::KeyPair::new(SECP256K1, &mut rand::thread_rng()); // TODO: Fetch oracle public key from oracle.
let (cfd_feed_sender, cfd_feed_receiver) = watch::channel::<Vec<Cfd>>(vec![]);
let (offer_feed_sender, offer_feed_receiver) = watch::channel::<Option<CfdOffer>>(None);
let (order_feed_sender, order_feed_receiver) = watch::channel::<Option<Order>>(None);
let (_balance_feed_sender, balance_feed_receiver) = watch::channel::<Amount>(Amount::ZERO);
let socket = tokio::net::TcpSocket::new_v4().unwrap();
@ -64,7 +64,7 @@ async fn main() -> Result<()> {
rocket::custom(figment)
.manage(cfd_feed_receiver)
.manage(offer_feed_receiver)
.manage(order_feed_receiver)
.manage(balance_feed_receiver)
.attach(Db::init())
.attach(AdHoc::try_on_ignite(
@ -94,7 +94,7 @@ async fn main() -> Result<()> {
wallet,
schnorrsig::PublicKey::from_keypair(SECP256K1, &oracle),
cfd_feed_sender,
offer_feed_sender,
order_feed_sender,
out_maker_actor_inbox,
);
let inc_maker_messages_actor =

58
daemon/src/taker_cfd_actor.rs

@ -1,10 +1,8 @@
use crate::db::{
insert_cfd, insert_cfd_offer, insert_new_cfd_state_by_offer_id, load_all_cfds,
load_offer_by_id, OfferOrigin,
};
use crate::model::cfd::{
AsBlocks, Cfd, CfdOffer, CfdOfferId, CfdState, CfdStateCommon, FinalizedCfd,
insert_cfd, insert_new_cfd_state_by_order_id, insert_order, load_all_cfds, load_order_by_id,
Origin,
};
use crate::model::cfd::{AsBlocks, Cfd, CfdState, CfdStateCommon, FinalizedCfd, Order, OrderId};
use crate::model::Usd;
use crate::wire;
use crate::wire::{AdaptorSignature, Msg0, Msg1, SetupMsg};
@ -23,14 +21,14 @@ use std::collections::HashMap;
use std::time::SystemTime;
use tokio::sync::{mpsc, watch};
/// A factor to be added to the CFD offer term for calculating the refund timelock.
/// A factor to be added to the CFD order term for calculating the refund timelock.
///
/// The refund timelock is important in case the oracle disappears or never publishes a signature.
/// Ideally, both users collaboratively settle in the refund scenario. This factor is important if
/// the users do not settle collaboratively.
/// `1.5` times the term as defined in CFD offer should be safe in the extreme case where a user
/// publishes the commit transaction right after the contract was initialized. In this case, the
/// oracle still has `1.0 * cfdoffer.term` time to attest and no one can publish the refund
/// `1.5` times the term as defined in CFD order should be safe in the extreme case where a user
/// publishes the commit transaction right after the contract was initialized. In this case, the
/// oracle still has `1.0 * cfdorder.term` time to attest and no one can publish the refund
/// transaction.
/// The downside is that if the oracle disappears: the users would only notice at the end
/// of the cfd term. In this case the users has to wait for another `1.5` times of the
@ -40,9 +38,9 @@ pub const REFUND_THRESHOLD: f32 = 1.5;
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Command {
TakeOffer { offer_id: CfdOfferId, quantity: Usd },
NewOffer(Option<CfdOffer>),
OfferAccepted(CfdOfferId),
TakeOrder { order_id: OrderId, quantity: Usd },
NewOrder(Option<Order>),
OrderAccepted(OrderId),
IncProtocolMsg(SetupMsg),
CfdSetupCompleted(FinalizedCfd),
}
@ -52,7 +50,7 @@ pub fn new<B, D>(
wallet: bdk::Wallet<B, D>,
oracle_pk: schnorrsig::PublicKey,
cfd_feed_actor_inbox: watch::Sender<Vec<Cfd>>,
offer_feed_actor_inbox: watch::Sender<Option<CfdOffer>>,
order_feed_actor_inbox: watch::Sender<Option<Order>>,
out_msg_maker_inbox: mpsc::UnboundedSender<wire::TakerToMaker>,
) -> (impl Future<Output = ()>, mpsc::UnboundedSender<Command>)
where
@ -73,22 +71,22 @@ where
while let Some(message) = receiver.recv().await {
match message {
Command::TakeOffer { offer_id, quantity } => {
Command::TakeOrder { order_id, quantity } => {
let mut conn = db.acquire().await.unwrap();
let current_offer = load_offer_by_id(offer_id, &mut conn).await.unwrap();
let current_order = load_order_by_id(order_id, &mut conn).await.unwrap();
println!("Accepting current offer: {:?}", &current_offer);
println!("Accepting current order: {:?}", &current_order);
let cfd = Cfd::new(
current_offer.clone(),
current_order.clone(),
quantity,
CfdState::PendingTakeRequest {
common: CfdStateCommon {
transition_timestamp: SystemTime::now(),
},
},
current_offer.position.counter_position(),
current_order.position.counter_position(),
);
insert_cfd(cfd, &mut conn).await.unwrap();
@ -97,24 +95,24 @@ where
.send(load_all_cfds(&mut conn).await.unwrap())
.unwrap();
out_msg_maker_inbox
.send(wire::TakerToMaker::TakeOffer { offer_id, quantity })
.send(wire::TakerToMaker::TakeOrder { order_id, quantity })
.unwrap();
}
Command::NewOffer(Some(offer)) => {
Command::NewOrder(Some(order)) => {
let mut conn = db.acquire().await.unwrap();
insert_cfd_offer(&offer, &mut conn, OfferOrigin::Others)
insert_order(&order, &mut conn, Origin::Theirs)
.await
.unwrap();
offer_feed_actor_inbox.send(Some(offer)).unwrap();
order_feed_actor_inbox.send(Some(order)).unwrap();
}
Command::NewOffer(None) => {
offer_feed_actor_inbox.send(None).unwrap();
Command::NewOrder(None) => {
order_feed_actor_inbox.send(None).unwrap();
}
Command::OfferAccepted(offer_id) => {
Command::OrderAccepted(order_id) => {
let mut conn = db.acquire().await.unwrap();
insert_new_cfd_state_by_offer_id(
offer_id,
insert_new_cfd_state_by_order_id(
order_id,
CfdState::ContractSetup {
common: CfdStateCommon {
transition_timestamp: SystemTime::now(),
@ -135,7 +133,7 @@ where
.build_party_params(bitcoin::Amount::ZERO, pk) // TODO: Load correct quantity from DB
.unwrap();
let cfd = load_offer_by_id(offer_id, &mut conn).await.unwrap();
let cfd = load_order_by_id(order_id, &mut conn).await.unwrap();
let (actor, inbox) = setup_contract(
{
@ -188,7 +186,7 @@ fn setup_contract(
taker: PartyParams,
sk: SecretKey,
oracle_pk: schnorrsig::PublicKey,
offer: CfdOffer,
order: Order,
) -> (
impl Future<Output = FinalizedCfd>,
mpsc::UnboundedSender<SetupMsg>,
@ -212,7 +210,7 @@ fn setup_contract(
(maker.clone(), maker_punish),
(taker.clone(), taker_punish),
oracle_pk,
offer.term.mul_f32(REFUND_THRESHOLD).as_blocks().ceil() as u32,
order.term.mul_f32(REFUND_THRESHOLD).as_blocks().ceil() as u32,
vec![],
sk,
)

10
daemon/src/taker_inc_message_actor.rs

@ -18,18 +18,18 @@ pub fn new(
async move {
while let Some(message) = messages.next().await {
match message {
Ok(wire::MakerToTaker::CurrentOffer(offer)) => {
Ok(wire::MakerToTaker::CurrentOrder(order)) => {
cfd_actor
.send(taker_cfd_actor::Command::NewOffer(offer))
.send(taker_cfd_actor::Command::NewOrder(order))
.unwrap();
}
Ok(wire::MakerToTaker::ConfirmTakeOffer(offer_id)) => {
Ok(wire::MakerToTaker::ConfirmTakeOrder(order_id)) => {
// TODO: This naming is not well aligned.
cfd_actor
.send(taker_cfd_actor::Command::OfferAccepted(offer_id))
.send(taker_cfd_actor::Command::OrderAccepted(order_id))
.unwrap();
}
Ok(wire::MakerToTaker::InvalidOfferId(_)) => {
Ok(wire::MakerToTaker::InvalidOrderId(_)) => {
todo!()
}
Ok(wire::MakerToTaker::Protocol(msg)) => {

38
daemon/src/to_sse_event.rs

@ -1,5 +1,5 @@
use crate::model;
use crate::model::cfd::CfdOfferId;
use crate::model::cfd::OrderId;
use crate::model::{Leverage, Position, TradingPair, Usd};
use bdk::bitcoin::Amount;
use rocket::response::stream::Event;
@ -8,7 +8,7 @@ use std::time::UNIX_EPOCH;
#[derive(Debug, Clone, Serialize)]
pub struct Cfd {
pub offer_id: CfdOfferId,
pub order_id: OrderId,
pub initial_price: Usd,
pub leverage: Leverage,
@ -30,8 +30,8 @@ pub struct Cfd {
}
#[derive(Debug, Clone, Serialize)]
pub struct CfdOffer {
pub id: CfdOfferId,
pub struct CfdOrder {
pub id: OrderId,
pub trading_pair: TradingPair,
pub position: Position,
@ -63,7 +63,7 @@ impl ToSseEvent for Vec<model::cfd::Cfd> {
let (profit_btc, profit_usd) = cfd.calc_profit(current_price).unwrap();
Cfd {
offer_id: cfd.offer_id,
order_id: cfd.order_id,
initial_price: cfd.initial_price,
leverage: cfd.leverage,
trading_pair: cfd.trading_pair.clone(),
@ -91,26 +91,26 @@ impl ToSseEvent for Vec<model::cfd::Cfd> {
}
}
impl ToSseEvent for Option<model::cfd::CfdOffer> {
impl ToSseEvent for Option<model::cfd::Order> {
fn to_sse_event(&self) -> Event {
let offer = self.clone().map(|offer| CfdOffer {
id: offer.id,
trading_pair: offer.trading_pair,
position: offer.position,
price: offer.price,
min_quantity: offer.min_quantity,
max_quantity: offer.max_quantity,
leverage: offer.leverage,
liquidation_price: offer.liquidation_price,
creation_unix_timestamp: offer
let order = self.clone().map(|order| CfdOrder {
id: order.id,
trading_pair: order.trading_pair,
position: order.position,
price: order.price,
min_quantity: order.min_quantity,
max_quantity: order.max_quantity,
leverage: order.leverage,
liquidation_price: order.liquidation_price,
creation_unix_timestamp: order
.creation_timestamp
.duration_since(UNIX_EPOCH)
.expect("timestamp to be convertiblae to dureation since epoch")
.expect("timestamp to be convertible to duration since epoch")
.as_secs(),
term_in_secs: offer.term.as_secs(),
term_in_secs: order.term.as_secs(),
});
Event::json(&offer).event("offer")
Event::json(&order).event("order")
}
}

16
daemon/src/wire.rs

@ -1,6 +1,6 @@
use crate::model::cfd::CfdOfferId;
use crate::model::cfd::OrderId;
use crate::model::Usd;
use crate::CfdOffer;
use crate::Order;
use bdk::bitcoin::secp256k1::Signature;
use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
use bdk::bitcoin::{Address, Amount, PublicKey, Txid};
@ -24,10 +24,10 @@ impl std::ops::Deref for AdaptorSignature {
#[serde(tag = "type", content = "payload")]
#[allow(clippy::large_enum_variant)]
pub enum TakerToMaker {
TakeOffer { offer_id: CfdOfferId, quantity: Usd },
TakeOrder { order_id: OrderId, quantity: Usd },
// TODO: Currently the taker starts, can already send some stuff for signing over in the first
// message.
StartContractSetup(CfdOfferId),
StartContractSetup(OrderId),
Protocol(SetupMsg),
}
@ -35,10 +35,10 @@ pub enum TakerToMaker {
#[serde(tag = "type", content = "payload")]
#[allow(clippy::large_enum_variant)]
pub enum MakerToTaker {
CurrentOffer(Option<CfdOffer>),
// TODO: Needs RejectOffer as well
ConfirmTakeOffer(CfdOfferId), // TODO: Include payout curve in "accept" message from maker
InvalidOfferId(CfdOfferId),
CurrentOrder(Option<Order>),
// TODO: Needs RejectOrder as well
ConfirmTakeOrder(OrderId), // TODO: Include payout curve in "accept" message from maker
InvalidOrderId(OrderId),
Protocol(SetupMsg),
}

6
docs/asset/mvp_maker_taker_db.puml

@ -6,7 +6,7 @@ hide circle
' avoid problems with angled crows feet
skinparam linetype ortho
entity "offers" as offer {
entity "orders" as order {
*id : number <<PK>> <<generated>>
--
...
@ -15,7 +15,7 @@ entity "offers" as offer {
entity "cfds" as cfd {
*id : number <<PK>> <<generated>>
--
*offer_id : text <<FK>>
*order_id : text <<FK>>
--
quantity_usd: long
creation_timestamp: Date
@ -29,7 +29,7 @@ entity "cfd_states" as cfd_states {
note left: state de-/serialized \nfrom rust state enum \nthis is not backwards\ncompatible, but that's \nOK for the MVP
offer ||--|| cfd
order ||--|| cfd
cfd ||--|{ cfd_states

34
docs/asset/mvp_maker_taker_messaging.puml

@ -2,48 +2,48 @@
actor "Buyer=Taker \n[frontend]" as Buyer
participant "Buyer \n[daemon]" as BuyerApp
participant "Buyer Offer Feed \n[in daemon]" as BuyerOfferFeed
participant "Buyer OrderTile Feed \n[in daemon]" as BuyerOrderFeed
participant "Buyer CFD Feed \n[in daemon]" as BuyerCfdFeed
participant "Seller CFD Feed \n[in daemon]" as SellerCfdFeed
participant "Seller Offer Feed \n[in daemon]" as SellerOfferFeed
participant "Seller OrderTile Feed \n[in daemon]" as SellerOrderFeed
participant "Seller \n[daemon]" as SellerApp
actor "Seller=Maker \n[frontend]" as Seller
participant Oracle as Oracle
note over Seller : currently static offer in the frontend
Seller -> SellerOfferFeed: Subscribe
note over Seller: The seller should see the current active offer \nInitially there is none (until one POSTed)
Seller -> SellerApp: POST sell offer
note over Seller : currently static order in the frontend
Seller -> SellerOrderFeed: Subscribe
note over Seller: The seller should see the current active order \nInitially there is none (until one POSTed)
Seller -> SellerApp: POST sell order
group Oracle stuff?
SellerApp -> Oracle: Attestation for sell offer
SellerApp -> Oracle: Attestation for sell order
Oracle --> SellerApp: Attestation pubkey
end group
SellerApp -> SellerApp: Store current offer
SellerApp -> SellerOfferFeed: Push offer
SellerOfferFeed --> Seller: offer [Untaken]
SellerApp -> SellerApp: Store current order
SellerApp -> SellerOrderFeed: Push order
SellerOrderFeed --> Seller: order [Untaken]
Buyer -> BuyerApp: Start daemon & UI
BuyerApp -> BuyerOfferFeed: Subscribe
BuyerApp -> BuyerOrderFeed: Subscribe
BuyerApp -> BuyerCfdFeed: Subscribe
BuyerApp -> SellerApp: Open TCP (socket) connection
SellerApp -> SellerApp: New connection
SellerApp -> BuyerApp: {TCP} Current offer
SellerApp -> BuyerApp: {TCP} Current order
note over SellerOfferFeed : Assumption: Current offer \nalways available for new subscriptions
BuyerApp -> BuyerOfferFeed: push offer
BuyerOfferFeed --> Buyer: offer
note over SellerOrderFeed : Assumption: Current order \nalways available for new subscriptions
BuyerApp -> BuyerOrderFeed: push order
BuyerOrderFeed --> Buyer: order
Buyer -> Buyer: Click BUY
Buyer -> BuyerApp: POST cfd_take_request
BuyerApp -> BuyerApp: Create cfd [TakeRequested]
note over BuyerApp: Must include offer_id
note over BuyerApp: Must include order_id
BuyerApp -> BuyerCfdFeed: Push cfd
BuyerCfdFeed --> Buyer: cfd [TakeRequested]
BuyerApp -> SellerApp: {TCP} cfd_take_request (offer_id, quantity)
BuyerApp -> SellerApp: {TCP} cfd_take_request (order_id, quantity)
SellerApp -> SellerApp: Create cfd [TakeRequested]
SellerApp -> SellerCfdFeed: cfd [TakeRequested]
SellerCfdFeed --> Seller: cfd [TakeRequested]

42
frontend/src/Maker.tsx

@ -17,28 +17,28 @@ import { useAsync } from "react-async";
import { Route, Routes } from "react-router-dom";
import { useEventSource } from "react-sse-hooks";
import "./App.css";
import CfdOffer from "./components/CfdOffer";
import OrderTile from "./components/OrderTile";
import CfdTile from "./components/CfdTile";
import CurrencyInputField from "./components/CurrencyInputField";
import useLatestEvent from "./components/Hooks";
import NavLink from "./components/NavLink";
import { Cfd, Offer } from "./components/Types";
import { Cfd, Order } from "./components/Types";
/* TODO: Change from localhost:8001 */
const BASE_URL = "http://localhost:8001";
interface CfdSellOfferPayload {
interface CfdSellOrderPayload {
price: number;
min_quantity: number;
max_quantity: number;
}
async function postCfdSellOfferRequest(payload: CfdSellOfferPayload) {
let res = await axios.post(BASE_URL + `/offer/sell`, JSON.stringify(payload));
async function postCfdSellOrderRequest(payload: CfdSellOrderPayload) {
let res = await axios.post(BASE_URL + `/order/sell`, JSON.stringify(payload));
if (!res.status.toString().startsWith("2")) {
console.log("Status: " + res.status + ", " + res.statusText);
throw new Error("failed to publish new offer");
throw new Error("failed to publish new order");
}
}
@ -46,7 +46,7 @@ export default function App() {
let source = useEventSource({ source: BASE_URL + "/maker-feed" });
const cfds = useLatestEvent<Cfd[]>(source, "cfds");
const offer = useLatestEvent<Offer>(source, "offer");
const order = useLatestEvent<Order>(source, "order");
console.log(cfds);
@ -55,15 +55,15 @@ export default function App() {
const toast = useToast();
let [minQuantity, setMinQuantity] = useState<string>("100");
let [maxQuantity, setMaxQuantity] = useState<string>("1000");
let [offerPrice, setOfferPrice] = useState<string>("10000");
let [orderPrice, setOrderPrice] = useState<string>("10000");
const format = (val: any) => `$` + val;
const parse = (val: any) => val.replace(/^\$/, "");
let { run: makeNewCfdSellOffer, isLoading: isCreatingNewCfdOffer } = useAsync({
let { run: makeNewCfdSellOrder, isLoading: isCreatingNewCfdOrder } = useAsync({
deferFn: async ([payload]: any[]) => {
try {
await postCfdSellOfferRequest(payload as CfdSellOfferPayload);
await postCfdSellOrderRequest(payload as CfdSellOrderPayload);
} catch (e) {
const description = typeof e === "string" ? e : JSON.stringify(e);
@ -139,11 +139,11 @@ export default function App() {
/>
</HStack>
<HStack>
<Text>Offer Price:</Text>
<Text>Order Price:</Text>
</HStack>
<CurrencyInputField
onChange={(valueString: string) => setOfferPrice(parse(valueString))}
value={format(offerPrice)}
onChange={(valueString: string) => setOrderPrice(parse(valueString))}
value={format(orderPrice)}
/>
<Text>Leverage:</Text>
<Flex justifyContent={"space-between"}>
@ -154,26 +154,26 @@ export default function App() {
<VStack>
<Center><Text>Maker UI</Text></Center>
<Button
disabled={isCreatingNewCfdOffer}
disabled={isCreatingNewCfdOrder}
variant={"solid"}
colorScheme={"blue"}
onClick={() => {
let payload: CfdSellOfferPayload = {
price: Number.parseFloat(offerPrice),
let payload: CfdSellOrderPayload = {
price: Number.parseFloat(orderPrice),
min_quantity: Number.parseFloat(minQuantity),
max_quantity: Number.parseFloat(maxQuantity),
};
makeNewCfdSellOffer(payload);
makeNewCfdSellOrder(payload);
}}
>
{offer ? "Update Sell Offer" : "Create Sell Offer"}
{order ? "Update Sell Order" : "Create Sell Order"}
</Button>
<Divider />
<Box width={"100%"} overflow={"scroll"}>
<Box>
{offer
&& <CfdOffer
offer={offer}
{order
&& <OrderTile
order={order}
/>}
</Box>
</Box>

24
frontend/src/Taker.tsx

@ -9,13 +9,13 @@ import CfdTile from "./components/CfdTile";
import CurrencyInputField from "./components/CurrencyInputField";
import useLatestEvent from "./components/Hooks";
import NavLink from "./components/NavLink";
import { Cfd, Offer } from "./components/Types";
import { Cfd, Order } from "./components/Types";
/* TODO: Change from localhost:8000 */
const BASE_URL = "http://localhost:8000";
interface CfdTakeRequestPayload {
offer_id: string;
order_id: string;
quantity: number;
}
@ -51,7 +51,7 @@ export default function App() {
let source = useEventSource({ source: BASE_URL + "/feed" });
const cfds = useLatestEvent<Cfd[]>(source, "cfds");
const offer = useLatestEvent<Offer>(source, "offer");
const order = useLatestEvent<Order>(source, "order");
const balance = useLatestEvent<number>(source, "balance");
const toast = useToast();
@ -140,13 +140,13 @@ export default function App() {
<Text>{balance}</Text>
</HStack>
<HStack>
{/*TODO: Do we need this? does it make sense to only display the price from the offer?*/}
{/*TODO: Do we need this? does it make sense to only display the price from the order?*/}
<Text align={"left"}>Current Price (Kraken):</Text>
<Text>tbd</Text>
</HStack>
<HStack>
<Text align={"left"}>Offer Price:</Text>
<Text>{offer?.price}</Text>
<Text align={"left"}>Order Price:</Text>
<Text>{order?.price}</Text>
</HStack>
<HStack>
<Text>Quantity:</Text>
@ -154,14 +154,14 @@ export default function App() {
onChange={(valueString: string) => {
setQuantity(parse(valueString))
if (!offer) {
if (!order) {
return;
}
let quantity = valueString ? Number.parseFloat(valueString) : 0;
let payload: MarginRequestPayload = {
leverage: offer.leverage,
price: offer.price,
leverage: order.leverage,
price: order.price,
quantity
}
calculateMargin(payload);
@ -178,15 +178,15 @@ export default function App() {
<Flex justifyContent={"space-between"}>
<Button disabled={true}>x1</Button>
<Button disabled={true}>x2</Button>
<Button colorScheme="blue" variant="solid">x{offer?.leverage}</Button>
<Button colorScheme="blue" variant="solid">x{order?.leverage}</Button>
</Flex>
{<Button
disabled={isCreatingNewTakeRequest || !offer}
disabled={isCreatingNewTakeRequest || !order}
variant={"solid"}
colorScheme={"blue"}
onClick={() => {
let payload: CfdTakeRequestPayload = {
offer_id: offer!.id,
order_id: order!.id,
quantity: Number.parseFloat(quantity),
};
makeNewTakeRequest(payload);

30
frontend/src/components/CfdOffer.tsx → frontend/src/components/OrderTile.tsx

@ -1,21 +1,21 @@
import { Box, SimpleGrid, Text, VStack } from "@chakra-ui/react";
import React from "react";
import { Offer } from "./Types";
import { Order } from "./Types";
interface CfdOfferProps {
offer: Offer;
interface OrderProps {
order: Order;
}
function CfdOffer(
function OrderTile(
{
offer,
}: CfdOfferProps,
order,
}: OrderProps,
) {
return (
<Box borderRadius={"md"} borderColor={"blue.800"} borderWidth={2} bg={"gray.50"}>
<VStack>
<Box bg="blue.800" w="100%">
<Text padding={2} color={"white"} fontWeight={"bold"}>Current CFD Sell Offer</Text>
<Text padding={2} color={"white"} fontWeight={"bold"}>Current CFD Sell Order</Text>
</Box>
<SimpleGrid padding={5} columns={2} spacing={5}>
<Text>ID</Text>
@ -25,18 +25,18 @@ function CfdOffer(
whiteSpace="nowrap"
_hover={{ overflow: "visible" }}
>
{offer.id}
{order.id}
</Text>
<Text>Trading Pair</Text>
<Text>{offer.trading_pair}</Text>
<Text>{order.trading_pair}</Text>
<Text>Price</Text>
<Text>{offer.price}</Text>
<Text>{order.price}</Text>
<Text>Min Quantity</Text>
<Text>{offer.min_quantity}</Text>
<Text>{order.min_quantity}</Text>
<Text>Max Quantity</Text>
<Text>{offer.max_quantity}</Text>
<Text>{order.max_quantity}</Text>
<Text>Leverage</Text>
<Text>{offer.leverage}</Text>
<Text>{order.leverage}</Text>
<Text>Liquidation Price</Text>
<Text
overflow="hidden"
@ -44,7 +44,7 @@ function CfdOffer(
whiteSpace="nowrap"
_hover={{ overflow: "visible" }}
>
{offer.liquidation_price}
{order.liquidation_price}
</Text>
</SimpleGrid>
</VStack>
@ -52,4 +52,4 @@ function CfdOffer(
);
}
export default CfdOffer;
export default OrderTile;

4
frontend/src/components/Types.tsx

@ -1,4 +1,4 @@
export interface Offer {
export interface Order {
id: string;
trading_pair: string;
position: string;
@ -12,7 +12,7 @@ export interface Offer {
}
export interface Cfd {
offer_id: string;
order_id: string;
initial_price: number;
leverage: number;

Loading…
Cancel
Save