Browse Source

Merge #664

664: Withdraw through UI r=bonomat a=bonomat

Enable withdraw through UI.

If the withdraw fails the error messages are being swallowed somewhere and I don't know how to get it. Maybe `@da-kami` or `@thomaseizinger` has an idea?
The HTTP post request fails and I get an `Internal Server Error` but the actual error message is not shown. 

I solved the _withdraw all_ feature by saying: if input == 0, withdraw all. 

resolves #608   

<img width="882" alt="image" src="https://user-images.githubusercontent.com/224613/142815791-da5380d9-52aa-4fcd-9372-fcc5ccc9aefa.png">

URL: https://mempool.space/testnet/tx/c33de791867148c3fa7e67858b48521a72bd83edd4efe21d0666af26e5939d0d

Co-authored-by: bonomat <philipp@hoenisch.at>
debug-collab-settlement
bors[bot] 3 years ago
committed by GitHub
parent
commit
a3e8427615
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      daemon/src/maker.rs
  2. 48
      daemon/src/routes_maker.rs
  3. 47
      daemon/src/routes_taker.rs
  4. 8
      daemon/src/taker.rs
  5. 14
      daemon/src/wallet.rs
  6. 18
      taker-frontend/src/App.tsx
  7. 6
      taker-frontend/src/components/Types.tsx
  8. 126
      taker-frontend/src/components/Wallet.tsx

10
daemon/src/maker.rs

@ -180,7 +180,7 @@ async fn main() -> Result<()> {
fee,
}) = opts.network.withdraw()
{
let txid = wallet
wallet
.send(wallet::Withdraw {
amount: *amount,
address: address.clone(),
@ -188,8 +188,6 @@ async fn main() -> Result<()> {
})
.await??;
tracing::info!(%txid, "Withdraw successful");
return Ok(());
}
@ -300,7 +298,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);
@ -315,13 +313,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)
}

8
daemon/src/taker.rs

@ -192,7 +192,7 @@ async fn main() -> Result<()> {
fee,
}) = opts.network.withdraw()
{
let txid = wallet
wallet
.send(wallet::Withdraw {
amount: *amount,
address: address.clone(),
@ -200,8 +200,6 @@ async fn main() -> Result<()> {
})
.await??;
tracing::info!(%txid, "Withdraw successful");
return Ok(());
}
@ -278,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);
@ -291,6 +289,7 @@ async fn main() -> Result<()> {
.manage(wallet_feed_receiver)
.manage(quote_receiver)
.manage(bitcoin_network)
.manage(wallet)
.mount(
"/api",
rocket::routes![
@ -299,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(

14
daemon/src/wallet.rs

@ -177,6 +177,17 @@ impl Actor {
}
pub async fn handle_withdraw(&self, msg: Withdraw) -> Result<Txid> {
{
let wallet = self.wallet.lock().await;
if msg.address.network != wallet.network() {
bail!(
"Address has invalid network. It was {} but the wallet is connected to {}",
msg.address.network,
wallet.network()
)
}
}
let fee_rate = msg.fee.unwrap_or_else(FeeRate::default_min_relay_fee);
let address = msg.address;
@ -191,6 +202,7 @@ impl Actor {
tracing::info!(%amount, %address, "Amount to be sent to address");
let wallet = self.wallet.lock().await;
let mut tx_builder = wallet.build_tx();
tx_builder
@ -205,6 +217,8 @@ impl Actor {
let txid = wallet.broadcast(psbt.extract_tx())?;
tracing::info!(%txid, "Withdraw successful");
Ok(txid)
}
}

18
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";
@ -41,7 +43,8 @@ async function getMargin(payload: MarginRequestPayload): Promise<MarginResponse>
let res = await fetch(`/api/calculate/margin`, { method: "POST", body: JSON.stringify(payload) });
if (!res.status.toString().startsWith("2")) {
throw new Error("failed to create new CFD order request: " + res.status + ", " + res.statusText);
const resp = await res.json();
throw new HttpError(resp);
}
return res.json();
@ -50,11 +53,20 @@ async function getMargin(payload: MarginRequestPayload): Promise<MarginResponse>
async function postCfdOrderRequest(payload: CfdOrderRequestPayload) {
let res = await fetch(`/api/cfd/order`, { method: "POST", body: JSON.stringify(payload) });
if (!res.status.toString().startsWith("2")) {
console.log(`Error${JSON.stringify(res)}`);
throw new Error("failed to create new CFD order request: " + res.status + ", " + res.statusText);
const resp = await res.json();
throw new HttpError(resp);
}
}
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 { FormEvent, 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 ([event]: FormEvent<HTMLFormElement>[]) => {
event.preventDefault();
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) {
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