mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-05-01 22:36:11 -04:00
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:
parent
beccd23280
commit
167e031172
13 changed files with 164 additions and 57 deletions
|
@ -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";
|
||||
|
||||
const API_BASE_URL = "https://api.unstoppableswap.net";
|
||||
|
|
|
@ -11,11 +11,15 @@ import {
|
|||
TextField,
|
||||
Theme,
|
||||
} from "@material-ui/core";
|
||||
import { Multiaddr } from "multiaddr";
|
||||
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 } from "renderer/rpc";
|
||||
import { discoveredProvidersByRendezvous } from "store/features/providersSlice";
|
||||
import { useAppDispatch } from "store/hooks";
|
||||
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
||||
|
||||
const PRESET_RENDEZVOUS_POINTS = [
|
||||
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
||||
|
@ -42,27 +46,23 @@ export default function ListSellersDialog({
|
|||
const classes = useStyles();
|
||||
const [rendezvousAddress, setRendezvousAddress] = useState("");
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
function handleMultiAddrChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
setRendezvousAddress(event.target.value);
|
||||
}
|
||||
|
||||
function getMultiAddressError(): string | null {
|
||||
try {
|
||||
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";
|
||||
}
|
||||
return isValidMultiAddressWithPeerId(rendezvousAddress) ? null : "Address is invalid or missing peer ID";
|
||||
}
|
||||
|
||||
function handleSuccess(amountOfSellers: number) {
|
||||
function handleSuccess({ sellers }: ListSellersResponse) {
|
||||
dispatch(discoveredProvidersByRendezvous(sellers));
|
||||
|
||||
const discoveredSellersCount = sellers.length;
|
||||
let message: string;
|
||||
|
||||
switch (amountOfSellers) {
|
||||
switch (discoveredSellersCount) {
|
||||
case 0:
|
||||
message = `No providers were discovered at the rendezvous point`;
|
||||
break;
|
||||
|
@ -70,7 +70,7 @@ export default function ListSellersDialog({
|
|||
message = `Discovered one provider at the rendezvous point`;
|
||||
break;
|
||||
default:
|
||||
message = `Discovered ${amountOfSellers} providers at the rendezvous point`;
|
||||
message = `Discovered ${discoveredSellersCount} providers at the rendezvous point`;
|
||||
}
|
||||
|
||||
enqueueSnackbar(message, {
|
||||
|
@ -119,12 +119,13 @@ export default function ListSellersDialog({
|
|||
<Button onClick={onClose}>Cancel</Button>
|
||||
<PromiseInvokeButton
|
||||
variant="contained"
|
||||
disabled={!(rendezvousAddress && !getMultiAddressError())}
|
||||
disabled={
|
||||
// We disable the button if the multiaddress is invalid
|
||||
getMultiAddressError() !== null
|
||||
}
|
||||
color="primary"
|
||||
onSuccess={handleSuccess}
|
||||
onInvoke={() => {
|
||||
throw new Error("Not implemented");
|
||||
}}
|
||||
onInvoke={() => listSellersAtRendezvousPoint(rendezvousAddress)}
|
||||
>
|
||||
Connect
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
@ -39,7 +39,7 @@ export default function ProviderInfo({
|
|||
{provider.multiAddr}
|
||||
</Typography>
|
||||
<Typography color="textSecondary" gutterBottom>
|
||||
<TruncatedText limit={12}>{provider.peerId}</TruncatedText>
|
||||
<TruncatedText>{provider.peerId}</TruncatedText>
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
Exchange rate:{" "}
|
||||
|
|
|
@ -177,7 +177,7 @@ function HasNoProvidersSwapWidget() {
|
|||
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
|
||||
const isPublicRegistryDown = useAppSelector((state) =>
|
||||
isRegistryDown(
|
||||
state.providers.registry.failedReconnectAttemptsSinceLastSuccess,
|
||||
state.providers.registry.connectionFailsCount,
|
||||
),
|
||||
);
|
||||
const classes = useStyles();
|
||||
|
@ -255,7 +255,7 @@ export default function SwapWidget() {
|
|||
(state) =>
|
||||
state.providers.registry.providers === null &&
|
||||
!isRegistryDown(
|
||||
state.providers.registry.failedReconnectAttemptsSinceLastSuccess,
|
||||
state.providers.registry.connectionFailsCount,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -2,7 +2,10 @@ import { createRoot } from "react-dom/client";
|
|||
import { Provider } from "react-redux";
|
||||
import { PersistGate } from "redux-persist/integration/react";
|
||||
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 logger from "../utils/logger";
|
||||
import {
|
||||
|
@ -12,18 +15,9 @@ import {
|
|||
fetchXmrPrice,
|
||||
} from "./api";
|
||||
import App from "./components/App";
|
||||
import {
|
||||
checkBitcoinBalance,
|
||||
getAllSwapInfos,
|
||||
initEventListeners,
|
||||
} from "./rpc";
|
||||
import { initEventListeners } from "./rpc";
|
||||
import { persistor, store } from "./store/storeRenderer";
|
||||
|
||||
setInterval(() => {
|
||||
checkBitcoinBalance();
|
||||
getAllSwapInfos();
|
||||
}, 30 * 1000);
|
||||
|
||||
const container = document.getElementById("root");
|
||||
const root = createRoot(container!);
|
||||
root.render(
|
||||
|
@ -44,6 +38,7 @@ async function fetchInitialData() {
|
|||
"Fetched providers via UnstoppableSwap HTTP API",
|
||||
);
|
||||
} catch (e) {
|
||||
store.dispatch(registryConnectionFailed());
|
||||
logger.error(e, "Failed to fetch providers via UnstoppableSwap HTTP API");
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
GetLogsArgs,
|
||||
GetLogsResponse,
|
||||
GetSwapInfoResponse,
|
||||
ListSellersArgs,
|
||||
MoneroRecoveryArgs,
|
||||
ResumeSwapArgs,
|
||||
ResumeSwapResponse,
|
||||
|
@ -27,6 +28,7 @@ import { store } from "./store/storeRenderer";
|
|||
import { Provider } from "models/apiModel";
|
||||
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
|
||||
import { MoneroRecoveryResponse } from "models/rpcModel";
|
||||
import { ListSellersResponse } from "../models/tauriModel";
|
||||
|
||||
export async function initEventListeners() {
|
||||
// This operation is in-expensive
|
||||
|
@ -144,3 +146,11 @@ export async function getLogsOfSwap(
|
|||
redact,
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSellersAtRendezvousPoint(
|
||||
rendezvousPointAddress: string,
|
||||
): Promise<ListSellersResponse> {
|
||||
return await invoke<ListSellersArgs, ListSellersResponse>("list_sellers", {
|
||||
rendezvous_point: rendezvousPointAddress,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
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 { isProviderCompatible } from "utils/multiAddrUtils";
|
||||
import { sortProviderList } from "utils/sortUtils";
|
||||
|
||||
|
@ -12,7 +14,8 @@ export interface ProvidersSlice {
|
|||
};
|
||||
registry: {
|
||||
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;
|
||||
}
|
||||
|
@ -23,7 +26,7 @@ const initialState: ProvidersSlice = {
|
|||
},
|
||||
registry: {
|
||||
providers: stubTestnetProvider ? [stubTestnetProvider] : null,
|
||||
failedReconnectAttemptsSinceLastSuccess: 0,
|
||||
connectionFailsCount: 0,
|
||||
},
|
||||
selectedProvider: null,
|
||||
};
|
||||
|
@ -47,35 +50,42 @@ export const providersSlice = createSlice({
|
|||
name: "providers",
|
||||
initialState,
|
||||
reducers: {
|
||||
discoveredProvidersByRendezvous(
|
||||
slice,
|
||||
action: PayloadAction<ProviderStatus[]>,
|
||||
) {
|
||||
action.payload.forEach((discoveredProvider) => {
|
||||
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
|
||||
if (
|
||||
!slice.registry.providers?.some(
|
||||
(prov) =>
|
||||
prov.peerId === discoveredProvider.peerId &&
|
||||
prov.multiAddr === discoveredProvider.multiAddr,
|
||||
prov.peerId === discoveredProviderStatus.peerId &&
|
||||
prov.multiAddr === discoveredProviderStatus.multiAddr,
|
||||
)
|
||||
) {
|
||||
const indexOfExistingProvider = slice.rendezvous.providers.findIndex(
|
||||
(prov) =>
|
||||
prov.peerId === discoveredProvider.peerId &&
|
||||
prov.multiAddr === discoveredProvider.multiAddr,
|
||||
prov.peerId === discoveredProviderStatus.peerId &&
|
||||
prov.multiAddr === discoveredProviderStatus.multiAddr,
|
||||
);
|
||||
|
||||
// Avoid duplicates, replace instead
|
||||
// Avoid duplicate entries, replace them instead
|
||||
if (indexOfExistingProvider !== -1) {
|
||||
slice.rendezvous.providers[indexOfExistingProvider] =
|
||||
discoveredProvider;
|
||||
discoveredProviderStatus;
|
||||
} else {
|
||||
slice.rendezvous.providers.push(discoveredProvider);
|
||||
slice.rendezvous.providers.push(discoveredProviderStatus);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
slice.rendezvous.providers = sortProviderList(slice.rendezvous.providers);
|
||||
slice.selectedProvider = selectNewSelectedProvider(slice);
|
||||
},
|
||||
setRegistryProviders(
|
||||
slice,
|
||||
|
@ -90,8 +100,8 @@ export const providersSlice = createSlice({
|
|||
);
|
||||
slice.selectedProvider = selectNewSelectedProvider(slice);
|
||||
},
|
||||
increaseFailedRegistryReconnectAttemptsSinceLastSuccess(slice) {
|
||||
slice.registry.failedReconnectAttemptsSinceLastSuccess += 1;
|
||||
registryConnectionFailed(slice) {
|
||||
slice.registry.connectionFailsCount += 1;
|
||||
},
|
||||
setSelectedProvider(
|
||||
slice,
|
||||
|
@ -110,7 +120,7 @@ export const providersSlice = createSlice({
|
|||
export const {
|
||||
discoveredProvidersByRendezvous,
|
||||
setRegistryProviders,
|
||||
increaseFailedRegistryReconnectAttemptsSinceLastSuccess,
|
||||
registryConnectionFailed,
|
||||
setSelectedProvider,
|
||||
} = providersSlice.actions;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
return sats / 100000000;
|
||||
}
|
||||
|
@ -40,3 +45,25 @@ export function getMoneroTxExplorerUrl(txid: string, stagenet: boolean) {
|
|||
export function secondsToDays(seconds: number): number {
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { CliLog } from "models/cliModel";
|
||||
import { Multiaddr } from "multiaddr";
|
||||
|
||||
/*
|
||||
Extract btc amount from string
|
||||
|
@ -72,3 +73,38 @@ export function getLogsFromRawFileString(rawFileData: string): CliLog[] {
|
|||
export function logsToRawString(logs: (CliLog | string)[]): string {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ use swap::cli::{
|
|||
api::{
|
||||
request::{
|
||||
BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetLogsArgs, GetSwapInfosAllArgs,
|
||||
MoneroRecoveryArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||
ListSellersArgs, MoneroRecoveryArgs, ResumeSwapArgs, SuspendCurrentSwapArgs,
|
||||
WithdrawBtcArgs,
|
||||
},
|
||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle},
|
||||
Context, ContextBuilder,
|
||||
|
@ -169,6 +170,7 @@ pub fn run() {
|
|||
get_history,
|
||||
monero_recovery,
|
||||
get_logs,
|
||||
list_sellers,
|
||||
suspend_current_swap,
|
||||
is_context_available,
|
||||
])
|
||||
|
@ -208,6 +210,7 @@ tauri_command!(resume_swap, ResumeSwapArgs);
|
|||
tauri_command!(withdraw_btc, WithdrawBtcArgs);
|
||||
tauri_command!(monero_recovery, MoneroRecoveryArgs);
|
||||
tauri_command!(get_logs, GetLogsArgs);
|
||||
tauri_command!(list_sellers, ListSellersArgs);
|
||||
|
||||
// These commands require no arguments
|
||||
tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args);
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::tauri_bindings::TauriHandle;
|
|||
use crate::bitcoin::{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, SellerStatus};
|
||||
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, Seller, SellerStatus};
|
||||
use crate::common::get_logs;
|
||||
use crate::libp2p_ext::MultiAddrExt;
|
||||
use crate::network::quote::{BidQuote, ZeroQuoteReceived};
|
||||
|
@ -156,8 +156,14 @@ pub struct ListSellersArgs {
|
|||
pub rendezvous_point: Multiaddr,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct ListSellersResponse {
|
||||
sellers: Vec<Seller>,
|
||||
}
|
||||
|
||||
impl Request for ListSellersArgs {
|
||||
type Response = serde_json::Value;
|
||||
type Response = ListSellersResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
list_sellers(self, ctx).await
|
||||
|
@ -193,7 +199,7 @@ pub struct GetSwapInfoArgs {
|
|||
pub struct GetSwapInfoResponse {
|
||||
#[typeshare(serialized_as = "string")]
|
||||
pub swap_id: Uuid,
|
||||
pub seller: Seller,
|
||||
pub seller: AliceAddress,
|
||||
pub completed: bool,
|
||||
pub start_date: String,
|
||||
#[typeshare(serialized_as = "string")]
|
||||
|
@ -280,8 +286,8 @@ impl Request for GetHistoryArgs {
|
|||
|
||||
// Additional structs
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Seller {
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct AliceAddress {
|
||||
#[typeshare(serialized_as = "string")]
|
||||
pub peer_id: PeerId,
|
||||
pub addresses: Vec<String>,
|
||||
|
@ -507,7 +513,7 @@ pub async fn get_swap_info(
|
|||
|
||||
Ok(GetSwapInfoResponse {
|
||||
swap_id: args.swap_id,
|
||||
seller: Seller {
|
||||
seller: AliceAddress {
|
||||
peer_id,
|
||||
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(
|
||||
list_sellers: ListSellersArgs,
|
||||
context: Arc<Context>,
|
||||
) -> Result<serde_json::Value> {
|
||||
) -> Result<ListSellersResponse> {
|
||||
let ListSellersArgs { rendezvous_point } = list_sellers;
|
||||
let rendezvous_node_peer_id = rendezvous_point
|
||||
.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))]
|
||||
|
|
|
@ -14,6 +14,7 @@ use serde_with::{serde_as, DisplayFromStr};
|
|||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use typeshare::typeshare;
|
||||
|
||||
/// Returns sorted list of sellers, with [Online](Status::Online) listed first.
|
||||
///
|
||||
|
@ -60,14 +61,18 @@ pub async fn list_sellers(
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[typeshare]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
||||
pub struct Seller {
|
||||
pub status: Status,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[typeshare(serialized_as = "string")]
|
||||
pub multiaddr: Multiaddr,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum Status {
|
||||
Online(BidQuote),
|
||||
Unreachable,
|
||||
|
|
|
@ -398,6 +398,14 @@ async fn next_state(
|
|||
}
|
||||
ExpiredTimelocks::Cancel { .. } => {
|
||||
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)
|
||||
}
|
||||
ExpiredTimelocks::Punish => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue