Skip to content

feat(payin): verification-gated admin recovery for SendUncertain sends#4265

Merged
TaprootFreak merged 1 commit into
developfrom
feat/payin-send-uncertain-recovery
Jul 18, 2026
Merged

feat(payin): verification-gated admin recovery for SendUncertain sends#4265
TaprootFreak merged 1 commit into
developfrom
feat/payin-send-uncertain-recovery

Conversation

@TaprootFreak

Copy link
Copy Markdown
Collaborator

Closes #4264.

Summary

Adds a verification-gated, admin-only recovery endpoint for pay-ins stranded in SendUncertain.

#4263/#4249 fail closed when a pay-in send fails ambiguously at or after broadcast: the pay-in is
left in Sending and the cron escalates it to SendUncertain, because re-broadcasting could
double-pay the customer. Until now there was no recovery path — such a pay-in was neither
forwarded/returned nor retryable, and returnPayIn rejects an in-flight-send status, so the funds
stayed frozen until a manual DB edit.

This mirrors the payout side, which already solved the equivalent problem in #4231
(retryUncertainPayoutPOST payout/retry), one-to-one.

Endpoint

POST payIn/retry (admin-only, @ApiExcludeEndpoint, same guards as the existing admin pay-in routes).

Body (RetryPayInSendDto):

  • id — the pay-in to recover
  • noBroadcastVerified — explicit operator attestation that on-chain absence was verified (must be literal true)
  • verificationReference — what was checked (explorer links, ticket…), recorded in the audit log

Behaviour (fail-closed)

  • Rejects anything but status SendUncertain.
  • Rejects a pay-in that already carries an outTxId/returnTxId — it did broadcast, so it must be
    reconciled against the chain, never re-sent.
  • Requires noBroadcastVerified === true; a blind reset would re-introduce the exact double-send
    fix(payin): fail closed on ambiguous non-EVM pay-in sends #4263 prevents.
  • On success, an atomic conditional transition (compare-and-set on status = SendUncertain)
    returns the pay-in to its direction's initial send status — Acknowledged (forward) or ToReturn
    (return) — so the next send-cron run re-runs it under the full designate-before-broadcast
    protection (staged chains rebuild a fresh preparation). A pay-in that changed state concurrently
    (e.g. a late confirmation) is not resurrected (ConflictException).
  • The authorization (account id + verification reference) is written to the info log.

Scope

No migration, env, or frontend change: CryptoInput.status is a nullable varchar and every status
used already exists; the endpoint is admin-only tooling.

Tests

  • Entity #resetSend(...): forward → Acknowledged, return → ToReturn, invalid action throws.
  • Service #retryUncertainSend(...): not-found, wrong-status, outTxId/returnTxId set,
    missing attestation, forward/return happy path with the exact conditional update, and the
    concurrent-change conflict.

@TaprootFreak

Copy link
Copy Markdown
Collaborator Author

Ready-for-review checks complete: one review pass across two independent dimensions (conformance/completeness and logic/fund-safety), 0 findings — no changes were required.

Verified before pushing: type-check, lint, format:check and jest (20/20 across the two touched spec files) all green, plus a red-proof confirming the new tests fail against the pre-change code (resetSend/retryUncertainSend undefined). No migration/env/frontend change (status is a nullable varchar; endpoint is admin-only, @ApiExcludeEndpoint).

@TaprootFreak
TaprootFreak marked this pull request as ready for review July 18, 2026 11:08
@TaprootFreak
TaprootFreak merged commit ff28947 into develop Jul 18, 2026
7 checks passed
@TaprootFreak
TaprootFreak deleted the feat/payin-send-uncertain-recovery branch July 18, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(payin): verification-gated admin recovery for SEND_UNCERTAIN pay-in sends (#4263 follow-up)

1 participant