feat(gui): Allow discovery of sellers via connecting to rendezvous point (#83)

We,
- add a new list_sellers Tauri IPC command
- we rename the Seller struct to AliceAddress to name clash
This commit is contained in:
binarybaron 2024-09-19 00:40:51 +02:00 committed by GitHub
parent beccd23280
commit 167e031172
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 164 additions and 57 deletions

View file

@ -1,3 +1,9 @@
// This file is responsible for making HTTP requests to the Unstoppable API and to the CoinGecko API.
// The APIs are used to:
// - fetch provider status from the public registry
// - fetch alerts to be displayed to the user
// - and to submit feedback
// - fetch currency rates from CoinGecko
import { Alert, ExtendedProviderStatus } from "models/apiModel"; import { Alert, ExtendedProviderStatus } from "models/apiModel";
const API_BASE_URL = "https://api.unstoppableswap.net"; const API_BASE_URL = "https://api.unstoppableswap.net";

View file

@ -11,11 +11,15 @@ import {
TextField, TextField,
Theme, Theme,
} from "@material-ui/core"; } from "@material-ui/core";
import { Multiaddr } from "multiaddr"; import { ListSellersResponse } from "models/tauriModel";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
import TruncatedText from "renderer/components/other/TruncatedText"; import TruncatedText from "renderer/components/other/TruncatedText";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { listSellersAtRendezvousPoint } from "renderer/rpc";
import { discoveredProvidersByRendezvous } from "store/features/providersSlice";
import { useAppDispatch } from "store/hooks";
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
const PRESET_RENDEZVOUS_POINTS = [ const PRESET_RENDEZVOUS_POINTS = [
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
@ -42,27 +46,23 @@ export default function ListSellersDialog({
const classes = useStyles(); const classes = useStyles();
const [rendezvousAddress, setRendezvousAddress] = useState(""); const [rendezvousAddress, setRendezvousAddress] = useState("");
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
const dispatch = useAppDispatch();
function handleMultiAddrChange(event: ChangeEvent<HTMLInputElement>) { function handleMultiAddrChange(event: ChangeEvent<HTMLInputElement>) {
setRendezvousAddress(event.target.value); setRendezvousAddress(event.target.value);
} }
function getMultiAddressError(): string | null { function getMultiAddressError(): string | null {
try { return isValidMultiAddressWithPeerId(rendezvousAddress) ? null : "Address is invalid or missing peer ID";
const multiAddress = new Multiaddr(rendezvousAddress);
if (!multiAddress.protoNames().includes("p2p")) {
return "The multi address must contain the peer id (/p2p/)";
}
return null;
} catch {
return "Not a valid multi address";
}
} }
function handleSuccess(amountOfSellers: number) { function handleSuccess({ sellers }: ListSellersResponse) {
dispatch(discoveredProvidersByRendezvous(sellers));
const discoveredSellersCount = sellers.length;
let message: string; let message: string;
switch (amountOfSellers) { switch (discoveredSellersCount) {
case 0: case 0:
message = `No providers were discovered at the rendezvous point`; message = `No providers were discovered at the rendezvous point`;
break; break;
@ -70,7 +70,7 @@ export default function ListSellersDialog({
message = `Discovered one provider at the rendezvous point`; message = `Discovered one provider at the rendezvous point`;
break; break;
default: default:
message = `Discovered ${amountOfSellers} providers at the rendezvous point`; message = `Discovered ${discoveredSellersCount} providers at the rendezvous point`;
} }
enqueueSnackbar(message, { enqueueSnackbar(message, {
@ -119,12 +119,13 @@ export default function ListSellersDialog({
<Button onClick={onClose}>Cancel</Button> <Button onClick={onClose}>Cancel</Button>
<PromiseInvokeButton <PromiseInvokeButton
variant="contained" variant="contained"
disabled={!(rendezvousAddress && !getMultiAddressError())} disabled={
// We disable the button if the multiaddress is invalid
getMultiAddressError() !== null
}
color="primary" color="primary"
onSuccess={handleSuccess} onSuccess={handleSuccess}
onInvoke={() => { onInvoke={() => listSellersAtRendezvousPoint(rendezvousAddress)}
throw new Error("Not implemented");
}}
> >
Connect Connect
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -39,7 +39,7 @@ export default function ProviderInfo({
{provider.multiAddr} {provider.multiAddr}
</Typography> </Typography>
<Typography color="textSecondary" gutterBottom> <Typography color="textSecondary" gutterBottom>
<TruncatedText limit={12}>{provider.peerId}</TruncatedText> <TruncatedText>{provider.peerId}</TruncatedText>
</Typography> </Typography>
<Typography variant="caption"> <Typography variant="caption">
Exchange rate:{" "} Exchange rate:{" "}

View file

@ -177,7 +177,7 @@ function HasNoProvidersSwapWidget() {
const forceShowDialog = useAppSelector((state) => state.swap.state !== null); const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
const isPublicRegistryDown = useAppSelector((state) => const isPublicRegistryDown = useAppSelector((state) =>
isRegistryDown( isRegistryDown(
state.providers.registry.failedReconnectAttemptsSinceLastSuccess, state.providers.registry.connectionFailsCount,
), ),
); );
const classes = useStyles(); const classes = useStyles();
@ -255,7 +255,7 @@ export default function SwapWidget() {
(state) => (state) =>
state.providers.registry.providers === null && state.providers.registry.providers === null &&
!isRegistryDown( !isRegistryDown(
state.providers.registry.failedReconnectAttemptsSinceLastSuccess, state.providers.registry.connectionFailsCount,
), ),
); );

View file

@ -2,7 +2,10 @@ import { createRoot } from "react-dom/client";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react"; import { PersistGate } from "redux-persist/integration/react";
import { setAlerts } from "store/features/alertsSlice"; import { setAlerts } from "store/features/alertsSlice";
import { setRegistryProviders } from "store/features/providersSlice"; import {
registryConnectionFailed,
setRegistryProviders,
} from "store/features/providersSlice";
import { setBtcPrice, setXmrPrice } from "store/features/ratesSlice"; import { setBtcPrice, setXmrPrice } from "store/features/ratesSlice";
import logger from "../utils/logger"; import logger from "../utils/logger";
import { import {
@ -12,18 +15,9 @@ import {
fetchXmrPrice, fetchXmrPrice,
} from "./api"; } from "./api";
import App from "./components/App"; import App from "./components/App";
import { import { initEventListeners } from "./rpc";
checkBitcoinBalance,
getAllSwapInfos,
initEventListeners,
} from "./rpc";
import { persistor, store } from "./store/storeRenderer"; import { persistor, store } from "./store/storeRenderer";
setInterval(() => {
checkBitcoinBalance();
getAllSwapInfos();
}, 30 * 1000);
const container = document.getElementById("root"); const container = document.getElementById("root");
const root = createRoot(container!); const root = createRoot(container!);
root.render( root.render(
@ -44,6 +38,7 @@ async function fetchInitialData() {
"Fetched providers via UnstoppableSwap HTTP API", "Fetched providers via UnstoppableSwap HTTP API",
); );
} catch (e) { } catch (e) {
store.dispatch(registryConnectionFailed());
logger.error(e, "Failed to fetch providers via UnstoppableSwap HTTP API"); logger.error(e, "Failed to fetch providers via UnstoppableSwap HTTP API");
} }

View file

@ -8,6 +8,7 @@ import {
GetLogsArgs, GetLogsArgs,
GetLogsResponse, GetLogsResponse,
GetSwapInfoResponse, GetSwapInfoResponse,
ListSellersArgs,
MoneroRecoveryArgs, MoneroRecoveryArgs,
ResumeSwapArgs, ResumeSwapArgs,
ResumeSwapResponse, ResumeSwapResponse,
@ -27,6 +28,7 @@ import { store } from "./store/storeRenderer";
import { Provider } from "models/apiModel"; import { Provider } from "models/apiModel";
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils"; import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
import { MoneroRecoveryResponse } from "models/rpcModel"; import { MoneroRecoveryResponse } from "models/rpcModel";
import { ListSellersResponse } from "../models/tauriModel";
export async function initEventListeners() { export async function initEventListeners() {
// This operation is in-expensive // This operation is in-expensive
@ -144,3 +146,11 @@ export async function getLogsOfSwap(
redact, redact,
}); });
} }
export async function listSellersAtRendezvousPoint(
rendezvousPointAddress: string,
): Promise<ListSellersResponse> {
return await invoke<ListSellersArgs, ListSellersResponse>("list_sellers", {
rendezvous_point: rendezvousPointAddress,
});
}

View file

@ -1,6 +1,8 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel"; import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
import { Seller } from "models/tauriModel";
import { getStubTestnetProvider } from "store/config"; import { getStubTestnetProvider } from "store/config";
import { rendezvousSellerToProviderStatus } from "utils/conversionUtils";
import { isProviderCompatible } from "utils/multiAddrUtils"; import { isProviderCompatible } from "utils/multiAddrUtils";
import { sortProviderList } from "utils/sortUtils"; import { sortProviderList } from "utils/sortUtils";
@ -12,7 +14,8 @@ export interface ProvidersSlice {
}; };
registry: { registry: {
providers: ExtendedProviderStatus[] | null; providers: ExtendedProviderStatus[] | null;
failedReconnectAttemptsSinceLastSuccess: number; // This counts how many failed connections attempts we have counted since the last successful connection
connectionFailsCount: number;
}; };
selectedProvider: ExtendedProviderStatus | null; selectedProvider: ExtendedProviderStatus | null;
} }
@ -23,7 +26,7 @@ const initialState: ProvidersSlice = {
}, },
registry: { registry: {
providers: stubTestnetProvider ? [stubTestnetProvider] : null, providers: stubTestnetProvider ? [stubTestnetProvider] : null,
failedReconnectAttemptsSinceLastSuccess: 0, connectionFailsCount: 0,
}, },
selectedProvider: null, selectedProvider: null,
}; };
@ -47,35 +50,42 @@ export const providersSlice = createSlice({
name: "providers", name: "providers",
initialState, initialState,
reducers: { reducers: {
discoveredProvidersByRendezvous( discoveredProvidersByRendezvous(slice, action: PayloadAction<Seller[]>) {
slice, action.payload.forEach((discoveredSeller) => {
action: PayloadAction<ProviderStatus[]>, const discoveredProviderStatus =
) { rendezvousSellerToProviderStatus(discoveredSeller);
action.payload.forEach((discoveredProvider) => {
// 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
if ( if (
!slice.registry.providers?.some( !slice.registry.providers?.some(
(prov) => (prov) =>
prov.peerId === discoveredProvider.peerId && prov.peerId === discoveredProviderStatus.peerId &&
prov.multiAddr === discoveredProvider.multiAddr, prov.multiAddr === discoveredProviderStatus.multiAddr,
) )
) { ) {
const indexOfExistingProvider = slice.rendezvous.providers.findIndex( const indexOfExistingProvider = slice.rendezvous.providers.findIndex(
(prov) => (prov) =>
prov.peerId === discoveredProvider.peerId && prov.peerId === discoveredProviderStatus.peerId &&
prov.multiAddr === discoveredProvider.multiAddr, prov.multiAddr === discoveredProviderStatus.multiAddr,
); );
// Avoid duplicates, replace instead // Avoid duplicate entries, replace them instead
if (indexOfExistingProvider !== -1) { if (indexOfExistingProvider !== -1) {
slice.rendezvous.providers[indexOfExistingProvider] = slice.rendezvous.providers[indexOfExistingProvider] =
discoveredProvider; discoveredProviderStatus;
} else { } else {
slice.rendezvous.providers.push(discoveredProvider); slice.rendezvous.providers.push(discoveredProviderStatus);
} }
} }
}); });
slice.rendezvous.providers = sortProviderList(slice.rendezvous.providers); slice.rendezvous.providers = sortProviderList(slice.rendezvous.providers);
slice.selectedProvider = selectNewSelectedProvider(slice);
}, },
setRegistryProviders( setRegistryProviders(
slice, slice,
@ -90,8 +100,8 @@ export const providersSlice = createSlice({
); );
slice.selectedProvider = selectNewSelectedProvider(slice); slice.selectedProvider = selectNewSelectedProvider(slice);
}, },
increaseFailedRegistryReconnectAttemptsSinceLastSuccess(slice) { registryConnectionFailed(slice) {
slice.registry.failedReconnectAttemptsSinceLastSuccess += 1; slice.registry.connectionFailsCount += 1;
}, },
setSelectedProvider( setSelectedProvider(
slice, slice,
@ -110,7 +120,7 @@ export const providersSlice = createSlice({
export const { export const {
discoveredProvidersByRendezvous, discoveredProvidersByRendezvous,
setRegistryProviders, setRegistryProviders,
increaseFailedRegistryReconnectAttemptsSinceLastSuccess, registryConnectionFailed,
setSelectedProvider, setSelectedProvider,
} = providersSlice.actions; } = providersSlice.actions;

View file

@ -1,3 +1,8 @@
import { ProviderStatus } from "models/apiModel";
import { Seller } from "models/tauriModel";
import { isTestnet } from "store/config";
import { splitPeerIdFromMultiAddress } from "./parseUtils";
export function satsToBtc(sats: number): number { export function satsToBtc(sats: number): number {
return sats / 100000000; return sats / 100000000;
} }
@ -40,3 +45,25 @@ export function getMoneroTxExplorerUrl(txid: string, stagenet: boolean) {
export function secondsToDays(seconds: number): number { export function secondsToDays(seconds: number): number {
return seconds / 86400; return seconds / 86400;
} }
// Convert the "Seller" object returned by the list sellers tauri endpoint to a "ProviderStatus" 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(
seller: Seller,
): ProviderStatus | null {
if (seller.status.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,
testnet: isTestnet(),
};
}

View file

@ -1,4 +1,5 @@
import { CliLog } from "models/cliModel"; import { CliLog } from "models/cliModel";
import { Multiaddr } from "multiaddr";
/* /*
Extract btc amount from string Extract btc amount from string
@ -72,3 +73,38 @@ export function getLogsFromRawFileString(rawFileData: string): CliLog[] {
export function logsToRawString(logs: (CliLog | string)[]): string { export function logsToRawString(logs: (CliLog | string)[]): string {
return logs.map((l) => JSON.stringify(l)).join("\n"); return logs.map((l) => JSON.stringify(l)).join("\n");
} }
// This function checks if a given multi address string is a valid multi address
// and contains a peer ID component.
export function isValidMultiAddressWithPeerId(
multiAddressStr: string,
): boolean {
try {
const multiAddress = new Multiaddr(multiAddressStr);
const peerId = multiAddress.getPeerId();
return peerId !== null;
} catch {
return false;
}
}
// This function splits a multi address string into the multi address and peer ID components.
// It throws an error if the multi address string is invalid or does not contain a peer ID component.
export function splitPeerIdFromMultiAddress(
multiAddressStr: string,
): [multiAddress: string, peerId: string] {
const multiAddress = new Multiaddr(multiAddressStr);
// Extract the peer ID
const peerId = multiAddress.getPeerId();
if (peerId) {
// Remove the peer ID component
const p2pMultiaddr = new Multiaddr("/p2p/" + peerId);
const multiAddressWithoutPeerId = multiAddress.decapsulate(p2pMultiaddr);
return [multiAddressWithoutPeerId.toString(), peerId];
} else {
throw new Error("No peer id encapsulated in multi address");
}
}

View file

@ -4,7 +4,8 @@ use swap::cli::{
api::{ api::{
request::{ request::{
BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetLogsArgs, GetSwapInfosAllArgs, BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetLogsArgs, GetSwapInfosAllArgs,
MoneroRecoveryArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs, ListSellersArgs, MoneroRecoveryArgs, ResumeSwapArgs, SuspendCurrentSwapArgs,
WithdrawBtcArgs,
}, },
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle}, tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle},
Context, ContextBuilder, Context, ContextBuilder,
@ -169,6 +170,7 @@ pub fn run() {
get_history, get_history,
monero_recovery, monero_recovery,
get_logs, get_logs,
list_sellers,
suspend_current_swap, suspend_current_swap,
is_context_available, is_context_available,
]) ])
@ -208,6 +210,7 @@ tauri_command!(resume_swap, ResumeSwapArgs);
tauri_command!(withdraw_btc, WithdrawBtcArgs); tauri_command!(withdraw_btc, WithdrawBtcArgs);
tauri_command!(monero_recovery, MoneroRecoveryArgs); tauri_command!(monero_recovery, MoneroRecoveryArgs);
tauri_command!(get_logs, GetLogsArgs); tauri_command!(get_logs, GetLogsArgs);
tauri_command!(list_sellers, ListSellersArgs);
// These commands require no arguments // These commands require no arguments
tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args); tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args);

View file

@ -2,7 +2,7 @@ use super::tauri_bindings::TauriHandle;
use crate::bitcoin::{CancelTimelock, ExpiredTimelocks, PunishTimelock, TxLock}; use crate::bitcoin::{CancelTimelock, ExpiredTimelocks, PunishTimelock, TxLock};
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent}; use crate::cli::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent};
use crate::cli::api::Context; use crate::cli::api::Context;
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus}; use crate::cli::{list_sellers as list_sellers_impl, EventLoop, Seller, SellerStatus};
use crate::common::get_logs; use crate::common::get_logs;
use crate::libp2p_ext::MultiAddrExt; use crate::libp2p_ext::MultiAddrExt;
use crate::network::quote::{BidQuote, ZeroQuoteReceived}; use crate::network::quote::{BidQuote, ZeroQuoteReceived};
@ -156,8 +156,14 @@ pub struct ListSellersArgs {
pub rendezvous_point: Multiaddr, pub rendezvous_point: Multiaddr,
} }
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize)]
pub struct ListSellersResponse {
sellers: Vec<Seller>,
}
impl Request for ListSellersArgs { impl Request for ListSellersArgs {
type Response = serde_json::Value; type Response = ListSellersResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
list_sellers(self, ctx).await list_sellers(self, ctx).await
@ -193,7 +199,7 @@ pub struct GetSwapInfoArgs {
pub struct GetSwapInfoResponse { pub struct GetSwapInfoResponse {
#[typeshare(serialized_as = "string")] #[typeshare(serialized_as = "string")]
pub swap_id: Uuid, pub swap_id: Uuid,
pub seller: Seller, pub seller: AliceAddress,
pub completed: bool, pub completed: bool,
pub start_date: String, pub start_date: String,
#[typeshare(serialized_as = "string")] #[typeshare(serialized_as = "string")]
@ -280,8 +286,8 @@ impl Request for GetHistoryArgs {
// Additional structs // Additional structs
#[typeshare] #[typeshare]
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Seller { pub struct AliceAddress {
#[typeshare(serialized_as = "string")] #[typeshare(serialized_as = "string")]
pub peer_id: PeerId, pub peer_id: PeerId,
pub addresses: Vec<String>, pub addresses: Vec<String>,
@ -507,7 +513,7 @@ pub async fn get_swap_info(
Ok(GetSwapInfoResponse { Ok(GetSwapInfoResponse {
swap_id: args.swap_id, swap_id: args.swap_id,
seller: Seller { seller: AliceAddress {
peer_id, peer_id,
addresses: addresses.iter().map(|a| a.to_string()).collect(), addresses: addresses.iter().map(|a| a.to_string()).collect(),
}, },
@ -1016,7 +1022,7 @@ pub async fn get_balance(balance: BalanceArgs, context: Arc<Context>) -> Result<
pub async fn list_sellers( pub async fn list_sellers(
list_sellers: ListSellersArgs, list_sellers: ListSellersArgs,
context: Arc<Context>, context: Arc<Context>,
) -> Result<serde_json::Value> { ) -> Result<ListSellersResponse> {
let ListSellersArgs { rendezvous_point } = list_sellers; let ListSellersArgs { rendezvous_point } = list_sellers;
let rendezvous_node_peer_id = rendezvous_point let rendezvous_node_peer_id = rendezvous_point
.extract_peer_id() .extract_peer_id()
@ -1060,7 +1066,7 @@ pub async fn list_sellers(
} }
} }
Ok(json!({ "sellers": sellers })) Ok(ListSellersResponse { sellers })
} }
#[tracing::instrument(fields(method = "export_bitcoin_wallet"), skip(context))] #[tracing::instrument(fields(method = "export_bitcoin_wallet"), skip(context))]

View file

@ -14,6 +14,7 @@ use serde_with::{serde_as, DisplayFromStr};
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
use typeshare::typeshare;
/// Returns sorted list of sellers, with [Online](Status::Online) listed first. /// Returns sorted list of sellers, with [Online](Status::Online) listed first.
/// ///
@ -60,14 +61,18 @@ pub async fn list_sellers(
} }
#[serde_as] #[serde_as]
#[typeshare]
#[derive(Debug, Serialize, PartialEq, Eq, Hash, Ord, PartialOrd)] #[derive(Debug, Serialize, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Seller { pub struct Seller {
pub status: Status, pub status: Status,
#[serde_as(as = "DisplayFromStr")] #[serde_as(as = "DisplayFromStr")]
#[typeshare(serialized_as = "string")]
pub multiaddr: Multiaddr, pub multiaddr: Multiaddr,
} }
#[typeshare]
#[derive(Debug, Serialize, PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd)] #[derive(Debug, Serialize, PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd)]
#[serde(tag = "type", content = "content")]
pub enum Status { pub enum Status {
Online(BidQuote), Online(BidQuote),
Unreachable, Unreachable,

View file

@ -398,6 +398,14 @@ async fn next_state(
} }
ExpiredTimelocks::Cancel { .. } => { ExpiredTimelocks::Cancel { .. } => {
state.publish_refund_btc(bitcoin_wallet).await?; state.publish_refund_btc(bitcoin_wallet).await?;
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::BtcRefunded {
btc_refund_txid: state.signed_refund_transaction()?.txid(),
},
);
BobState::BtcRefunded(state) BobState::BtcRefunded(state)
} }
ExpiredTimelocks::Punish => { ExpiredTimelocks::Punish => {