mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-27 12:44:13 +00:00
211 lines
5.7 KiB
Go
211 lines
5.7 KiB
Go
package verifier
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"log"
|
|
"time"
|
|
|
|
dbpkg "github.com/debros/orama-website/invest-api/db"
|
|
"github.com/debros/orama-website/invest-api/helius"
|
|
)
|
|
|
|
const (
|
|
SweepInterval = 30 * time.Minute
|
|
GracePeriod = 5 * time.Minute
|
|
)
|
|
|
|
// Verifier periodically checks that claim holders still hold their assets.
|
|
// State machine: active → flagged → warned → revoked (each transition requires
|
|
// the asset to still be missing after GracePeriod).
|
|
type Verifier struct {
|
|
db *sql.DB
|
|
helius *helius.Client
|
|
}
|
|
|
|
func New(db *sql.DB, heliusClient *helius.Client) *Verifier {
|
|
return &Verifier{db: db, helius: heliusClient}
|
|
}
|
|
|
|
// Run starts the background sweep loop. Blocks until ctx is cancelled.
|
|
func (v *Verifier) Run(ctx context.Context) {
|
|
log.Println("[verifier] starting background verification (interval:", SweepInterval, ")")
|
|
|
|
// Run once on startup to catch anything missed during downtime
|
|
v.sweep()
|
|
|
|
ticker := time.NewTicker(SweepInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Println("[verifier] shutting down")
|
|
return
|
|
case <-ticker.C:
|
|
v.sweep()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Verifier) sweep() {
|
|
log.Println("[verifier] starting sweep...")
|
|
v.verifyAnchatClaims()
|
|
v.verifyNftLicenses()
|
|
log.Println("[verifier] sweep complete")
|
|
}
|
|
|
|
// ── $ANCHAT claim verification ──
|
|
|
|
func (v *Verifier) verifyAnchatClaims() {
|
|
rows, err := v.db.Query(
|
|
"SELECT id, wallet, status, flagged_at, warned_at FROM anchat_claims WHERE status != 'revoked'",
|
|
)
|
|
if err != nil {
|
|
log.Printf("[verifier] failed to query anchat_claims: %v", err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var id int
|
|
var wallet, status string
|
|
var flaggedAt, warnedAt sql.NullString
|
|
|
|
if err := rows.Scan(&id, &wallet, &status, &flaggedAt, &warnedAt); err != nil {
|
|
log.Printf("[verifier] scan error: %v", err)
|
|
continue
|
|
}
|
|
|
|
balance, err := v.helius.GetTokenBalanceFresh(wallet, dbpkg.AnchatMint)
|
|
if err != nil {
|
|
log.Printf("[verifier] helius error for %s: %v", truncate(wallet), err)
|
|
continue // Don't change state on RPC errors
|
|
}
|
|
|
|
holdsAsset := balance >= dbpkg.AnchatMinBalance
|
|
v.updateState(id, "anchat_claims", wallet, status, holdsAsset, flaggedAt, warnedAt, "anchat_claim")
|
|
}
|
|
}
|
|
|
|
// ── NFT license verification ──
|
|
|
|
func (v *Verifier) verifyNftLicenses() {
|
|
rows, err := v.db.Query(
|
|
"SELECT id, wallet, status, flagged_at, warned_at FROM nft_license_verification WHERE status != 'revoked'",
|
|
)
|
|
if err != nil {
|
|
log.Printf("[verifier] failed to query nft_license_verification: %v", err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var id int
|
|
var wallet, status string
|
|
var flaggedAt, warnedAt sql.NullString
|
|
|
|
if err := rows.Scan(&id, &wallet, &status, &flaggedAt, &warnedAt); err != nil {
|
|
log.Printf("[verifier] scan error: %v", err)
|
|
continue
|
|
}
|
|
|
|
nftResult, err := v.helius.CheckNFTs(wallet, dbpkg.TeamNFTCollection, dbpkg.CommunityNFTCollection)
|
|
if err != nil {
|
|
log.Printf("[verifier] helius NFT error for %s: %v", truncate(wallet), err)
|
|
continue
|
|
}
|
|
|
|
holdsAsset := nftResult.HasTeamNFT
|
|
v.updateState(id, "nft_license_verification", wallet, status, holdsAsset, flaggedAt, warnedAt, "nft_license")
|
|
}
|
|
}
|
|
|
|
// ── State machine logic ──
|
|
|
|
func (v *Verifier) updateState(
|
|
id int,
|
|
table, wallet, currentStatus string,
|
|
holdsAsset bool,
|
|
flaggedAt, warnedAt sql.NullString,
|
|
claimType string,
|
|
) {
|
|
now := time.Now()
|
|
|
|
if holdsAsset {
|
|
// Asset is back — reset to active
|
|
if currentStatus != "active" {
|
|
v.db.Exec(
|
|
"UPDATE "+table+" SET status = 'active', flagged_at = NULL, warned_at = NULL, revoked_at = NULL WHERE id = ?",
|
|
id,
|
|
)
|
|
log.Printf("[verifier] %s %s: RESTORED to active (asset found again)", table, truncate(wallet))
|
|
v.logActivity(claimType+"_restored", wallet, "Asset detected, claim restored")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Asset NOT found — advance state machine
|
|
switch currentStatus {
|
|
case "active":
|
|
// First miss → flag
|
|
v.db.Exec("UPDATE "+table+" SET status = 'flagged', flagged_at = ? WHERE id = ?", now.Format(time.RFC3339), id)
|
|
log.Printf("[verifier] %s %s: FLAGGED (asset not found, check 1/3)", table, truncate(wallet))
|
|
|
|
case "flagged":
|
|
// Second miss — check grace period
|
|
if !flaggedAt.Valid {
|
|
return
|
|
}
|
|
flagTime, err := time.Parse(time.RFC3339, flaggedAt.String)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if now.Sub(flagTime) < GracePeriod {
|
|
return // Too soon, wait
|
|
}
|
|
v.db.Exec("UPDATE "+table+" SET status = 'warned', warned_at = ? WHERE id = ?", now.Format(time.RFC3339), id)
|
|
log.Printf("[verifier] %s %s: WARNED (asset not found, check 2/3)", table, truncate(wallet))
|
|
|
|
case "warned":
|
|
// Third miss — check grace period from warned_at
|
|
if !warnedAt.Valid {
|
|
return
|
|
}
|
|
warnTime, err := time.Parse(time.RFC3339, warnedAt.String)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if now.Sub(warnTime) < GracePeriod {
|
|
return
|
|
}
|
|
// REVOKE
|
|
v.db.Exec("UPDATE "+table+" SET status = 'revoked', revoked_at = ? WHERE id = ?", now.Format(time.RFC3339), id)
|
|
log.Printf("[verifier] %s %s: REVOKED (asset not found, check 3/3)", table, truncate(wallet))
|
|
v.logActivity(claimType+"_revoked", wallet, "Claim revoked — asset no longer held")
|
|
|
|
// For NFT licenses: also mark the license_purchase as revoked
|
|
if table == "nft_license_verification" {
|
|
v.db.Exec(
|
|
"UPDATE license_purchases SET claimed_via_nft = -1 WHERE wallet = ? AND claimed_via_nft = 1",
|
|
wallet,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Verifier) logActivity(eventType, wallet, detail string) {
|
|
truncated := truncate(wallet)
|
|
v.db.Exec(
|
|
"INSERT INTO activity_log (event_type, wallet, detail) VALUES (?, ?, ?)",
|
|
eventType, truncated, detail,
|
|
)
|
|
}
|
|
|
|
func truncate(wallet string) string {
|
|
if len(wallet) > 10 {
|
|
return wallet[:6] + "..." + wallet[len(wallet)-4:]
|
|
}
|
|
return wallet
|
|
}
|