From c951fe6f576c5d90823fdc9a2ec6060c6169d888 Mon Sep 17 00:00:00 2001 From: bonomat Date: Mon, 22 Nov 2021 17:39:26 +1100 Subject: [PATCH] Add new endpoint to allow withdrawing through UI --- daemon/src/maker.rs | 6 +- daemon/src/routes_maker.rs | 48 ++++++++- daemon/src/routes_taker.rs | 47 ++++++++- daemon/src/taker.rs | 4 +- taker-frontend/src/App.tsx | 11 ++ taker-frontend/src/components/Types.tsx | 6 ++ taker-frontend/src/components/Wallet.tsx | 126 ++++++++++++++++++++++- 7 files changed, 239 insertions(+), 9 deletions(-) diff --git a/daemon/src/maker.rs b/daemon/src/maker.rs index 9a6465d..8127ecf 100644 --- a/daemon/src/maker.rs +++ b/daemon/src/maker.rs @@ -304,7 +304,7 @@ async fn main() -> Result<()> { tasks.add(incoming_connection_addr.attach_stream(listener_stream)); - tasks.add(wallet_sync::new(wallet, wallet_feed_sender)); + tasks.add(wallet_sync::new(wallet.clone(), wallet_feed_sender)); let cfd_action_channel = MessageChannel::::clone_channel(&cfd_actor_addr); let new_order_channel = MessageChannel::::clone_channel(&cfd_actor_addr); @@ -319,13 +319,15 @@ async fn main() -> Result<()> { .manage(auth_password) .manage(quote_receiver) .manage(bitcoin_network) + .manage(wallet) .mount( "/api", rocket::routes![ routes_maker::maker_feed, routes_maker::post_sell_order, routes_maker::post_cfd_action, - routes_maker::get_health_check + routes_maker::get_health_check, + routes_maker::post_withdraw_request ], ) .register("/api", rocket::catchers![routes_maker::unauthorized]) diff --git a/daemon/src/routes_maker.rs b/daemon/src/routes_maker.rs index eae0485..1126f5d 100644 --- a/daemon/src/routes_maker.rs +++ b/daemon/src/routes_maker.rs @@ -5,7 +5,7 @@ use daemon::model::cfd::{Cfd, Order, OrderId, Role, UpdateCfdProposals}; use daemon::model::{Price, Usd, WalletInfo}; use daemon::routes::EmbeddedFileExt; use daemon::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; -use daemon::{bitmex_price_feed, maker_cfd}; +use daemon::{bitmex_price_feed, maker_cfd, wallet}; use http_api_problem::{HttpApiProblem, StatusCode}; use rocket::http::{ContentType, Header, Status}; use rocket::response::stream::EventStream; @@ -208,6 +208,52 @@ pub fn index<'r>(_paths: PathBuf, _auth: Authenticated) -> impl Responder<'r, 's Ok::<(ContentType, Cow<[u8]>), Status>((ContentType::HTML, asset.data)) } +#[derive(Debug, Clone, Deserialize)] +pub struct WithdrawRequest { + address: bdk::bitcoin::Address, + #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] + amount: bdk::bitcoin::Amount, + fee: f32, +} + +#[rocket::post("/withdraw", data = "")] +pub async fn post_withdraw_request( + withdraw_request: Json, + wallet: &State>, + network: &State, + _auth: Authenticated, +) -> Result { + let amount = + (withdraw_request.amount != bdk::bitcoin::Amount::ZERO).then(|| withdraw_request.amount); + + let txid = wallet + .send(wallet::Withdraw { + amount, + address: withdraw_request.address.clone(), + fee: Some(bdk::FeeRate::from_sat_per_vb(withdraw_request.fee)), + }) + .await + .map_err(|e| { + HttpApiProblem::new(StatusCode::INTERNAL_SERVER_ERROR) + .title("Could not proceed with withdraw request") + .detail(e.to_string()) + })? + .map_err(|e| { + HttpApiProblem::new(StatusCode::BAD_REQUEST) + .title("Could not withdraw funds") + .detail(e.to_string()) + })?; + + let url = match network.inner() { + Network::Bitcoin => format!("https://mempool.space/tx/{}", txid), + Network::Testnet => format!("https://mempool.space/testnet/tx/{}", txid), + Network::Signet => format!("https://mempool.space/signet/tx/{}", txid), + Network::Regtest => txid.to_string(), + }; + + Ok(url) +} + #[cfg(test)] mod tests { use super::*; diff --git a/daemon/src/routes_taker.rs b/daemon/src/routes_taker.rs index 5130321..e218be3 100644 --- a/daemon/src/routes_taker.rs +++ b/daemon/src/routes_taker.rs @@ -3,7 +3,7 @@ use daemon::model::cfd::{calculate_long_margin, Cfd, Order, OrderId, Role, Updat use daemon::model::{Leverage, Price, Usd, WalletInfo}; use daemon::routes::EmbeddedFileExt; use daemon::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; -use daemon::{bitmex_price_feed, taker_cfd}; +use daemon::{bitmex_price_feed, taker_cfd, wallet}; use http_api_problem::{HttpApiProblem, StatusCode}; use rocket::http::{ContentType, Status}; use rocket::response::stream::EventStream; @@ -211,3 +211,48 @@ pub fn index<'r>(_paths: PathBuf) -> impl Responder<'r, 'static> { let asset = Asset::get("index.html").ok_or(Status::NotFound)?; Ok::<(ContentType, Cow<[u8]>), Status>((ContentType::HTML, asset.data)) } + +#[derive(Debug, Clone, Deserialize)] +pub struct WithdrawRequest { + address: bdk::bitcoin::Address, + #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] + amount: Amount, + fee: f32, +} + +#[rocket::post("/withdraw", data = "")] +pub async fn post_withdraw_request( + withdraw_request: Json, + wallet: &State>, + network: &State, +) -> Result { + let amount = + (withdraw_request.amount != bdk::bitcoin::Amount::ZERO).then(|| withdraw_request.amount); + + let txid = wallet + .send(wallet::Withdraw { + amount, + address: withdraw_request.address.clone(), + fee: Some(bdk::FeeRate::from_sat_per_vb(withdraw_request.fee)), + }) + .await + .map_err(|e| { + HttpApiProblem::new(StatusCode::INTERNAL_SERVER_ERROR) + .title("Could not proceed with withdraw request") + .detail(e.to_string()) + })? + .map_err(|e| { + HttpApiProblem::new(StatusCode::BAD_REQUEST) + .title("Could not withdraw funds") + .detail(e.to_string()) + })?; + + let url = match network.inner() { + Network::Bitcoin => format!("https://mempool.space/tx/{}", txid), + Network::Testnet => format!("https://mempool.space/testnet/tx/{}", txid), + Network::Signet => format!("https://mempool.space/signet/tx/{}", txid), + Network::Regtest => txid.to_string(), + }; + + Ok(url) +} diff --git a/daemon/src/taker.rs b/daemon/src/taker.rs index e4b2d99..bb5c34d 100644 --- a/daemon/src/taker.rs +++ b/daemon/src/taker.rs @@ -276,7 +276,7 @@ async fn main() -> Result<()> { connect(connection_actor_addr, opts.maker_id, opts.maker).await?; - tasks.add(wallet_sync::new(wallet, wallet_feed_sender)); + tasks.add(wallet_sync::new(wallet.clone(), wallet_feed_sender)); let take_offer_channel = MessageChannel::::clone_channel(&cfd_actor_addr); let cfd_action_channel = MessageChannel::::clone_channel(&cfd_actor_addr); @@ -289,6 +289,7 @@ async fn main() -> Result<()> { .manage(wallet_feed_receiver) .manage(quote_receiver) .manage(bitcoin_network) + .manage(wallet) .mount( "/api", rocket::routes![ @@ -297,6 +298,7 @@ async fn main() -> Result<()> { routes_taker::get_health_check, routes_taker::margin_calc, routes_taker::post_cfd_action, + routes_taker::post_withdraw_request, ], ) .mount( diff --git a/taker-frontend/src/App.tsx b/taker-frontend/src/App.tsx index 6b856b3..f295a66 100644 --- a/taker-frontend/src/App.tsx +++ b/taker-frontend/src/App.tsx @@ -20,6 +20,7 @@ import { useBackendMonitor } from "./components/BackendMonitor"; import createErrorToast from "./components/ErrorToast"; import Footer from "./components/Footer"; import History from "./components/History"; +import { HttpError } from "./components/HttpError"; import Nav from "./components/NavBar"; import Trade from "./components/Trade"; import { @@ -33,6 +34,7 @@ import { Order, StateGroupKey, WalletInfo, + WithdrawRequest, } from "./components/Types"; import { Wallet, WalletInfoBar } from "./components/Wallet"; import useLatestEvent from "./Hooks"; @@ -56,6 +58,15 @@ async function postCfdOrderRequest(payload: CfdOrderRequestPayload) { } } +export async function postWithdraw(payload: WithdrawRequest) { + let res = await fetch(`/api/withdraw`, { method: "POST", body: JSON.stringify(payload) }); + if (!res.status.toString().startsWith("2")) { + const resp = await res.json(); + throw new HttpError(resp); + } + return res.text(); +} + export const App = () => { const toast = useToast(); useBackendMonitor(toast, 5000, "Please start the taker again to reconnect..."); // 5s timeout diff --git a/taker-frontend/src/components/Types.tsx b/taker-frontend/src/components/Types.tsx index 66d01d6..7b158af 100644 --- a/taker-frontend/src/components/Types.tsx +++ b/taker-frontend/src/components/Types.tsx @@ -271,3 +271,9 @@ export interface BXBTData { markPrice: number; timestamp: string; } + +export interface WithdrawRequest { + address: string; + amount?: number; + fee: number; +} diff --git a/taker-frontend/src/components/Wallet.tsx b/taker-frontend/src/components/Wallet.tsx index cd9a67c..6e9367f 100644 --- a/taker-frontend/src/components/Wallet.tsx +++ b/taker-frontend/src/components/Wallet.tsx @@ -1,7 +1,34 @@ import { CheckIcon, CopyIcon, ExternalLinkIcon } from "@chakra-ui/icons"; -import { Box, Center, Divider, HStack, IconButton, Skeleton, Text, useClipboard } from "@chakra-ui/react"; +import { + Box, + Button, + Center, + Divider, + FormControl, + FormHelperText, + FormLabel, + Heading, + HStack, + IconButton, + Input, + Link, + NumberDecrementStepper, + NumberIncrementStepper, + NumberInput, + NumberInputField, + NumberInputStepper, + Skeleton, + Text, + useClipboard, + useToast, + VStack, +} from "@chakra-ui/react"; import * as React from "react"; +import { useState } from "react"; +import { useAsync } from "react-async"; import { useNavigate } from "react-router-dom"; +import { postWithdraw } from "../App"; +import createErrorToast from "./ErrorToast"; import Timestamp from "./Timestamp"; import { WalletInfo } from "./Types"; @@ -14,14 +41,46 @@ export default function Wallet( walletInfo, }: WalletProps, ) { + const toast = useToast(); const { hasCopied, onCopy } = useClipboard(walletInfo ? walletInfo.address : ""); const { balance, address, last_updated_at } = walletInfo || {}; + const [withdrawAmount, setWithdrawAmount] = useState(0); + const [fee, setFee] = useState(1); + const [withdrawAddress, setWithdrawAddress] = useState(""); + + let { run: runWithdraw, isLoading: isWithdrawing } = useAsync({ + deferFn: async () => { + try { + const url = await postWithdraw({ + amount: withdrawAmount, + fee, + address: withdrawAddress, + }); + window.open(url, "_blank"); + toast({ + title: "Withdraw successful", + description: + {url} + , + status: "info", + duration: 10000, + isClosable: true, + }); + } catch (e) { + console.log(`Caught an error: ${e}`); + createErrorToast(toast, e); + } + }, + }); + return (
- -
Your wallet
- + +
+ Wallet Details +
+ Balance: {balance} BTC @@ -45,6 +104,65 @@ export default function Wallet( + + + + +
+ Withdraw + + Address + setWithdrawAddress(event.target.value)} + value={withdrawAddress} + placeholder="Target address" + > + + + + + Amount + setWithdrawAmount(amount)} + value={withdrawAmount} + precision={8} + step={0.001} + placeholder="How much do you want to withdraw? (0 to withdraw all)" + > + + + + + + + How much do you want to withdraw? (0 to withdraw all) + + + Fee + setFee(amount)} + value={fee} + step={1} + placeholder="In sats/vbyte" + > + + + + + + + In sats/vbyte + + + +
+
);