Browse Source

Wallet updates in the UI 🎉

- 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.
no-contract-setup-message
Daniel Karzel 3 years ago
parent
commit
d5bbee3edd
No known key found for this signature in database GPG Key ID: 30C3FC2E438ADB6E
  1. 1
      Cargo.lock
  2. 1
      daemon/Cargo.toml
  3. 23
      daemon/src/maker.rs
  4. 8
      daemon/src/maker_cfd_actor.rs
  5. 9
      daemon/src/model.rs
  6. 18
      daemon/src/routes_maker.rs
  7. 16
      daemon/src/routes_taker.rs
  8. 22
      daemon/src/taker.rs
  9. 8
      daemon/src/taker_cfd_actor.rs
  10. 30
      daemon/src/to_sse_event.rs
  11. 23
      daemon/src/wallet.rs
  12. 1
      daemon/util/testnet_seeds/maker_seed
  13. BIN
      daemon/util/testnet_seeds/taker_seed
  14. 14
      frontend/src/Maker.tsx
  15. 10
      frontend/src/Taker.tsx
  16. 4
      frontend/src/components/CfdTile.tsx
  17. 14
      frontend/src/components/Types.tsx
  18. 49
      frontend/src/components/Wallet.tsx

1
Cargo.lock

@ -427,6 +427,7 @@ dependencies = [
"sha2",
"sqlx",
"tempfile",
"thiserror",
"tokio",
"tokio-util",
"uuid",

1
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"] }

23
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<Cfd>>(vec![]);
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 (wallet_feed_sender, wallet_feed_receiver) = watch::channel::<WalletInfo>(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);

8
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<maker_inc_connections_actor::Command>,
cfd_feed_actor_inbox: watch::Sender<Vec<Cfd>>,
order_feed_sender: watch::Sender<Option<Order>>,
wallet_feed_sender: watch::Sender<WalletInfo>,
) -> (
impl Future<Output = ()>,
mpsc::UnboundedSender<maker_cfd_actor::Command>,
@ -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,

9
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,
}

18
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<watch::Receiver<Vec<Cfd>>>,
rx_order: &State<watch::Receiver<Option<Order>>>,
rx_balance: &State<watch::Receiver<Amount>>,
rx_wallet: &State<watch::Receiver<WalletInfo>>,
) -> 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();

16
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<watch::Receiver<Vec<Cfd>>>,
rx_order: &State<watch::Receiver<Option<Order>>>,
rx_balance: &State<watch::Receiver<Amount>>,
rx_wallet: &State<watch::Receiver<WalletInfo>>,
) -> 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();

22
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<Cfd>>(vec![]);
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 (wallet_feed_sender, wallet_feed_receiver) = watch::channel::<WalletInfo>(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);

8
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<Order>),
OrderAccepted(OrderId),
@ -30,6 +31,7 @@ pub fn new(
cfd_feed_actor_inbox: watch::Sender<Vec<Cfd>>,
order_feed_actor_inbox: watch::Sender<Option<Order>>,
out_msg_maker_inbox: mpsc::UnboundedSender<wire::TakerToMaker>,
wallet_feed_sender: watch::Sender<WalletInfo>,
) -> (impl Future<Output = ()>, mpsc::UnboundedSender<Command>) {
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();

30
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<model::cfd::Cfd> {
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<model::cfd::Order> {
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<model::cfd::Order> {
}
}
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")
}
}

23
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<PartyParams> {
self.wallet.build_party_params(amount, identity_pk)
}
pub fn sync(&self) -> Result<WalletInfo> {
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)
}
}

1
daemon/util/testnet_seeds/maker_seed

@ -0,0 +1 @@
%÷_×U›“�]ÿv@cz¸ìáSÆRJ£ƒ®ºÕo–¤o{ø³ôâT¸&hìö‡ÜƬu_ÏÖ¬÷œh*( ÷èBBm^ö[6ÅNm»IÇ+댵[cm‹ £GºjëuC¸òó>Áx…A´¼L¯Ccx¨ÎZEåÞX#ßz…´}øÒÚ€œlA*Áº}"Ú¬ˆIü¥pµ`�ólr=BþÙ³¯ôT7_)DÓpÃ,k�TÑRór–‚T“ÖA¡$ìÕ¯šÎ¨ žÜ^¯¯ÙVSA�+Ñ“‡Â”Íò^F¶\=¿ÑU^H{÷€KŠ/ ,‰¥Ã¨(ŸñEß#8KO†Ðɨ–è×¹

BIN
daemon/util/testnet_seeds/taker_seed

Binary file not shown.

14
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<number>(source, "balance");
const walletInfo = useLatestEvent<WalletInfo>(source, "wallet");
const toast = useToast();
let [minQuantity, setMinQuantity] = useState<string>("100");
@ -111,12 +112,9 @@ export default function App() {
</Box>
</VStack>
</Flex>
<Flex width={"50%"} marginLeft={5}>
<VStack spacing={5} shadow={"md"} padding={5} align={"stretch"}>
<HStack>
<Text align={"left"}>Your balance:</Text>
<Text>{balance}</Text>
</HStack>
<Flex width={"50%"} marginLeft={5} direction={"column"}>
<Wallet walletInfo={walletInfo} />
<VStack spacing={5} shadow={"md"} padding={5} align={"stretch"} height={"100%"}>
<HStack>
<Text align={"left"}>Current Price:</Text>
<Text>{49000}</Text>

10
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<Cfd[]>(source, "cfds");
const order = useLatestEvent<Order>(source, "order");
const balance = useLatestEvent<number>(source, "balance");
const walletInfo = useLatestEvent<WalletInfo>(source, "wallet");
const toast = useToast();
let [quantity, setQuantity] = useState("0");
@ -132,10 +133,7 @@ export default function App() {
</Flex>
<Flex width={"50%"} marginLeft={5}>
<VStack spacing={5} shadow={"md"} padding={5} align={"stretch"}>
<HStack>
<Text align={"left"}>Your balance:</Text>
<Text>{balance}</Text>
</HStack>
<Wallet walletInfo={walletInfo} />
<HStack>
{/*TODO: Do we need this? does it make sense to only display the price from the order?*/}
<Text align={"left"}>Current Price (Kraken):</Text>

4
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(
<Text>Open since</Text>
{/* TODO: Format date in a more compact way */}
<Text>
{(new Date(cfd.state_transition_unix_timestamp * 1000).toString())}
{unixTimestampToDate(cfd.state_transition_timestamp).toString()}
</Text>
<Text>Status</Text>
<Text>{cfd.state}</Text>

14
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);
}

49
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 = <Skeleton height="20px" />;
let address = <Skeleton height="20px" />;
let timestamp = <Skeleton height="20px" />;
if (walletInfo) {
balance = <Text>{walletInfo.balance} BTC</Text>;
address = (
<HStack>
<Text>{walletInfo.address}</Text>
<IconButton
aria-label="Copy to clipboard"
icon={hasCopied ? <CheckIcon /> : <CopyIcon />}
onClick={onCopy}
/>
</HStack>
);
timestamp = <Text>{unixTimestampToDate(walletInfo.last_updated_at).toString()}</Text>;
}
return (
<Box shadow={"md"} marginBottom={5} padding={5}>
<Center><Text fontWeight={"bold"}>Your wallet</Text></Center>
<HStack>
<Text align={"left"}>Balance:</Text>
{balance}
</HStack>
<Divider marginTop={2} marginBottom={2} />
{address}
<Divider marginTop={2} marginBottom={2} />
{timestamp}
</Box>
);
}
Loading…
Cancel
Save