Skip to content

Commit 9d3e4d1

Browse files
feat(gui): add Details to transaction view, show fee and splits (if available)
1 parent f1f3506 commit 9d3e4d1

7 files changed

Lines changed: 203 additions & 17 deletions

File tree

src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Props = {
1818
enableQrCode?: boolean;
1919
light?: boolean;
2020
spoilerText?: string;
21+
centered?: boolean;
2122
};
2223

2324
function QRCodeModal({ open, onClose, content }: ModalProps) {
@@ -65,6 +66,7 @@ export default function ActionableMonospaceTextBox({
6566
enableQrCode = true,
6667
light = false,
6768
spoilerText,
69+
centered = false,
6870
}: Props) {
6971
const [copied, setCopied] = useState(false);
7072
const [qrCodeOpen, setQrCodeOpen] = useState(false);
@@ -101,6 +103,7 @@ export default function ActionableMonospaceTextBox({
101103
>
102104
<MonospaceTextBox
103105
light={light}
106+
centered={centered}
104107
actions={
105108
<>
106109
{displayCopyIcon && (

src-gui/src/renderer/components/other/MonospaceTextBox.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { Box, Typography } from "@mui/material";
33
type Props = {
44
children: React.ReactNode;
55
light?: boolean;
6+
centered?: boolean;
67
actions?: React.ReactNode;
78
};
89

910
export default function MonospaceTextBox({
1011
children,
1112
light = false,
13+
centered = false,
1214
actions,
1315
}: Props) {
1416
return (
@@ -33,6 +35,7 @@ export default function MonospaceTextBox({
3335
fontFamily: "monospace",
3436
lineHeight: 1.5,
3537
flex: 1,
38+
...(centered ? { textAlign: "center" } : {}),
3639
}}
3740
>
3841
{children}

src-gui/src/renderer/components/other/Units.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,19 +231,20 @@ export function SatsAmount({
231231
return <BitcoinAmount amount={btcAmount} disableTooltip={disableTooltip} />;
232232
}
233233

234+
export interface PiconeroAmountArgs {
235+
amount: Amount;
236+
fixedPrecision?: number;
237+
labelStyles?: SxProps;
238+
amountStyles?: SxProps;
239+
disableTooltip?: boolean;
240+
}
234241
export function PiconeroAmount({
235242
amount,
236243
fixedPrecision = 8,
237244
labelStyles,
238245
amountStyles,
239246
disableTooltip = false,
240-
}: {
241-
amount: Amount;
242-
fixedPrecision?: number;
243-
labelStyles?: SxProps;
244-
amountStyles?: SxProps;
245-
disableTooltip?: boolean;
246-
}) {
247+
}: PiconeroAmountArgs) {
247248
return (
248249
<MoneroAmount
249250
amount={amount == null ? null : piconerosToXmr(amount)}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {
2+
Box,
3+
Chip,
4+
IconButton,
5+
Menu,
6+
MenuItem,
7+
Typography,
8+
Dialog,
9+
DialogActions,
10+
Button,
11+
TableContainer,
12+
Table,
13+
TableHead,
14+
TableBody,
15+
TableRow,
16+
TableCell,
17+
} from "@mui/material";
18+
import {
19+
TransactionDirection,
20+
TransactionInfo,
21+
Amount,
22+
} from "models/tauriModel";
23+
import { PiconeroAmountArgs } from "renderer/components/other/Units";
24+
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
25+
26+
// https://stackoverflow.com/a/22015930/2851815
27+
function zip<A, B>(a: A[], b: B[]) {
28+
return Array(Math.max(b.length, a.length))
29+
.fill(undefined)
30+
.map((_, i) => [a[i], b[i]]);
31+
}
32+
33+
export default function TransactionDetailsDialog({
34+
open,
35+
onClose,
36+
transaction,
37+
UnitAmount,
38+
}: {
39+
open: boolean;
40+
onClose: () => void;
41+
transaction: TransactionInfo;
42+
UnitAmount: React.FC<PiconeroAmountArgs>;
43+
}) {
44+
const rowKey = (input: [string, number], output: [string, number]) =>
45+
`${input && input[0]}${output && output[0]}`;
46+
const rowPair = (split: [string, number]) => {
47+
if (!split) return <TableCell colSpan={2} />;
48+
49+
const [id, amount] = split;
50+
return (
51+
<>
52+
<TableCell>
53+
<ActionableMonospaceTextBox
54+
displayCopyIcon={false}
55+
enableQrCode={false}
56+
content={id}
57+
/>
58+
</TableCell>
59+
<TableCell>
60+
<UnitAmount
61+
amount={amount}
62+
labelStyles={{ fontSize: 14, ml: -0.3 }}
63+
disableTooltip
64+
/>
65+
</TableCell>
66+
</>
67+
);
68+
};
69+
const rows =
70+
transaction.splits &&
71+
zip(transaction.splits.inputs, transaction.splits.outputs).map(
72+
([input, output]) => {
73+
return (
74+
<TableRow key={rowKey(input, output)}>
75+
{rowPair(input)}
76+
{rowPair(output)}
77+
</TableRow>
78+
);
79+
},
80+
);
81+
82+
return (
83+
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
84+
<ActionableMonospaceTextBox
85+
displayCopyIcon={false}
86+
enableQrCode={false}
87+
content={transaction.tx_hash}
88+
centered
89+
/>
90+
91+
<TableContainer>
92+
<Table>
93+
<TableHead>
94+
{transaction.splits && (
95+
<TableRow>
96+
<TableCell>Input</TableCell>
97+
<TableCell>Amount</TableCell>
98+
<TableCell>Output</TableCell>
99+
<TableCell>Amount</TableCell>
100+
</TableRow>
101+
)}
102+
</TableHead>
103+
<TableBody>
104+
{rows}
105+
<TableRow>
106+
<TableCell component="th">Fee</TableCell>
107+
<TableCell colSpan={4}>
108+
<UnitAmount
109+
amount={transaction.fee}
110+
labelStyles={{ fontSize: 14, ml: -0.3 }}
111+
disableTooltip
112+
/>
113+
</TableCell>
114+
</TableRow>
115+
</TableBody>
116+
</Table>
117+
</TableContainer>
118+
119+
<DialogActions>
120+
<Button onClick={onClose} color="primary" variant="text">
121+
Close
122+
</Button>
123+
</DialogActions>
124+
</Dialog>
125+
);
126+
}

src-gui/src/renderer/components/pages/monero/components/TransactionItem.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import {
2121
FiatSatsAmount,
2222
PiconeroAmount,
2323
SatsAmount,
24+
PiconeroAmountArgs,
2425
} from "renderer/components/other/Units";
2526
import ConfirmationsBadge from "./ConfirmationsBadge";
27+
import TransactionDetailsDialog from "./TransactionDetailsDialog";
2628
import {
2729
getMoneroTxExplorerUrl,
2830
getBitcoinTxExplorerUrl,
@@ -52,12 +54,9 @@ export default function TransactionItem({
5254

5355
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
5456
const menuOpen = Boolean(menuAnchorEl);
57+
const [showDetails, setShowDetails] = useState(false);
5558

56-
const UnitAmount =
57-
currency == "monero"
58-
? PiconeroAmount
59-
: (args: { amount: Amount }) =>
60-
SatsAmount({ disableTooltip: true, ...args });
59+
const UnitAmount = currency == "monero" ? PiconeroAmount : SatsAmount;
6160
const FiatUnitAmount =
6261
currency == "monero" ? FiatPiconeroAmount : FiatSatsAmount;
6362
const getExplorerUrl =
@@ -72,6 +71,12 @@ export default function TransactionItem({
7271
justifyContent: "space-between",
7372
}}
7473
>
74+
<TransactionDetailsDialog
75+
open={showDetails}
76+
onClose={() => setShowDetails(false)}
77+
transaction={transaction}
78+
UnitAmount={UnitAmount}
79+
/>
7580
<Box
7681
sx={{
7782
display: "flex",
@@ -165,12 +170,20 @@ export default function TransactionItem({
165170
</MenuItem>
166171
<MenuItem
167172
onClick={() => {
168-
open(getMoneroTxExplorerUrl(transaction.tx_hash, isTestnet()));
173+
open(getExplorerUrl(transaction.tx_hash, isTestnet()));
169174
setMenuAnchorEl(null);
170175
}}
171176
>
172177
<Typography>View on Explorer</Typography>
173178
</MenuItem>
179+
<MenuItem
180+
onClick={() => {
181+
setShowDetails(true);
182+
setMenuAnchorEl(null);
183+
}}
184+
>
185+
<Typography>Details</Typography>
186+
</MenuItem>
174187
</Menu>
175188
</Box>
176189
</Box>

swap/src/asb/rpc/server.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use std::sync::Arc;
1010
use swap_controller_api::{
1111
ActiveConnectionsResponse, AsbApiServer, BitcoinBalanceResponse, BitcoinSeedResponse,
1212
MoneroAddressResponse, MoneroBalanceResponse, MoneroSeedResponse, MultiaddressesResponse,
13-
PeerIdResponse, RegistrationStatusItem, RegistrationStatusResponse,
14-
RendezvousConnectionStatus, RendezvousRegistrationStatus, Swap,
13+
PeerIdResponse, RegistrationStatusItem, RegistrationStatusResponse, RendezvousConnectionStatus,
14+
RendezvousRegistrationStatus, Swap,
1515
};
1616
use tokio_util::task::AbortOnDropHandle;
1717

swap/src/bitcoin/wallet.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use bdk_wallet::WalletPersister;
2020
use bdk_wallet::{Balance, PersistedWallet};
2121
use bitcoin::bip32::Xpriv;
2222
use bitcoin::{psbt::Psbt as PartiallySignedTransaction, Txid};
23-
use bitcoin::{Psbt, ScriptBuf, Weight};
23+
use bitcoin::{OutPoint, Psbt, ScriptBuf, Weight};
2424
use derive_builder::Builder;
2525
use electrum_pool::ElectrumBalancer;
2626
use moka;
@@ -64,6 +64,7 @@ pub struct TransactionInfo {
6464
pub direction: TransactionDirection,
6565
#[typeshare(serialized_as = "number")]
6666
pub timestamp: u64,
67+
pub splits: TransactionSplits,
6768
}
6869

6970
#[typeshare]
@@ -73,6 +74,15 @@ pub enum TransactionDirection {
7374
Out,
7475
}
7576

77+
#[derive(Debug, Clone, Serialize, Deserialize)]
78+
#[typeshare]
79+
pub struct TransactionSplits {
80+
#[typeshare(serialized_as = "[string, number][]")]
81+
inputs: Vec<(String, Amount)>,
82+
#[typeshare(serialized_as = "[string, number][]")]
83+
outputs: Vec<(String, Amount)>,
84+
}
85+
7686
/// This is our wrapper around a bdk wallet and a corresponding
7787
/// bdk electrum client.
7888
/// It unifies all the functionality we need when interacting
@@ -1327,7 +1337,6 @@ where
13271337
Ok(address)
13281338
}
13291339

1330-
/// Get list
13311340
pub async fn history(&self) -> Vec<TransactionInfo> {
13321341
let wallet = self.wallet.lock().await;
13331342
let current_height = wallet.latest_checkpoint().height();
@@ -1356,6 +1365,37 @@ where
13561365
false => TransactionDirection::Out,
13571366
},
13581367
timestamp,
1368+
splits: TransactionSplits {
1369+
inputs: txd
1370+
.tx
1371+
.input
1372+
.iter()
1373+
.map(|i| {
1374+
(
1375+
i.previous_output.to_string(),
1376+
wallet
1377+
.get_tx(i.previous_output.txid)
1378+
.and_then(|tx| {
1379+
tx.tx_node
1380+
.tx
1381+
.output
1382+
.get(i.previous_output.vout as usize)
1383+
.map(|txo| txo.value)
1384+
})
1385+
.unwrap_or_default(),
1386+
)
1387+
})
1388+
.collect(),
1389+
outputs: txd
1390+
.tx
1391+
.output
1392+
.iter()
1393+
.enumerate()
1394+
.map(|(vout, o)| {
1395+
(OutPoint::new(txd.txid, vout as _).to_string(), o.value)
1396+
})
1397+
.collect(),
1398+
},
13591399
}
13601400
})
13611401
.collect();

0 commit comments

Comments
 (0)