diff --git a/Cargo.lock b/Cargo.lock index 86edc37..86d9de4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,7 @@ dependencies = [ "sha2", "sqlx", "tempfile", + "thiserror", "tokio", "tokio-util", "uuid", diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index 09e26e6..bb1b230 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -20,6 +20,7 @@ serde_json = "1" serde_with = { version = "1", features = ["macros"] } sha2 = "0.9" sqlx = { version = "0.5", features = ["offline"] } +thiserror = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "net"] } tokio-util = { version = "0.6", features = ["codec"] } uuid = { version = "0.8", features = ["serde", "v4"] } diff --git a/daemon/src/maker.rs b/daemon/src/maker.rs index ae6564c..1293521 100644 --- a/daemon/src/maker.rs +++ b/daemon/src/maker.rs @@ -1,13 +1,16 @@ +use crate::maker_cfd_actor::Command; use crate::seed::Seed; use crate::wallet::Wallet; use anyhow::Result; use bdk::bitcoin::secp256k1::{schnorrsig, SECP256K1}; -use bdk::bitcoin::{Amount, Network}; +use bdk::bitcoin::Network; use clap::Clap; use model::cfd::{Cfd, Order}; +use model::WalletInfo; use rocket::fairing::AdHoc; use rocket_db_pools::Database; use std::path::PathBuf; +use std::time::Duration; use tokio::sync::{mpsc, watch}; mod db; @@ -72,12 +75,13 @@ async fn main() -> Result<()> { ext_priv_key, ) .await?; + let wallet_info = wallet.sync().unwrap(); 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![]); let (order_feed_sender, order_feed_receiver) = watch::channel::>(None); - let (_balance_feed_sender, balance_feed_receiver) = watch::channel::(Amount::ZERO); + let (wallet_feed_sender, wallet_feed_receiver) = watch::channel::(wallet_info); let figment = rocket::Config::figment() .merge(("databases.maker.url", data_dir.join("maker.sqlite"))) @@ -91,7 +95,7 @@ async fn main() -> Result<()> { rocket::custom(figment) .manage(cfd_feed_receiver) .manage(order_feed_receiver) - .manage(balance_feed_receiver) + .manage(wallet_feed_receiver) .attach(Db::init()) .attach(AdHoc::try_on_ignite( "SQL migrations", @@ -123,6 +127,7 @@ async fn main() -> Result<()> { connections_actor_inbox_sender, cfd_feed_sender, order_feed_sender, + wallet_feed_sender, ); let connections_actor = maker_inc_connections_actor::new( listener, @@ -130,6 +135,18 @@ async fn main() -> Result<()> { connections_actor_inbox_recv, ); + // consecutive wallet syncs handled by task that triggers sync + let wallet_sync_interval = Duration::from_secs(10); + tokio::spawn({ + let cfd_actor_inbox = cfd_maker_actor_inbox.clone(); + async move { + loop { + cfd_actor_inbox.send(Command::SyncWallet).unwrap(); + tokio::time::sleep(wallet_sync_interval).await; + } + } + }); + tokio::spawn(cfd_maker_actor); tokio::spawn(connections_actor); diff --git a/daemon/src/maker_cfd_actor.rs b/daemon/src/maker_cfd_actor.rs index b931335..535673e 100644 --- a/daemon/src/maker_cfd_actor.rs +++ b/daemon/src/maker_cfd_actor.rs @@ -1,6 +1,6 @@ use crate::db::{insert_cfd, insert_order, load_all_cfds, load_cfd_by_order_id, load_order_by_id}; use crate::model::cfd::{Cfd, CfdState, CfdStateCommon, FinalizedCfd, Order, OrderId}; -use crate::model::{TakerId, Usd}; +use crate::model::{TakerId, Usd, WalletInfo}; use crate::wallet::Wallet; use crate::wire::SetupMsg; use crate::{maker_cfd_actor, maker_inc_connections_actor, setup_contract_actor}; @@ -12,6 +12,7 @@ use tokio::sync::{mpsc, watch}; #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum Command { + SyncWallet, TakeOrder { taker_id: TakerId, order_id: OrderId, @@ -36,6 +37,7 @@ pub fn new( takers: mpsc::UnboundedSender, cfd_feed_actor_inbox: watch::Sender>, order_feed_sender: watch::Sender>, + wallet_feed_sender: watch::Sender, ) -> ( impl Future, mpsc::UnboundedSender, @@ -57,6 +59,10 @@ pub fn new( while let Some(message) = receiver.recv().await { match message { + maker_cfd_actor::Command::SyncWallet => { + let wallet_info = wallet.sync().unwrap(); + wallet_feed_sender.send(wallet_info).unwrap(); + } maker_cfd_actor::Command::TakeOrder { taker_id, order_id, diff --git a/daemon/src/model.rs b/daemon/src/model.rs index 8968b19..6e0023f 100644 --- a/daemon/src/model.rs +++ b/daemon/src/model.rs @@ -5,6 +5,8 @@ use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use bdk::bitcoin::{Address, Amount}; +use std::time::SystemTime; use uuid::Uuid; pub mod cfd; @@ -83,3 +85,10 @@ impl Display for TakerId { self.0.fmt(f) } } + +#[derive(Debug, Clone)] +pub struct WalletInfo { + pub balance: Amount, + pub address: Address, + pub last_updated_at: SystemTime, +} diff --git a/daemon/src/routes_maker.rs b/daemon/src/routes_maker.rs index a5f7bab..97668b3 100644 --- a/daemon/src/routes_maker.rs +++ b/daemon/src/routes_maker.rs @@ -1,9 +1,9 @@ use crate::maker_cfd_actor; use crate::model::cfd::{Cfd, Order, Origin}; -use crate::model::Usd; +use crate::model::{Usd, WalletInfo}; use crate::to_sse_event::ToSseEvent; use anyhow::Result; -use bdk::bitcoin::Amount; + use rocket::response::status; use rocket::response::stream::EventStream; use rocket::serde::json::Json; @@ -16,15 +16,15 @@ use tokio::sync::{mpsc, watch}; pub async fn maker_feed( rx_cfds: &State>>, rx_order: &State>>, - rx_balance: &State>, + rx_wallet: &State>, ) -> EventStream![] { let mut rx_cfds = rx_cfds.inner().clone(); let mut rx_order = rx_order.inner().clone(); - let mut rx_balance = rx_balance.inner().clone(); + let mut rx_wallet = rx_wallet.inner().clone(); EventStream! { - let balance = rx_balance.borrow().clone(); - yield balance.to_sse_event(); + let wallet_info = rx_wallet.borrow().clone(); + yield wallet_info.to_sse_event(); let order = rx_order.borrow().clone(); yield order.to_sse_event(); @@ -34,9 +34,9 @@ pub async fn maker_feed( loop{ select! { - Ok(()) = rx_balance.changed() => { - let balance = rx_balance.borrow().clone(); - yield balance.to_sse_event(); + Ok(()) = rx_wallet.changed() => { + let wallet_info = rx_wallet.borrow().clone(); + yield wallet_info.to_sse_event(); }, Ok(()) = rx_order.changed() => { let order = rx_order.borrow().clone(); diff --git a/daemon/src/routes_taker.rs b/daemon/src/routes_taker.rs index 30d69ff..70530c1 100644 --- a/daemon/src/routes_taker.rs +++ b/daemon/src/routes_taker.rs @@ -1,5 +1,5 @@ use crate::model::cfd::{calculate_buy_margin, Cfd, Order, OrderId}; -use crate::model::{Leverage, Usd}; +use crate::model::{Leverage, Usd, WalletInfo}; use crate::taker_cfd_actor; use crate::to_sse_event::ToSseEvent; use bdk::bitcoin::Amount; @@ -15,15 +15,15 @@ use tokio::sync::{mpsc, watch}; pub async fn feed( rx_cfds: &State>>, rx_order: &State>>, - rx_balance: &State>, + rx_wallet: &State>, ) -> EventStream![] { let mut rx_cfds = rx_cfds.inner().clone(); let mut rx_order = rx_order.inner().clone(); - let mut rx_balance = rx_balance.inner().clone(); + let mut rx_wallet = rx_wallet.inner().clone(); EventStream! { - let balance = rx_balance.borrow().clone(); - yield balance.to_sse_event(); + let wallet_info = rx_wallet.borrow().clone(); + yield wallet_info.to_sse_event(); let order = rx_order.borrow().clone(); yield order.to_sse_event(); @@ -33,9 +33,9 @@ pub async fn feed( loop{ select! { - Ok(()) = rx_balance.changed() => { - let balance = rx_balance.borrow().clone(); - yield balance.to_sse_event(); + Ok(()) = rx_wallet.changed() => { + let wallet_info = rx_wallet.borrow().clone(); + yield wallet_info.to_sse_event(); }, Ok(()) = rx_order.changed() => { let order = rx_order.borrow().clone(); diff --git a/daemon/src/taker.rs b/daemon/src/taker.rs index f8499d2..fefc54f 100644 --- a/daemon/src/taker.rs +++ b/daemon/src/taker.rs @@ -1,7 +1,9 @@ +use crate::model::WalletInfo; +use crate::taker_cfd_actor::Command; use crate::wallet::Wallet; use anyhow::Result; use bdk::bitcoin::secp256k1::{schnorrsig, SECP256K1}; -use bdk::bitcoin::{Amount, Network}; +use bdk::bitcoin::Network; use clap::Clap; use model::cfd::{Cfd, Order}; use rocket::fairing::AdHoc; @@ -77,12 +79,13 @@ async fn main() -> Result<()> { ext_priv_key, ) .await?; + let wallet_info = wallet.sync().unwrap(); 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![]); let (order_feed_sender, order_feed_receiver) = watch::channel::>(None); - let (_balance_feed_sender, balance_feed_receiver) = watch::channel::(Amount::ZERO); + let (wallet_feed_sender, wallet_feed_receiver) = watch::channel::(wallet_info); let (read, write) = loop { let socket = tokio::net::TcpSocket::new_v4()?; @@ -104,7 +107,7 @@ async fn main() -> Result<()> { rocket::custom(figment) .manage(cfd_feed_receiver) .manage(order_feed_receiver) - .manage(balance_feed_receiver) + .manage(wallet_feed_receiver) .attach(Db::init()) .attach(AdHoc::try_on_ignite( "SQL migrations", @@ -135,10 +138,23 @@ async fn main() -> Result<()> { cfd_feed_sender, order_feed_sender, out_maker_actor_inbox, + wallet_feed_sender, ); let inc_maker_messages_actor = taker_inc_message_actor::new(read, cfd_actor_inbox.clone()); + // consecutive wallet syncs handled by task that triggers sync + let wallet_sync_interval = Duration::from_secs(10); + tokio::spawn({ + let cfd_actor_inbox = cfd_actor_inbox.clone(); + async move { + loop { + cfd_actor_inbox.send(Command::SyncWallet).unwrap(); + tokio::time::sleep(wallet_sync_interval).await; + } + } + }); + tokio::spawn(cfd_actor); tokio::spawn(inc_maker_messages_actor); tokio::spawn(out_maker_messages_actor); diff --git a/daemon/src/taker_cfd_actor.rs b/daemon/src/taker_cfd_actor.rs index 0e3cd9d..563ed56 100644 --- a/daemon/src/taker_cfd_actor.rs +++ b/daemon/src/taker_cfd_actor.rs @@ -3,7 +3,7 @@ use crate::db::{ load_cfd_by_order_id, load_order_by_id, }; use crate::model::cfd::{Cfd, CfdState, CfdStateCommon, FinalizedCfd, Order, OrderId}; -use crate::model::Usd; +use crate::model::{Usd, WalletInfo}; use crate::wallet::Wallet; use crate::wire::SetupMsg; use crate::{setup_contract_actor, wire}; @@ -16,6 +16,7 @@ use tokio::sync::{mpsc, watch}; #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum Command { + SyncWallet, TakeOrder { order_id: OrderId, quantity: Usd }, NewOrder(Option), OrderAccepted(OrderId), @@ -30,6 +31,7 @@ pub fn new( cfd_feed_actor_inbox: watch::Sender>, order_feed_actor_inbox: watch::Sender>, out_msg_maker_inbox: mpsc::UnboundedSender, + wallet_feed_sender: watch::Sender, ) -> (impl Future, mpsc::UnboundedSender) { let (sender, mut receiver) = mpsc::unbounded_channel(); let mut current_contract_setup = None; @@ -46,6 +48,10 @@ pub fn new( while let Some(message) = receiver.recv().await { match message { + Command::SyncWallet => { + let wallet_info = wallet.sync().unwrap(); + wallet_feed_sender.send(wallet_info).unwrap(); + } Command::TakeOrder { order_id, quantity } => { let mut conn = db.acquire().await.unwrap(); diff --git a/daemon/src/to_sse_event.rs b/daemon/src/to_sse_event.rs index 61e3fe8..faee19d 100644 --- a/daemon/src/to_sse_event.rs +++ b/daemon/src/to_sse_event.rs @@ -26,7 +26,7 @@ pub struct Cfd { pub profit_usd: Usd, pub state: String, - pub state_transition_unix_timestamp: u64, + pub state_transition_timestamp: u64, } #[derive(Debug, Clone, Serialize)] @@ -44,7 +44,7 @@ pub struct CfdOrder { pub leverage: Leverage, pub liquidation_price: Usd, - pub creation_unix_timestamp: u64, + pub creation_timestamp: u64, pub term_in_secs: u64, } @@ -73,7 +73,7 @@ impl ToSseEvent for Vec { profit_btc, profit_usd, state: cfd.state.to_string(), - state_transition_unix_timestamp: cfd + state_transition_timestamp: cfd .state .get_transition_timestamp() .duration_since(UNIX_EPOCH) @@ -102,7 +102,7 @@ impl ToSseEvent for Option { max_quantity: order.max_quantity, leverage: order.leverage, liquidation_price: order.liquidation_price, - creation_unix_timestamp: order + creation_timestamp: order .creation_timestamp .duration_since(UNIX_EPOCH) .expect("timestamp to be convertible to duration since epoch") @@ -114,8 +114,26 @@ impl ToSseEvent for Option { } } -impl ToSseEvent for Amount { +#[derive(Debug, Clone, Serialize)] +pub struct WalletInfo { + #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] + balance: Amount, + address: String, + last_updated_at: u64, +} + +impl ToSseEvent for model::WalletInfo { fn to_sse_event(&self) -> Event { - Event::json(&self.as_btc()).event("balance") + let wallet_info = WalletInfo { + balance: self.balance, + address: self.address.to_string(), + last_updated_at: self + .last_updated_at + .duration_since(UNIX_EPOCH) + .expect("timestamp to be convertible to duration since epoch") + .as_secs(), + }; + + Event::json(&wallet_info).event("wallet") } } diff --git a/daemon/src/wallet.rs b/daemon/src/wallet.rs index 1ec26ec..abb9f05 100644 --- a/daemon/src/wallet.rs +++ b/daemon/src/wallet.rs @@ -1,10 +1,13 @@ +use crate::model::WalletInfo; use anyhow::{Context, Result}; use bdk::bitcoin::util::bip32::ExtendedPrivKey; use bdk::bitcoin::{Amount, PublicKey}; use bdk::blockchain::{ElectrumBlockchain, NoopProgress}; +use bdk::wallet::AddressIndex; use bdk::KeychainKind; use cfd_protocol::{PartyParams, WalletExt}; use std::path::Path; +use std::time::SystemTime; const SLED_TREE_NAME: &str = "wallet"; @@ -32,10 +35,6 @@ impl Wallet { ElectrumBlockchain::from(client), )?; - wallet - .sync(NoopProgress, None) - .context("Failed to sync the wallet")?; // TODO: Use LogProgress once we have logging. - Ok(Self { wallet }) } @@ -46,4 +45,20 @@ impl Wallet { ) -> Result { self.wallet.build_party_params(amount, identity_pk) } + + pub fn sync(&self) -> Result { + self.wallet.sync(NoopProgress, None)?; + + let balance = self.wallet.get_balance()?; + + let address = self.wallet.get_address(AddressIndex::LastUnused)?.address; + + let wallet_info = WalletInfo { + balance: Amount::from_sat(balance), + address, + last_updated_at: SystemTime::now(), + }; + + Ok(wallet_info) + } } diff --git a/daemon/util/testnet_seeds/maker_seed b/daemon/util/testnet_seeds/maker_seed new file mode 100644 index 0000000..abb5e82 --- /dev/null +++ b/daemon/util/testnet_seeds/maker_seed @@ -0,0 +1 @@ +%_U]v@czSRJoo{T&hƬu_֬h*( BBm^[6NmI+댵[cmGjuC>xALCcxZEX#z}ڀlA*}"ڬIp`lr=BٳT7_)Dp,kTRrTA$Ψ^VSA+ѓ”^F\=U^H{K/ ,è(E#8KOɨ׹ \ No newline at end of file diff --git a/daemon/util/testnet_seeds/taker_seed b/daemon/util/testnet_seeds/taker_seed new file mode 100644 index 0000000..e56a85e Binary files /dev/null and b/daemon/util/testnet_seeds/taker_seed differ diff --git a/frontend/src/Maker.tsx b/frontend/src/Maker.tsx index 18264c5..d9694c4 100644 --- a/frontend/src/Maker.tsx +++ b/frontend/src/Maker.tsx @@ -22,7 +22,8 @@ import CurrencyInputField from "./components/CurrencyInputField"; import useLatestEvent from "./components/Hooks"; import NavLink from "./components/NavLink"; import OrderTile from "./components/OrderTile"; -import { Cfd, Order } from "./components/Types"; +import { Cfd, Order, WalletInfo } from "./components/Types"; +import Wallet from "./components/Wallet"; interface CfdSellOrderPayload { price: number; @@ -47,7 +48,7 @@ export default function App() { console.log(cfds); - const balance = useLatestEvent(source, "balance"); + const walletInfo = useLatestEvent(source, "wallet"); const toast = useToast(); let [minQuantity, setMinQuantity] = useState("100"); @@ -111,12 +112,9 @@ export default function App() { - - - - Your balance: - {balance} - + + + Current Price: {49000} diff --git a/frontend/src/Taker.tsx b/frontend/src/Taker.tsx index acbea83..b01beab 100644 --- a/frontend/src/Taker.tsx +++ b/frontend/src/Taker.tsx @@ -9,7 +9,8 @@ import CfdTile from "./components/CfdTile"; import CurrencyInputField from "./components/CurrencyInputField"; import useLatestEvent from "./components/Hooks"; import NavLink from "./components/NavLink"; -import { Cfd, Order } from "./components/Types"; +import { Cfd, Order, WalletInfo } from "./components/Types"; +import Wallet from "./components/Wallet"; interface CfdTakeRequestPayload { order_id: string; @@ -49,7 +50,7 @@ export default function App() { const cfds = useLatestEvent(source, "cfds"); const order = useLatestEvent(source, "order"); - const balance = useLatestEvent(source, "balance"); + const walletInfo = useLatestEvent(source, "wallet"); const toast = useToast(); let [quantity, setQuantity] = useState("0"); @@ -132,10 +133,7 @@ export default function App() { - - Your balance: - {balance} - + {/*TODO: Do we need this? does it make sense to only display the price from the order?*/} Current Price (Kraken): diff --git a/frontend/src/components/CfdTile.tsx b/frontend/src/components/CfdTile.tsx index 747897d..1aaf249 100644 --- a/frontend/src/components/CfdTile.tsx +++ b/frontend/src/components/CfdTile.tsx @@ -1,6 +1,6 @@ import { Box, Button, SimpleGrid, Text, VStack } from "@chakra-ui/react"; import React from "react"; -import { Cfd } from "./Types"; +import { Cfd, unixTimestampToDate } from "./Types"; interface CfdTileProps { index: number; @@ -46,7 +46,7 @@ export default function CfdTile( Open since {/* TODO: Format date in a more compact way */} - {(new Date(cfd.state_transition_unix_timestamp * 1000).toString())} + {unixTimestampToDate(cfd.state_transition_timestamp).toString()} Status {cfd.state} diff --git a/frontend/src/components/Types.tsx b/frontend/src/components/Types.tsx index 708ece9..fb5cea9 100644 --- a/frontend/src/components/Types.tsx +++ b/frontend/src/components/Types.tsx @@ -7,7 +7,7 @@ export interface Order { max_quantity: number; leverage: number; liquidation_price: number; - creation_unix_timestamp: number; + creation_timestamp: number; term_in_secs: number; } @@ -28,5 +28,15 @@ export interface Cfd { profit_usd: number; state: string; - state_transition_unix_timestamp: number; + state_transition_timestamp: number; +} + +export interface WalletInfo { + balance: number; + address: string; + last_updated_at: number; +} + +export function unixTimestampToDate(unixTimestamp: number): Date { + return new Date(unixTimestamp * 1000); } diff --git a/frontend/src/components/Wallet.tsx b/frontend/src/components/Wallet.tsx new file mode 100644 index 0000000..d3e0694 --- /dev/null +++ b/frontend/src/components/Wallet.tsx @@ -0,0 +1,49 @@ +import { CheckIcon, CopyIcon } from "@chakra-ui/icons"; +import { Box, Center, Divider, HStack, IconButton, Skeleton, Text, useClipboard } from "@chakra-ui/react"; +import React from "react"; +import { unixTimestampToDate, WalletInfo } from "./Types"; + +interface WalletProps { + walletInfo: WalletInfo | null; +} + +export default function Wallet( + { + walletInfo, + }: WalletProps, +) { + const { hasCopied, onCopy } = useClipboard(walletInfo ? walletInfo.address : ""); + + let balance = ; + let address = ; + let timestamp = ; + + if (walletInfo) { + balance = {walletInfo.balance} BTC; + address = ( + + {walletInfo.address} + : } + onClick={onCopy} + /> + + ); + timestamp = {unixTimestampToDate(walletInfo.last_updated_at).toString()}; + } + + return ( + +
Your wallet
+ + Balance: + {balance} + + + {address} + + {timestamp} +
+ ); +}