feat: Maker avatar (#205)

- GUI: Changed terminology from "swap providers" to "makers"
- GUI: For each maker, we now display a unique deterministically generated avatar derived from the maker's public key
This commit is contained in:
binarybaron 2024-11-25 20:15:09 +01:00 committed by GitHub
parent 23d22b5792
commit b2e74df37e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 511 additions and 429 deletions

View file

@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- GUI: Changed terminology from "swap providers" to "makers"
- GUI: For each maker, we now display a unique deterministically generated avatar derived from the maker's public key
## [1.0.0-rc.6] - 2024-11-21
- CLI + GUI: Tor is now bundled with the application. All libp2p connections between peers are routed through Tor, if the `--enable-tor` flag is set. The `--tor-socks5-port` argument has been removed. This feature is powered by [arti](https://tpo.pages.torproject.net/core/arti/), an implementation of the Tor protocol in Rust by the Tor Project.

View file

@ -1,22 +1,22 @@
import { useState, useEffect } from "react";
import { Table, Td, Th, Tr } from 'nextra/components'
export default function SwapProviderTable() {
export default function SwapMakerTable() {
function satsToBtc(sats) {
return sats / 100000000;
}
async function getProviders() {
async function getMakers() {
const response = await fetch("https://api.unstoppableswap.net/api/list");
const data = await response.json();
return data;
}
const [providers, setProviders] = useState([]);
const [makers, setMakers] = useState([]);
useEffect(() => {
getProviders().then((data) => {
setProviders(data);
getMakers().then((data) => {
setMakers(data);
});
}, []);
@ -38,16 +38,16 @@ export default function SwapProviderTable() {
</Tr>
</thead>
<tbody>
{providers.map((provider) => (
<Tr key={provider.peerId}>
{makers.map((maker) => (
<Tr key={maker.peerId}>
<Td>
{provider.testnet ? "Testnet" : "Mainnet"}
{maker.testnet ? "Testnet" : "Mainnet"}
</Td>
<Td>{provider.multiAddr}</Td>
<Td>{provider.peerId}</Td>
<Td>{satsToBtc(provider.minSwapAmount)} BTC</Td>
<Td>{satsToBtc(provider.maxSwapAmount)} BTC</Td>
<Td>{satsToBtc(provider.price)} XMR/BTC</Td>
<Td>{maker.multiAddr}</Td>
<Td>{maker.peerId}</Td>
<Td>{satsToBtc(maker.minSwapAmount)} BTC</Td>
<Td>{satsToBtc(maker.maxSwapAmount)} BTC</Td>
<Td>{satsToBtc(maker.price)} XMR/BTC</Td>
</Tr>
))}
</tbody>

View file

@ -1,5 +1,5 @@
{
"first_swap": "Complete your first swap",
"market_maker_discovery": "Swap Provider discovery",
"market_maker_discovery": "Maker discovery",
"refund_punish": "Cancel, Refund and Punish explained"
}

View file

@ -16,16 +16,16 @@ import { Steps } from 'nextra/components'
## Performing the swap
<Steps>
### Choose a _Swap Provider_ to swap with
### Choose a _maker_ to swap with
In the bottom of the screen you can see the currently selected _Swap Provider_.
In the bottom of the screen you can see the currently selected _maker_.
This is who you'll send your Bitcoin to and who you'll receive the Monero from.
You can change the _Swap Provider_ by clicking on the arrow and selecting a different _Swap Provider_ from the list.
You can change the _maker_ by clicking on the arrow and selecting a different _maker_ from the list.
import { Callout } from 'nextra/components'
<Callout type="info">
Different _Swap Providers_ offer different exchange rates and differing amounts of liquidity. You may want to choose the _Swap Provider_ that best suits your needs.
Different _makers_ offer different exchange rates and differing amounts of liquidity. You may want to choose the _maker_ that best suits your needs.
</Callout>
You can use the input field to calculate the approximate amount of Monero you'll receive for a given amount of Bitcoin.
@ -39,7 +39,7 @@ This is only used as a reference for you to get a rough idea of how much Monero
### Start the Swap
Once you've selected a _Swap Provider_, you can start the swap by clicking the `Swap` button.
Once you've selected a _maker_, you can start the swap by clicking the `Swap` button.
This will open a new window where you need to enter two addresses:
1. the Monero address you want to receive the Monero to
@ -54,7 +54,7 @@ After pressing the <img src="/start_swap_button.png" style={{
display: "inline-block",
// center vertically
verticalAlign: "middle",
}}/> button, you'll be shown an offer by the _Swap Provider_. This includes:
}}/> button, you'll be shown an offer by the _maker_. This includes:
- the exchange rate (how much Bitcoin they demand for 1 Monero)
- the minimum and maximum amounts you can swap
@ -88,10 +88,10 @@ The swap will go through four stages:
1. **Locking the Bitcoin**:
Your Bitcoin is locked in a 2-of-2 multisig address.
2. **_Swap Provider_ locks the Monero**:
2. **_Maker_ locks the Monero**:
The other party locks their Monero as well.
3. **_Swap Provider_ redeems _Bitcoin_**:
3. **_Maker_ redeems _Bitcoin_**:
The other party redeems the Bitcoin.
4. **Redeeming the Monero**:

View file

@ -1,16 +1,16 @@
# _Swap Provider_ discovery
# _Maker_ discovery
A _Swap Provider_ is a service run by a pseudonymous entity that offers to sell Monero in exchange for Bitcoin. To swap your Bitcoin for Monero you need to connect to a one of these _Swap Providers_.
The different ways to discover _Swap Providers_ are described below.
A _maker_ is a service run by a pseudonymous entity that offers to sell Monero in exchange for Bitcoin. To swap your Bitcoin for Monero you need to connect to a one of these _makers_.
The different ways to discover _makers_ are described below.
There are two ways to discover _Swap Providers_:
There are two ways to discover _makers_:
1. **Public Registry**: Community volunteers maintain a list of _Swap Providers_ that is provided to the GUI and is kept up to date automatically. This list is displayed in the GUI by default. The _Public Registry_ also stores additional information about the _Swap Providers_ such as their uptime and age, and makes it available to the GUI.
2. **Rendezvous**: The GUI can discover Swap Providers using the [_Rendezvous_ protocol](https://docs.libp2p.io/concepts/discovery-routing/rendezvous/). This protocol enables the GUI to find providers that register themselves at a _Rendezvous Point_. The GUI can query these points to get a list of registered providers. _Rendezvous Points_ are operated by community volunteers, and anyone can run one. The GUI can connect to various _Rendezvous Points_ to discover different _Swap Providers_.
1. **Public Registry**: Community volunteers maintain a list of _makers_ that is provided to the GUI and is kept up to date automatically. This list is displayed in the GUI by default. The _Public Registry_ also stores additional information about the _makers_ such as their uptime and age, and makes it available to the GUI.
2. **Rendezvous**: The GUI can discover makers using the [_Rendezvous_ protocol](https://docs.libp2p.io/concepts/discovery-routing/rendezvous/). This protocol enables the GUI to find makers that register themselves at a _Rendezvous Point_. The GUI can query these points to get a list of registered makers. _Rendezvous Points_ are operated by community volunteers, and anyone can run one. The GUI can connect to various _Rendezvous Points_ to discover different _makers_.
## _Public Registry_
The providers from the registry are displayed in the GUI. If you want to connect to them directly without the GUI choose one from the table below.
The makers from the registry are displayed in the GUI. If you want to connect to them directly without the GUI choose one from the table below.
import SwapProviderTable from "../../components/SwapProviderTable";
@ -19,9 +19,9 @@ import SwapProviderTable from "../../components/SwapProviderTable";
<SwapProviderTable />
</div>
## How to discover _Swap Providers_ via _Rendezvous_
## How to discover _makers_ via _Rendezvous_
1. Open the _Swap Provider_ list by clicking the right-facing arrow in the widget on the _Swap_ tab.
1. Open the _maker_ list by clicking the right-facing arrow in the widget on the _Swap_ tab.
<img src="/rendezvous_1.png" />
@ -30,14 +30,14 @@ import SwapProviderTable from "../../components/SwapProviderTable";
display: "inline-block",
// center vertically
verticalAlign: "middle",
}}/> button to open the _Discover swap providers_ dialog. Enter the _Multiaddress_ of the _Rendezvous Point_ you want to connect to. You can also choose one of the predined ones from the list below the Textfield. Click the _Connect_ button to connect to the rendezvous point.
}}/> button to open the _Discover makers_ dialog. Enter the _Multiaddress_ of the _Rendezvous Point_ you want to connect to. You can also choose one of the predined ones from the list below the Textfield. Click the _Connect_ button to connect to the rendezvous point.
<img src="/rendezvous_2.png" />
## How to add a _Swap Provider_ to the _Public Registry_
## How to add a _maker_ to the _Public Registry_
If you know of a _Swap Provider_ that is not yet in the _Public Registry_, you can submit it manually. Here's how you can do it:
If you know of a _maker_ that is not yet in the _Public Registry_, you can submit it manually. Here's how you can do it:
1. Open the _Swap Provider_ list by clicking the right-facing arrow in the widget on the _Swap_ tab.
1. Open the _maker_ list by clicking the right-facing arrow in the widget on the _Swap_ tab.
<img src="/rendezvous_1.png" />
@ -46,5 +46,5 @@ If you know of a _Swap Provider_ that is not yet in the _Public Registry_, you c
display: "inline-block",
// center vertically
verticalAlign: "middle",
}}/> button. Enter the _Multiaddress_ of the _Swap Provider_ as well as the _Peer ID_ of the provider. Click the _Submit_ button to submit the provider to the _Public Registry_.
}}/> button. Enter the _Multiaddress_ of the _maker_ as well as the _Peer ID_ of the provider. Click the _Submit_ button to submit the provider to the _Public Registry_.
<img src="/public_registry.png" />

View file

@ -8,17 +8,17 @@ We have chosen to include a fairly technical explanation here. However, the GUI
## Cancel
If the _Swap Provider_ has not been able to redeem the Bitcoin within 12 hours (72 Bitcoin blocks) from the start of the swap, the swap will be cancelled.
This is done by either you or the _Swap Provider_ publishing a special Bitcoin transaction called the `Bitcoin Cancel Transaction`.
If the _maker_ has not been able to redeem the Bitcoin within 12 hours (72 Bitcoin blocks) from the start of the swap, the swap will be cancelled.
This is done by either you or the _maker_ publishing a special Bitcoin transaction called the `Bitcoin Cancel Transaction`.
As soon as this transaction is included in the Bitcoin blockchain, the swap is locked in a state where only the [_Refund_](#refund) and [_Punish_](#punish) paths can be activated. The _Happy Path_ path where you redeem the Monero is no longer possible.
## Refund
As soon as the swap is cancelled, you can refund your Bitcoin. This is done by publishing the `Bitcoin Refund Transaction` on the Bitcoin blockchain.
If this is done within 24 hours (144 Bitcoin blocks) from the inclusion of the `Bitcoin Cancel Transaction`, you will get your Bitcoin back.
If you do not refund your Bitcoin within this time frame, the _Swap Provider_ can punish you. This is a security measure to ensure that you do not cancel the swap and then refuse to refund your Bitcoin which would result in the _Swap Provider_ losing their Monero.
If you do not refund your Bitcoin within this time frame, the _maker_ can punish you. This is a security measure to ensure that you do not cancel the swap and then refuse to refund your Bitcoin which would result in the _maker_ losing their Monero.
## Punish
If you do not refund your Bitcoin within 24 hours (144 Bitcoin blocks) from the inclusion of the `Bitcoin Cancel Transaction`, the _Swap Provider_ will _punish_ you. This will result in the _Swap Provider_ taking your Bitcoin as a penalty for not refunding it in time.
Even if this state is reached and the _Swap Provider_ has punished you, there's still hope to redeem the Monero. The _Swap Provider_ can choose to allow you to redeem the Monero by transmitting a secret key to you. This however is at the discretion of the _Swap Provider_ and they are not obligated to do so.
If you do not refund your Bitcoin within 24 hours (144 Bitcoin blocks) from the inclusion of the `Bitcoin Cancel Transaction`, the _maker_ will _punish_ you. This will result in the _maker_ taking your Bitcoin as a penalty for not refunding it in time.
Even if this state is reached and the _maker_ has punished you, there's still hope to redeem the Monero. The _maker_ can choose to allow you to redeem the Monero by transmitting a secret key to you. This however is at the discretion of the _maker_ and they are not obligated to do so.

View file

@ -29,6 +29,7 @@
"@tauri-apps/plugin-updater": "^2.0.0",
"@types/react-redux": "^7.1.34",
"humanize-duration": "^3.32.1",
"jdenticon": "^3.3.0",
"lodash": "^4.17.21",
"multiaddr": "^10.0.1",
"notistack": "^3.0.1",

View file

@ -1,4 +1,4 @@
export interface ExtendedProviderStatus extends ProviderStatus {
export interface ExtendedMakerStatus extends MakerStatus {
uptime?: number;
age?: number;
relevancy?: number;
@ -6,15 +6,15 @@ export interface ExtendedProviderStatus extends ProviderStatus {
recommended?: boolean;
}
export interface ProviderStatus extends ProviderQuote, Provider {}
export interface MakerStatus extends MakerQuote, Maker { }
export interface ProviderQuote {
export interface MakerQuote {
price: number;
minSwapAmount: number;
maxSwapAmount: number;
}
export interface Provider {
export interface Maker {
multiAddr: string;
testnet: boolean;
peerId: string;

View file

@ -5,21 +5,21 @@
// - and to submit feedback
// - fetch currency rates from CoinGecko
import { Alert, ExtendedProviderStatus } from "models/apiModel";
import { Alert, ExtendedMakerStatus } from "models/apiModel";
import { store } from "./store/storeRenderer";
import { setBtcPrice, setXmrBtcRate, setXmrPrice } from "store/features/ratesSlice";
import { FiatCurrency } from "store/features/settingsSlice";
import { setAlerts } from "store/features/alertsSlice";
import { registryConnectionFailed, setRegistryProviders } from "store/features/providersSlice";
import { registryConnectionFailed, setRegistryMakers } from "store/features/makersSlice";
import logger from "utils/logger";
const PUBLIC_REGISTRY_API_BASE_URL = "https://api.unstoppableswap.net";
async function fetchProvidersViaHttp(): Promise<
ExtendedProviderStatus[]
async function fetchMakersViaHttp(): Promise<
ExtendedMakerStatus[]
> {
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/list`);
return (await response.json()) as ExtendedProviderStatus[];
return (await response.json()) as ExtendedMakerStatus[];
}
async function fetchAlertsViaHttp(): Promise<Alert[]> {
@ -114,8 +114,8 @@ export async function updateRates(): Promise<void> {
*/
export async function updatePublicRegistry(): Promise<void> {
try {
const providers = await fetchProvidersViaHttp();
store.dispatch(setRegistryProviders(providers));
const providers = await fetchMakersViaHttp();
store.dispatch(setRegistryMakers(providers));
} catch (error) {
store.dispatch(registryConnectionFailed());
logger.error(error, "Error fetching providers");

View file

@ -27,7 +27,7 @@ export default function RemainingFundsWillBeUsedAlert() {
>
The remaining funds of <SatsAmount amount={balance} /> in the wallet
will be used for the next swap. If the remaining funds exceed the
minimum swap amount of the provider, a swap will be initiated
minimum swap amount of the maker, a swap will be initiated
instantaneously.
</Alert>
</Box>

View file

@ -0,0 +1,29 @@
import React, { useEffect, useRef } from 'react';
import * as jdenticon from 'jdenticon';
interface IdentIconProps {
value: string;
size?: number | string;
className?: string;
}
function IdentIcon({ value, size = 40, className = '' }: IdentIconProps) {
const iconRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (iconRef.current) {
jdenticon.update(iconRef.current, value);
}
}, [value]);
return (
<svg
ref={iconRef}
width={size}
height={size}
className={className}
data-jdenticon-value={value} />
);
}
export default IdentIcon;

View file

@ -17,7 +17,7 @@ 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 { discoveredProvidersByRendezvous } from "store/features/providersSlice";
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
import { useAppDispatch } from "store/hooks";
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
@ -54,20 +54,20 @@ export default function ListSellersDialog({
}
function handleSuccess({ sellers }: ListSellersResponse) {
dispatch(discoveredProvidersByRendezvous(sellers));
dispatch(discoveredMakersByRendezvous(sellers));
const discoveredSellersCount = sellers.length;
let message: string;
switch (discoveredSellersCount) {
case 0:
message = `No providers were discovered at the rendezvous point`;
message = `No makers were discovered at the rendezvous point`;
break;
case 1:
message = `Discovered one provider at the rendezvous point`;
message = `Discovered one maker at the rendezvous point`;
break;
default:
message = `Discovered ${discoveredSellersCount} providers at the rendezvous point`;
message = `Discovered ${discoveredSellersCount} makers at the rendezvous point`;
}
enqueueSnackbar(message, {
@ -80,13 +80,13 @@ export default function ListSellersDialog({
return (
<Dialog onClose={onClose} open={open}>
<DialogTitle>Discover swap providers</DialogTitle>
<DialogTitle>Discover makers</DialogTitle>
<DialogContent dividers>
<DialogContentText>
The rendezvous protocol provides a way to discover providers (trading
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 providers and then connect and swap with them.
discover makers and then connect and swap with them.
</DialogContentText>
<TextField
autoFocus

View file

@ -0,0 +1,127 @@
import { Box, Chip, makeStyles, Paper, Tooltip, Typography } from "@material-ui/core";
import { VerifiedUser } from "@material-ui/icons";
import { ExtendedMakerStatus } from "models/apiModel";
import TruncatedText from "renderer/components/other/TruncatedText";
import {
MoneroBitcoinExchangeRate,
SatsAmount,
} from "renderer/components/other/Units";
import { satsToBtc, secondsToDays } from "utils/conversionUtils";
import { isMakerOutdated } from 'utils/multiAddrUtils';
import WarningIcon from '@material-ui/icons/Warning';
import { useAppSelector } from "store/hooks";
import IdentIcon from "renderer/components/icons/IdentIcon";
const useStyles = makeStyles((theme) => ({
content: {
flex: 1,
"& *": {
lineBreak: "anywhere",
},
display: "flex",
flexDirection: "column",
gap: theme.spacing(1),
},
chipsOuter: {
display: "flex",
flexWrap: "wrap",
gap: theme.spacing(0.5),
},
quoteOuter: {
display: "flex",
flexDirection: "column",
},
peerIdContainer: {
display: "flex",
alignItems: "center",
gap: theme.spacing(1),
},
}));
/**
* A chip that displays the markup of the maker's exchange rate compared to the market rate.
*/
function MakerMarkupChip({ maker }: { maker: ExtendedMakerStatus }) {
const marketExchangeRate = useAppSelector(s => s.rates?.xmrBtcRate);
if (marketExchangeRate === null)
return null;
const makerExchangeRate = satsToBtc(maker.price);
/** The markup of the exchange rate compared to the market rate in percent */
const markup = (makerExchangeRate - marketExchangeRate) / marketExchangeRate * 100;
return (
<Tooltip title="The markup this maker charges compared to centralized markets. A lower markup means that you get more Monero for your Bitcoin.">
<Chip label={`Markup ${markup.toFixed(2)}%`} />
</Tooltip>
);
}
export default function MakerInfo({
maker,
}: {
maker: ExtendedMakerStatus;
}) {
const classes = useStyles();
const isOutdated = isMakerOutdated(maker);
return (
<Box className={classes.content}>
<Box className={classes.peerIdContainer}>
<Tooltip title={"This avatar is deterministically derived from the public key of the maker"} arrow>
<Box className={classes.peerIdContainer}>
<IdentIcon value={maker.peerId} size={"3rem"} />
</Box>
</Tooltip>
<Box>
<Typography variant="subtitle1">
<TruncatedText limit={16} truncateMiddle>{maker.peerId}</TruncatedText>
</Typography>
<Typography color="textSecondary" variant="body2">
{maker.multiAddr}
</Typography>
</Box>
</Box>
<Box className={classes.quoteOuter}>
<Typography variant="caption">
Exchange rate:{" "}
<MoneroBitcoinExchangeRate rate={satsToBtc(maker.price)} />
</Typography>
<Typography variant="caption">
Minimum amount: <SatsAmount amount={maker.minSwapAmount} />
</Typography>
<Typography variant="caption">
Maximum amount: <SatsAmount amount={maker.maxSwapAmount} />
</Typography>
</Box>
<Box className={classes.chipsOuter}>
{maker.testnet && <Chip label="Testnet" />}
{maker.uptime && (
<Tooltip title="A high uptime (>90%) indicates reliability. Makers with very low uptime may be unreliable and cause swaps to take longer to complete or fail entirely.">
<Chip label={`${Math.round(maker.uptime * 100)}% uptime`} />
</Tooltip>
)}
{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">
<Chip label="Recommended" icon={<VerifiedUser />} color="primary" />
</Tooltip>
)}
{isOutdated && (
<Tooltip title="This maker is running an older version of the software. Outdated makers may be unreliable and cause swaps to take longer to complete or fail entirely.">
<Chip label="Outdated" icon={<WarningIcon />} color="primary" />
</Tooltip>
)}
<MakerMarkupChip maker={maker} />
</Box>
</Box >
);
}

View file

@ -13,13 +13,13 @@ import {
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import SearchIcon from "@material-ui/icons/Search";
import { ExtendedProviderStatus } from "models/apiModel";
import { ExtendedMakerStatus } from "models/apiModel";
import { useState } from "react";
import { setSelectedProvider } from "store/features/providersSlice";
import { useAllProviders, useAppDispatch } from "store/hooks";
import { setSelectedMaker } from "store/features/makersSlice";
import { useAllMakers, useAppDispatch } from "store/hooks";
import ListSellersDialog from "../listSellers/ListSellersDialog";
import ProviderInfo from "./ProviderInfo";
import ProviderSubmitDialog from "./ProviderSubmitDialog";
import MakerInfo from "./MakerInfo";
import MakerSubmitDialog from "./MakerSubmitDialog";
const useStyles = makeStyles({
dialogContent: {
@ -27,12 +27,12 @@ const useStyles = makeStyles({
},
});
type ProviderSelectDialogProps = {
type MakerSelectDialogProps = {
open: boolean;
onClose: () => void;
};
export function ProviderSubmitDialogOpenButton() {
export function MakerSubmitDialogOpenButton() {
const [open, setOpen] = useState(false);
return (
@ -46,13 +46,13 @@ export function ProviderSubmitDialogOpenButton() {
}
}}
>
<ProviderSubmitDialog open={open} onClose={() => setOpen(false)} />
<MakerSubmitDialog open={open} onClose={() => setOpen(false)} />
<ListItemAvatar>
<Avatar>
<AddIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Add a new provider to public registry" />
<ListItemText primary="Add a new maker to public registry" />
</ListItem>
);
}
@ -77,41 +77,41 @@ export function ListSellersDialogOpenButton() {
<SearchIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Discover providers by connecting to a rendezvous point" />
<ListItemText primary="Discover makers by connecting to a rendezvous point" />
</ListItem>
);
}
export default function ProviderListDialog({
export default function MakerListDialog({
open,
onClose,
}: ProviderSelectDialogProps) {
}: MakerSelectDialogProps) {
const classes = useStyles();
const providers = useAllProviders();
const makers = useAllMakers();
const dispatch = useAppDispatch();
function handleProviderChange(provider: ExtendedProviderStatus) {
dispatch(setSelectedProvider(provider));
function handleMakerChange(maker: ExtendedMakerStatus) {
dispatch(setSelectedMaker(maker));
onClose();
}
return (
<Dialog onClose={onClose} open={open}>
<DialogTitle>Select a swap provider</DialogTitle>
<DialogTitle>Select a maker</DialogTitle>
<DialogContent className={classes.dialogContent} dividers>
<List>
{providers.map((provider) => (
{makers.map((maker) => (
<ListItem
button
onClick={() => handleProviderChange(provider)}
key={provider.peerId}
onClick={() => handleMakerChange(maker)}
key={maker.peerId}
>
<ProviderInfo provider={provider} key={provider.peerId} />
<MakerInfo maker={maker} key={maker.peerId} />
</ListItem>
))}
<ListSellersDialogOpenButton />
<ProviderSubmitDialogOpenButton />
<MakerSubmitDialogOpenButton />
</List>
</DialogContent>

View file

@ -8,8 +8,8 @@ import {
import ArrowForwardIosIcon from "@material-ui/icons/ArrowForwardIos";
import { useState } from "react";
import { useAppSelector } from "store/hooks";
import ProviderInfo from "./ProviderInfo";
import ProviderListDialog from "./ProviderListDialog";
import MakerInfo from "./MakerInfo";
import MakerListDialog from "./MakerListDialog";
const useStyles = makeStyles({
inner: {
@ -17,23 +17,23 @@ const useStyles = makeStyles({
width: "100%",
height: "100%",
},
providerCard: {
makerCard: {
width: "100%",
},
providerCardContent: {
makerCardContent: {
display: "flex",
alignItems: "center",
},
});
export default function ProviderSelect() {
export default function MakerSelect() {
const classes = useStyles();
const [selectDialogOpen, setSelectDialogOpen] = useState(false);
const selectedProvider = useAppSelector(
(state) => state.providers.selectedProvider,
const selectedMaker = useAppSelector(
(state) => state.makers.selectedMaker,
);
if (!selectedProvider) return <>No provider selected</>;
if (!selectedMaker) return <>No maker selected</>;
function handleSelectDialogClose() {
setSelectDialogOpen(false);
@ -45,13 +45,13 @@ export default function ProviderSelect() {
return (
<Box>
<ProviderListDialog
<MakerListDialog
open={selectDialogOpen}
onClose={handleSelectDialogClose}
/>
<Card variant="outlined" className={classes.providerCard}>
<CardContent className={classes.providerCardContent}>
<ProviderInfo provider={selectedProvider} />
<Card variant="outlined" className={classes.makerCard}>
<CardContent className={classes.makerCardContent}>
<MakerInfo maker={selectedMaker} />
<IconButton onClick={handleSelectDialogOpen} size="small">
<ArrowForwardIosIcon />
</IconButton>

View file

@ -10,19 +10,19 @@ import {
import { Multiaddr } from "multiaddr";
import { ChangeEvent, useState } from "react";
type ProviderSubmitDialogProps = {
type MakerSubmitDialogProps = {
open: boolean;
onClose: () => void;
};
export default function ProviderSubmitDialog({
export default function MakerSubmitDialog({
open,
onClose,
}: ProviderSubmitDialogProps) {
}: MakerSubmitDialogProps) {
const [multiAddr, setMultiAddr] = useState("");
const [peerId, setPeerId] = useState("");
async function handleProviderSubmit() {
async function handleMakerSubmit() {
if (multiAddr && peerId) {
await fetch("https://api.unstoppableswap.net/api/submit-provider", {
method: "post",
@ -55,7 +55,7 @@ export default function ProviderSubmitDialog({
return "The multi address should not contain the peer id (/p2p/)";
}
if (multiAddress.protoNames().find((name) => name.includes("onion"))) {
return "It is currently not possible to add a provider that is only reachable via Tor";
return "It is currently not possible to add a maker that is only reachable via Tor";
}
return null;
} catch (e) {
@ -65,10 +65,10 @@ export default function ProviderSubmitDialog({
return (
<Dialog onClose={onClose} open={open}>
<DialogTitle>Submit a provider to the public registry</DialogTitle>
<DialogTitle>Submit a maker to the public registry</DialogTitle>
<DialogContent dividers>
<DialogContentText>
If the provider is valid and reachable, it will be displayed to all
If the maker is valid and reachable, it will be displayed to all
other users to trade with.
</DialogContentText>
<TextField
@ -78,7 +78,7 @@ export default function ProviderSubmitDialog({
fullWidth
helperText={
getMultiAddressError() ||
"Tells the swap client where the provider can be reached"
"Tells the swap client where the maker can be reached"
}
value={multiAddr}
onChange={handleMultiAddrChange}
@ -89,7 +89,7 @@ export default function ProviderSubmitDialog({
margin="dense"
label="Peer ID"
fullWidth
helperText="Identifies the provider and allows for secure communication"
helperText="Identifies the maker and allows for secure communication"
value={peerId}
onChange={handlePeerIdChange}
placeholder="12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"
@ -99,7 +99,7 @@ export default function ProviderSubmitDialog({
<Button onClick={onClose}>Cancel</Button>
<Button
variant="contained"
onClick={handleProviderSubmit}
onClick={handleMakerSubmit}
disabled={!(multiAddr && peerId && !getMultiAddressError())}
color="primary"
>

View file

@ -1,105 +0,0 @@
import { Box, Chip, makeStyles, Tooltip, Typography } from "@material-ui/core";
import { VerifiedUser } from "@material-ui/icons";
import { ExtendedProviderStatus } from "models/apiModel";
import TruncatedText from "renderer/components/other/TruncatedText";
import {
MoneroBitcoinExchangeRate,
SatsAmount,
} from "renderer/components/other/Units";
import { satsToBtc, secondsToDays } from "utils/conversionUtils";
import { isProviderOutdated } from 'utils/multiAddrUtils';
import WarningIcon from '@material-ui/icons/Warning';
import { useAppSelector } from "store/hooks";
const useStyles = makeStyles((theme) => ({
content: {
flex: 1,
"& *": {
lineBreak: "anywhere",
},
},
chipsOuter: {
display: "flex",
marginTop: theme.spacing(1),
gap: theme.spacing(0.5),
flexWrap: "wrap",
},
}));
/**
* A chip that displays the markup of the provider's exchange rate compared to the market rate.
*/
function ProviderMarkupChip({ provider }: { provider: ExtendedProviderStatus }) {
const marketExchangeRate = useAppSelector(s => s.rates?.xmrBtcRate);
if (marketExchangeRate === null)
return null;
const providerExchangeRate = satsToBtc(provider.price);
/** The markup of the exchange rate compared to the market rate in percent */
const markup = (providerExchangeRate - marketExchangeRate) / marketExchangeRate * 100;
return (
<Tooltip title="The markup this provider charges compared to centralized markets. A lower markup means that you get more Monero for your Bitcoin.">
<Chip label={`Markup ${markup.toFixed(2)}%`} />
</Tooltip>
);
}
export default function ProviderInfo({
provider,
}: {
provider: ExtendedProviderStatus;
}) {
const classes = useStyles();
const isOutdated = isProviderOutdated(provider);
return (
<Box className={classes.content}>
<Typography color="textSecondary" gutterBottom>
Swap Provider
</Typography>
<Typography variant="h5" component="h2">
{provider.multiAddr}
</Typography>
<Typography color="textSecondary" gutterBottom>
<TruncatedText limit={16} truncateMiddle>{provider.peerId}</TruncatedText>
</Typography>
<Typography variant="caption">
Exchange rate:{" "}
<MoneroBitcoinExchangeRate rate={satsToBtc(provider.price)} />
<br />
Minimum swap amount: <SatsAmount amount={provider.minSwapAmount} />
<br />
Maximum swap amount: <SatsAmount amount={provider.maxSwapAmount} />
</Typography>
<Box className={classes.chipsOuter}>
{provider.testnet && <Chip label="Testnet" />}
{provider.uptime && (
<Tooltip title="A high uptime (>90%) indicates reliability. Providers with very low uptime may be unreliable and cause swaps to take longer to complete or fail entirely.">
<Chip label={`${Math.round(provider.uptime * 100)}% uptime`} />
</Tooltip>
)}
{provider.age ? (
<Chip
label={`Went online ${Math.round(secondsToDays(provider.age))} ${provider.age === 1 ? "day" : "days"
} ago`}
/>
) : (
<Chip label="Discovered via rendezvous point" />
)}
{provider.recommended === true && (
<Tooltip title="This provider has shown to be exceptionally reliable">
<Chip label="Recommended" icon={<VerifiedUser />} color="primary" />
</Tooltip>
)}
{isOutdated && (
<Tooltip title="This provider is running an older version of the software. Outdated providers may be unreliable and cause swaps to take longer to complete or fail entirely.">
<Chip label="Outdated" icon={<WarningIcon />} color="primary" />
</Tooltip>
)}
<ProviderMarkupChip provider={provider} />
</Box>
</Box>
);
}

View file

@ -37,7 +37,7 @@ export default function BitcoinLockTxInMempoolPage({
loading
additionalContent={
<>
Most swap providers require one confirmation before locking their
Most makers require one confirmation before locking their
Monero. After they lock their funds and the Monero transaction
receives one confirmation, the swap will proceed to the next step.
<br />

View file

@ -10,7 +10,7 @@ export default function SwapSetupInflightPage({
<CircularProgressWithSubtitle
description={
<>
Starting swap with provider to lock <SatsAmount amount={btc_lock_amount} />
Starting swap with maker to lock <SatsAmount amount={btc_lock_amount} />
</>
}
/>

View file

@ -37,13 +37,13 @@ export default function InitPage() {
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
const [refundAddressValid, setRefundAddressValid] = useState(false);
const selectedProvider = useAppSelector(
(state) => state.providers.selectedProvider,
const selectedMaker = useAppSelector(
(state) => state.makers.selectedMaker,
);
async function init() {
await buyXmr(
selectedProvider,
selectedMaker,
useExternalRefundAddress ? refundAddress : null,
redeemAddress,
);
@ -99,7 +99,7 @@ export default function InitPage() {
disabled={
(!refundAddressValid && useExternalRefundAddress) ||
!redeemAddressValid ||
!selectedProvider
!selectedMaker
}
variant="contained"
color="primary"

View file

@ -35,7 +35,7 @@ export default function TorInfoBox() {
internet. It is a free and open network that is operated by
volunteers. You can start and stop Tor by clicking the buttons
below. If Tor is running, all traffic will be routed through it and
the swap provider will not be able to see your IP address.
the maker will not be able to see your IP address.
</Typography>
<CliLogsBox label="Tor Daemon Logs" logs={torStdOut.split("\n")} />
</Box>

View file

@ -91,7 +91,7 @@ export default function HistoryRowExpanded({
</TableCell>
</TableRow>
<TableRow>
<TableCell>Provider Address</TableCell>
<TableCell>Maker Address</TableCell>
<TableCell>
<Box className={classes.outerAddressBox}>
{swap.seller.addresses.map((addr) => (

View file

@ -11,15 +11,15 @@ import InputAdornment from "@material-ui/core/InputAdornment";
import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward";
import SwapHorizIcon from "@material-ui/icons/SwapHoriz";
import { Alert } from "@material-ui/lab";
import { ExtendedProviderStatus } from "models/apiModel";
import { ExtendedMakerStatus } from "models/apiModel";
import { ChangeEvent, useEffect, useState } from "react";
import { useAppSelector } from "store/hooks";
import { satsToBtc } from "utils/conversionUtils";
import {
ListSellersDialogOpenButton,
ProviderSubmitDialogOpenButton,
} from "../../modal/provider/ProviderListDialog";
import ProviderSelect from "../../modal/provider/ProviderSelect";
MakerSubmitDialogOpenButton,
} from "../../modal/provider/MakerListDialog";
import MakerSelect from "../../modal/provider/MakerSelect";
import SwapDialog from "../../modal/swap/SwapDialog";
// After RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN failed reconnection attempts we can assume the public registry is down
@ -43,7 +43,7 @@ const useStyles = makeStyles((theme) => ({
headerText: {
padding: theme.spacing(1),
},
providerInfo: {
makerInfo: {
padding: theme.spacing(1),
},
swapIconOuter: {
@ -53,12 +53,12 @@ const useStyles = makeStyles((theme) => ({
swapIcon: {
marginRight: theme.spacing(1),
},
noProvidersAlertOuter: {
noMakersAlertOuter: {
display: "flex",
flexDirection: "column",
gap: theme.spacing(1),
},
noProvidersAlertButtonsOuter: {
noMakersAlertButtonsOuter: {
display: "flex",
gap: theme.spacing(1),
},
@ -76,17 +76,17 @@ function Title() {
);
}
function HasProviderSwapWidget({
selectedProvider,
function HasMakerSwapWidget({
selectedMaker,
}: {
selectedProvider: ExtendedProviderStatus;
selectedMaker: ExtendedMakerStatus;
}) {
const classes = useStyles();
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
const [showDialog, setShowDialog] = useState(false);
const [btcFieldValue, setBtcFieldValue] = useState<number | string>(
satsToBtc(selectedProvider.minSwapAmount),
satsToBtc(selectedMaker.minSwapAmount),
);
const [xmrFieldValue, setXmrFieldValue] = useState(1);
@ -100,7 +100,7 @@ function HasProviderSwapWidget({
setXmrFieldValue(0);
} else {
const convertedXmrAmount =
parsedBtcAmount / satsToBtc(selectedProvider.price);
parsedBtcAmount / satsToBtc(selectedMaker.price);
setXmrFieldValue(convertedXmrAmount);
}
}
@ -110,15 +110,15 @@ function HasProviderSwapWidget({
if (Number.isNaN(parsedBtcAmount)) {
return "This is not a valid number";
}
if (parsedBtcAmount < satsToBtc(selectedProvider.minSwapAmount)) {
if (parsedBtcAmount < satsToBtc(selectedMaker.minSwapAmount)) {
return `The minimum swap amount is ${satsToBtc(
selectedProvider.minSwapAmount,
)} BTC. Switch to a different provider if you want to swap less.`;
selectedMaker.minSwapAmount,
)} BTC. Switch to a different maker if you want to swap less.`;
}
if (parsedBtcAmount > satsToBtc(selectedProvider.maxSwapAmount)) {
if (parsedBtcAmount > satsToBtc(selectedMaker.maxSwapAmount)) {
return `The maximum swap amount is ${satsToBtc(
selectedProvider.maxSwapAmount,
)} BTC. Switch to a different provider if you want to swap more.`;
selectedMaker.maxSwapAmount,
)} BTC. Switch to a different maker if you want to swap more.`;
}
return null;
}
@ -127,7 +127,7 @@ function HasProviderSwapWidget({
setShowDialog(true);
}
useEffect(updateXmrValue, [btcFieldValue, selectedProvider]);
useEffect(updateXmrValue, [btcFieldValue, selectedMaker]);
return (
// 'elevation' prop can't be passed down (type def issue)
@ -160,7 +160,7 @@ function HasProviderSwapWidget({
endAdornment: <InputAdornment position="end">XMR</InputAdornment>,
}}
/>
<ProviderSelect />
<MakerSelect />
<Fab variant="extended" color="primary" onClick={handleGuideDialogOpen}>
<SwapHorizIcon className={classes.swapIcon} />
Swap
@ -173,22 +173,22 @@ function HasProviderSwapWidget({
);
}
function HasNoProvidersSwapWidget() {
function HasNoMakersSwapWidget() {
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
const isPublicRegistryDown = useAppSelector((state) =>
isRegistryDown(state.providers.registry.connectionFailsCount),
isRegistryDown(state.makers.registry.connectionFailsCount),
);
const classes = useStyles();
const alertBox = isPublicRegistryDown ? (
<Alert severity="info">
<Box className={classes.noProvidersAlertOuter}>
<Box className={classes.noMakersAlertOuter}>
<Typography>
Currently, the public registry of providers seems to be unreachable.
Currently, the public registry of makers seems to be unreachable.
Here&apos;s what you can do:
<ul>
<li>
Try discovering a provider by connecting to a rendezvous point
Try discovering a maker by connecting to a rendezvous point
</li>
<li>
Try again later when the public registry may be reachable again
@ -202,20 +202,20 @@ function HasNoProvidersSwapWidget() {
</Alert>
) : (
<Alert severity="info">
<Box className={classes.noProvidersAlertOuter}>
<Box className={classes.noMakersAlertOuter}>
<Typography>
Currently, there are no providers (trading partners) available in the
Currently, there are no makers (trading partners) available in the
official registry. Here&apos;s what you can do:
<ul>
<li>
Try discovering a provider by connecting to a rendezvous point
Try discovering a maker by connecting to a rendezvous point
</li>
<li>Add a new provider to the public registry</li>
<li>Try again later when more providers may be available</li>
<li>Add a new maker to the public registry</li>
<li>Try again later when more makers may be available</li>
</ul>
</Typography>
<Box>
<ProviderSubmitDialogOpenButton />
<MakerSubmitDialogOpenButton />
<ListSellersDialogOpenButton />
</Box>
</Box>
@ -225,12 +225,12 @@ function HasNoProvidersSwapWidget() {
return (
<Box>
{alertBox}
<SwapDialog open={forceShowDialog} onClose={() => {}} />
<SwapDialog open={forceShowDialog} onClose={() => { }} />
</Box>
);
}
function ProviderLoadingSwapWidget() {
function MakerLoadingSwapWidget() {
const classes = useStyles();
return (
@ -245,21 +245,21 @@ function ProviderLoadingSwapWidget() {
}
export default function SwapWidget() {
const selectedProvider = useAppSelector(
(state) => state.providers.selectedProvider,
const selectedMaker = useAppSelector(
(state) => state.makers.selectedMaker,
);
// If we fail more than RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN reconnect attempts, we'll show the "no providers" widget. We can assume the public registry is down.
const providerLoading = useAppSelector(
// If we fail more than RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN reconnect attempts, we'll show the "no makers" widget. We can assume the public registry is down.
const makerLoading = useAppSelector(
(state) =>
state.providers.registry.providers === null &&
!isRegistryDown(state.providers.registry.connectionFailsCount),
state.makers.registry.makers === null &&
!isRegistryDown(state.makers.registry.connectionFailsCount),
);
if (providerLoading) {
return <ProviderLoadingSwapWidget />;
if (makerLoading) {
return <MakerLoadingSwapWidget />;
}
if (selectedProvider) {
return <HasProviderSwapWidget selectedProvider={selectedProvider} />;
if (selectedMaker) {
return <HasMakerSwapWidget selectedMaker={selectedMaker} />;
}
return <HasNoProvidersSwapWidget />;
return <HasNoMakersSwapWidget />;
}

View file

@ -40,7 +40,7 @@ import {
} from "store/features/rpcSlice";
import { swapProgressEventReceived } from "store/features/swapSlice";
import { store } from "./store/storeRenderer";
import { Provider } from "models/apiModel";
import { Maker } from "models/apiModel";
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
import { MoneroRecoveryResponse } from "models/rpcModel";
import { ListSellersResponse } from "../models/tauriModel";
@ -48,7 +48,7 @@ import logger from "utils/logger";
import { getNetwork, getNetworkName, isTestnet } from "store/config";
import { Blockchain, Network } from "store/features/settingsSlice";
import { setStatus } from "store/features/nodesSlice";
import { discoveredProvidersByRendezvous } from "store/features/providersSlice";
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
export const PRESET_RENDEZVOUS_POINTS = [
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
@ -57,7 +57,7 @@ export const PRESET_RENDEZVOUS_POINTS = [
export async function fetchSellersAtPresetRendezvousPoints() {
await Promise.all(PRESET_RENDEZVOUS_POINTS.map(async (rendezvousPoint) => {
const response = await listSellersAtRendezvousPoint(rendezvousPoint);
store.dispatch(discoveredProvidersByRendezvous(response.sellers));
store.dispatch(discoveredMakersByRendezvous(response.sellers));
logger.log(`Discovered ${response.sellers.length} sellers at rendezvous point ${rendezvousPoint} during startup fetch`);
}),
@ -118,7 +118,7 @@ export async function withdrawBtc(address: string): Promise<string> {
}
export async function buyXmr(
seller: Provider,
seller: Maker,
bitcoin_change_address: string | null,
monero_receive_address: string,
) {

View file

@ -1,14 +1,15 @@
import alertsSlice from "./features/alertsSlice";
import providersSlice from "./features/providersSlice";
import makersSlice from "./features/makersSlice";
import ratesSlice from "./features/ratesSlice";
import rpcSlice from "./features/rpcSlice";
import swapReducer from "./features/swapSlice";
import torSlice from "./features/torSlice";
import settingsSlice from "./features/settingsSlice";
import nodesSlice from "./features/nodesSlice";
export const reducers = {
swap: swapReducer,
providers: providersSlice,
makers: makersSlice,
tor: torSlice,
rpc: rpcSlice,
alerts: alertsSlice,

View file

@ -1,4 +1,4 @@
import { ExtendedProviderStatus } from "models/apiModel";
import { ExtendedMakerStatus } from "models/apiModel";
import { splitPeerIdFromMultiAddress } from "utils/parseUtils";
import { getMatches } from '@tauri-apps/plugin-cli';
import { Network } from "./features/settingsSlice";
@ -19,14 +19,14 @@ export function isTestnet() {
export const isDevelopment = true;
export function getStubTestnetProvider(): ExtendedProviderStatus | null {
const stubProviderAddress = import.meta.env
export function getStubTestnetMaker(): ExtendedMakerStatus | null {
const stubMakerAddress = import.meta.env
.VITE_TESTNET_STUB_PROVIDER_ADDRESS;
if (stubProviderAddress != null) {
if (stubMakerAddress != null) {
try {
const [multiAddr, peerId] =
splitPeerIdFromMultiAddress(stubProviderAddress);
splitPeerIdFromMultiAddress(stubMakerAddress);
return {
multiAddr,

View file

@ -0,0 +1,127 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedMakerStatus, MakerStatus } from "models/apiModel";
import { Seller } from "models/tauriModel";
import { getStubTestnetMaker } from "store/config";
import { rendezvousSellerToMakerStatus } from "utils/conversionUtils";
import { isMakerOutdated } from "utils/multiAddrUtils";
import { sortMakerList } from "utils/sortUtils";
const stubTestnetMaker = getStubTestnetMaker();
export interface MakersSlice {
rendezvous: {
makers: (ExtendedMakerStatus | MakerStatus)[];
};
registry: {
makers: ExtendedMakerStatus[] | null;
// This counts how many failed connections attempts we have counted since the last successful connection
connectionFailsCount: number;
};
selectedMaker: ExtendedMakerStatus | null;
}
const initialState: MakersSlice = {
rendezvous: {
makers: [],
},
registry: {
makers: stubTestnetMaker ? [stubTestnetMaker] : null,
connectionFailsCount: 0,
},
selectedMaker: null,
};
function selectNewSelectedMaker(
slice: MakersSlice,
peerId?: string,
): MakerStatus {
const selectedPeerId = peerId || slice.selectedMaker?.peerId;
// Check if we still have a record of the currently selected provider
const currentMaker = slice.registry.makers?.find((prov) => prov.peerId === selectedPeerId) || slice.rendezvous.makers.find((prov) => prov.peerId === selectedPeerId);
// If the currently selected provider is not outdated, keep it
if (currentMaker != null && !isMakerOutdated(currentMaker)) {
return currentMaker;
}
// Otherwise we'd prefer to switch to a provider that has the newest version
const providers = sortMakerList([
...(slice.registry.makers ?? []),
...(slice.rendezvous.makers ?? []),
]);
return providers.at(0) || null;
}
export const makersSlice = createSlice({
name: "providers",
initialState,
reducers: {
discoveredMakersByRendezvous(slice, action: PayloadAction<Seller[]>) {
action.payload.forEach((discoveredSeller) => {
const discoveredMakerStatus =
rendezvousSellerToMakerStatus(discoveredSeller);
// If the seller has a status of "Unreachable" the provider is not added to the list
if (discoveredMakerStatus === null) {
return;
}
// If the provider was already discovered via the public registry, don't add it again
const indexOfExistingMaker = slice.rendezvous.makers.findIndex(
(prov) =>
prov.peerId === discoveredMakerStatus.peerId &&
prov.multiAddr === discoveredMakerStatus.multiAddr,
);
// Avoid duplicate entries, replace them instead
if (indexOfExistingMaker !== -1) {
slice.rendezvous.makers[indexOfExistingMaker] =
discoveredMakerStatus;
} else {
slice.rendezvous.makers.push(discoveredMakerStatus);
}
});
// Sort the provider list and select a new provider if needed
slice.rendezvous.makers = sortMakerList(slice.rendezvous.makers);
slice.selectedMaker = selectNewSelectedMaker(slice);
},
setRegistryMakers(
slice,
action: PayloadAction<ExtendedMakerStatus[]>,
) {
if (stubTestnetMaker) {
action.payload.push(stubTestnetMaker);
}
// Sort the provider list and select a new provider if needed
slice.registry.makers = sortMakerList(action.payload);
slice.selectedMaker = selectNewSelectedMaker(slice);
},
registryConnectionFailed(slice) {
slice.registry.connectionFailsCount += 1;
},
setSelectedMaker(
slice,
action: PayloadAction<{
peerId: string;
}>,
) {
slice.selectedMaker = selectNewSelectedMaker(
slice,
action.payload.peerId,
);
},
},
});
export const {
discoveredMakersByRendezvous,
setRegistryMakers,
registryConnectionFailed,
setSelectedMaker,
} = makersSlice.actions;
export default makersSlice.reducer;

View file

@ -1,127 +0,0 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
import { Seller } from "models/tauriModel";
import { getStubTestnetProvider } from "store/config";
import { rendezvousSellerToProviderStatus } from "utils/conversionUtils";
import { isProviderOutdated } from "utils/multiAddrUtils";
import { sortProviderList } from "utils/sortUtils";
const stubTestnetProvider = getStubTestnetProvider();
export interface ProvidersSlice {
rendezvous: {
providers: (ExtendedProviderStatus | ProviderStatus)[];
};
registry: {
providers: ExtendedProviderStatus[] | null;
// This counts how many failed connections attempts we have counted since the last successful connection
connectionFailsCount: number;
};
selectedProvider: ExtendedProviderStatus | null;
}
const initialState: ProvidersSlice = {
rendezvous: {
providers: [],
},
registry: {
providers: stubTestnetProvider ? [stubTestnetProvider] : null,
connectionFailsCount: 0,
},
selectedProvider: null,
};
function selectNewSelectedProvider(
slice: ProvidersSlice,
peerId?: string,
): ProviderStatus {
const selectedPeerId = peerId || slice.selectedProvider?.peerId;
// Check if we still have a record of the currently selected provider
const currentProvider = slice.registry.providers?.find((prov) => prov.peerId === selectedPeerId) || slice.rendezvous.providers.find((prov) => prov.peerId === selectedPeerId);
// If the currently selected provider is not outdated, keep it
if (currentProvider != null && !isProviderOutdated(currentProvider)) {
return currentProvider;
}
// Otherwise we'd prefer to switch to a provider that has the newest version
const providers = sortProviderList([
...(slice.registry.providers ?? []),
...(slice.rendezvous.providers ?? []),
]);
return providers.at(0) || null;
}
export const providersSlice = createSlice({
name: "providers",
initialState,
reducers: {
discoveredProvidersByRendezvous(slice, action: PayloadAction<Seller[]>) {
action.payload.forEach((discoveredSeller) => {
const discoveredProviderStatus =
rendezvousSellerToProviderStatus(discoveredSeller);
// If the seller has a status of "Unreachable" the provider is not added to the list
if (discoveredProviderStatus === null) {
return;
}
// If the provider was already discovered via the public registry, don't add it again
const indexOfExistingProvider = slice.rendezvous.providers.findIndex(
(prov) =>
prov.peerId === discoveredProviderStatus.peerId &&
prov.multiAddr === discoveredProviderStatus.multiAddr,
);
// Avoid duplicate entries, replace them instead
if (indexOfExistingProvider !== -1) {
slice.rendezvous.providers[indexOfExistingProvider] =
discoveredProviderStatus;
} else {
slice.rendezvous.providers.push(discoveredProviderStatus);
}
});
// Sort the provider list and select a new provider if needed
slice.rendezvous.providers = sortProviderList(slice.rendezvous.providers);
slice.selectedProvider = selectNewSelectedProvider(slice);
},
setRegistryProviders(
slice,
action: PayloadAction<ExtendedProviderStatus[]>,
) {
if (stubTestnetProvider) {
action.payload.push(stubTestnetProvider);
}
// Sort the provider list and select a new provider if needed
slice.registry.providers = sortProviderList(action.payload);
slice.selectedProvider = selectNewSelectedProvider(slice);
},
registryConnectionFailed(slice) {
slice.registry.connectionFailsCount += 1;
},
setSelectedProvider(
slice,
action: PayloadAction<{
peerId: string;
}>,
) {
slice.selectedProvider = selectNewSelectedProvider(
slice,
action.payload.peerId,
);
},
},
});
export const {
discoveredProvidersByRendezvous,
setRegistryProviders,
registryConnectionFailed,
setSelectedProvider,
} = providersSlice.actions;
export default providersSlice.reducer;

View file

@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
import { ExtendedMakerStatus, MakerStatus } from "models/apiModel";
import {
TauriLogEvent,
GetSwapInfoResponse,
@ -16,7 +16,7 @@ import logger from "utils/logger";
interface State {
balance: number | null;
withdrawTxId: string | null;
rendezvous_discovered_sellers: (ExtendedProviderStatus | ProviderStatus)[];
rendezvous_discovered_sellers: (ExtendedMakerStatus | MakerStatus)[];
swapInfos: {
[swapId: string]: GetSwapInfoResponseExt;
};
@ -103,9 +103,9 @@ export const rpcSlice = createSlice({
rpcSetWithdrawTxId(slice, action: PayloadAction<string>) {
slice.state.withdrawTxId = action.payload;
},
rpcSetRendezvousDiscoveredProviders(
rpcSetRendezvousDiscoveredMakers(
slice,
action: PayloadAction<(ExtendedProviderStatus | ProviderStatus)[]>,
action: PayloadAction<(ExtendedMakerStatus | MakerStatus)[]>,
) {
slice.state.rendezvous_discovered_sellers = action.payload;
},
@ -146,7 +146,7 @@ export const {
rpcSetBalance,
rpcSetWithdrawTxId,
rpcResetWithdrawTxId,
rpcSetRendezvousDiscoveredProviders,
rpcSetRendezvousDiscoveredMakers,
rpcSetSwapInfo,
rpcSetMoneroRecoveryKeys,
rpcResetMoneroRecoveryKeys,

View file

@ -8,7 +8,7 @@ import { isCliLogRelatedToSwap } from "models/cliModel";
import { SettingsState } from "./features/settingsSlice";
import { NodesSlice } from "./features/nodesSlice";
import { RatesState } from "./features/ratesSlice";
import { sortProviderList } from "utils/sortUtils";
import { sortMakerList } from "utils/sortUtils";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
@ -70,13 +70,13 @@ export function useActiveSwapLogs() {
);
}
export function useAllProviders() {
export function useAllMakers() {
return useAppSelector((state) => {
const registryProviders = state.providers.registry.providers || [];
const listSellersProviders = state.providers.rendezvous.providers || [];
const all = [...registryProviders, ...listSellersProviders];
const registryMakers = state.makers.registry.makers || [];
const listSellersMakers = state.makers.rendezvous.makers || [];
const all = [...registryMakers, ...listSellersMakers];
return sortProviderList(all);
return sortMakerList(all);
});
}

View file

@ -1,4 +1,4 @@
import { ProviderStatus } from "models/apiModel";
import { MakerStatus } from "models/apiModel";
import { Seller } from "models/tauriModel";
import { isTestnet } from "store/config";
import { splitPeerIdFromMultiAddress } from "./parseUtils";
@ -45,12 +45,12 @@ export function secondsToDays(seconds: number): number {
return seconds / 86400;
}
// Convert the "Seller" object returned by the list sellers tauri endpoint to a "ProviderStatus" object
// Convert the "Seller" object returned by the list sellers tauri endpoint to a "MakerStatus" object
// 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 rendezvousSellerToProviderStatus(
export function rendezvousSellerToMakerStatus(
seller: Seller,
): ProviderStatus | null {
): MakerStatus | null {
if (seller.status.type === "Unreachable") {
return null;
}

View file

@ -1,23 +1,23 @@
import { ExtendedProviderStatus, Provider } from "models/apiModel";
import { ExtendedMakerStatus, Maker } from "models/apiModel";
import { Multiaddr } from "multiaddr";
import semver from "semver";
import { isTestnet } from "store/config";
const MIN_ASB_VERSION = "1.0.0-alpha.1"
export function providerToConcatenatedMultiAddr(provider: Provider) {
export function providerToConcatenatedMultiAddr(provider: Maker) {
return new Multiaddr(provider.multiAddr)
.encapsulate(`/p2p/${provider.peerId}`)
.toString();
}
export function isProviderOnCorrectNetwork(
provider: ExtendedProviderStatus,
export function isMakerOnCorrectNetwork(
provider: ExtendedMakerStatus,
): boolean {
return provider.testnet === isTestnet();
}
export function isProviderOutdated(provider: ExtendedProviderStatus): boolean {
export function isMakerOutdated(provider: ExtendedMakerStatus): boolean {
if (provider.version != null) {
if (semver.satisfies(provider.version, `>=${MIN_ASB_VERSION}`))
return false;

View file

@ -1,16 +1,16 @@
import { ExtendedProviderStatus } from "models/apiModel";
import { isProviderOnCorrectNetwork, isProviderOutdated } from "./multiAddrUtils";
import { ExtendedMakerStatus } from "models/apiModel";
import { isMakerOnCorrectNetwork, isMakerOutdated } from "./multiAddrUtils";
export function sortProviderList(list: ExtendedProviderStatus[]) {
export function sortMakerList(list: ExtendedMakerStatus[]) {
return list
// Filter out providers that are on the wrong network (testnet / mainnet)
.filter(isProviderOnCorrectNetwork)
// Filter out makers that are on the wrong network (testnet / mainnet)
.filter(isMakerOnCorrectNetwork)
.concat()
// Sort by criteria
.sort((firstEl, secondEl) => {
// If either provider is outdated, prioritize the one that isn't
if (isProviderOutdated(firstEl) && !isProviderOutdated(secondEl)) return 1;
if (!isProviderOutdated(firstEl) && isProviderOutdated(secondEl)) return -1;
if (isMakerOutdated(firstEl) && !isMakerOutdated(secondEl)) return 1;
if (!isMakerOutdated(firstEl) && isMakerOutdated(secondEl)) return -1;
// If neither of them have a relevancy score or they are the same, sort by price
if (firstEl.relevancy == secondEl.relevancy) {
@ -24,7 +24,7 @@ export function sortProviderList(list: ExtendedProviderStatus[]) {
// Otherwise, sort by relevancy score
return secondEl.relevancy - firstEl.relevancy;
})
// Remove duplicate providers
// Remove duplicate makers
.filter((provider, index, self) =>
index === self.findIndex((p) => p.peerId === provider.peerId)
)

View file

@ -1001,6 +1001,13 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.6.tgz#193ced6a40c8006cfc1ca3f4553444fb38f0e543"
integrity sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==
"@types/node@*":
version "22.9.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.3.tgz#08f3d64b3bc6d74b162d36f60213e8a6704ef2b4"
integrity sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==
dependencies:
undici-types "~6.19.8"
"@types/node@^20.14.10":
version "20.14.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a"
@ -1432,6 +1439,13 @@ caniuse-lite@^1.0.30001629:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz#32c467d4bf1f1a0faa63fc793c2ba81169e7652f"
integrity sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==
canvas-renderer@~2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/canvas-renderer/-/canvas-renderer-2.2.1.tgz#c1d131f78a9799aca8af9679ad0a005052b65550"
integrity sha512-RrBgVL5qCEDIXpJ6NrzyRNoTnXxYarqm/cS/W6ERhUJts5UQtt/XPEosGN3rqUkZ4fjBArlnCbsISJ+KCFnIAg==
dependencies:
"@types/node" "*"
chai@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.1.tgz#f035d9792a22b481ead1c65908d14bb62ec1c82c"
@ -2538,6 +2552,13 @@ iterator.prototype@^1.1.2:
reflect.getprototypeof "^1.0.4"
set-function-name "^2.0.1"
jdenticon@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/jdenticon/-/jdenticon-3.3.0.tgz#64bae9f9b3cf5c2a210e183648117afe3a89b367"
integrity sha512-DhuBRNRIybGPeAjMjdHbkIfiwZCCmf8ggu7C49jhp6aJ7DYsZfudnvnTY5/1vgUhrGA7JaDAx1WevnpjCPvaGg==
dependencies:
canvas-renderer "~2.2.0"
joycon@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
@ -3750,6 +3771,11 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici-types@~6.19.8:
version "6.19.8"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
update-browserslist-db@^1.0.16:
version "1.1.0"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e"

View file

@ -26,7 +26,7 @@ You can read [this blogpost](https://comit.network/blog/2021/07/02/transaction-p
For more detailed documentation on the CLI, see [this README](./dev-docs/cli/README.md).
## Becoming a Market Maker
## Becoming a market maker
Swapping of course needs two parties - and the CLI is only one of them: The taker that occasionally starts a swap with a market maker.

View file

@ -109,7 +109,7 @@ async fn next_state(
.estimate_fee(TxCancel::weight(), btc_amount)
.await?;
// Emit an event to tauri that we are negotiating with the swap provider to lock the Bitcoin
// Emit an event to tauri that we are negotiating with the maker to lock the Bitcoin
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::SwapSetupInflight {