Browse Source

Add new endpoint to allow withdrawing through UI

debug-collab-settlement
bonomat 3 years ago
parent
commit
c951fe6f57
No known key found for this signature in database GPG Key ID: E5F8E74C672BC666
  1. 6
      daemon/src/maker.rs
  2. 48
      daemon/src/routes_maker.rs
  3. 47
      daemon/src/routes_taker.rs
  4. 4
      daemon/src/taker.rs
  5. 11
      taker-frontend/src/App.tsx
  6. 6
      taker-frontend/src/components/Types.tsx
  7. 126
      taker-frontend/src/components/Wallet.tsx

6
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::<maker_cfd::CfdAction>::clone_channel(&cfd_actor_addr);
let new_order_channel = MessageChannel::<maker_cfd::NewOrder>::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])

48
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 = "<withdraw_request>")]
pub async fn post_withdraw_request(
withdraw_request: Json<WithdrawRequest>,
wallet: &State<Address<wallet::Actor>>,
network: &State<Network>,
_auth: Authenticated,
) -> Result<String, HttpApiProblem> {
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::*;

47
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 = "<withdraw_request>")]
pub async fn post_withdraw_request(
withdraw_request: Json<WithdrawRequest>,
wallet: &State<Address<wallet::Actor>>,
network: &State<Network>,
) -> Result<String, HttpApiProblem> {
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)
}

4
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::<taker_cfd::TakeOffer>::clone_channel(&cfd_actor_addr);
let cfd_action_channel = MessageChannel::<taker_cfd::CfdAction>::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(

11
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

6
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;
}

126
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: <Link href={url} isExternal>
{url}
</Link>,
status: "info",
duration: 10000,
isClosable: true,
});
} catch (e) {
console.log(`Caught an error: ${e}`);
createErrorToast(toast, e);
}
},
});
return (
<Center>
<Box shadow={"md"} marginBottom={5} padding={5} boxSize={"sm"}>
<Center><Text fontWeight={"bold"}>Your wallet</Text></Center>
<HStack>
<Box shadow={"md"} marginBottom={5} padding={5}>
<Center>
<Heading size="sm">Wallet Details</Heading>
</Center>
<HStack padding={2}>
<Text align={"left"}>Balance:</Text>
<Skeleton isLoaded={balance != null}>
<Text>{balance} BTC</Text>
@ -45,6 +104,65 @@ export default function Wallet(
<Timestamp timestamp={last_updated_at!} />
</Skeleton>
</HStack>
<Divider marginTop={2} marginBottom={2} />
<VStack padding={2}>
<form onSubmit={runWithdraw}>
<Heading as="h3" size="sm">Withdraw</Heading>
<FormControl id="address">
<FormLabel>Address</FormLabel>
<Input
onChange={(event) => setWithdrawAddress(event.target.value)}
value={withdrawAddress}
placeholder="Target address"
>
</Input>
</FormControl>
<HStack>
<FormControl id="amount">
<FormLabel>Amount</FormLabel>
<NumberInput
min={0}
max={balance}
default={0}
onChange={(_, amount) => setWithdrawAmount(amount)}
value={withdrawAmount}
precision={8}
step={0.001}
placeholder="How much do you want to withdraw? (0 to withdraw all)"
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<FormHelperText>How much do you want to withdraw? (0 to withdraw all)</FormHelperText>
</FormControl>
<FormControl id="fee" w={"30%"}>
<FormLabel>Fee</FormLabel>
<NumberInput
min={1}
max={100}
default={0}
onChange={(_, amount) => setFee(amount)}
value={fee}
step={1}
placeholder="In sats/vbyte"
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<FormHelperText>In sats/vbyte</FormHelperText>
</FormControl>
</HStack>
<Button type="submit" isLoading={isWithdrawing}>Withdraw</Button>
</form>
</VStack>
</Box>
</Center>
);

Loading…
Cancel
Save