mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-26 10:46:23 -05:00
feat: Reliable Peer Discovery (#408)
* feat(swap): Allow discovery at multiple rendezvous points, request quotes from locally stored peers - Ensure uniqueness of the peer_addresses table (no duplicate entries) - Add peer to local database even if we just request a quote, and no swap (call to list_sellers) - Refactor list_sellers to take multiple rendezvous points - Allow db to be passed into list_sellers, if so request quote from all locally stored peers * feat: editable list of rendezvous points in settings, new maker box on help page * Recover old commits * fix small compile errors due to rebase * amend * fixes * fix(gui): Do not display "Core components are loading..." spinner * fix(gui): Prefer makers with m.minSwapAmount > 0 BTC * feat(cli, gui): Fetch version of maker * feat: display progress bar
This commit is contained in:
parent
686947e8dc
commit
4702bd5bf2
31 changed files with 1869 additions and 512 deletions
|
|
@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- GUI: Improved peer discovery: We can now connect to multiple rendezvous points at once. We also cache peers we have previously connected to locally and will attempt to connect to them again in the future, even if they aren't registered with a rendezvous point anymore.
|
||||
|
||||
## [2.0.3] - 2025-06-12
|
||||
|
||||
## [2.0.2] - 2025-06-12
|
||||
|
|
|
|||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -9296,6 +9296,7 @@ dependencies = [
|
|||
"rust_decimal",
|
||||
"rust_decimal_macros",
|
||||
"rustls 0.23.27",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_cbor",
|
||||
"serde_json",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
checkContextAvailability,
|
||||
getSwapInfo,
|
||||
initializeContext,
|
||||
listSellersAtRendezvousPoint,
|
||||
updateAllNodeStatuses,
|
||||
} from "./rpc";
|
||||
import { store } from "./store/storeRenderer";
|
||||
|
|
@ -30,6 +31,9 @@ const TAURI_UNIFIED_EVENT_CHANNEL_NAME = "tauri-unified-event";
|
|||
// Update the public registry every 5 minutes
|
||||
const PROVIDER_UPDATE_INTERVAL = 5 * 60 * 1_000;
|
||||
|
||||
// Discover peers every 5 minutes
|
||||
const DISCOVER_PEERS_INTERVAL = 5 * 60 * 1_000;
|
||||
|
||||
// Update node statuses every 2 minutes
|
||||
const STATUS_UPDATE_INTERVAL = 2 * 60 * 1_000;
|
||||
|
||||
|
|
@ -50,6 +54,11 @@ export async function setupBackgroundTasks(): Promise<void> {
|
|||
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
|
||||
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
|
||||
setIntervalImmediate(fetchAllConversations, FETCH_CONVERSATIONS_INTERVAL);
|
||||
setIntervalImmediate(
|
||||
() =>
|
||||
listSellersAtRendezvousPoint(store.getState().settings.rendezvousPoints),
|
||||
DISCOVER_PEERS_INTERVAL,
|
||||
);
|
||||
|
||||
// Fetch all alerts
|
||||
updateAlerts();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, Button, LinearProgress, Badge } from "@mui/material";
|
||||
import { Box, Button, LinearProgress, Badge, Typography } from "@mui/material";
|
||||
import { Alert } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAppSelector, usePendingBackgroundProcesses } from "store/hooks";
|
||||
|
|
@ -159,6 +159,35 @@ function PartialInitStatus({
|
|||
</>
|
||||
</LoadingSpinnerAlert>
|
||||
);
|
||||
case "ListSellers": {
|
||||
const progress = status.progress.content;
|
||||
const totalExpected =
|
||||
progress.rendezvous_points_total + progress.peers_discovered;
|
||||
const totalCompleted =
|
||||
progress.rendezvous_points_connected +
|
||||
progress.quotes_received +
|
||||
progress.quotes_failed;
|
||||
const progressValue =
|
||||
totalExpected > 0 ? (totalCompleted / totalExpected) * 100 : 0;
|
||||
|
||||
return (
|
||||
<AlertWithLinearProgress
|
||||
title={
|
||||
<>
|
||||
Discovering peers
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Box color="success.main">
|
||||
{progress.quotes_received} online
|
||||
</Box>
|
||||
<Box color="error.main">{progress.quotes_failed} offline</Box>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
progress={progressValue}
|
||||
count={totalOfType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return exhaustiveGuard(status);
|
||||
}
|
||||
|
|
@ -181,11 +210,7 @@ export default function DaemonStatusAlert() {
|
|||
|
||||
switch (contextStatus) {
|
||||
case TauriContextStatusEvent.Initializing:
|
||||
return (
|
||||
<LoadingSpinnerAlert severity="warning">
|
||||
Core components are loading
|
||||
</LoadingSpinnerAlert>
|
||||
);
|
||||
return null;
|
||||
case TauriContextStatusEvent.Available:
|
||||
return <Alert severity="success">The daemon is running</Alert>;
|
||||
case TauriContextStatusEvent.Failed:
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { ListSellersResponse } from "models/tauriModel";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import {
|
||||
listSellersAtRendezvousPoint,
|
||||
PRESET_RENDEZVOUS_POINTS,
|
||||
} from "renderer/rpc";
|
||||
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
|
||||
import { useAppDispatch } from "store/hooks";
|
||||
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
||||
|
||||
type ListSellersDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function ListSellersDialog({
|
||||
open,
|
||||
onClose,
|
||||
}: ListSellersDialogProps) {
|
||||
const [rendezvousAddress, setRendezvousAddress] = useState("");
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
function handleMultiAddrChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
setRendezvousAddress(event.target.value);
|
||||
}
|
||||
|
||||
function getMultiAddressError(): string | null {
|
||||
return isValidMultiAddressWithPeerId(rendezvousAddress)
|
||||
? null
|
||||
: "Address is invalid or missing peer ID";
|
||||
}
|
||||
|
||||
function handleSuccess({ sellers }: ListSellersResponse) {
|
||||
dispatch(discoveredMakersByRendezvous(sellers));
|
||||
|
||||
const discoveredSellersCount = sellers.length;
|
||||
let message: string;
|
||||
|
||||
switch (discoveredSellersCount) {
|
||||
case 0:
|
||||
message = `No makers were discovered at the rendezvous point`;
|
||||
break;
|
||||
case 1:
|
||||
message = `Discovered one maker at the rendezvous point`;
|
||||
break;
|
||||
default:
|
||||
message = `Discovered ${discoveredSellersCount} makers at the rendezvous point`;
|
||||
}
|
||||
|
||||
enqueueSnackbar(message, {
|
||||
variant: "success",
|
||||
autoHideDuration: 5000,
|
||||
});
|
||||
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} open={open}>
|
||||
<DialogTitle>Discover makers</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<DialogContentText>
|
||||
The rendezvous protocol provides a way to discover makers (trading
|
||||
partners) without relying on one singular centralized institution. By
|
||||
manually connecting to a rendezvous point run by a volunteer, you can
|
||||
discover makers and then connect and swap with them.
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label="Rendezvous point"
|
||||
fullWidth
|
||||
helperText={
|
||||
getMultiAddressError() || "Multiaddress of the rendezvous point"
|
||||
}
|
||||
value={rendezvousAddress}
|
||||
onChange={handleMultiAddrChange}
|
||||
placeholder="/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE"
|
||||
error={!!getMultiAddressError()}
|
||||
/>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
|
||||
{PRESET_RENDEZVOUS_POINTS.map((rAddress) => (
|
||||
<Chip
|
||||
key={rAddress}
|
||||
clickable
|
||||
label={<TruncatedText limit={30}>{rAddress}</TruncatedText>}
|
||||
onClick={() => setRendezvousAddress(rAddress)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<PromiseInvokeButton
|
||||
variant="contained"
|
||||
disabled={
|
||||
// We disable the button if the multiaddress is invalid
|
||||
getMultiAddressError() !== null
|
||||
}
|
||||
color="primary"
|
||||
onSuccess={handleSuccess}
|
||||
onInvoke={() => listSellersAtRendezvousPoint(rendezvousAddress)}
|
||||
>
|
||||
Connect
|
||||
</PromiseInvokeButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -86,14 +86,12 @@ export default function MakerInfo({ maker }: { maker: ExtendedMakerStatus }) {
|
|||
<Chip label={`${Math.round(maker.uptime * 100)}% uptime`} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{maker.age ? (
|
||||
{maker.age && (
|
||||
<Chip
|
||||
label={`Went online ${Math.round(secondsToDays(maker.age))} ${
|
||||
maker.age === 1 ? "day" : "days"
|
||||
} ago`}
|
||||
/>
|
||||
) : (
|
||||
<Chip label="Discovered via rendezvous point" />
|
||||
)}
|
||||
{maker.recommended === true && (
|
||||
<Tooltip title="This maker has shown to be exceptionally reliable">
|
||||
|
|
@ -105,6 +103,11 @@ export default function MakerInfo({ maker }: { maker: ExtendedMakerStatus }) {
|
|||
<Chip label="Outdated" icon={<WarningIcon />} color="primary" />
|
||||
</Tooltip>
|
||||
)}
|
||||
{maker.version && (
|
||||
<Tooltip title="The version of the maker's software">
|
||||
<Chip label={`v${maker.version}`} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<MakerMarkupChip maker={maker} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { ExtendedMakerStatus } from "models/apiModel";
|
|||
import { useState } from "react";
|
||||
import { setSelectedMaker } from "store/features/makersSlice";
|
||||
import { useAllMakers, useAppDispatch } from "store/hooks";
|
||||
import ListSellersDialog from "../listSellers/ListSellersDialog";
|
||||
import MakerInfo from "./MakerInfo";
|
||||
import MakerSubmitDialog from "./MakerSubmitDialog";
|
||||
|
||||
|
|
@ -50,30 +49,6 @@ export function MakerSubmitDialogOpenButton() {
|
|||
);
|
||||
}
|
||||
|
||||
export function ListSellersDialogOpenButton() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
autoFocus
|
||||
onClick={() => {
|
||||
// Prevents background from being clicked and reopening dialog
|
||||
if (!open) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListSellersDialog open={open} onClose={() => setOpen(false)} />
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<SearchIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Discover makers by connecting to a rendezvous point" />
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MakerListDialog({
|
||||
open,
|
||||
onClose,
|
||||
|
|
@ -99,7 +74,6 @@ export default function MakerListDialog({
|
|||
<MakerInfo maker={maker} key={maker.peerId} />
|
||||
</ListItemButton>
|
||||
))}
|
||||
<ListSellersDialogOpenButton />
|
||||
<MakerSubmitDialogOpenButton />
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
|
|
|||
58
src-gui/src/renderer/components/pages/help/DiscoveryBox.tsx
Normal file
58
src-gui/src/renderer/components/pages/help/DiscoveryBox.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { Box, Typography, styled } from "@mui/material";
|
||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||
import { useSettings } from "store/hooks";
|
||||
import { Search } from "@mui/icons-material";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { listSellersAtRendezvousPoint } from "renderer/rpc";
|
||||
import { useAppDispatch } from "store/hooks";
|
||||
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
const StyledPromiseButton = styled(PromiseInvokeButton)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
}));
|
||||
|
||||
export default function DiscoveryBox() {
|
||||
const rendezvousPoints = useSettings((s) => s.rendezvousPoints);
|
||||
const dispatch = useAppDispatch();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const handleDiscovery = async () => {
|
||||
const { sellers } = await listSellersAtRendezvousPoint(rendezvousPoints);
|
||||
dispatch(discoveredMakersByRendezvous(sellers));
|
||||
};
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
title={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
Discover Makers
|
||||
</Box>
|
||||
}
|
||||
mainContent={
|
||||
<Typography variant="subtitle2">
|
||||
By connecting to rendezvous points run by volunteers, you can discover
|
||||
makers and then connect and swap with them in a decentralized manner.
|
||||
You have {rendezvousPoints.length} stored rendezvous{" "}
|
||||
{rendezvousPoints.length === 1 ? "point" : "points"} which we will
|
||||
connect to. We will also attempt to connect to peers which you have
|
||||
previously connected to.
|
||||
</Typography>
|
||||
}
|
||||
additionalContent={
|
||||
<StyledPromiseButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onInvoke={handleDiscovery}
|
||||
disabled={rendezvousPoints.length === 0}
|
||||
startIcon={<Search />}
|
||||
displayErrorSnackbar
|
||||
>
|
||||
Discover Makers
|
||||
</StyledPromiseButton>
|
||||
}
|
||||
icon={null}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -21,19 +21,20 @@ import {
|
|||
Switch,
|
||||
SelectChangeEvent,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
removeNode,
|
||||
resetSettings,
|
||||
setFetchFiatPrices,
|
||||
setFiatCurrency,
|
||||
} from "store/features/settingsSlice";
|
||||
import {
|
||||
addNode,
|
||||
addRendezvousPoint,
|
||||
Blockchain,
|
||||
FiatCurrency,
|
||||
moveUpNode,
|
||||
Network,
|
||||
removeNode,
|
||||
removeRendezvousPoint,
|
||||
resetSettings,
|
||||
setFetchFiatPrices,
|
||||
setFiatCurrency,
|
||||
setTheme,
|
||||
setTorEnabled,
|
||||
} from "store/features/settingsSlice";
|
||||
import { useAppDispatch, useNodes, useSettings } from "store/hooks";
|
||||
import ValidatedTextField from "renderer/components/other/ValidatedTextField";
|
||||
|
|
@ -49,8 +50,8 @@ import {
|
|||
} from "@mui/icons-material";
|
||||
import { getNetwork } from "store/config";
|
||||
import { currencySymbol } from "utils/formatUtils";
|
||||
import { setTorEnabled } from "store/features/settingsSlice";
|
||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
||||
|
||||
const PLACEHOLDER_ELECTRUM_RPC_URL = "ssl://blockstream.info:700";
|
||||
const PLACEHOLDER_MONERO_NODE_URL = "http://xmr-node.cakewallet.com:18081";
|
||||
|
|
@ -85,6 +86,7 @@ export default function SettingsBox() {
|
|||
<MoneroNodeUrlSetting />
|
||||
<FetchFiatPricesSetting />
|
||||
<ThemeSetting />
|
||||
<RendezvousPointsSetting />
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
|
@ -590,3 +592,121 @@ export function TorSettings() {
|
|||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A setting that allows you to manage rendezvous points for maker discovery
|
||||
*/
|
||||
function RendezvousPointsSetting() {
|
||||
const [tableVisible, setTableVisible] = useState(false);
|
||||
const rendezvousPoints = useSettings((s) => s.rendezvousPoints);
|
||||
const dispatch = useAppDispatch();
|
||||
const [newPoint, setNewPoint] = useState("");
|
||||
|
||||
const onAddNewPoint = () => {
|
||||
dispatch(addRendezvousPoint(newPoint));
|
||||
setNewPoint("");
|
||||
};
|
||||
|
||||
const onRemovePoint = (point: string) => {
|
||||
dispatch(removeRendezvousPoint(point));
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<SettingLabel
|
||||
label="Rendezvous Points"
|
||||
tooltip="These are the points where makers can be discovered. Add custom rendezvous points here to expand your maker discovery options."
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton onClick={() => setTableVisible(true)}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
{tableVisible && (
|
||||
<Dialog
|
||||
open={true}
|
||||
onClose={() => setTableVisible(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Rendezvous Points</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="subtitle2">
|
||||
Add or remove rendezvous points where makers can be discovered.
|
||||
These points help you find trading partners in a decentralized
|
||||
way.
|
||||
</Typography>
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
style={{ marginTop: "1rem" }}
|
||||
elevation={0}
|
||||
>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{ width: "85%" }}>
|
||||
Rendezvous Point
|
||||
</TableCell>
|
||||
<TableCell style={{ width: "15%" }} align="right">
|
||||
Actions
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rendezvousPoints.map((point, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell style={{ wordBreak: "break-all" }}>
|
||||
<Typography variant="overline">{point}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Remove this rendezvous point">
|
||||
<IconButton onClick={() => onRemovePoint(point)}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<ValidatedTextField
|
||||
label="Add new rendezvous point"
|
||||
value={newPoint}
|
||||
onValidatedChange={setNewPoint}
|
||||
placeholder="/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE"
|
||||
fullWidth
|
||||
isValid={isValidMultiAddressWithPeerId}
|
||||
variant="outlined"
|
||||
noErrorWhenEmpty
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Add this rendezvous point">
|
||||
<IconButton
|
||||
onClick={onAddNewPoint}
|
||||
disabled={
|
||||
!isValidMultiAddressWithPeerId(newPoint) ||
|
||||
newPoint.length === 0
|
||||
}
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setTableVisible(false)} size="large">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import DonateInfoBox from "./DonateInfoBox";
|
|||
import DaemonControlBox from "./DaemonControlBox";
|
||||
import SettingsBox from "./SettingsBox";
|
||||
import ExportDataBox from "./ExportDataBox";
|
||||
import DiscoveryBox from "./DiscoveryBox";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ export default function SettingsPage() {
|
|||
}}
|
||||
>
|
||||
<SettingsBox />
|
||||
<DiscoveryBox />
|
||||
<ExportDataBox />
|
||||
<DaemonControlBox />
|
||||
<DonateInfoBox />
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@ import { ExtendedMakerStatus } from "models/apiModel";
|
|||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import { satsToBtc } from "utils/conversionUtils";
|
||||
import {
|
||||
ListSellersDialogOpenButton,
|
||||
MakerSubmitDialogOpenButton,
|
||||
} from "../../modal/provider/MakerListDialog";
|
||||
import { MakerSubmitDialogOpenButton } from "../../modal/provider/MakerListDialog";
|
||||
import MakerSelect from "../../modal/provider/MakerSelect";
|
||||
import SwapDialog from "../../modal/swap/SwapDialog";
|
||||
|
||||
|
|
@ -162,7 +159,7 @@ function HasNoMakersSwapWidget() {
|
|||
</ul>
|
||||
</Typography>
|
||||
<Box>
|
||||
<ListSellersDialogOpenButton />
|
||||
<MakerSubmitDialogOpenButton />
|
||||
</Box>
|
||||
</Box>
|
||||
</Alert>
|
||||
|
|
@ -180,7 +177,6 @@ function HasNoMakersSwapWidget() {
|
|||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<MakerSubmitDialogOpenButton />
|
||||
<ListSellersDialogOpenButton />
|
||||
</Box>
|
||||
</Box>
|
||||
</Alert>
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ import { CliLog } from "models/cliModel";
|
|||
import { logsToRawString, parseLogsFromString } from "utils/parseUtils";
|
||||
|
||||
export const PRESET_RENDEZVOUS_POINTS = [
|
||||
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
||||
"/dnsaddr/xxmr.cheap/p2p/12D3KooWMk3QyPS8D1d1vpHZoY7y2MnXdPE5yV6iyPvyuj4zcdxT",
|
||||
];
|
||||
|
||||
export async function fetchSellersAtPresetRendezvousPoints() {
|
||||
await Promise.all(
|
||||
PRESET_RENDEZVOUS_POINTS.map(async (rendezvousPoint) => {
|
||||
const response = await listSellersAtRendezvousPoint(rendezvousPoint);
|
||||
const response = await listSellersAtRendezvousPoint([rendezvousPoint]);
|
||||
store.dispatch(discoveredMakersByRendezvous(response.sellers));
|
||||
|
||||
logger.info(
|
||||
|
|
@ -206,10 +206,10 @@ export async function redactLogs(
|
|||
}
|
||||
|
||||
export async function listSellersAtRendezvousPoint(
|
||||
rendezvousPointAddress: string,
|
||||
rendezvousPointAddresses: string[],
|
||||
): Promise<ListSellersResponse> {
|
||||
return await invoke<ListSellersArgs, ListSellersResponse>("list_sellers", {
|
||||
rendezvous_point: rendezvousPointAddress,
|
||||
rendezvous_points: rendezvousPointAddresses,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { ExtendedMakerStatus, MakerStatus } from "models/apiModel";
|
||||
import { Seller } from "models/tauriModel";
|
||||
import { SellerStatus } from "models/tauriModel";
|
||||
import { getStubTestnetMaker } from "store/config";
|
||||
import { rendezvousSellerToMakerStatus } from "utils/conversionUtils";
|
||||
import { isMakerOutdated } from "utils/multiAddrUtils";
|
||||
|
|
@ -60,7 +60,7 @@ export const makersSlice = createSlice({
|
|||
name: "providers",
|
||||
initialState,
|
||||
reducers: {
|
||||
discoveredMakersByRendezvous(slice, action: PayloadAction<Seller[]>) {
|
||||
discoveredMakersByRendezvous(slice, action: PayloadAction<SellerStatus[]>) {
|
||||
action.payload.forEach((discoveredSeller) => {
|
||||
const discoveredMakerStatus =
|
||||
rendezvousSellerToMakerStatus(discoveredSeller);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Theme } from "renderer/components/theme";
|
||||
|
||||
const DEFAULT_RENDEZVOUS_POINTS = [
|
||||
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
||||
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
|
||||
"/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU",
|
||||
];
|
||||
|
||||
export interface SettingsState {
|
||||
/// This is an ordered list of node urls for each network and blockchain
|
||||
nodes: Record<Network, Record<Blockchain, string[]>>;
|
||||
|
|
@ -12,6 +18,8 @@ export interface SettingsState {
|
|||
/// Whether to enable Tor for p2p connections
|
||||
enableTor: boolean;
|
||||
userHasSeenIntroduction: boolean;
|
||||
/// List of rendezvous points
|
||||
rendezvousPoints: string[];
|
||||
}
|
||||
|
||||
export enum FiatCurrency {
|
||||
|
|
@ -112,6 +120,7 @@ const initialState: SettingsState = {
|
|||
fiatCurrency: FiatCurrency.Usd,
|
||||
enableTor: true,
|
||||
userHasSeenIntroduction: false,
|
||||
rendezvousPoints: DEFAULT_RENDEZVOUS_POINTS,
|
||||
};
|
||||
|
||||
const alertsSlice = createSlice({
|
||||
|
|
@ -147,6 +156,14 @@ const alertsSlice = createSlice({
|
|||
setFiatCurrency(slice, action: PayloadAction<FiatCurrency>) {
|
||||
slice.fiatCurrency = action.payload;
|
||||
},
|
||||
addRendezvousPoint(slice, action: PayloadAction<string>) {
|
||||
slice.rendezvousPoints.push(action.payload);
|
||||
},
|
||||
removeRendezvousPoint(slice, action: PayloadAction<string>) {
|
||||
slice.rendezvousPoints = slice.rendezvousPoints.filter(
|
||||
(point) => point !== action.payload,
|
||||
);
|
||||
},
|
||||
addNode(
|
||||
slice,
|
||||
action: PayloadAction<{
|
||||
|
|
@ -202,6 +219,8 @@ export const {
|
|||
setFiatCurrency,
|
||||
setTorEnabled,
|
||||
setUserHasSeenIntroduction,
|
||||
addRendezvousPoint,
|
||||
removeRendezvousPoint,
|
||||
} = alertsSlice.actions;
|
||||
|
||||
export default alertsSlice.reducer;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MakerStatus } from "models/apiModel";
|
||||
import { Seller } from "models/tauriModel";
|
||||
import { MakerStatus, ExtendedMakerStatus } from "models/apiModel";
|
||||
import { SellerStatus } from "models/tauriModel";
|
||||
import { isTestnet } from "store/config";
|
||||
import { splitPeerIdFromMultiAddress } from "./parseUtils";
|
||||
|
||||
|
|
@ -48,21 +48,20 @@ export function secondsToDays(seconds: number): number {
|
|||
// which we use internally to represent the status of a provider. This provides consistency between
|
||||
// the models returned by the public registry and the models used internally.
|
||||
export function rendezvousSellerToMakerStatus(
|
||||
seller: Seller,
|
||||
): MakerStatus | null {
|
||||
if (seller.status.type === "Unreachable") {
|
||||
seller: SellerStatus,
|
||||
): ExtendedMakerStatus | null {
|
||||
if (seller.type === "Unreachable") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [multiAddr, peerId] = splitPeerIdFromMultiAddress(seller.multiaddr);
|
||||
|
||||
return {
|
||||
maxSwapAmount: seller.status.content.max_quantity,
|
||||
minSwapAmount: seller.status.content.min_quantity,
|
||||
price: seller.status.content.price,
|
||||
peerId,
|
||||
multiAddr,
|
||||
maxSwapAmount: seller.content.quote.max_quantity,
|
||||
minSwapAmount: seller.content.quote.min_quantity,
|
||||
price: seller.content.quote.price,
|
||||
peerId: seller.content.peer_id,
|
||||
multiAddr: seller.content.multiaddr,
|
||||
testnet: isTestnet(),
|
||||
version: seller.content.version,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export function sortMakerList(list: ExtendedMakerStatus[]) {
|
|||
(m) => (m.relevancy == null ? 1 : 0),
|
||||
// Prefer makers with a higher relevancy score
|
||||
(m) => -(m.relevancy ?? 0),
|
||||
// Prefer makers with a minimum quantity > 0
|
||||
(m) => ((m.minSwapAmount ?? 0) > 0 ? 0 : 1),
|
||||
// Prefer makers with a lower price
|
||||
(m) => m.price,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1033,7 +1033,7 @@
|
|||
dependencies:
|
||||
"@tauri-apps/api" "^2.0.0"
|
||||
|
||||
"@tauri-apps/plugin-updater@>=2.7.1":
|
||||
"@tauri-apps/plugin-updater@2.7.1":
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.7.1.tgz#82adcfd06cdd4bc6d64343c8934b700c9174913a"
|
||||
integrity sha512-1OPqEY/z7NDVSeTEMIhD2ss/vXWdpfZ5Th2Mk0KtPR/RA6FKuOTDGZQhxoyYBk0pcZJ+nNZUbl/IujDCLBApjA==
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n insert into peer_addresses (\n peer_id,\n address\n ) values (?, ?);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0ab84c094964968e96a3f2bf590d9ae92227d057386921e0e57165b887de3c75"
|
||||
}
|
||||
12
swap/.sqlx/query-50ef34b4efabe650d40096a390d9240b9a7cd62878dfaa6805563cfc21284cd5.json
generated
Normal file
12
swap/.sqlx/query-50ef34b4efabe650d40096a390d9240b9a7cd62878dfaa6805563cfc21284cd5.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n insert or ignore into peer_addresses (\n peer_id,\n address\n ) values (?, ?);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "50ef34b4efabe650d40096a390d9240b9a7cd62878dfaa6805563cfc21284cd5"
|
||||
}
|
||||
26
swap/.sqlx/query-6130b6cdd184181f890964eb460741f5cf23b5237fb676faed009106627a4ca6.json
generated
Normal file
26
swap/.sqlx/query-6130b6cdd184181f890964eb460741f5cf23b5237fb676faed009106627a4ca6.json
generated
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT peer_id, address FROM peer_addresses",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "peer_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "address",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6130b6cdd184181f890964eb460741f5cf23b5237fb676faed009106627a4ca6"
|
||||
}
|
||||
|
|
@ -55,6 +55,7 @@ reqwest = { version = "0.12", features = ["http2", "rustls-tls-native-roots", "s
|
|||
rust_decimal = { version = "1", features = ["serde-float"] }
|
||||
rust_decimal_macros = "1"
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_cbor = "0.11"
|
||||
serde_json = "1"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
-- SQLite doesn't support adding constraints via ALTER TABLE
|
||||
-- We need to recreate the table with the constraint
|
||||
CREATE TABLE peer_addresses_new (
|
||||
peer_id TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
UNIQUE(peer_id, address)
|
||||
);
|
||||
|
||||
-- Copy existing data, ensuring only unique combinations are inserted
|
||||
INSERT INTO peer_addresses_new
|
||||
SELECT DISTINCT peer_id, address
|
||||
FROM peer_addresses;
|
||||
|
||||
-- Drop the old table
|
||||
DROP TABLE peer_addresses;
|
||||
|
||||
-- Rename the new table to the original name
|
||||
ALTER TABLE peer_addresses_new RENAME TO peer_addresses;
|
||||
|
|
@ -263,15 +263,15 @@ where
|
|||
let idx = self.next.load(Ordering::SeqCst);
|
||||
|
||||
// Get client for this index
|
||||
let client = self.get_or_init_client_sync(idx).map_err(|e| {
|
||||
let client = self.get_or_init_client_sync(idx).map_err(|err| {
|
||||
trace!(
|
||||
server_url = self.urls[idx],
|
||||
attempt = errors.len(),
|
||||
error = ?e,
|
||||
error = ?err,
|
||||
"Client initialization failed, switching to next client"
|
||||
);
|
||||
|
||||
errors.push(e);
|
||||
errors.push(err);
|
||||
|
||||
BackoffError::transient(())
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ pub mod watcher;
|
|||
pub use behaviour::{Behaviour, OutEvent};
|
||||
pub use cancel_and_refund::{cancel, cancel_and_refund, refund};
|
||||
pub use event_loop::{EventLoop, EventLoopHandle};
|
||||
pub use list_sellers::{list_sellers, Seller, Status as SellerStatus};
|
||||
pub use list_sellers::{list_sellers, SellerStatus};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::asb;
|
||||
use crate::asb::rendezvous::RendezvousNode;
|
||||
use crate::cli::list_sellers::{Seller, Status};
|
||||
use crate::cli::list_sellers::{QuoteWithAddress, SellerStatus};
|
||||
use crate::network::quote;
|
||||
use crate::network::quote::BidQuote;
|
||||
use crate::network::rendezvous::XmrBtcNamespace;
|
||||
|
|
@ -29,11 +29,18 @@ mod tests {
|
|||
ConnectionDenied, ConnectionId, FromSwarm, THandlerInEvent, THandlerOutEvent, ToSwarm,
|
||||
};
|
||||
use libp2p::{identity, rendezvous, request_response, Multiaddr, PeerId};
|
||||
use semver::Version;
|
||||
use std::collections::HashSet;
|
||||
use std::iter::FromIterator;
|
||||
use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
|
||||
// Test-only struct for compatibility
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct Seller {
|
||||
multiaddr: Multiaddr,
|
||||
status: SellerStatus,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
// Due to an issue with the libp2p rendezvous library
|
||||
|
|
@ -52,19 +59,32 @@ mod tests {
|
|||
let expected_seller_2 = setup_asb(rendezvous_peer_id, &rendezvous_address, namespace).await;
|
||||
|
||||
let list_sellers = list_sellers(
|
||||
rendezvous_peer_id,
|
||||
rendezvous_address,
|
||||
vec![(rendezvous_peer_id, rendezvous_address)],
|
||||
namespace,
|
||||
None,
|
||||
identity::Keypair::generate_ed25519(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let sellers = tokio::time::timeout(Duration::from_secs(15), list_sellers)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Convert SellerStatus to test Seller struct
|
||||
let actual_sellers: Vec<Seller> = sellers
|
||||
.into_iter()
|
||||
.map(|status| Seller {
|
||||
multiaddr: match &status {
|
||||
SellerStatus::Online(quote_with_addr) => quote_with_addr.multiaddr.clone(),
|
||||
SellerStatus::Unreachable(_) => "/ip4/0.0.0.0/tcp/0".parse().unwrap(), // placeholder
|
||||
},
|
||||
status,
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
HashSet::<Seller>::from_iter(sellers),
|
||||
HashSet::<Seller>::from_iter(actual_sellers),
|
||||
HashSet::<Seller>::from_iter([expected_seller_1, expected_seller_2])
|
||||
)
|
||||
}
|
||||
|
|
@ -126,9 +146,15 @@ mod tests {
|
|||
}
|
||||
});
|
||||
|
||||
let full_address = asb_address.with(Protocol::P2p(asb_peer_id));
|
||||
Seller {
|
||||
multiaddr: asb_address.with(Protocol::P2p(asb_peer_id)),
|
||||
status: Status::Online(static_quote),
|
||||
multiaddr: full_address.clone(),
|
||||
status: SellerStatus::Online(QuoteWithAddress {
|
||||
multiaddr: full_address,
|
||||
peer_id: asb_peer_id,
|
||||
quote: static_quote,
|
||||
version: Version::parse("1.0.0").unwrap(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use super::tauri_bindings::TauriHandle;
|
|||
use crate::bitcoin::{wallet, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxLock};
|
||||
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent};
|
||||
use crate::cli::api::Context;
|
||||
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, Seller, SellerStatus};
|
||||
use crate::cli::list_sellers::{QuoteWithAddress, UnreachableSeller};
|
||||
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus};
|
||||
use crate::common::{get_logs, redact};
|
||||
use crate::libp2p_ext::MultiAddrExt;
|
||||
use crate::monero::wallet_rpc::MoneroDaemon;
|
||||
|
|
@ -172,14 +173,16 @@ impl Request for WithdrawBtcArgs {
|
|||
#[typeshare]
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ListSellersArgs {
|
||||
#[typeshare(serialized_as = "string")]
|
||||
pub rendezvous_point: Multiaddr,
|
||||
/// The rendezvous points to search for sellers
|
||||
/// The address must contain a peer ID
|
||||
#[typeshare(serialized_as = "Vec<string>")]
|
||||
pub rendezvous_points: Vec<Multiaddr>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct ListSellersResponse {
|
||||
sellers: Vec<Seller>,
|
||||
sellers: Vec<SellerStatus>,
|
||||
}
|
||||
|
||||
impl Request for ListSellersArgs {
|
||||
|
|
@ -1079,10 +1082,11 @@ pub async fn list_sellers(
|
|||
list_sellers: ListSellersArgs,
|
||||
context: Arc<Context>,
|
||||
) -> Result<ListSellersResponse> {
|
||||
let ListSellersArgs { rendezvous_point } = list_sellers;
|
||||
let rendezvous_node_peer_id = rendezvous_point
|
||||
.extract_peer_id()
|
||||
.context("Rendezvous node address must contain peer ID")?;
|
||||
let ListSellersArgs { rendezvous_points } = list_sellers;
|
||||
let rendezvous_nodes: Vec<_> = rendezvous_points
|
||||
.iter()
|
||||
.filter_map(|rendezvous_point| rendezvous_point.split_peer_id())
|
||||
.collect();
|
||||
|
||||
let identity = context
|
||||
.config
|
||||
|
|
@ -1092,30 +1096,46 @@ pub async fn list_sellers(
|
|||
.derive_libp2p_identity();
|
||||
|
||||
let sellers = list_sellers_impl(
|
||||
rendezvous_node_peer_id,
|
||||
rendezvous_point,
|
||||
rendezvous_nodes,
|
||||
context.config.namespace,
|
||||
context.tor_client.clone(),
|
||||
identity,
|
||||
Some(context.db.clone()),
|
||||
context.tauri_handle(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for seller in &sellers {
|
||||
match seller.status {
|
||||
SellerStatus::Online(quote) => {
|
||||
tracing::info!(
|
||||
match seller {
|
||||
SellerStatus::Online(QuoteWithAddress {
|
||||
quote,
|
||||
multiaddr,
|
||||
peer_id,
|
||||
version,
|
||||
}) => {
|
||||
tracing::debug!(
|
||||
status = "Online",
|
||||
price = %quote.price.to_string(),
|
||||
min_quantity = %quote.min_quantity.to_string(),
|
||||
max_quantity = %quote.max_quantity.to_string(),
|
||||
status = "Online",
|
||||
address = %seller.multiaddr.to_string(),
|
||||
address = %multiaddr.clone().to_string(),
|
||||
peer_id = %peer_id,
|
||||
version = %version,
|
||||
"Fetched peer status"
|
||||
);
|
||||
|
||||
// Add the peer as known to the database
|
||||
// This'll allow us to later request a quote again
|
||||
// without having to re-discover the peer at the rendezvous point
|
||||
context
|
||||
.db
|
||||
.insert_address(*peer_id, multiaddr.clone())
|
||||
.await?;
|
||||
}
|
||||
SellerStatus::Unreachable => {
|
||||
tracing::info!(
|
||||
SellerStatus::Unreachable(UnreachableSeller { peer_id }) => {
|
||||
tracing::debug!(
|
||||
status = "Unreachable",
|
||||
address = %seller.multiaddr.to_string(),
|
||||
peer_id = %peer_id.to_string(),
|
||||
"Fetched peer status"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -546,6 +546,7 @@ pub enum TauriBackgroundProgress {
|
|||
SyncingBitcoinWallet(PendingCompleted<TauriBitcoinSyncProgress>),
|
||||
FullScanningBitcoinWallet(PendingCompleted<TauriBitcoinFullScanProgress>),
|
||||
BackgroundRefund(PendingCompleted<BackgroundRefundProgress>),
|
||||
ListSellers(PendingCompleted<ListSellersProgress>),
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
|
@ -707,3 +708,14 @@ pub struct TauriSettings {
|
|||
/// Whether to initialize and use a tor client.
|
||||
pub use_tor: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct ListSellersProgress {
|
||||
pub rendezvous_points_connected: u32,
|
||||
pub rendezvous_points_total: u32,
|
||||
pub rendezvous_points_failed: u32,
|
||||
pub peers_discovered: u32,
|
||||
pub quotes_received: u32,
|
||||
pub quotes_failed: u32,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,7 +247,9 @@ where
|
|||
.await?,
|
||||
);
|
||||
|
||||
ListSellersArgs { rendezvous_point }
|
||||
ListSellersArgs {
|
||||
rendezvous_points: vec![rendezvous_point],
|
||||
}
|
||||
.request(context.clone())
|
||||
.await?;
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -154,7 +154,7 @@ impl Database for SqliteDatabase {
|
|||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
insert into peer_addresses (
|
||||
insert or ignore into peer_addresses (
|
||||
peer_id,
|
||||
address
|
||||
) values (?, ?);
|
||||
|
|
@ -193,6 +193,42 @@ impl Database for SqliteDatabase {
|
|||
addresses
|
||||
}
|
||||
|
||||
async fn get_all_peer_addresses(&self) -> Result<Vec<(PeerId, Vec<Multiaddr>)>> {
|
||||
let rows = sqlx::query!("SELECT peer_id, address FROM peer_addresses")
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
let mut peer_map: std::collections::HashMap<PeerId, Vec<Multiaddr>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for row in rows.iter() {
|
||||
match (
|
||||
PeerId::from_str(&row.peer_id),
|
||||
Multiaddr::from_str(&row.address),
|
||||
) {
|
||||
(Ok(peer_id), Ok(multiaddr)) => {
|
||||
peer_map.entry(peer_id).or_default().push(multiaddr);
|
||||
}
|
||||
(Err(e), _) => {
|
||||
tracing::warn!(
|
||||
peer_id = %row.peer_id,
|
||||
error = %e,
|
||||
"Failed to parse peer ID, skipping entry"
|
||||
);
|
||||
}
|
||||
(_, Err(e)) => {
|
||||
tracing::warn!(
|
||||
address = %row.address,
|
||||
error = %e,
|
||||
"Failed to parse multiaddr, skipping entry"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(peer_map.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn get_swap_start_date(&self, swap_id: Uuid) -> Result<String> {
|
||||
let swap_id = swap_id.to_string();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use libp2p::{Multiaddr, PeerId};
|
|||
|
||||
pub trait MultiAddrExt {
|
||||
fn extract_peer_id(&self) -> Option<PeerId>;
|
||||
fn split_peer_id(&self) -> Option<(PeerId, Multiaddr)>;
|
||||
}
|
||||
|
||||
impl MultiAddrExt for Multiaddr {
|
||||
|
|
@ -12,4 +13,12 @@ impl MultiAddrExt for Multiaddr {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Takes a peer id like /ip4/192.168.178.64/tcp/9939/p2p/12D3KooWQsqsCyJ9ae1YEAJZAfoVdVFZdDdUq3yvZ92btq7hSv9f
|
||||
// and returns the peer id and the original address *with* the peer id
|
||||
fn split_peer_id(&self) -> Option<(PeerId, Multiaddr)> {
|
||||
let peer_id = self.extract_peer_id()?;
|
||||
let address = self.clone();
|
||||
Some((peer_id, address))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ pub trait Database {
|
|||
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_all_peer_addresses(&self) -> Result<Vec<(PeerId, Vec<Multiaddr>)>>;
|
||||
async fn get_swap_start_date(&self, swap_id: Uuid) -> Result<String>;
|
||||
async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()>;
|
||||
async fn get_state(&self, swap_id: Uuid) -> Result<State>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue