// Package watcher polls TronGrid for incoming USDT and marks paid orders. // It only reads the chain and flips order state — it never holds keys or moves // funds (sweeping is a separate offline step). package watcher import ( "context" "log/slog" "time" "github.com/wangjia/pangolin/pay/internal/store" "github.com/wangjia/pangolin/pay/internal/tron" ) type Watcher struct { st *store.Store tron tron.Fetcher log *slog.Logger now func() time.Time } func New(st *store.Store, f tron.Fetcher, log *slog.Logger) *Watcher { if log == nil { log = slog.Default() } return &Watcher{st: st, tron: f, log: log, now: time.Now} } // Tick: (1) expire overdue pending orders; (2) for each still-pending order, // look for a confirmed incoming transfer >= the expected amount on its unique // address and mark it paid. Idempotent — a transfer seen twice flips the order // at most once (MarkPaid only affects a still-pending row). func (w *Watcher) Tick(ctx context.Context) error { if n, err := w.st.MarkExpired(ctx, w.now()); err != nil { return err } else if n > 0 { w.log.Info("orders expired", "count", n) } pending, err := w.st.ListPending(ctx) if err != nil { return err } for _, o := range pending { transfers, err := w.tron.IncomingTransfers(ctx, o.Address) if err != nil { // Transient (rate limit / network): log and move on; retried next tick. w.log.Warn("fetch transfers failed", "order", o.OrderNo, "err", err) continue } for _, t := range transfers { if t.Value < o.ExpectAmount { continue } ok, err := w.st.MarkPaid(ctx, o.OrderNo, t.TxID) if err != nil { w.log.Error("mark paid", "order", o.OrderNo, "err", err) break } if ok { w.log.Info("order paid", "order", o.OrderNo, "tx", t.TxID, "value", t.Value, "address", o.Address) } break } } return nil } // Loop runs Tick every interval until ctx is cancelled. func (w *Watcher) Loop(ctx context.Context, interval time.Duration) { t := time.NewTicker(interval) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: if err := w.Tick(ctx); err != nil { w.log.Error("watcher tick", "err", err) } } } }