4bb92209ca
- store(SQLite,modernc 纯 Go):pay_orders + addr_cursor(HD 派生游标,地址不复用);
建单/查单/ListPending/MarkPaid(幂等,仅 pending→paid)/MarkExpired。
- pay 服务:CreateOrder 每单 NextAddrIndex→从 xpub watch-only 派生唯一收款地址→写 pending 单(TTL 15min)。
- tron:TronGrid 客户端读已确认 TRC20 到账(only_confirmed + USDT 合约,micro-USDT 整数)。
- watcher:Tick 先过期逾期单,再对每个 pending 单查到账、金额≥期望→MarkPaid;幂等(同 tx 只认一次)、
网络错误跳过下轮重试;Loop 定时轮询。
- httpapi:POST /order、GET /order/{orderNo}、/healthz;cmd/paywatch 用 env 装配 + 优雅退出。
- 测试:store/service/watcher(mock TronGrid)/httpapi 全绿——建单派生地址正确、到账侦测、
欠额不认、幂等、超时过期、404/400。热服务无私钥。
- README:安全模型 + Phase A 离线备钱包步骤 + 运行/API/测试。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
1.8 KiB
Go
60 lines
1.8 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/wangjia/pangolin/pay/internal/pay"
|
|
"github.com/wangjia/pangolin/pay/internal/store"
|
|
)
|
|
|
|
const testXpub = "xpub6D1AabNHCupeiLM65ZR9UStMhJ1vCpyV4XbZdyhMZBiJXALQtmn9p42VTQckoHVn8WNqS7dqnJokZHAHcHGoaQgmv8D45oNUKx6DZMNZBCd"
|
|
|
|
func TestCreateAndGetOrder(t *testing.T) {
|
|
st, _ := store.Open(":memory:")
|
|
t.Cleanup(func() { _ = st.Close() })
|
|
srv := httptest.NewServer(New(pay.New(st, pay.Config{AccountXpub: testXpub})))
|
|
t.Cleanup(srv.Close)
|
|
|
|
body, _ := json.Marshal(map[string]any{"sku": "pro-year", "amount": 5_000000})
|
|
resp, err := http.Post(srv.URL+"/order", "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("create status %d", resp.StatusCode)
|
|
}
|
|
var created orderResp
|
|
_ = json.NewDecoder(resp.Body).Decode(&created)
|
|
_ = resp.Body.Close()
|
|
if created.Address != "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH" || created.Status != "pending" {
|
|
t.Fatalf("create resp: %+v", created)
|
|
}
|
|
|
|
r2, _ := http.Get(srv.URL + "/order/" + created.OrderNo)
|
|
if r2.StatusCode != http.StatusOK {
|
|
t.Fatalf("get status %d", r2.StatusCode)
|
|
}
|
|
var got orderResp
|
|
_ = json.NewDecoder(r2.Body).Decode(&got)
|
|
_ = r2.Body.Close()
|
|
if got.OrderNo != created.OrderNo || got.Address != created.Address {
|
|
t.Fatalf("get mismatch: %+v vs %+v", got, created)
|
|
}
|
|
|
|
r3, _ := http.Get(srv.URL + "/order/NOPE")
|
|
if r3.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("want 404, got %d", r3.StatusCode)
|
|
}
|
|
_ = r3.Body.Close()
|
|
|
|
r4, _ := http.Post(srv.URL+"/order", "application/json", bytes.NewReader([]byte(`{"sku":"x","amount":0}`)))
|
|
if r4.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("want 400 for bad amount, got %d", r4.StatusCode)
|
|
}
|
|
_ = r4.Body.Close()
|
|
}
|