efa6cffecd
selfcheck 子命令:用 PAY_ACCOUNT_XPUB 打印前 N 个派生收款地址,供与自己钱包/Ian Coleman 逐个核对(派生错=钱打到无私钥地址)。README 加『收真钱前必过』四步验证:①金标准测试 ②Ian Coleman 独立交叉核对 ③真钱包 A selfcheck 比对 ④真链 1 USDT 收→控→测→归闭环。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
3.6 KiB
Go
120 lines
3.6 KiB
Go
// Command paywatch is the pangolin-pay service: it hands out per-order TRON
|
|
// receiving addresses (watch-only, derived from an account xpub), watches
|
|
// TronGrid for confirmed USDT payments, and marks orders paid. It holds NO
|
|
// private keys — sweeping funds to cold storage is a separate offline step.
|
|
//
|
|
// Env:
|
|
//
|
|
// PAY_ACCOUNT_XPUB (required) watch-only account xpub, m/44'/195'/0'
|
|
// PAY_DB SQLite path (default pay.db)
|
|
// PAY_ADDR HTTP listen addr (default :8090)
|
|
// PAY_POLL_SECONDS watcher poll interval (default 20)
|
|
// TRONGRID_BASE TronGrid base URL (default https://api.trongrid.io)
|
|
// TRONGRID_API_KEY TronGrid API key (recommended)
|
|
// USDT_CONTRACT TRC20 USDT contract (default mainnet)
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/wangjia/pangolin/pay/internal/httpapi"
|
|
"github.com/wangjia/pangolin/pay/internal/pay"
|
|
"github.com/wangjia/pangolin/pay/internal/store"
|
|
"github.com/wangjia/pangolin/pay/internal/tron"
|
|
"github.com/wangjia/pangolin/pay/internal/wallet"
|
|
"github.com/wangjia/pangolin/pay/internal/watcher"
|
|
)
|
|
|
|
func env(k, def string) string {
|
|
if v := os.Getenv(k); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
// selfcheck prints the first N receiving addresses derived from PAY_ACCOUNT_XPUB.
|
|
// MONEY-CRITICAL: compare these against your own wallet app / iancoleman.io for
|
|
// the SAME account xpub. If they don't match, the service would hand buyers
|
|
// addresses you cannot spend — do NOT accept any payment until they match.
|
|
func selfcheck() {
|
|
xpub := os.Getenv("PAY_ACCOUNT_XPUB")
|
|
if xpub == "" {
|
|
fmt.Fprintln(os.Stderr, "PAY_ACCOUNT_XPUB is required")
|
|
os.Exit(1)
|
|
}
|
|
n := 5
|
|
if len(os.Args) > 2 {
|
|
if v, err := strconv.Atoi(os.Args[2]); err == nil && v > 0 {
|
|
n = v
|
|
}
|
|
}
|
|
fmt.Println("Derived receiving addresses (compare against your wallet for this xpub):")
|
|
for i := 0; i < n; i++ {
|
|
addr, err := wallet.AddressFromAccountXpub(xpub, 0, uint32(i))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "derive %d: %v\n", i, err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf(" m/44'/195'/0'/0/%d -> %s\n", i, addr)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) > 1 && os.Args[1] == "selfcheck" {
|
|
selfcheck()
|
|
return
|
|
}
|
|
|
|
log := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
|
|
xpub := os.Getenv("PAY_ACCOUNT_XPUB")
|
|
if xpub == "" {
|
|
log.Error("PAY_ACCOUNT_XPUB is required (watch-only account xpub, m/44'/195'/0')")
|
|
os.Exit(1)
|
|
}
|
|
dbPath := env("PAY_DB", "pay.db")
|
|
addr := env("PAY_ADDR", ":8090")
|
|
pollSec, _ := strconv.Atoi(env("PAY_POLL_SECONDS", "20"))
|
|
if pollSec <= 0 {
|
|
pollSec = 20
|
|
}
|
|
|
|
st, err := store.Open("file:" + dbPath + "?_txlock=immediate")
|
|
if err != nil {
|
|
log.Error("open store", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = st.Close() }()
|
|
|
|
svc := pay.New(st, pay.Config{AccountXpub: xpub, OrderTTL: 15 * time.Minute})
|
|
fetcher := tron.NewClient(env("TRONGRID_BASE", ""), env("USDT_CONTRACT", ""), os.Getenv("TRONGRID_API_KEY"))
|
|
w := watcher.New(st, fetcher, log)
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
go w.Loop(ctx, time.Duration(pollSec)*time.Second)
|
|
|
|
srv := &http.Server{Addr: addr, Handler: httpapi.New(svc), ReadHeaderTimeout: 10 * time.Second}
|
|
go func() {
|
|
log.Info("pangolin-pay listening", "addr", addr, "poll_seconds", pollSec)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Error("http server", "err", err)
|
|
stop()
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
sc, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_ = srv.Shutdown(sc)
|
|
log.Info("pangolin-pay stopped")
|
|
}
|