fix(gui): "Approval not found or already handled"

This commit is contained in:
Binarybaron 2025-07-04 13:40:54 +02:00
parent 3ebaaad1fa
commit 293ff2cdf3
11 changed files with 167 additions and 64 deletions

4
Cargo.lock generated
View file

@ -9769,7 +9769,7 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142"
[[package]] [[package]]
name = "swap" name = "swap"
version = "2.3.3" version = "2.4.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arti-client", "arti-client",
@ -12323,7 +12323,7 @@ dependencies = [
[[package]] [[package]]
name = "unstoppableswap-gui-rs" name = "unstoppableswap-gui-rs"
version = "2.3.3" version = "2.4.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"monero-rpc-pool", "monero-rpc-pool",

View file

@ -23,7 +23,8 @@
"monero-sys/monero/", "monero-sys/monero/",
".git/**", ".git/**",
"**/node_modules/**", "**/node_modules/**",
"**/dist/**" "**/dist/**",
"seed/**"
], ],
"plugins": [ "plugins": [
"https://plugins.dprint.dev/markdown-0.18.0.wasm", "https://plugins.dprint.dev/markdown-0.18.0.wasm",

View file

@ -22,6 +22,7 @@ import {
getSwapInfo, getSwapInfo,
initializeContext, initializeContext,
listSellersAtRendezvousPoint, listSellersAtRendezvousPoint,
refreshApprovals,
updateAllNodeStatuses, updateAllNodeStatuses,
} from "./rpc"; } from "./rpc";
import { store } from "./store/storeRenderer"; import { store } from "./store/storeRenderer";
@ -44,6 +45,9 @@ const UPDATE_RATE_INTERVAL = 5 * 60 * 1_000;
// Fetch all conversations every 10 minutes // Fetch all conversations every 10 minutes
const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000; const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
// Fetch pending approvals every 10 seconds
const FETCH_PENDING_APPROVALS_INTERVAL = 2 * 1_000;
function setIntervalImmediate(callback: () => void, interval: number): void { function setIntervalImmediate(callback: () => void, interval: number): void {
callback(); callback();
setInterval(callback, interval); setInterval(callback, interval);
@ -60,6 +64,7 @@ export async function setupBackgroundTasks(): Promise<void> {
listSellersAtRendezvousPoint(store.getState().settings.rendezvousPoints), listSellersAtRendezvousPoint(store.getState().settings.rendezvousPoints),
DISCOVER_PEERS_INTERVAL, DISCOVER_PEERS_INTERVAL,
); );
setIntervalImmediate(refreshApprovals, FETCH_PENDING_APPROVALS_INTERVAL);
// Fetch all alerts // Fetch all alerts
updateAlerts(); updateAlerts();

View file

@ -19,10 +19,17 @@ export default function SwapWidget() {
sx={{ display: "flex", flexDirection: "column", gap: 2, width: "100%" }} sx={{ display: "flex", flexDirection: "column", gap: 2, width: "100%" }}
> >
<SwapStatusAlert swap={swapInfo} onlyShowIfUnusualAmountOfTimeHasPassed /> <SwapStatusAlert swap={swapInfo} onlyShowIfUnusualAmountOfTimeHasPassed />
<Dialog fullWidth maxWidth="md" open={debug} onClose={() => setDebug(false)}> <Dialog
fullWidth
maxWidth="md"
open={debug}
onClose={() => setDebug(false)}
>
<DebugPage /> <DebugPage />
<DialogActions> <DialogActions>
<Button variant="outlined" onClick={() => setDebug(false)}>Close</Button> <Button variant="outlined" onClick={() => setDebug(false)}>
Close
</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Paper <Paper

View file

@ -31,8 +31,14 @@ import {
RedactResponse, RedactResponse,
GetCurrentSwapResponse, GetCurrentSwapResponse,
LabeledMoneroAddress, LabeledMoneroAddress,
GetPendingApprovalsArgs,
GetPendingApprovalsResponse,
} from "models/tauriModel"; } from "models/tauriModel";
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice"; import {
rpcSetBalance,
rpcSetSwapInfo,
approvalRequestsReplaced,
} from "store/features/rpcSlice";
import { store } from "./store/storeRenderer"; import { store } from "./store/storeRenderer";
import { Maker } from "models/apiModel"; import { Maker } from "models/apiModel";
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils"; import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
@ -422,10 +428,23 @@ export async function resolveApproval(
requestId: string, requestId: string,
accept: object, accept: object,
): Promise<void> { ): Promise<void> {
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>( try {
"resolve_approval_request", await invoke<ResolveApprovalArgs, ResolveApprovalResponse>(
{ request_id: requestId, accept }, "resolve_approval_request",
{ request_id: requestId, accept },
);
} catch (error) {
// Refresh approval list when resolve fails to keep UI in sync
await refreshApprovals();
throw error;
}
}
export async function refreshApprovals(): Promise<void> {
const response = await invokeNoArgs<GetPendingApprovalsResponse>(
"get_pending_approvals",
); );
store.dispatch(approvalRequestsReplaced(response.approvals));
} }
export async function checkSeed(seed: string): Promise<boolean> { export async function checkSeed(seed: string): Promise<boolean> {

View file

@ -140,6 +140,13 @@ export const rpcSlice = createSlice({
const requestId = event.request_id; const requestId = event.request_id;
slice.state.approvalRequests[requestId] = event; slice.state.approvalRequests[requestId] = event;
}, },
approvalRequestsReplaced(slice, action: PayloadAction<ApprovalRequest[]>) {
// Clear existing approval requests and replace with new ones
slice.state.approvalRequests = {};
action.payload.forEach((approval) => {
slice.state.approvalRequests[approval.request_id] = approval;
});
},
backgroundProgressEventReceived( backgroundProgressEventReceived(
slice, slice,
action: PayloadAction<TauriBackgroundProgressWrapper>, action: PayloadAction<TauriBackgroundProgressWrapper>,
@ -165,6 +172,7 @@ export const {
rpcSetBackgroundRefundState, rpcSetBackgroundRefundState,
timelockChangeEventReceived, timelockChangeEventReceived,
approvalEventReceived, approvalEventReceived,
approvalRequestsReplaced,
backgroundProgressEventReceived, backgroundProgressEventReceived,
backgroundProgressEventRemoved, backgroundProgressEventRemoved,
} = rpcSlice.actions; } = rpcSlice.actions;

View file

@ -10,8 +10,8 @@ use swap::cli::{
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs, BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs, CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
CheckSeedResponse, ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs, CheckSeedResponse, ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs,
GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetSwapInfoArgs, GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetPendingApprovalsResponse,
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
ResolveApprovalArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs, ResolveApprovalArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
}, },
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings}, tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
@ -201,6 +201,7 @@ pub fn run() {
redact, redact,
save_txt_files, save_txt_files,
check_seed, check_seed,
get_pending_approvals,
]) ])
.setup(setup) .setup(setup)
.build(tauri::generate_context!()) .build(tauri::generate_context!())
@ -354,17 +355,30 @@ async fn resolve_approval_request(
.resolve_approval(args.request_id.parse().unwrap(), args.accept) .resolve_approval(args.request_id.parse().unwrap(), args.accept)
.await .await
.to_string_result()?; .to_string_result()?;
println!("Resolved approval request");
Ok(()) Ok(())
} }
#[tauri::command]
async fn get_pending_approvals(
state: tauri::State<'_, RwLock<State>>,
) -> Result<GetPendingApprovalsResponse, String> {
let approvals = state
.read()
.await
.handle
.get_pending_approvals()
.await
.to_string_result()?;
Ok(GetPendingApprovalsResponse { approvals })
}
/// Tauri command to initialize the Context /// Tauri command to initialize the Context
#[tauri::command] #[tauri::command]
async fn initialize_context( async fn initialize_context(
settings: TauriSettings, settings: TauriSettings,
testnet: bool, testnet: bool,
app_handle: tauri::AppHandle,
state: tauri::State<'_, RwLock<State>>, state: tauri::State<'_, RwLock<State>>,
) -> Result<(), String> { ) -> Result<(), String> {
// When the app crashes, the monero-wallet-rpc process may not be killed // When the app crashes, the monero-wallet-rpc process may not be killed

View file

@ -8,7 +8,7 @@ use crate::common::{get_logs, redact};
use crate::libp2p_ext::MultiAddrExt; use crate::libp2p_ext::MultiAddrExt;
use crate::monero::wallet_rpc::MoneroDaemon; use crate::monero::wallet_rpc::MoneroDaemon;
use crate::monero::MoneroAddressPool; use crate::monero::MoneroAddressPool;
use crate::network::quote::{BidQuote, ZeroQuoteReceived}; use crate::network::quote::BidQuote;
use crate::network::rendezvous::XmrBtcNamespace; use crate::network::rendezvous::XmrBtcNamespace;
use crate::network::swarm; use crate::network::swarm;
use crate::protocol::bob::{BobState, Swap}; use crate::protocol::bob::{BobState, Swap};
@ -1658,3 +1658,9 @@ impl CheckSeedArgs {
}) })
} }
} }
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct GetPendingApprovalsResponse {
pub approvals: Vec<crate::cli::api::tauri_bindings::ApprovalRequest>,
}

View file

@ -113,6 +113,7 @@ struct PendingApproval {
responder: Option<oneshot::Sender<serde_json::Value>>, responder: Option<oneshot::Sender<serde_json::Value>>,
#[allow(dead_code)] #[allow(dead_code)]
expiration_ts: u64, expiration_ts: u64,
request: ApprovalRequest,
} }
impl Drop for PendingApproval { impl Drop for PendingApproval {
@ -182,7 +183,7 @@ impl TauriHandle {
timeout_secs: Option<u64>, timeout_secs: Option<u64>,
) -> Result<Response> ) -> Result<Response>
where where
Response: serde::de::DeserializeOwned + Clone, Response: serde::de::DeserializeOwned + Clone + Serialize,
{ {
#[cfg(not(feature = "tauri"))] #[cfg(not(feature = "tauri"))]
{ {
@ -191,31 +192,28 @@ impl TauriHandle {
#[cfg(feature = "tauri")] #[cfg(feature = "tauri")]
{ {
// Emit the creation of the approval request to the frontend // Create the approval request
// TODO: We need to send a UUID with it here // Generate the UUID
// Set the expiration timestamp
let (responder, receiver) = oneshot::channel();
let request_id = Uuid::new_v4(); let request_id = Uuid::new_v4();
// No timeout = one week
let timeout_secs = timeout_secs.unwrap_or(60 * 60 * 24 * 7); let timeout_secs = timeout_secs.unwrap_or(60 * 60 * 24 * 7);
let timeout_duration = Duration::from_secs(timeout_secs);
let expiration_ts = SystemTime::now() let expiration_ts = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.map_err(|e| anyhow!("Failed to get current time: {}", e))? .map_err(|e| anyhow!("Failed to get current time: {}", e))?
.as_secs() .as_secs()
+ timeout_secs; + timeout_duration.as_secs();
let request = ApprovalRequest { let request = ApprovalRequest {
request: request_type, request: request_type,
request_status: RequestStatus::Pending { expiration_ts }, request_status: RequestStatus::Pending { expiration_ts },
request_id, request_id,
}; };
use anyhow::bail; // Emit the "pending" event
self.emit_approval(request.clone()); self.emit_approval(request.clone());
tracing::debug!(%request, "Emitted approval request event"); tracing::debug!(%request, "Emitted approval request event");
// Construct the data structure we use to internally track the approval request
let (responder, receiver) = oneshot::channel();
let timeout_duration = Duration::from_secs(timeout_secs);
let pending = PendingApproval { let pending = PendingApproval {
responder: Some(responder), responder: Some(responder),
@ -224,6 +222,7 @@ impl TauriHandle {
.map_err(|e| anyhow!("Failed to get current time: {}", e))? .map_err(|e| anyhow!("Failed to get current time: {}", e))?
.as_secs() .as_secs()
+ timeout_secs, + timeout_secs,
request: request.clone(),
}; };
// Lock map and insert the pending approval // Lock map and insert the pending approval
@ -246,41 +245,48 @@ impl TauriHandle {
// Determine if the request will be accepted or rejected // Determine if the request will be accepted or rejected
// Either by being resolved by the user, or by timing out // Either by being resolved by the user, or by timing out
let unparsed_response = tokio::select! { let unparsed_response = tokio::select! {
res = receiver => res.map_err(|_| anyhow!("Approval responder dropped"))?, res = receiver => Some(res.map_err(|_| anyhow!("Approval responder dropped"))?),
_ = tokio::time::sleep(timeout_duration) => { _ = tokio::time::sleep(timeout_duration) => {
bail!("Approval request timed out and was therefore rejected"); None
}, },
}; };
tracing::debug!(%unparsed_response, "Received approval response"); let maybe_response: Option<Response> = match &unparsed_response {
Some(value) => serde_json::from_value(value.clone())
let response: Result<Response> = serde_json::from_value(unparsed_response.clone()) .inspect_err(|e| {
.context("Failed to parse approval response to expected type"); tracing::error!("Failed to parse approval response to expected type: {}", e)
})
.ok(),
None => None,
};
let mut map = self let mut map = self
.0 .0
.pending_approvals .pending_approvals
.lock() .lock()
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?; .map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
if let Some(_pending) = map.remove(&request_id) { if let Some(_pending) = map.remove(&request_id) {
let status = if response.is_ok() { let status = match &maybe_response {
RequestStatus::Resolved { Some(_) => RequestStatus::Resolved {
approve_input: unparsed_response, approve_input: unparsed_response.unwrap_or(serde_json::Value::Bool(false)),
} },
} else { None => RequestStatus::Rejected,
RequestStatus::Rejected
}; };
// Set the status and emit the event
let mut approval = request.clone(); let mut approval = request.clone();
approval.request_status = status.clone(); approval.request_status = status;
self.emit_approval(approval.clone());
tracing::debug!(%approval, "Resolved approval request"); tracing::debug!(%approval, "Resolved approval request");
self.emit_approval(approval);
} }
cleanup_guard.disarm();
tracing::debug!("Returning approval response"); tracing::debug!("Returning approval response");
response maybe_response.context("Approval was rejected")
} }
} }
@ -316,6 +322,29 @@ impl TauriHandle {
} }
} }
} }
pub async fn get_pending_approvals(&self) -> Result<Vec<ApprovalRequest>> {
#[cfg(not(feature = "tauri"))]
{
return Ok(Vec::new());
}
#[cfg(feature = "tauri")]
{
let pending_map = self
.0
.pending_approvals
.lock()
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
let approvals: Vec<ApprovalRequest> = pending_map
.values()
.map(|pending| pending.request.clone())
.collect();
Ok(approvals)
}
}
} }
impl Display for ApprovalRequest { impl Display for ApprovalRequest {
@ -892,19 +921,30 @@ impl ApprovalCleanupGuard {
impl Drop for ApprovalCleanupGuard { impl Drop for ApprovalCleanupGuard {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(request_id) = self.request_id { if let Some(request_id) = self.request_id.take() {
tracing::debug!(%request_id, "Approval handle dropped, we should cleanup now"); let approval_store = self.approval_store.clone();
let handle = self.handle.clone();
// Lock the Mutex tokio::task::spawn_blocking(move || {
if let Ok(mut approval_store) = self.approval_store.lock() { tracing::debug!(%request_id, "Approval handle dropped, we should cleanup now");
// Check if the request id still present in the map
if let Some(mut pending_approval) = approval_store.remove(&request_id) { // Lock the Mutex
// If there is still someone listening, send a rejection if let Ok(mut approval_store) = approval_store.lock() {
if let Some(responder) = pending_approval.responder.take() { // Check if the request id still present in the map
let _ = responder.send(serde_json::Value::Bool(false)); if let Some(mut pending_approval) = approval_store.remove(&request_id) {
// If there is still someone listening, send a rejection
if let Some(responder) = pending_approval.responder.take() {
let _ = responder.send(serde_json::Value::Bool(false));
}
handle.emit_approval(ApprovalRequest {
request: pending_approval.request.clone().request,
request_status: RequestStatus::Rejected,
request_id,
});
} }
} }
} });
} }
} }
} }

View file

@ -99,7 +99,7 @@ pub async fn list_sellers_init(
Some(db) => match db.get_all_peer_addresses().await { Some(db) => match db.get_all_peer_addresses().await {
Ok(peers) => VecDeque::from(peers), Ok(peers) => VecDeque::from(peers),
Err(err) => { Err(err) => {
tracing::error!(%err, "Failed to get peers from database for list_sellers"); tracing::trace!(%err, "Failed to get peers from database for list_sellers");
VecDeque::new() VecDeque::new()
} }
}, },
@ -714,7 +714,7 @@ impl EventLoop {
.insert(*peer_id, RendezvousPointStatus::Dialed); .insert(*peer_id, RendezvousPointStatus::Dialed);
if let Err(e) = self.swarm.dial(dial_opts) { if let Err(e) = self.swarm.dial(dial_opts) {
tracing::error!(%peer_id, %multiaddr, error = %e, "Failed to dial rendezvous point"); tracing::trace!(%peer_id, %multiaddr, error = %e, "Failed to dial rendezvous point");
self.rendezvous_points_status self.rendezvous_points_status
.insert(*peer_id, RendezvousPointStatus::Failed); .insert(*peer_id, RendezvousPointStatus::Failed);
@ -754,7 +754,7 @@ impl EventLoop {
match swarm_event { match swarm_event {
SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } => { SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } => {
if self.is_rendezvous_point(&peer_id) { if self.is_rendezvous_point(&peer_id) {
tracing::info!( tracing::trace!(
"Connected to rendezvous point, discovering nodes in '{}' namespace ...", "Connected to rendezvous point, discovering nodes in '{}' namespace ...",
self.namespace self.namespace
); );
@ -769,7 +769,7 @@ impl EventLoop {
); );
} else { } else {
let address = endpoint.get_remote_address(); let address = endpoint.get_remote_address();
tracing::debug!(%peer_id, %address, "Connection established to peer for list-sellers"); tracing::trace!(%peer_id, %address, "Connection established to peer for list-sellers");
self.reachable_asb_address.insert(peer_id, address.clone()); self.reachable_asb_address.insert(peer_id, address.clone());
// Update the peer state with the reachable address // Update the peer state with the reachable address
@ -782,7 +782,7 @@ impl EventLoop {
SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
if let Some(peer_id) = peer_id { if let Some(peer_id) = peer_id {
if let Some(rendezvous_point) = self.get_rendezvous_point(&peer_id) { if let Some(rendezvous_point) = self.get_rendezvous_point(&peer_id) {
tracing::warn!( tracing::trace!(
%peer_id, %peer_id,
%rendezvous_point, %rendezvous_point,
"Failed to connect to rendezvous point: {}", "Failed to connect to rendezvous point: {}",
@ -792,7 +792,7 @@ impl EventLoop {
// Update the status of the rendezvous point to failed // Update the status of the rendezvous point to failed
self.rendezvous_points_status.insert(peer_id, RendezvousPointStatus::Failed); self.rendezvous_points_status.insert(peer_id, RendezvousPointStatus::Failed);
} else { } else {
tracing::warn!( tracing::trace!(
%peer_id, %peer_id,
"Failed to connect to peer: {}", "Failed to connect to peer: {}",
error error
@ -804,13 +804,13 @@ impl EventLoop {
} }
} }
} else { } else {
tracing::warn!("Failed to connect (no peer id): {}", error); tracing::trace!("Failed to connect (no peer id): {}", error);
} }
} }
SwarmEvent::Behaviour(OutEvent::Rendezvous( SwarmEvent::Behaviour(OutEvent::Rendezvous(
libp2p::rendezvous::client::Event::Discovered { registrations, rendezvous_node, .. }, libp2p::rendezvous::client::Event::Discovered { registrations, rendezvous_node, .. },
)) => { )) => {
tracing::debug!(%rendezvous_node, num_peers = %registrations.len(), "Discovered peers at rendezvous point"); tracing::trace!(%rendezvous_node, num_peers = %registrations.len(), "Discovered peers at rendezvous point");
for registration in registrations { for registration in registrations {
let peer = registration.record.peer_id(); let peer = registration.record.peer_id();
@ -836,7 +836,7 @@ impl EventLoop {
let new_state = state.apply_quote(Ok(response)); let new_state = state.apply_quote(Ok(response));
self.peer_states.insert(peer, new_state); self.peer_states.insert(peer, new_state);
} else { } else {
tracing::warn!(%peer, "Received bid quote from unexpected peer, this record will be removed!"); tracing::trace!(%peer, "Received bid quote from unexpected peer, this record will be removed!");
} }
} }
request_response::Message::Request { .. } => unreachable!("we only request quotes, not respond") request_response::Message::Request { .. } => unreachable!("we only request quotes, not respond")
@ -844,7 +844,7 @@ impl EventLoop {
} }
request_response::Event::OutboundFailure { peer, error, .. } => { request_response::Event::OutboundFailure { peer, error, .. } => {
if self.is_rendezvous_point(&peer) { if self.is_rendezvous_point(&peer) {
tracing::debug!(%peer, "Outbound failure when communicating with rendezvous node: {:#}", error); tracing::trace!(%peer, "Outbound failure when communicating with rendezvous node: {:#}", error);
// Update the status of the rendezvous point to failed // Update the status of the rendezvous point to failed
self.rendezvous_points_status.insert(peer, RendezvousPointStatus::Failed); self.rendezvous_points_status.insert(peer, RendezvousPointStatus::Failed);
@ -855,7 +855,7 @@ impl EventLoop {
} }
request_response::Event::InboundFailure { peer, error, .. } => { request_response::Event::InboundFailure { peer, error, .. } => {
if self.is_rendezvous_point(&peer) { if self.is_rendezvous_point(&peer) {
tracing::debug!(%peer, "Inbound failure when communicating with rendezvous node: {:#}", error); tracing::trace!(%peer, "Inbound failure when communicating with rendezvous node: {:#}", error);
// Update the status of the rendezvous point to failed // Update the status of the rendezvous point to failed
self.rendezvous_points_status.insert(peer, RendezvousPointStatus::Failed); self.rendezvous_points_status.insert(peer, RendezvousPointStatus::Failed);
@ -876,7 +876,7 @@ impl EventLoop {
} }
} }
identify::Event::Error { peer_id, error } => { identify::Event::Error { peer_id, error } => {
tracing::error!(%peer_id, error = %error, "Error when identifying peer"); tracing::trace!(%peer_id, error = %error, "Error when identifying peer");
if let Some(state) = self.peer_states.remove(&peer_id) { if let Some(state) = self.peer_states.remove(&peer_id) {
let failed_state = state.mark_failed(format!("Error when identifying peer: {}", error)); let failed_state = state.mark_failed(format!("Error when identifying peer: {}", error));
@ -937,7 +937,7 @@ impl EventLoop {
error_message, error_message,
.. ..
} => { } => {
tracing::warn!(%peer_id, error = %error_message, "Peer failed"); tracing::trace!(%peer_id, error = %error_message, "Peer failed");
Ok(SellerStatus::Unreachable(UnreachableSeller { Ok(SellerStatus::Unreachable(UnreachableSeller {
peer_id: *peer_id, peer_id: *peer_id,

View file

@ -55,7 +55,10 @@ impl SqliteDatabase {
} }
async fn run_migrations(&mut self) -> anyhow::Result<()> { async fn run_migrations(&mut self) -> anyhow::Result<()> {
sqlx::migrate!("./migrations").run(&self.pool).await?; sqlx::migrate!("./migrations")
.set_ignore_missing(true)
.run(&self.pool)
.await?;
Ok(()) Ok(())
} }
} }