mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
Public-only social-tips foundation ported from legacy smirk-backend: - migration 20260710000001_social_tips.sql: consolidated public-tip table (status CHECK + public_tip_has_hash + worker indexes), targeted/recipient columns and the socials/bot surface dropped. - models::tip_status: TipStatus single-source-of-truth enum (8 variants, as_str/from_db/is_terminal) + confirmations_for_asset. - infra::db::social_tips: SocialTipRow (FromRow) + TIP_COLS + get_social_tip. Inert (no routes yet); FEATURE_TIPS stays off. Verified: migration applies, 5 TipStatus unit tests pass.
119 lines
5.0 KiB
PL/PgSQL
119 lines
5.0 KiB
PL/PgSQL
-- Social tips (PUBLIC-only): non-custodial share-URL tips.
|
|
--
|
|
-- A sender funds a tip address and publishes a claim URL whose fragment carries
|
|
-- the claim key; anyone holding the URL claims by sweeping the tip address to
|
|
-- their own wallet. The backend stores only the encrypted claim blob and a hash
|
|
-- of the claim key (for status tracking) — it never sees the claim key or the
|
|
-- funds.
|
|
--
|
|
-- Ported (public subset) from the legacy smirk-backend social_tips subsystem.
|
|
-- Targeted / @username tips and the socials + bot surface are intentionally
|
|
-- dropped: no recipient_* columns, no user_socials, no announcer.
|
|
|
|
-- Bump updated_at on every UPDATE. Self-contained: core defines no shared
|
|
-- updated_at trigger function of its own.
|
|
CREATE OR REPLACE FUNCTION social_tips_touch_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TABLE social_tips (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
sender_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
asset VARCHAR(8) NOT NULL, -- btc | ltc | xmr | wow
|
|
amount BIGINT NOT NULL, -- smallest unit
|
|
|
|
-- Public tip: anyone holding the claim URL can claim.
|
|
is_public BOOLEAN NOT NULL DEFAULT TRUE,
|
|
claim_key_hash VARCHAR(64), -- SHA256(claim key); the key never touches the server
|
|
encrypted_key BYTEA, -- claim secret, encrypted; backend cannot decrypt
|
|
|
|
-- Funding: the sender funds tip_address, which is swept to the claimer on claim.
|
|
tip_address VARCHAR(255),
|
|
funding_txid VARCHAR(128),
|
|
|
|
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
|
|
|
-- Funding-confirmation tracking.
|
|
funding_confirmations INTEGER NOT NULL DEFAULT 0,
|
|
funding_confirmed_at TIMESTAMPTZ,
|
|
last_confirmation_check TIMESTAMPTZ,
|
|
confirmations_required INTEGER NOT NULL DEFAULT 0,
|
|
|
|
-- Funding-amount verification: guards against a sender declaring more than
|
|
-- they funded (the verifier only flips a tip claimable once the on-chain
|
|
-- receipt covers `amount`).
|
|
funding_amount_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
funding_amount_observed BIGINT,
|
|
funding_amount_verified_at TIMESTAMPTZ,
|
|
|
|
-- Claim.
|
|
claimed_at TIMESTAMPTZ,
|
|
claimed_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
clawed_back_at TIMESTAMPTZ,
|
|
|
|
-- Sweep: a claim settles ('claimed') only once its sweep confirms on-chain.
|
|
sweep_txid VARCHAR(128),
|
|
sweep_confirmed_at TIMESTAMPTZ,
|
|
sweep_block_height INTEGER,
|
|
sweep_block_hash TEXT,
|
|
sweep_confirmed_dm_sent_at TIMESTAMPTZ, -- exactly-once notify stamp
|
|
reorg_notified_at TIMESTAMPTZ, -- exactly-once reorg-notify stamp
|
|
|
|
-- LWS (XMR/WOW): the tip address is registered with a view key so the backend
|
|
-- can scan it for funding + sweep.
|
|
tip_view_key VARCHAR(64),
|
|
lws_registered_at TIMESTAMPTZ,
|
|
lws_deactivated_at TIMESTAMPTZ,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Lifecycle:
|
|
-- draft -> pending_confirmation -> pending -> claiming -> claimed
|
|
-- draft -> cancelled
|
|
-- pending_confirmation -> funding_mismatch (verifier saw a short receipt)
|
|
-- {pending, pending_confirmation, claiming, funding_mismatch, cancelled} -> clawed_back
|
|
CONSTRAINT social_tips_status_check CHECK (status IN (
|
|
'draft',
|
|
'pending_confirmation',
|
|
'pending',
|
|
'claiming',
|
|
'claimed',
|
|
'clawed_back',
|
|
'cancelled',
|
|
'funding_mismatch'
|
|
)),
|
|
-- Public tips must carry the claim-key hash.
|
|
CONSTRAINT public_tip_has_hash CHECK (is_public = FALSE OR claim_key_hash IS NOT NULL)
|
|
);
|
|
|
|
CREATE INDEX idx_social_tips_sender ON social_tips(sender_user_id, created_at DESC);
|
|
|
|
CREATE INDEX idx_social_tips_claim_hash ON social_tips(claim_key_hash)
|
|
WHERE claim_key_hash IS NOT NULL;
|
|
|
|
-- Confirmation poller: rows still awaiting funding confirmations.
|
|
CREATE INDEX idx_social_tips_pending_confirmation
|
|
ON social_tips(asset, last_confirmation_check)
|
|
WHERE status IN ('pending', 'pending_confirmation')
|
|
AND confirmations_required > 0
|
|
AND funding_confirmed_at IS NULL;
|
|
|
|
-- Amount verifier: confirmed-but-not-yet-amount-verified rows.
|
|
CREATE INDEX idx_social_tips_status_verified
|
|
ON social_tips(status, funding_amount_verified)
|
|
WHERE status IN ('pending', 'pending_confirmation', 'funding_mismatch');
|
|
|
|
-- Sweep reconciler + lifecycle GC: rows mid-claim.
|
|
CREATE INDEX idx_social_tips_claiming ON social_tips(status, updated_at)
|
|
WHERE status = 'claiming';
|
|
|
|
CREATE TRIGGER social_tips_updated_at
|
|
BEFORE UPDATE ON social_tips
|
|
FOR EACH ROW EXECUTE FUNCTION social_tips_touch_updated_at();
|