From d5bbee3edd695f2af7c187952090495ff2f5f30e Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Fri, 17 Sep 2021 18:39:26 +1000 Subject: [PATCH] =?UTF-8?q?Wallet=20updates=20in=20the=20UI=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wallet feed that sends balance + current address - display in UI (balance + address) in separate wallet component (shared for taker and maker for now) Includes two seed files that are already funded with some testnet coins. We can share those for testing for now. If more funds are needed I'm happy to top them up. --- Cargo.lock | 1 + daemon/Cargo.toml | 1 + daemon/src/maker.rs | 23 +++++++++++-- daemon/src/maker_cfd_actor.rs | 8 ++++- daemon/src/model.rs | 9 +++++ daemon/src/routes_maker.rs | 18 +++++----- daemon/src/routes_taker.rs | 16 ++++----- daemon/src/taker.rs | 22 ++++++++++-- daemon/src/taker_cfd_actor.rs | 8 ++++- daemon/src/to_sse_event.rs | 30 ++++++++++++---- daemon/src/wallet.rs | 23 ++++++++++--- daemon/util/testnet_seeds/maker_seed | 1 + daemon/util/testnet_seeds/taker_seed | Bin 0 -> 256 bytes frontend/src/Maker.tsx | 14 ++++---- frontend/src/Taker.tsx | 10 +++--- frontend/src/components/CfdTile.tsx | 4 +-- frontend/src/components/Types.tsx | 14 ++++++-- frontend/src/components/Wallet.tsx | 49 +++++++++++++++++++++++++++ 18 files changed, 198 insertions(+), 53 deletions(-) create mode 100644 daemon/util/testnet_seeds/maker_seed create mode 100644 daemon/util/testnet_seeds/taker_seed create mode 100644 frontend/src/components/Wallet.tsx 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 0000000000000000000000000000000000000000..e56a85ef91b9906830b060fdf839d6aa8d1fcf36 GIT binary patch literal 256 zcmV+b0ssE-^Rkn;V`gah+xtBdeUI8|s_4fs#zq)nA0WVVuG~g9EQTg znU8s$bd560w!9tw3bSn!=0A^@_Kgh5U@ADiVMFBrCLfcfL^A(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} +
+ ); +}