mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-04-22 16:59:17 -04:00
feat(gui): Allow to select from recently used monero addresses (#139)
* feat(gui): Allow user to select from recently used monero addresses in textfield
This commit is contained in:
parent
4867d2713f
commit
bd3fca7e41
@ -10,6 +10,14 @@ import SwapPage from "./pages/swap/SwapPage";
|
||||
import WalletPage from "./pages/wallet/WalletPage";
|
||||
import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
|
||||
import UpdaterDialog from "./modal/updater/UpdaterDialog";
|
||||
import { initEventListeners } from "renderer/rpc";
|
||||
import { useEffect } from "react";
|
||||
import { fetchProvidersViaHttp, fetchAlertsViaHttp, fetchXmrPrice, fetchBtcPrice, fetchXmrBtcRate } from "renderer/api";
|
||||
import { store } from "renderer/store/storeRenderer";
|
||||
import { setAlerts } from "store/features/alertsSlice";
|
||||
import { setRegistryProviders, registryConnectionFailed } from "store/features/providersSlice";
|
||||
import { setXmrPrice, setBtcPrice, setXmrBtcRate } from "store/features/ratesSlice";
|
||||
import logger from "utils/logger";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
innerContent: {
|
||||
@ -52,6 +60,11 @@ function InnerContent() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
fetchInitialData();
|
||||
initEventListeners();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalSnackbarProvider>
|
||||
@ -65,3 +78,46 @@ export default function App() {
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchInitialData() {
|
||||
try {
|
||||
const providerList = await fetchProvidersViaHttp();
|
||||
store.dispatch(setRegistryProviders(providerList));
|
||||
|
||||
logger.info(
|
||||
{ providerList },
|
||||
"Fetched providers via UnstoppableSwap HTTP API",
|
||||
);
|
||||
} catch (e) {
|
||||
store.dispatch(registryConnectionFailed());
|
||||
logger.error(e, "Failed to fetch providers via UnstoppableSwap HTTP API");
|
||||
}
|
||||
|
||||
try {
|
||||
const alerts = await fetchAlertsViaHttp();
|
||||
store.dispatch(setAlerts(alerts));
|
||||
logger.info({ alerts }, "Fetched alerts via UnstoppableSwap HTTP API");
|
||||
} catch (e) {
|
||||
logger.error(e, "Failed to fetch alerts via UnstoppableSwap HTTP API");
|
||||
}
|
||||
|
||||
try {
|
||||
const xmrPrice = await fetchXmrPrice();
|
||||
store.dispatch(setXmrPrice(xmrPrice));
|
||||
logger.info({ xmrPrice }, "Fetched XMR price");
|
||||
|
||||
const btcPrice = await fetchBtcPrice();
|
||||
store.dispatch(setBtcPrice(btcPrice));
|
||||
logger.info({ btcPrice }, "Fetched BTC price");
|
||||
} catch (e) {
|
||||
logger.error(e, "Error retrieving fiat prices");
|
||||
}
|
||||
|
||||
try {
|
||||
const xmrBtcRate = await fetchXmrBtcRate();
|
||||
store.dispatch(setXmrBtcRate(xmrBtcRate));
|
||||
logger.info({ xmrBtcRate }, "Fetched XMR/BTC rate");
|
||||
} catch (e) {
|
||||
logger.error(e, "Error retrieving XMR/BTC rate");
|
||||
}
|
||||
}
|
@ -1,8 +1,18 @@
|
||||
import { TextField } from "@material-ui/core";
|
||||
import { Box, Button, Dialog, DialogActions, DialogContent, IconButton, List, ListItem, ListItemText, TextField } from "@material-ui/core";
|
||||
import { TextFieldProps } from "@material-ui/core/TextField/TextField";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getMoneroAddresses } from "renderer/rpc";
|
||||
import { isTestnet } from "store/config";
|
||||
import { isXmrAddressValid } from "utils/conversionUtils";
|
||||
import ImportContactsIcon from '@material-ui/icons/ImportContacts';
|
||||
import TruncatedText from "../other/TruncatedText";
|
||||
|
||||
type MoneroAddressTextFieldProps = TextFieldProps & {
|
||||
address: string;
|
||||
onAddressChange: (address: string) => void;
|
||||
onAddressValidityChange: (valid: boolean) => void;
|
||||
helperText: string;
|
||||
}
|
||||
|
||||
export default function MoneroAddressTextField({
|
||||
address,
|
||||
@ -10,30 +20,116 @@ export default function MoneroAddressTextField({
|
||||
onAddressValidityChange,
|
||||
helperText,
|
||||
...props
|
||||
}: {
|
||||
address: string;
|
||||
onAddressChange: (address: string) => void;
|
||||
onAddressValidityChange: (valid: boolean) => void;
|
||||
helperText: string;
|
||||
} & TextFieldProps) {
|
||||
}: MoneroAddressTextFieldProps) {
|
||||
const [addresses, setAddresses] = useState<string[]>([]);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
// Validation
|
||||
const placeholder = isTestnet() ? "59McWTPGc745..." : "888tNkZrPN6J...";
|
||||
const errorText = isXmrAddressValid(address, isTestnet())
|
||||
? null
|
||||
: "Not a valid Monero address";
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
onAddressValidityChange(!errorText);
|
||||
}, [address, onAddressValidityChange, errorText]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAddresses = async () => {
|
||||
const response = await getMoneroAddresses();
|
||||
setAddresses(response.addresses);
|
||||
};
|
||||
fetchAddresses();
|
||||
}, []);
|
||||
|
||||
// Event handlers
|
||||
const handleClose = () => setShowDialog(false);
|
||||
const handleAddressSelect = (selectedAddress: string) => {
|
||||
onAddressChange(selectedAddress);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
value={address}
|
||||
onChange={(e) => onAddressChange(e.target.value)}
|
||||
error={!!errorText && address.length > 0}
|
||||
helperText={address.length > 0 ? errorText || helperText : helperText}
|
||||
placeholder={placeholder}
|
||||
variant="outlined"
|
||||
{...props}
|
||||
/>
|
||||
<Box>
|
||||
<TextField
|
||||
value={address}
|
||||
onChange={(e) => onAddressChange(e.target.value)}
|
||||
error={!!errorText && address.length > 0}
|
||||
helperText={address.length > 0 ? errorText || helperText : helperText}
|
||||
placeholder={placeholder}
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
endAdornment: addresses?.length > 0 && (
|
||||
<IconButton onClick={() => setShowDialog(true)} size="small">
|
||||
<ImportContactsIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<RecentlyUsedAddressesDialog
|
||||
open={showDialog}
|
||||
onClose={handleClose}
|
||||
addresses={addresses}
|
||||
onAddressSelect={handleAddressSelect}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface RecentlyUsedAddressesDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
addresses: string[];
|
||||
onAddressSelect: (address: string) => void;
|
||||
}
|
||||
|
||||
function RecentlyUsedAddressesDialog({
|
||||
open,
|
||||
onClose,
|
||||
addresses,
|
||||
onAddressSelect
|
||||
}: RecentlyUsedAddressesDialogProps) {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogContent>
|
||||
<List>
|
||||
{addresses.map((addr) => (
|
||||
<ListItem
|
||||
button
|
||||
key={addr}
|
||||
onClick={() => onAddressSelect(addr)}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box fontFamily="monospace">
|
||||
<TruncatedText limit={40} truncateMiddle>
|
||||
{addr}
|
||||
</TruncatedText>
|
||||
</Box>
|
||||
}
|
||||
secondary="Recently used as a redeem address"
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
@ -2,13 +2,20 @@ export default function TruncatedText({
|
||||
children,
|
||||
limit = 6,
|
||||
ellipsis = "...",
|
||||
truncateMiddle = false,
|
||||
}: {
|
||||
children: string;
|
||||
limit?: number;
|
||||
ellipsis?: string;
|
||||
truncateMiddle?: boolean;
|
||||
}) {
|
||||
const truncatedText =
|
||||
children.length > limit ? children.slice(0, limit) + ellipsis : children;
|
||||
const truncatedText = children.length > limit
|
||||
? truncateMiddle
|
||||
? children.slice(0, Math.floor(limit/2)) +
|
||||
ellipsis +
|
||||
children.slice(children.length - Math.floor(limit/2))
|
||||
: children.slice(0, limit) + ellipsis
|
||||
: children;
|
||||
|
||||
return truncatedText;
|
||||
return <span>{truncatedText}</span>;
|
||||
}
|
||||
|
@ -28,50 +28,4 @@ root.render(
|
||||
<App />
|
||||
</PersistGate>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
async function fetchInitialData() {
|
||||
try {
|
||||
const providerList = await fetchProvidersViaHttp();
|
||||
store.dispatch(setRegistryProviders(providerList));
|
||||
|
||||
logger.info(
|
||||
{ providerList },
|
||||
"Fetched providers via UnstoppableSwap HTTP API",
|
||||
);
|
||||
} catch (e) {
|
||||
store.dispatch(registryConnectionFailed());
|
||||
logger.error(e, "Failed to fetch providers via UnstoppableSwap HTTP API");
|
||||
}
|
||||
|
||||
try {
|
||||
const alerts = await fetchAlertsViaHttp();
|
||||
store.dispatch(setAlerts(alerts));
|
||||
logger.info({ alerts }, "Fetched alerts via UnstoppableSwap HTTP API");
|
||||
} catch (e) {
|
||||
logger.error(e, "Failed to fetch alerts via UnstoppableSwap HTTP API");
|
||||
}
|
||||
|
||||
try {
|
||||
const xmrPrice = await fetchXmrPrice();
|
||||
store.dispatch(setXmrPrice(xmrPrice));
|
||||
logger.info({ xmrPrice }, "Fetched XMR price");
|
||||
|
||||
const btcPrice = await fetchBtcPrice();
|
||||
store.dispatch(setBtcPrice(btcPrice));
|
||||
logger.info({ btcPrice }, "Fetched BTC price");
|
||||
} catch (e) {
|
||||
logger.error(e, "Error retrieving fiat prices");
|
||||
}
|
||||
|
||||
try {
|
||||
const xmrBtcRate = await fetchXmrBtcRate();
|
||||
store.dispatch(setXmrBtcRate(xmrBtcRate));
|
||||
logger.info({ xmrBtcRate }, "Fetched XMR/BTC rate");
|
||||
} catch (e) {
|
||||
logger.error(e, "Error retrieving XMR/BTC rate");
|
||||
}
|
||||
}
|
||||
|
||||
fetchInitialData();
|
||||
initEventListeners();
|
||||
);
|
@ -22,6 +22,7 @@ import {
|
||||
TauriTimelockChangeEvent,
|
||||
GetSwapInfoArgs,
|
||||
ExportBitcoinWalletResponse,
|
||||
GetMoneroAddressesResponse,
|
||||
} from "models/tauriModel";
|
||||
import {
|
||||
contextStatusEventReceived,
|
||||
@ -218,4 +219,8 @@ export async function initializeContext() {
|
||||
|
||||
export async function getWalletDescriptor() {
|
||||
return await invokeNoArgs<ExportBitcoinWalletResponse>("get_wallet_descriptor");
|
||||
}
|
||||
|
||||
export async function getMoneroAddresses(): Promise<GetMoneroAddressesResponse> {
|
||||
return await invokeNoArgs<GetMoneroAddressesResponse>("get_monero_addresses");
|
||||
}
|
@ -5,8 +5,9 @@ use swap::cli::{
|
||||
api::{
|
||||
request::{
|
||||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetHistoryArgs,
|
||||
GetLogsArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs,
|
||||
ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||
GetLogsArgs, GetMoneroAddressesArgs, GetSwapInfoArgs, GetSwapInfosAllArgs,
|
||||
ListSellersArgs, MoneroRecoveryArgs, ResumeSwapArgs, SuspendCurrentSwapArgs,
|
||||
WithdrawBtcArgs,
|
||||
},
|
||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
||||
Context, ContextBuilder,
|
||||
@ -146,6 +147,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_balance,
|
||||
get_monero_addresses,
|
||||
get_swap_info,
|
||||
get_swap_infos_all,
|
||||
withdraw_btc,
|
||||
@ -200,12 +202,14 @@ tauri_command!(monero_recovery, MoneroRecoveryArgs);
|
||||
tauri_command!(get_logs, GetLogsArgs);
|
||||
tauri_command!(list_sellers, ListSellersArgs);
|
||||
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
|
||||
|
||||
// These commands require no arguments
|
||||
tauri_command!(get_wallet_descriptor, ExportBitcoinWalletArgs, no_args);
|
||||
tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args);
|
||||
tauri_command!(get_swap_info, GetSwapInfoArgs);
|
||||
tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args);
|
||||
tauri_command!(get_history, GetHistoryArgs, no_args);
|
||||
tauri_command!(get_monero_addresses, GetMoneroAddressesArgs, no_args);
|
||||
|
||||
/// Here we define Tauri commands whose implementation is not delegated to the Request trait
|
||||
#[tauri::command]
|
||||
|
@ -9,7 +9,9 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [false],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -35,7 +37,9 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [true],
|
||||
"nullable": [
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -56,7 +60,10 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [false, false],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
}
|
||||
@ -92,13 +99,33 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [false],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT state\n FROM swap_states\n WHERE swap_id = ?\n ORDER BY id desc\n LIMIT 1;\n\n "
|
||||
},
|
||||
"98a8b7f4971e0eb4ab8f5aa688aa22e7fdc6b925de211f7784782f051c2dcd8c": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "address",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
}
|
||||
},
|
||||
"query": "SELECT DISTINCT address FROM monero_addresses"
|
||||
},
|
||||
"b703032b4ddc627a1124817477e7a8e5014bdc694c36a14053ef3bb2fc0c69b0": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -118,7 +145,9 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [false],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -134,7 +163,9 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [false],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -150,7 +181,9 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [false],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -176,11 +209,13 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [false],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT proof\n FROM buffered_transfer_proofs\n WHERE swap_id = ?\n "
|
||||
}
|
||||
}
|
||||
}
|
@ -416,6 +416,26 @@ impl Request for GetLogsArgs {
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GetMoneroAddressesArgs;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetMoneroAddressesResponse {
|
||||
#[typeshare(serialized_as = "Vec<String>")]
|
||||
pub addresses: Vec<monero::Address>,
|
||||
}
|
||||
|
||||
impl Request for GetMoneroAddressesArgs {
|
||||
type Response = GetMoneroAddressesResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
let addresses = ctx.db.get_monero_addresses().await?;
|
||||
Ok(GetMoneroAddressesResponse { addresses })
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(fields(method = "suspend_current_swap"), skip(context))]
|
||||
pub async fn suspend_current_swap(context: Arc<Context>) -> Result<SuspendCurrentSwapResponse> {
|
||||
let swap_id = context.swap_lock.get_current_swap_id().await;
|
||||
|
@ -143,6 +143,21 @@ impl Database for SqliteDatabase {
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
async fn get_monero_addresses(&self) -> Result<Vec<monero::Address>> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
|
||||
let rows = sqlx::query!("SELECT DISTINCT address FROM monero_addresses")
|
||||
.fetch_all(&mut conn)
|
||||
.await?;
|
||||
|
||||
let addresses = rows
|
||||
.iter()
|
||||
.map(|row| row.address.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(addresses)
|
||||
}
|
||||
|
||||
async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
|
||||
|
@ -137,6 +137,7 @@ pub trait Database {
|
||||
async fn get_peer_id(&self, swap_id: Uuid) -> Result<PeerId>;
|
||||
async fn insert_monero_address(&self, swap_id: Uuid, address: monero::Address) -> Result<()>;
|
||||
async fn get_monero_address(&self, swap_id: Uuid) -> Result<monero::Address>;
|
||||
async fn get_monero_addresses(&self) -> Result<Vec<monero::Address>>;
|
||||
async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()>;
|
||||
async fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>>;
|
||||
async fn get_swap_start_date(&self, swap_id: Uuid) -> Result<String>;
|
||||
|
BIN
swap/tempdb
BIN
swap/tempdb
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user