mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-25 10:23:20 -05:00
fix(gui): "Approval not found or already handled"
This commit is contained in:
parent
3ebaaad1fa
commit
293ff2cdf3
11 changed files with 167 additions and 64 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -9769,7 +9769,7 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142"
|
|||
|
||||
[[package]]
|
||||
name = "swap"
|
||||
version = "2.3.3"
|
||||
version = "2.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arti-client",
|
||||
|
|
@ -12323,7 +12323,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "unstoppableswap-gui-rs"
|
||||
version = "2.3.3"
|
||||
version = "2.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monero-rpc-pool",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@
|
|||
"monero-sys/monero/",
|
||||
".git/**",
|
||||
"**/node_modules/**",
|
||||
"**/dist/**"
|
||||
"**/dist/**",
|
||||
"seed/**"
|
||||
],
|
||||
"plugins": [
|
||||
"https://plugins.dprint.dev/markdown-0.18.0.wasm",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
getSwapInfo,
|
||||
initializeContext,
|
||||
listSellersAtRendezvousPoint,
|
||||
refreshApprovals,
|
||||
updateAllNodeStatuses,
|
||||
} from "./rpc";
|
||||
import { store } from "./store/storeRenderer";
|
||||
|
|
@ -44,6 +45,9 @@ const UPDATE_RATE_INTERVAL = 5 * 60 * 1_000;
|
|||
// Fetch all conversations every 10 minutes
|
||||
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 {
|
||||
callback();
|
||||
setInterval(callback, interval);
|
||||
|
|
@ -60,6 +64,7 @@ export async function setupBackgroundTasks(): Promise<void> {
|
|||
listSellersAtRendezvousPoint(store.getState().settings.rendezvousPoints),
|
||||
DISCOVER_PEERS_INTERVAL,
|
||||
);
|
||||
setIntervalImmediate(refreshApprovals, FETCH_PENDING_APPROVALS_INTERVAL);
|
||||
|
||||
// Fetch all alerts
|
||||
updateAlerts();
|
||||
|
|
|
|||
|
|
@ -19,10 +19,17 @@ export default function SwapWidget() {
|
|||
sx={{ display: "flex", flexDirection: "column", gap: 2, width: "100%" }}
|
||||
>
|
||||
<SwapStatusAlert swap={swapInfo} onlyShowIfUnusualAmountOfTimeHasPassed />
|
||||
<Dialog fullWidth maxWidth="md" open={debug} onClose={() => setDebug(false)}>
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
open={debug}
|
||||
onClose={() => setDebug(false)}
|
||||
>
|
||||
<DebugPage />
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={() => setDebug(false)}>Close</Button>
|
||||
<Button variant="outlined" onClick={() => setDebug(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<Paper
|
||||
|
|
|
|||
|
|
@ -31,8 +31,14 @@ import {
|
|||
RedactResponse,
|
||||
GetCurrentSwapResponse,
|
||||
LabeledMoneroAddress,
|
||||
GetPendingApprovalsArgs,
|
||||
GetPendingApprovalsResponse,
|
||||
} from "models/tauriModel";
|
||||
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
|
||||
import {
|
||||
rpcSetBalance,
|
||||
rpcSetSwapInfo,
|
||||
approvalRequestsReplaced,
|
||||
} from "store/features/rpcSlice";
|
||||
import { store } from "./store/storeRenderer";
|
||||
import { Maker } from "models/apiModel";
|
||||
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
|
||||
|
|
@ -422,10 +428,23 @@ export async function resolveApproval(
|
|||
requestId: string,
|
||||
accept: object,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>(
|
||||
"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> {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,13 @@ export const rpcSlice = createSlice({
|
|||
const requestId = event.request_id;
|
||||
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(
|
||||
slice,
|
||||
action: PayloadAction<TauriBackgroundProgressWrapper>,
|
||||
|
|
@ -165,6 +172,7 @@ export const {
|
|||
rpcSetBackgroundRefundState,
|
||||
timelockChangeEventReceived,
|
||||
approvalEventReceived,
|
||||
approvalRequestsReplaced,
|
||||
backgroundProgressEventReceived,
|
||||
backgroundProgressEventRemoved,
|
||||
} = rpcSlice.actions;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ use swap::cli::{
|
|||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
|
||||
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
|
||||
CheckSeedResponse, ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs,
|
||||
GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetSwapInfoArgs,
|
||||
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
|
||||
GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetPendingApprovalsResponse,
|
||||
GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
|
||||
ResolveApprovalArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||
},
|
||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
||||
|
|
@ -201,6 +201,7 @@ pub fn run() {
|
|||
redact,
|
||||
save_txt_files,
|
||||
check_seed,
|
||||
get_pending_approvals,
|
||||
])
|
||||
.setup(setup)
|
||||
.build(tauri::generate_context!())
|
||||
|
|
@ -354,17 +355,30 @@ async fn resolve_approval_request(
|
|||
.resolve_approval(args.request_id.parse().unwrap(), args.accept)
|
||||
.await
|
||||
.to_string_result()?;
|
||||
println!("Resolved approval request");
|
||||
|
||||
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]
|
||||
async fn initialize_context(
|
||||
settings: TauriSettings,
|
||||
testnet: bool,
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, RwLock<State>>,
|
||||
) -> Result<(), String> {
|
||||
// When the app crashes, the monero-wallet-rpc process may not be killed
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::common::{get_logs, redact};
|
|||
use crate::libp2p_ext::MultiAddrExt;
|
||||
use crate::monero::wallet_rpc::MoneroDaemon;
|
||||
use crate::monero::MoneroAddressPool;
|
||||
use crate::network::quote::{BidQuote, ZeroQuoteReceived};
|
||||
use crate::network::quote::BidQuote;
|
||||
use crate::network::rendezvous::XmrBtcNamespace;
|
||||
use crate::network::swarm;
|
||||
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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ struct PendingApproval {
|
|||
responder: Option<oneshot::Sender<serde_json::Value>>,
|
||||
#[allow(dead_code)]
|
||||
expiration_ts: u64,
|
||||
request: ApprovalRequest,
|
||||
}
|
||||
|
||||
impl Drop for PendingApproval {
|
||||
|
|
@ -182,7 +183,7 @@ impl TauriHandle {
|
|||
timeout_secs: Option<u64>,
|
||||
) -> Result<Response>
|
||||
where
|
||||
Response: serde::de::DeserializeOwned + Clone,
|
||||
Response: serde::de::DeserializeOwned + Clone + Serialize,
|
||||
{
|
||||
#[cfg(not(feature = "tauri"))]
|
||||
{
|
||||
|
|
@ -191,31 +192,28 @@ impl TauriHandle {
|
|||
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
// Emit the creation of the approval request to the frontend
|
||||
// TODO: We need to send a UUID with it here
|
||||
|
||||
// Create the approval request
|
||||
// Generate the UUID
|
||||
// Set the expiration timestamp
|
||||
let (responder, receiver) = oneshot::channel();
|
||||
let request_id = Uuid::new_v4();
|
||||
// No timeout = one week
|
||||
let timeout_secs = timeout_secs.unwrap_or(60 * 60 * 24 * 7);
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
let expiration_ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|e| anyhow!("Failed to get current time: {}", e))?
|
||||
.as_secs()
|
||||
+ timeout_secs;
|
||||
+ timeout_duration.as_secs();
|
||||
let request = ApprovalRequest {
|
||||
request: request_type,
|
||||
request_status: RequestStatus::Pending { expiration_ts },
|
||||
request_id,
|
||||
};
|
||||
|
||||
use anyhow::bail;
|
||||
// Emit the "pending" event
|
||||
self.emit_approval(request.clone());
|
||||
|
||||
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 {
|
||||
responder: Some(responder),
|
||||
|
|
@ -224,6 +222,7 @@ impl TauriHandle {
|
|||
.map_err(|e| anyhow!("Failed to get current time: {}", e))?
|
||||
.as_secs()
|
||||
+ timeout_secs,
|
||||
request: request.clone(),
|
||||
};
|
||||
|
||||
// Lock map and insert the pending approval
|
||||
|
|
@ -246,41 +245,48 @@ impl TauriHandle {
|
|||
// Determine if the request will be accepted or rejected
|
||||
// Either by being resolved by the user, or by timing out
|
||||
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) => {
|
||||
bail!("Approval request timed out and was therefore rejected");
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
tracing::debug!(%unparsed_response, "Received approval response");
|
||||
|
||||
let response: Result<Response> = serde_json::from_value(unparsed_response.clone())
|
||||
.context("Failed to parse approval response to expected type");
|
||||
let maybe_response: Option<Response> = match &unparsed_response {
|
||||
Some(value) => serde_json::from_value(value.clone())
|
||||
.inspect_err(|e| {
|
||||
tracing::error!("Failed to parse approval response to expected type: {}", e)
|
||||
})
|
||||
.ok(),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut map = self
|
||||
.0
|
||||
.pending_approvals
|
||||
.lock()
|
||||
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
|
||||
|
||||
if let Some(_pending) = map.remove(&request_id) {
|
||||
let status = if response.is_ok() {
|
||||
RequestStatus::Resolved {
|
||||
approve_input: unparsed_response,
|
||||
}
|
||||
} else {
|
||||
RequestStatus::Rejected
|
||||
let status = match &maybe_response {
|
||||
Some(_) => RequestStatus::Resolved {
|
||||
approve_input: unparsed_response.unwrap_or(serde_json::Value::Bool(false)),
|
||||
},
|
||||
None => RequestStatus::Rejected,
|
||||
};
|
||||
|
||||
// Set the status and emit the event
|
||||
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");
|
||||
self.emit_approval(approval);
|
||||
}
|
||||
|
||||
cleanup_guard.disarm();
|
||||
|
||||
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 {
|
||||
|
|
@ -892,19 +921,30 @@ impl ApprovalCleanupGuard {
|
|||
|
||||
impl Drop for ApprovalCleanupGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(request_id) = self.request_id {
|
||||
if let Some(request_id) = self.request_id.take() {
|
||||
let approval_store = self.approval_store.clone();
|
||||
let handle = self.handle.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
tracing::debug!(%request_id, "Approval handle dropped, we should cleanup now");
|
||||
|
||||
// Lock the Mutex
|
||||
if let Ok(mut approval_store) = self.approval_store.lock() {
|
||||
if let Ok(mut approval_store) = approval_store.lock() {
|
||||
// Check if the request id still present in the map
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ pub async fn list_sellers_init(
|
|||
Some(db) => match db.get_all_peer_addresses().await {
|
||||
Ok(peers) => VecDeque::from(peers),
|
||||
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()
|
||||
}
|
||||
},
|
||||
|
|
@ -714,7 +714,7 @@ impl EventLoop {
|
|||
.insert(*peer_id, RendezvousPointStatus::Dialed);
|
||||
|
||||
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
|
||||
.insert(*peer_id, RendezvousPointStatus::Failed);
|
||||
|
|
@ -754,7 +754,7 @@ impl EventLoop {
|
|||
match swarm_event {
|
||||
SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } => {
|
||||
if self.is_rendezvous_point(&peer_id) {
|
||||
tracing::info!(
|
||||
tracing::trace!(
|
||||
"Connected to rendezvous point, discovering nodes in '{}' namespace ...",
|
||||
self.namespace
|
||||
);
|
||||
|
|
@ -769,7 +769,7 @@ impl EventLoop {
|
|||
);
|
||||
} else {
|
||||
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());
|
||||
|
||||
// Update the peer state with the reachable address
|
||||
|
|
@ -782,7 +782,7 @@ impl EventLoop {
|
|||
SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
|
||||
if let Some(peer_id) = peer_id {
|
||||
if let Some(rendezvous_point) = self.get_rendezvous_point(&peer_id) {
|
||||
tracing::warn!(
|
||||
tracing::trace!(
|
||||
%peer_id,
|
||||
%rendezvous_point,
|
||||
"Failed to connect to rendezvous point: {}",
|
||||
|
|
@ -792,7 +792,7 @@ impl EventLoop {
|
|||
// Update the status of the rendezvous point to failed
|
||||
self.rendezvous_points_status.insert(peer_id, RendezvousPointStatus::Failed);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
tracing::trace!(
|
||||
%peer_id,
|
||||
"Failed to connect to peer: {}",
|
||||
error
|
||||
|
|
@ -804,13 +804,13 @@ impl EventLoop {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Failed to connect (no peer id): {}", error);
|
||||
tracing::trace!("Failed to connect (no peer id): {}", error);
|
||||
}
|
||||
}
|
||||
SwarmEvent::Behaviour(OutEvent::Rendezvous(
|
||||
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 {
|
||||
let peer = registration.record.peer_id();
|
||||
|
|
@ -836,7 +836,7 @@ impl EventLoop {
|
|||
let new_state = state.apply_quote(Ok(response));
|
||||
self.peer_states.insert(peer, new_state);
|
||||
} 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")
|
||||
|
|
@ -844,7 +844,7 @@ impl EventLoop {
|
|||
}
|
||||
request_response::Event::OutboundFailure { peer, error, .. } => {
|
||||
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
|
||||
self.rendezvous_points_status.insert(peer, RendezvousPointStatus::Failed);
|
||||
|
|
@ -855,7 +855,7 @@ impl EventLoop {
|
|||
}
|
||||
request_response::Event::InboundFailure { peer, error, .. } => {
|
||||
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
|
||||
self.rendezvous_points_status.insert(peer, RendezvousPointStatus::Failed);
|
||||
|
|
@ -876,7 +876,7 @@ impl EventLoop {
|
|||
}
|
||||
}
|
||||
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) {
|
||||
let failed_state = state.mark_failed(format!("Error when identifying peer: {}", error));
|
||||
|
|
@ -937,7 +937,7 @@ impl EventLoop {
|
|||
error_message,
|
||||
..
|
||||
} => {
|
||||
tracing::warn!(%peer_id, error = %error_message, "Peer failed");
|
||||
tracing::trace!(%peer_id, error = %error_message, "Peer failed");
|
||||
|
||||
Ok(SellerStatus::Unreachable(UnreachableSeller {
|
||||
peer_id: *peer_id,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@ impl SqliteDatabase {
|
|||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue