Files
grim/src/nostr/types.rs
T
2ro 9768de2fbd Add manual 'Cancel payment' to reclaim a stuck outgoing send
A Goblin payment locks the sender's outputs until the recipient replies (S2)
and we finalize+post. If the recipient never connects to nostr, the funds stay
locked until the 24h auto-expiry. This adds a manual Cancel that reclaims them
on demand (after a 10-min grace, or immediately if the send never reached a
relay), marks the payment Cancelled, and best-effort voids it to the recipient.

- WalletTask::NostrCancelSend: authoritative tx lookup; refuses if already
  finalized/confirmed (race); marks meta Cancelled BEFORE cancelling the grin
  tx; serialized with nostr_finalize_post via a per-service lock so a cancel and
  a concurrent S2 finalize can't both commit.
- nostr_finalize_post returns Ok(false) (skip, no retry/re-post) when the tx is
  cancelled or the meta is Cancelled — covers the tx-list cancel path too.
- decide() already drops a late S2 on a Cancelled meta (new unit tests assert
  it); recipient-side void marks a received payment Cancelled for display WITHOUT
  deleting the output (a malicious sender could void-then-post otherwise).
- Void-before-S1 ordering handled via a (slate,sender)-bound marker.
- Receipt: tap-twice 'Cancel payment' with caveat + outcome notice; honest
  'Waiting for X to receive…' label; first-class Cancelled status. 6 locales.
- cancel_grace_secs config (default 600).
2026-06-15 19:34:44 -04:00

151 lines
4.7 KiB
Rust

// Copyright 2026 The Goblin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Shared types of the nostr payment-messaging subsystem.
use serde_derive::{Deserialize, Serialize};
/// Direction of a nostr-transported transaction relative to this wallet.
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
pub enum NostrTxDirection {
/// We sent funds (Standard flow, we created S1).
Sent,
/// We received funds (Standard flow, we replied S2).
Received,
/// We issued an invoice / requested funds (Invoice flow, we created I1).
RequestedByUs,
/// Someone requested funds of us (Invoice flow, we may pay I1).
RequestedOfUs,
}
/// Lifecycle status of a nostr-transported transaction.
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
pub enum NostrSendStatus {
/// Slate created locally, DM not dispatched yet.
Created,
/// S1 DM dispatched, waiting for the S2 reply.
AwaitingS2,
/// Incoming S1 processed, S2 reply not yet dispatched (crash recovery).
ReceivedNoReply,
/// S2 reply dispatched for a received payment.
RepliedS2,
/// I1 request dispatched, waiting for the I2 reply.
AwaitingI2,
/// We paid an invoice (I2 reply sent), their side finalizes.
PaidAwaitingFinalize,
/// Transaction finalized and posted.
Finalized,
/// DM dispatch failed, retry possible.
SendFailed,
/// Cancelled locally.
Cancelled,
}
/// Outcome of a manual payment-cancel, surfaced transiently on the receipt so
/// the user knows whether their funds came back or the payment had already
/// completed in the race window.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CancelOutcome {
/// The pending payment was cancelled and the locked funds released.
Cancelled,
/// The payment had already gone through; nothing was cancelled.
AlreadyCompleted,
}
/// Per-transaction nostr metadata, joined to wallet txs by slate id.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TxNostrMeta {
pub ver: u8,
/// Slate UUID string.
pub slate_id: String,
/// Counterparty public key, hex.
pub npub: String,
pub direction: NostrTxDirection,
/// Sanitized user note (subject line).
pub note: Option<String>,
pub status: NostrSendStatus,
/// Gift wrap event id of our outgoing message, hex.
pub sent_event_id: Option<String>,
/// Rumor id of the counterparty message we processed, hex.
pub received_rumor_id: Option<String>,
pub created_at: i64,
pub updated_at: i64,
}
/// A contact: another nostr user we can pay.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Contact {
pub ver: u8,
/// Public key, hex.
pub npub: String,
/// Local petname, overrides any resolved name.
pub petname: Option<String>,
/// NIP-05 identifier (user@domain).
pub nip05: Option<String>,
/// Unix time of last successful NIP-05 verification.
pub nip05_verified_at: Option<i64>,
/// Known DM relays (kind 10050) of the contact.
pub relays: Vec<String>,
/// Avatar palette index.
pub hue: u8,
/// Auto-added from an incoming payment, not yet confirmed by the user.
pub unknown: bool,
pub added_at: i64,
pub last_paid_at: Option<i64>,
/// Blocked at the nostr level: their incoming messages are dropped on
/// ingest, as if muted on nostr (which is what this is).
#[serde(default)]
pub blocked: bool,
}
/// Status of an incoming payment request (Invoice1).
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
pub enum RequestStatus {
Pending,
Approved,
Declined,
Expired,
/// Withdrawn by the requester (we received their cancel control message).
Cancelled,
}
/// An incoming Invoice1 payment request awaiting explicit user approval.
/// NEVER paid automatically.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PaymentRequest {
pub ver: u8,
/// Rumor event id, hex (storage key).
pub rumor_id: String,
/// Slate UUID string.
pub slate_id: String,
/// Raw slatepack armor to pay on approval.
pub slatepack: String,
/// Requester public key, hex.
pub npub: String,
/// Requested amount in atomic units.
pub amount: u64,
/// Sanitized note.
pub note: Option<String>,
pub received_at: i64,
pub status: RequestStatus,
}
/// Current unix time in seconds.
pub fn unix_time() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}