mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-25 18:26:28 -05:00
feat(gui): Redeem to internal Monero wallet (#448)
* fmt * remove old stuff * refactor
This commit is contained in:
parent
293ff2cdf3
commit
7b67dce140
21 changed files with 160 additions and 127 deletions
|
|
@ -500,12 +500,17 @@ impl MoneroWallet {
|
||||||
.context("No transaction receipts returned from sweep")
|
.context("No transaction receipts returned from sweep")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sweep_multi(&self, addresses: &[Address], ratios: &[f64]) -> Result<TxReceipt> {
|
/// Sweep multiple addresses with different ratios
|
||||||
|
/// If the address is `None`, the address will be set to the primary address of the
|
||||||
|
/// main wallet.
|
||||||
|
pub async fn sweep_multi(&self, addresses: &[impl Into<Option<Address>> + Clone], ratios: &[f64]) -> Result<TxReceipt> {
|
||||||
tracing::info!("`{}` sweeping multi ({:?})", self.name, ratios);
|
tracing::info!("`{}` sweeping multi ({:?})", self.name, ratios);
|
||||||
self.balance().await?;
|
self.balance().await?;
|
||||||
|
|
||||||
|
let addresses: Vec<Option<Address>> = addresses.iter().map(|a| a.clone().into()).collect();
|
||||||
|
|
||||||
self.wallet
|
self.wallet
|
||||||
.sweep_multi(addresses, ratios)
|
.sweep_multi(&addresses, ratios)
|
||||||
.await
|
.await
|
||||||
.context("Failed to perform sweep")?
|
.context("Failed to perform sweep")?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ fn main() {
|
||||||
.build_arg(match (is_github_actions, is_docker_build) {
|
.build_arg(match (is_github_actions, is_docker_build) {
|
||||||
(true, _) => "-j1",
|
(true, _) => "-j1",
|
||||||
(_, true) => "-j1",
|
(_, true) => "-j1",
|
||||||
(_, _) => "-j",
|
(_, _) => "-j4",
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -436,15 +436,20 @@ impl WalletHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sweep all funds to a set of addresses.
|
/// Sweep all funds to a set of addresses.
|
||||||
|
/// If the address is `None`, the address will be set to the primary address of the
|
||||||
|
/// wallet
|
||||||
pub async fn sweep_multi(
|
pub async fn sweep_multi(
|
||||||
&self,
|
&self,
|
||||||
addresses: &[monero::Address],
|
addresses: &[Option<monero::Address>],
|
||||||
percentages: &[f64],
|
percentages: &[f64],
|
||||||
) -> anyhow::Result<Vec<TxReceipt>> {
|
) -> anyhow::Result<Vec<TxReceipt>> {
|
||||||
let addresses = addresses.to_vec();
|
|
||||||
let percentages = percentages.to_vec();
|
|
||||||
|
|
||||||
tracing::debug!(addresses=?addresses, percentages=?percentages, "Sweeping multi");
|
tracing::debug!(addresses=?addresses, percentages=?percentages, "Sweeping multi");
|
||||||
|
|
||||||
|
let primary_address = self.main_address().await;
|
||||||
|
let addresses = addresses.iter().map(|address| address.unwrap_or(primary_address));
|
||||||
|
let addresses: Vec<_> = addresses.collect();
|
||||||
|
|
||||||
|
let percentages = percentages.to_vec();
|
||||||
|
|
||||||
self.call(move |wallet| wallet.sweep_multi(&addresses, &percentages))
|
self.call(move |wallet| wallet.sweep_multi(&addresses, &percentages))
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
import { Box, Dialog, DialogActions, DialogContent } from "@mui/material";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useAppSelector } from "store/hooks";
|
|
||||||
import DebugPage from "./pages/DebugPage";
|
|
||||||
import SwapStatePage from "renderer/components/pages/swap/swap/SwapStatePage";
|
|
||||||
import SwapDialogTitle from "./SwapDialogTitle";
|
|
||||||
import SwapStateStepper from "./SwapStateStepper";
|
|
||||||
import CancelButton from "renderer/components/pages/swap/swap/CancelButton";
|
|
||||||
|
|
||||||
export default function SwapDialog({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}) {
|
|
||||||
const swap = useAppSelector((state) => state.swap);
|
|
||||||
const [debug, setDebug] = useState(false);
|
|
||||||
|
|
||||||
// This prevents an issue where the Dialog is shown for a split second without a present swap state
|
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
|
||||||
<SwapDialogTitle
|
|
||||||
debug={debug}
|
|
||||||
setDebug={setDebug}
|
|
||||||
title="Swap Bitcoin for Monero"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogContent
|
|
||||||
dividers
|
|
||||||
sx={{
|
|
||||||
minHeight: "25rem",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
flex: 1,
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{debug ? (
|
|
||||||
<DebugPage />
|
|
||||||
) : (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: 2,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SwapStatePage state={swap.state} />
|
|
||||||
<SwapStateStepper state={swap.state} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
|
|
||||||
<DialogActions>
|
|
||||||
<CancelButton />
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -9,14 +9,16 @@ export default function TruncatedText({
|
||||||
ellipsis?: string;
|
ellipsis?: string;
|
||||||
truncateMiddle?: boolean;
|
truncateMiddle?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
let finalChildren = children ?? "";
|
||||||
|
|
||||||
const truncatedText =
|
const truncatedText =
|
||||||
children.length > limit
|
finalChildren.length > limit
|
||||||
? truncateMiddle
|
? truncateMiddle
|
||||||
? children.slice(0, Math.floor(limit / 2)) +
|
? finalChildren.slice(0, Math.floor(limit / 2)) +
|
||||||
ellipsis +
|
ellipsis +
|
||||||
children.slice(children.length - Math.floor(limit / 2))
|
finalChildren.slice(finalChildren.length - Math.floor(limit / 2))
|
||||||
: children.slice(0, limit) + ellipsis
|
: finalChildren.slice(0, limit) + ellipsis
|
||||||
: children;
|
: finalChildren;
|
||||||
|
|
||||||
return <span>{truncatedText}</span>;
|
return <span>{truncatedText}</span>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { Box, DialogContentText } from "@mui/material";
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import { formatConfirmations } from "utils/formatUtils";
|
import { formatConfirmations } from "utils/formatUtils";
|
||||||
import MoneroTransactionInfoBox from "../components/MoneroTransactionInfoBox";
|
import MoneroTransactionInfoBox from "../components/MoneroTransactionInfoBox";
|
||||||
import CancelButton from "../CancelButton";
|
|
||||||
|
|
||||||
export default function XmrLockTxInMempoolPage({
|
export default function XmrLockTxInMempoolPage({
|
||||||
xmr_lock_tx_confirmations,
|
xmr_lock_tx_confirmations,
|
||||||
|
|
@ -24,8 +23,6 @@ export default function XmrLockTxInMempoolPage({
|
||||||
additionalContent={additionalContent}
|
additionalContent={additionalContent}
|
||||||
loading
|
loading
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CancelButton />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ export default function InitPage() {
|
||||||
const [refundAddress, setRefundAddress] = useState("");
|
const [refundAddress, setRefundAddress] = useState("");
|
||||||
const [useExternalRefundAddress, setUseExternalRefundAddress] =
|
const [useExternalRefundAddress, setUseExternalRefundAddress] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [useExternalRedeemAddress, setUseExternalRedeemAddress] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
|
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
|
||||||
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
||||||
|
|
@ -21,7 +23,7 @@ export default function InitPage() {
|
||||||
async function init() {
|
async function init() {
|
||||||
await buyXmr(
|
await buyXmr(
|
||||||
useExternalRefundAddress ? refundAddress : null,
|
useExternalRefundAddress ? refundAddress : null,
|
||||||
redeemAddress,
|
useExternalRedeemAddress ? redeemAddress : null,
|
||||||
donationRatio,
|
donationRatio,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -35,14 +37,36 @@ export default function InitPage() {
|
||||||
gap: 1.5,
|
gap: 1.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MoneroAddressTextField
|
<Paper variant="outlined" style={{}}>
|
||||||
label="Monero redeem address"
|
<Tabs
|
||||||
address={redeemAddress}
|
value={useExternalRedeemAddress ? 1 : 0}
|
||||||
onAddressChange={setRedeemAddress}
|
indicatorColor="primary"
|
||||||
onAddressValidityChange={setRedeemAddressValid}
|
variant="fullWidth"
|
||||||
helperText="The monero will be sent to this address if the swap is successful."
|
onChange={(_, newValue) =>
|
||||||
fullWidth
|
setUseExternalRedeemAddress(newValue === 1)
|
||||||
/>
|
}
|
||||||
|
>
|
||||||
|
<Tab label="Redeem to internal Monero wallet" value={0} />
|
||||||
|
<Tab label="Redeem to external Monero address" value={1} />
|
||||||
|
</Tabs>
|
||||||
|
<Box style={{ padding: "16px" }}>
|
||||||
|
{useExternalRedeemAddress ? (
|
||||||
|
<MoneroAddressTextField
|
||||||
|
label="External Monero redeem address"
|
||||||
|
address={redeemAddress}
|
||||||
|
onAddressChange={setRedeemAddress}
|
||||||
|
onAddressValidityChange={setRedeemAddressValid}
|
||||||
|
helperText="The monero will be sent to this address if the swap is successful."
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography variant="caption">
|
||||||
|
The Monero will be sent to the internal Monero wallet of the GUI.
|
||||||
|
You can then withdraw them from there or use them for another swap directly.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
<Paper variant="outlined" style={{}}>
|
<Paper variant="outlined" style={{}}>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|
@ -80,7 +104,7 @@ export default function InitPage() {
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
disabled={
|
disabled={
|
||||||
(!refundAddressValid && useExternalRefundAddress) ||
|
(!refundAddressValid && useExternalRefundAddress) ||
|
||||||
!redeemAddressValid
|
(!redeemAddressValid && useExternalRedeemAddress)
|
||||||
}
|
}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [false]
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "081c729a0f1ad6e4ff3e13d6702c946bc4d37d50f40670b4f51d2efcce595aa6"
|
"hash": "081c729a0f1ad6e4ff3e13d6702c946bc4d37d50f40670b4f51d2efcce595aa6"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [true]
|
"nullable": [
|
||||||
|
true
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "0d465a17ebbb5761421def759c73cad023c30705d5b41a1399ef79d8d2571d7c"
|
"hash": "0d465a17ebbb5761421def759c73cad023c30705d5b41a1399ef79d8d2571d7c"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT DISTINCT address FROM monero_addresses",
|
"query": "SELECT DISTINCT address FROM monero_addresses WHERE address IS NOT NULL",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [false]
|
"nullable": [
|
||||||
|
true
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "98a8b7f4971e0eb4ab8f5aa688aa22e7fdc6b925de211f7784782f051c2dcd8c"
|
"hash": "1f332be08a5426f3fbcadea4e755d82ff1cdc2690eb464ccc607d3a613fa76a1"
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +17,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [true, true]
|
"nullable": [
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "5cc61dd0315571bc198401a354cd9431ee68360941f341386cbacf44ea598de8"
|
"hash": "5cc61dd0315571bc198401a354cd9431ee68360941f341386cbacf44ea598de8"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [false, false]
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "6130b6cdd184181f890964eb460741f5cf23b5237fb676faed009106627a4ca6"
|
"hash": "6130b6cdd184181f890964eb460741f5cf23b5237fb676faed009106627a4ca6"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [false]
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "88f761a4f7a0429cad1df0b1bebb1c0a27b2a45656549b23076d7542cfa21ecf"
|
"hash": "88f761a4f7a0429cad1df0b1bebb1c0a27b2a45656549b23076d7542cfa21ecf"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [false]
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "d78acba5eb8563826dd190e0886aa665aae3c6f1e312ee444e65df1c95afe8b2"
|
"hash": "d78acba5eb8563826dd190e0886aa665aae3c6f1e312ee444e65df1c95afe8b2"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,11 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [false, false, false]
|
"nullable": [
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "dff8b986c3dde27b8121775e48a58564fa346b038866699210a63f8a33b03f0b"
|
"hash": "dff8b986c3dde27b8121775e48a58564fa346b038866699210a63f8a33b03f0b"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [false]
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "e05620f420f8c1022971eeb66a803323a8cf258cbebb2834e3f7cf8f812fa646"
|
"hash": "e05620f420f8c1022971eeb66a803323a8cf258cbebb2834e3f7cf8f812fa646"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [false]
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "e9d422daf774d099fcbde6c4cda35821da948bd86cc57798b4d8375baf0b51ae"
|
"hash": "e9d422daf774d099fcbde6c4cda35821da948bd86cc57798b4d8375baf0b51ae"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
-- Users don't have to specify a receive address for the swap anymore, if none is present
|
||||||
|
-- we will use the internal wallet address instead.
|
||||||
|
-- Now, the monero_addresses.address column can be NULL.
|
||||||
|
|
||||||
|
-- SQLite doesn't support MODIFY COLUMN directly
|
||||||
|
-- We need to recreate the table with the desired schema
|
||||||
|
CREATE TABLE monero_addresses_temp
|
||||||
|
(
|
||||||
|
swap_id TEXT NOT NULL,
|
||||||
|
address TEXT NULL,
|
||||||
|
percentage REAL NOT NULL DEFAULT 1.0,
|
||||||
|
label TEXT NOT NULL DEFAULT 'user address'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Copy data from the original table
|
||||||
|
INSERT INTO monero_addresses_temp (swap_id, address, percentage, label)
|
||||||
|
SELECT swap_id, address, percentage, label FROM monero_addresses;
|
||||||
|
|
||||||
|
-- Drop the original table
|
||||||
|
DROP TABLE monero_addresses;
|
||||||
|
|
||||||
|
-- Rename the temporary table
|
||||||
|
ALTER TABLE monero_addresses_temp RENAME TO monero_addresses;
|
||||||
|
|
||||||
|
-- Create an index on swap_id for performance
|
||||||
|
CREATE INDEX idx_monero_addresses_swap_id ON monero_addresses(swap_id);
|
||||||
|
|
@ -1162,7 +1162,7 @@ pub async fn list_sellers(
|
||||||
peer_id,
|
peer_id,
|
||||||
version,
|
version,
|
||||||
}) => {
|
}) => {
|
||||||
tracing::debug!(
|
tracing::trace!(
|
||||||
status = "Online",
|
status = "Online",
|
||||||
price = %quote.price.to_string(),
|
price = %quote.price.to_string(),
|
||||||
min_quantity = %quote.min_quantity.to_string(),
|
min_quantity = %quote.min_quantity.to_string(),
|
||||||
|
|
@ -1182,7 +1182,7 @@ pub async fn list_sellers(
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
SellerStatus::Unreachable(UnreachableSeller { peer_id }) => {
|
SellerStatus::Unreachable(UnreachableSeller { peer_id }) => {
|
||||||
tracing::debug!(
|
tracing::trace!(
|
||||||
status = "Unreachable",
|
status = "Unreachable",
|
||||||
peer_id = %peer_id.to_string(),
|
peer_id = %peer_id.to_string(),
|
||||||
"Fetched peer status"
|
"Fetched peer status"
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ impl Database for SqliteDatabase {
|
||||||
let swap_id = swap_id.to_string();
|
let swap_id = swap_id.to_string();
|
||||||
|
|
||||||
for labeled_address in address.iter() {
|
for labeled_address in address.iter() {
|
||||||
let address_str = labeled_address.address().to_string();
|
let address_str = labeled_address.address().map(|address| address.to_string());
|
||||||
let percentage_f64 = labeled_address
|
let percentage_f64 = labeled_address
|
||||||
.percentage()
|
.percentage()
|
||||||
.to_f64()
|
.to_f64()
|
||||||
|
|
@ -163,12 +163,16 @@ impl Database for SqliteDatabase {
|
||||||
let addresses = row
|
let addresses = row
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| -> Result<LabeledMoneroAddress> {
|
.map(|row| -> Result<LabeledMoneroAddress> {
|
||||||
let address = row.address.parse()?;
|
let address: Option<monero::Address> = row.address.clone().map(|address| address.parse()).transpose()?;
|
||||||
let percentage = Decimal::from_f64(row.percentage).expect("Invalid percentage");
|
let percentage = Decimal::from_f64(row.percentage).expect("Invalid percentage");
|
||||||
let label = row.label.clone();
|
let label = row.label.clone();
|
||||||
|
|
||||||
LabeledMoneroAddress::new(address, percentage, label)
|
match address {
|
||||||
.map_err(|e| anyhow::anyhow!("Invalid percentage in database: {}", e))
|
Some(address) => LabeledMoneroAddress::with_address(address, percentage, label)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid percentage in database: {}", e)),
|
||||||
|
None => LabeledMoneroAddress::with_internal_address(percentage, label)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid percentage in database: {}", e)),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
|
@ -176,14 +180,14 @@ impl Database for SqliteDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_monero_addresses(&self) -> Result<Vec<monero::Address>> {
|
async fn get_monero_addresses(&self) -> Result<Vec<monero::Address>> {
|
||||||
let rows = sqlx::query!("SELECT DISTINCT address FROM monero_addresses")
|
let rows = sqlx::query!("SELECT DISTINCT address FROM monero_addresses WHERE address IS NOT NULL")
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let addresses = rows
|
let addresses = rows
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| row.address.parse())
|
.filter_map(|row| row.address.as_ref().and_then(|address| address.parse().inspect_err(|e| tracing::error!(%address, error = ?e, "Failed to parse monero address")).ok()))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Ok(addresses)
|
Ok(addresses)
|
||||||
}
|
}
|
||||||
|
|
@ -538,11 +542,11 @@ mod tests {
|
||||||
let address3 = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse()?; // Same as address1 for simplicity
|
let address3 = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse()?; // Same as address1 for simplicity
|
||||||
|
|
||||||
let labeled_addresses = vec![
|
let labeled_addresses = vec![
|
||||||
LabeledMoneroAddress::new(address1, Decimal::new(5, 1), "Primary".to_string())
|
LabeledMoneroAddress::with_address(address1, Decimal::new(5, 1), "Primary".to_string())
|
||||||
.map_err(|e| anyhow!(e))?, // 0.5
|
.map_err(|e| anyhow!(e))?, // 0.5
|
||||||
LabeledMoneroAddress::new(address2, Decimal::new(3, 1), "Secondary".to_string())
|
LabeledMoneroAddress::with_address(address2, Decimal::new(3, 1), "Secondary".to_string())
|
||||||
.map_err(|e| anyhow!(e))?, // 0.3
|
.map_err(|e| anyhow!(e))?, // 0.3
|
||||||
LabeledMoneroAddress::new(address3, Decimal::new(2, 1), "Tertiary".to_string())
|
LabeledMoneroAddress::with_address(address3, Decimal::new(2, 1), "Tertiary".to_string())
|
||||||
.map_err(|e| anyhow!(e))?, // 0.2
|
.map_err(|e| anyhow!(e))?, // 0.2
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -227,8 +227,9 @@ impl Amount {
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct LabeledMoneroAddress {
|
pub struct LabeledMoneroAddress {
|
||||||
|
// If this is None, we will use an address of the internal Monero wallet
|
||||||
#[typeshare(serialized_as = "string")]
|
#[typeshare(serialized_as = "string")]
|
||||||
address: monero::Address,
|
address: Option<monero::Address>,
|
||||||
#[typeshare(serialized_as = "number")]
|
#[typeshare(serialized_as = "number")]
|
||||||
percentage: Decimal,
|
percentage: Decimal,
|
||||||
label: String,
|
label: String,
|
||||||
|
|
@ -246,28 +247,36 @@ impl LabeledMoneroAddress {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if the percentage is not between 0.0 and 1.0 inclusive.
|
/// Returns an error if the percentage is not between 0.0 and 1.0 inclusive.
|
||||||
pub fn new(
|
fn new(
|
||||||
address: monero::Address,
|
address: impl Into<Option<monero::Address>>,
|
||||||
percentage: Decimal,
|
percentage: Decimal,
|
||||||
label: String,
|
label: String,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self> {
|
||||||
if percentage < Decimal::ZERO || percentage > Decimal::ONE {
|
if percentage < Decimal::ZERO || percentage > Decimal::ONE {
|
||||||
return Err(format!(
|
bail!(
|
||||||
"Percentage must be between 0 and 1 inclusive, got: {}",
|
"Percentage must be between 0 and 1 inclusive, got: {}",
|
||||||
percentage
|
percentage
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
address,
|
address: address.into(),
|
||||||
percentage,
|
percentage,
|
||||||
label,
|
label,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_address(address: monero::Address, percentage: Decimal, label: String) -> Result<Self> {
|
||||||
|
Self::new(address, percentage, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_internal_address(percentage: Decimal, label: String) -> Result<Self> {
|
||||||
|
Self::new(None, percentage, label)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the Monero address.
|
/// Returns the Monero address.
|
||||||
pub fn address(&self) -> monero::Address {
|
pub fn address(&self) -> Option<monero::Address> {
|
||||||
self.address
|
self.address.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the percentage as a decimal.
|
/// Returns the percentage as a decimal.
|
||||||
|
|
@ -303,7 +312,7 @@ impl MoneroAddressPool {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a vector of all Monero addresses in the pool.
|
/// Returns a vector of all Monero addresses in the pool.
|
||||||
pub fn addresses(&self) -> Vec<monero::Address> {
|
pub fn addresses(&self) -> Vec<Option<monero::Address>> {
|
||||||
self.0.iter().map(|address| address.address()).collect()
|
self.0.iter().map(|address| address.address()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,8 +345,10 @@ impl MoneroAddressPool {
|
||||||
/// Returns an error if any address is on a different network than expected.
|
/// Returns an error if any address is on a different network than expected.
|
||||||
pub fn assert_network(&self, network: Network) -> Result<()> {
|
pub fn assert_network(&self, network: Network) -> Result<()> {
|
||||||
for address in self.0.iter() {
|
for address in self.0.iter() {
|
||||||
if address.address().network != network {
|
if let Some(address) = address.address {
|
||||||
bail!("Address pool contains addresses on the wrong network (address {} is on {:?}, expected {:?})", address.address(), address.address().network, network);
|
if address.network != network {
|
||||||
|
bail!("Address pool contains addresses on the wrong network (address {} is on {:?}, expected {:?})", address, address.network, network);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -918,7 +929,7 @@ mod tests {
|
||||||
fn labeled_monero_address_percentage_validation() {
|
fn labeled_monero_address_percentage_validation() {
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
|
|
||||||
let address = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse().unwrap();
|
let address = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse::<monero::Address>().unwrap();
|
||||||
|
|
||||||
// Valid percentages should work (0-1 range)
|
// Valid percentages should work (0-1 range)
|
||||||
assert!(LabeledMoneroAddress::new(address, Decimal::ZERO, "test".to_string()).is_ok());
|
assert!(LabeledMoneroAddress::new(address, Decimal::ZERO, "test".to_string()).is_ok());
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue