mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
feat(tips): stage 4 — claim / clawback / confirm-sweep + sweep reconciler (money-out)
Escrow settlement, audited guards ported verbatim:
- mark_tip_claiming: status IN('pending','claiming') AND confs>=required AND
funding_amount_verified AND sweep_confirmed_at IS NULL (double-claim guard).
- confirm_tip_sweep: RECORD-not-settle, first-recorder-wins (COALESCE +
sweep_txid IS NULL OR =$3) — RBF-steal safe; reconciler owns claiming->claimed.
- confirm_sweep_onchain: SOLE writer of 'claimed', one-way status='claiming' AND
sweep_confirmed_at IS NULL lock; settles only after on-chain sweep confirms to
per-asset depth (btc/ltc 1, wow 4, xmr 10).
- clawback: 5-status set incl 'cancelled', blocked once sweep_confirmed_at set.
- revert_sweep_on_reorg: only from 'claimed', re-opens to 'claiming' + clears txid.
- sweep_reconciler: address-history-driven (not recorded txid), pick_winner
(earliest height, lexi-smallest tie), pg advisory locks on a pinned conn for
single-instance safety; reorg pass reverts only on affirmative vanish, never on
probe error. DMs stubbed, exactly-once stamps kept.
- POST claim/confirm-sweep/clawback endpoints + openapi.
- 16 integration + 12 reconciler unit tests; build clean; SocialTipRow dead_code
allow removed (all fields now read).
This commit is contained in:
+45
@@ -285,6 +285,51 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
});
|
||||
}
|
||||
|
||||
// Public-tips "money-out" reconciler: the settle-on-sweep-confirmation pass
|
||||
// (probe each claiming tip's ADDRESS, settle claiming -> claimed once the
|
||||
// sweep confirms to the per-asset depth). Sole new writer of 'claimed'. Runs
|
||||
// an immediate startup pass (so a restart doesn't wait a full interval before
|
||||
// settling anything that confirmed while we were down) then every 60s. Same
|
||||
// catch_unwind guard as the money-in worker.
|
||||
if state.config.features.tips {
|
||||
use futures::FutureExt;
|
||||
let state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut tick = tokio::time::interval(std::time::Duration::from_secs(60));
|
||||
loop {
|
||||
tick.tick().await;
|
||||
let cycle = std::panic::AssertUnwindSafe(
|
||||
smirk_backend_core::tips::run_sweep_reconcile_cycle(state.clone()),
|
||||
);
|
||||
if cycle.catch_unwind().await.is_err() {
|
||||
tracing::error!("sweep reconcile cycle panicked; continuing after backoff");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Public-tips reorg-check pass: re-probe settled tips and revert any whose
|
||||
// recorded sweep was orphaned by a chain reorg. Lowest-frequency, novel, and
|
||||
// recoverable, so it runs on a slower cadence (300s) than the confirm pass.
|
||||
if state.config.features.tips {
|
||||
use futures::FutureExt;
|
||||
let state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut tick = tokio::time::interval(std::time::Duration::from_secs(300));
|
||||
loop {
|
||||
tick.tick().await;
|
||||
let cycle = std::panic::AssertUnwindSafe(
|
||||
smirk_backend_core::tips::run_sweep_reorg_cycle(state.clone()),
|
||||
);
|
||||
if cycle.catch_unwind().await.is_err() {
|
||||
tracing::error!("sweep reorg cycle panicked; continuing after backoff");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let app = build_router(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
|
||||
Reference in New Issue
Block a user