mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
Compare commits
2 Commits
de96a5d3e7
..
v1.0.8
| Author | SHA1 | Date | |
|---|---|---|---|
| b4d1a4d8d4 | |||
| 8216c91739 |
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/GMWalletApp/epusdt/model/data"
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
"github.com/GMWalletApp/epusdt/model/response"
|
||||
"github.com/GMWalletApp/epusdt/model/service"
|
||||
"github.com/GMWalletApp/epusdt/util/constant"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
@@ -44,11 +45,7 @@ func buildSupportedAssets() ([]response.NetworkTokenSupport, error) {
|
||||
}
|
||||
symbols := make([]string, 0, len(tokens))
|
||||
for _, t := range tokens {
|
||||
contractAddress := strings.TrimSpace(t.ContractAddress)
|
||||
if network == mdb.NetworkAptos && !aptosPublicPaymentToken(t.Symbol) {
|
||||
continue
|
||||
}
|
||||
if network == mdb.NetworkAptos && contractAddress == "" {
|
||||
if !service.ChainTokenReadyForPayment(t) {
|
||||
continue
|
||||
}
|
||||
sym := strings.ToUpper(strings.TrimSpace(t.Symbol))
|
||||
@@ -75,15 +72,6 @@ func buildSupportedAssets() ([]response.NetworkTokenSupport, error) {
|
||||
return supports, nil
|
||||
}
|
||||
|
||||
func aptosPublicPaymentToken(symbol string) bool {
|
||||
switch strings.ToUpper(strings.TrimSpace(symbol)) {
|
||||
case "USDT", "USDC":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// GetPublicConfig returns payment/site config consumed by cashier/frontends.
|
||||
// The public route returns brand/site display config plus non-sensitive
|
||||
// epay/okpay knobs, while the authenticated admin route also includes OkPay
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
)
|
||||
|
||||
func TestBuildSupportedAssetsSkipsMisconfiguredAptosTokens(t *testing.T) {
|
||||
func TestBuildSupportedAssetsIncludesConfiguredAptosTokens(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -21,6 +21,7 @@ func TestBuildSupportedAssetsSkipsMisconfiguredAptosTokens(t *testing.T) {
|
||||
}
|
||||
for _, row := range []mdb.ChainToken{
|
||||
{Network: mdb.NetworkAptos, Symbol: "USDT", ContractAddress: "0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b", Decimals: 6, Enabled: true},
|
||||
{Network: mdb.NetworkAptos, Symbol: "MOVEUSD", ContractAddress: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Decimals: 6, Enabled: true},
|
||||
{Network: mdb.NetworkAptos, Symbol: "USDC", ContractAddress: "", Decimals: 6, Enabled: true},
|
||||
} {
|
||||
if err := dao.Mdb.Create(&row).Error; err != nil {
|
||||
@@ -38,7 +39,13 @@ func TestBuildSupportedAssetsSkipsMisconfiguredAptosTokens(t *testing.T) {
|
||||
if supports[0].Network != mdb.NetworkAptos {
|
||||
t.Fatalf("network = %q, want %q", supports[0].Network, mdb.NetworkAptos)
|
||||
}
|
||||
if len(supports[0].Tokens) != 1 || supports[0].Tokens[0] != "USDT" {
|
||||
t.Fatalf("tokens = %#v, want [USDT]", supports[0].Tokens)
|
||||
want := []string{"MOVEUSD", "USDT"}
|
||||
if len(supports[0].Tokens) != len(want) {
|
||||
t.Fatalf("tokens = %#v, want %#v", supports[0].Tokens, want)
|
||||
}
|
||||
for i := range want {
|
||||
if supports[0].Tokens[i] != want[i] {
|
||||
t.Fatalf("tokens = %#v, want %#v", supports[0].Tokens, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+103
-33
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/model/data"
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
@@ -16,15 +15,6 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const defaultAptosFullnodeURL = "https://aptos-rest.publicnode.com/"
|
||||
|
||||
var aptosFullnodeURL = defaultAptosFullnodeURL
|
||||
var aptosFixedNodeLogOnce sync.Once
|
||||
|
||||
func AptosFixedFullnodeURL() string {
|
||||
return strings.TrimSpace(aptosFullnodeURL)
|
||||
}
|
||||
|
||||
func AptosGetLedgerVersion() (int64, error) {
|
||||
body, err := aptosGet("/v1")
|
||||
if err != nil {
|
||||
@@ -37,7 +27,7 @@ func AptosGetLedgerVersion() (int64, error) {
|
||||
|
||||
func AptosGetTransactionByHash(txID string) ([]byte, error) {
|
||||
txID = normalizeAptosTxID(txID)
|
||||
log.Sugar.Infof("[APTOS] fetch transaction by hash tx=%s rpc=%s", txID, aptosFullnodeURL)
|
||||
log.Sugar.Infof("[APTOS] fetch transaction by hash tx=%s", txID)
|
||||
return aptosGet(fmt.Sprintf("/v1/transactions/by_hash/%s", txID))
|
||||
}
|
||||
|
||||
@@ -48,7 +38,7 @@ func AptosGetTransactions(start int64, limit int64) ([]byte, error) {
|
||||
if limit <= 0 {
|
||||
limit = 1
|
||||
}
|
||||
log.Sugar.Debugf("[APTOS] fetch transactions start=%d limit=%d rpc=%s", start, limit, aptosFullnodeURL)
|
||||
log.Sugar.Debugf("[APTOS] fetch transactions start=%d limit=%d", start, limit)
|
||||
return aptosGet(fmt.Sprintf("/v1/transactions?start=%d&limit=%d", start, limit))
|
||||
}
|
||||
|
||||
@@ -236,16 +226,7 @@ func aptosTransferMatchKey(symbol string, rawAmount *big.Int) string {
|
||||
}
|
||||
|
||||
func resolveAptosPaymentToken(assetID string, tokens []mdb.ChainToken) *mdb.ChainToken {
|
||||
token := resolveMoveToken(mdb.NetworkAptos, assetID, tokens)
|
||||
if token == nil {
|
||||
return nil
|
||||
}
|
||||
switch NormalizeAptosPaymentSymbol(token.Symbol) {
|
||||
case "USDT", "USDC":
|
||||
return token
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return resolveMoveToken(mdb.NetworkAptos, assetID, tokens)
|
||||
}
|
||||
|
||||
func NormalizeAptosPaymentSymbol(symbol string) string {
|
||||
@@ -336,17 +317,50 @@ func normalizeAptosTxID(txID string) string {
|
||||
|
||||
func ValidateManualAptosPayment(order *mdb.Orders, txID string) (string, error) {
|
||||
txID = normalizeAptosTxID(txID)
|
||||
body, err := AptosGetTransactionByHash(txID)
|
||||
nodes, err := data.ListManualPaymentRpcCandidates(mdb.NetworkAptos, mdb.RpcNodeTypeHttp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkAptos, mdb.RpcNodeTypeHttp)
|
||||
}
|
||||
|
||||
var verifyErrors []string
|
||||
for _, node := range nodes {
|
||||
if strings.TrimSpace(node.Url) == "" {
|
||||
continue
|
||||
}
|
||||
if _, err = validateManualAptosPaymentWithNode(order, txID, node); err != nil {
|
||||
verifyErrors = append(verifyErrors, fmt.Sprintf("%s: %v", data.RpcNodeLogLabel(node), err))
|
||||
continue
|
||||
}
|
||||
return txID, nil
|
||||
}
|
||||
if len(verifyErrors) > 0 {
|
||||
return "", fmt.Errorf("manual Aptos verification failed: %s", strings.Join(verifyErrors, "; "))
|
||||
}
|
||||
return "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkAptos, mdb.RpcNodeTypeHttp)
|
||||
}
|
||||
|
||||
func validateManualAptosPaymentWithNode(order *mdb.Orders, txID string, node mdb.RpcNode) (string, error) {
|
||||
txID = normalizeAptosTxID(txID)
|
||||
body, err := aptosGetWithNode(node, fmt.Sprintf("/v1/transactions/by_hash/%s", txID))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch aptos transaction: %w", err)
|
||||
}
|
||||
if err = validateManualAptosTransaction(order, txID, body); err != nil {
|
||||
if err = validateManualAptosTransactionWithConfirm(order, txID, body, func(version int64) error {
|
||||
return ensureAptosTransferConfirmedWithNode(version, node)
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return txID, nil
|
||||
}
|
||||
|
||||
func validateManualAptosTransaction(order *mdb.Orders, txID string, body []byte) error {
|
||||
return validateManualAptosTransactionWithConfirm(order, txID, body, EnsureAptosTransferConfirmed)
|
||||
}
|
||||
|
||||
func validateManualAptosTransactionWithConfirm(order *mdb.Orders, txID string, body []byte, confirm func(int64) error) error {
|
||||
tokens, err := data.ListEnabledChainTokensByNetwork(mdb.NetworkAptos)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -360,7 +374,7 @@ func validateManualAptosTransaction(order *mdb.Orders, txID string, body []byte)
|
||||
if !strings.EqualFold(transfer.TxID, txID) {
|
||||
continue
|
||||
}
|
||||
if err = EnsureAptosTransferConfirmed(transfer.Version); err != nil {
|
||||
if err = confirm(transfer.Version); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = EnsureMoveTransferMatchesOrder(order, transfer); err == nil {
|
||||
@@ -381,8 +395,58 @@ func validateManualAptosTransaction(order *mdb.Orders, txID string, body []byte)
|
||||
func aptosGet(path string) (body []byte, err error) {
|
||||
network := mdb.NetworkAptos
|
||||
defer recordMoveRPCResult(network, &err)
|
||||
logAptosFixedNode()
|
||||
endpoint, err := aptosJoinURL(aptosFullnodeURL, path)
|
||||
|
||||
tried := make([]uint64, 0, 3)
|
||||
var lastErr error
|
||||
for attempts := 0; attempts < 3; attempts++ {
|
||||
node, resolveErr := resolveAptosRpcNode(tried...)
|
||||
if resolveErr != nil {
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
return nil, resolveErr
|
||||
}
|
||||
if len(tried) > 0 {
|
||||
log.Sugar.Warnf("[APTOS] trying alternate RPC node path=%s node=%s", path, data.RpcNodeLogLabel(*node))
|
||||
}
|
||||
|
||||
body, err = aptosGetWithNode(*node, path)
|
||||
if err == nil {
|
||||
data.RecordRpcNodeSuccess(node.ID)
|
||||
return body, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
failures, cooling := data.RecordRpcNodeFailure(node.ID)
|
||||
nodeLabel := data.RpcNodeLogLabel(*node)
|
||||
if cooling {
|
||||
log.Sugar.Warnf("[APTOS] RPC node reached fail threshold path=%s node=%s failures=%d/%d", path, nodeLabel, failures, data.RpcFailoverThreshold)
|
||||
} else {
|
||||
log.Sugar.Warnf("[APTOS] RPC node failed path=%s node=%s failures=%d/%d", path, nodeLabel, failures, data.RpcFailoverThreshold)
|
||||
}
|
||||
tried = append(tried, node.ID)
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func resolveAptosRpcNode(excludeIDs ...uint64) (*mdb.RpcNode, error) {
|
||||
node, err := data.SelectGeneralRpcNode(mdb.NetworkAptos, mdb.RpcNodeTypeHttp, excludeIDs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if node == nil || node.ID == 0 {
|
||||
return nil, fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkAptos, mdb.RpcNodeTypeHttp)
|
||||
}
|
||||
rpcURL := strings.TrimSpace(node.Url)
|
||||
if rpcURL == "" {
|
||||
return nil, fmt.Errorf("rpc_nodes id=%d has empty url", node.ID)
|
||||
}
|
||||
node.Url = rpcURL
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func aptosGetWithNode(node mdb.RpcNode, path string) (body []byte, err error) {
|
||||
endpoint, err := aptosJoinURL(node.Url, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -423,12 +487,6 @@ func aptosJoinURL(base, path string) (string, error) {
|
||||
return trimmedBase + path, nil
|
||||
}
|
||||
|
||||
func logAptosFixedNode() {
|
||||
aptosFixedNodeLogOnce.Do(func() {
|
||||
log.Sugar.Infof("[APTOS] using fixed public fullnode url=%s", AptosFixedFullnodeURL())
|
||||
})
|
||||
}
|
||||
|
||||
func EnsureAptosTransferConfirmed(version int64) error {
|
||||
if version <= 0 {
|
||||
return fmt.Errorf("aptos transaction version missing")
|
||||
@@ -440,6 +498,18 @@ func EnsureAptosTransferConfirmed(version int64) error {
|
||||
return ensureAptosConfirmations(version, latest)
|
||||
}
|
||||
|
||||
func ensureAptosTransferConfirmedWithNode(version int64, node mdb.RpcNode) error {
|
||||
if version <= 0 {
|
||||
return fmt.Errorf("aptos transaction version missing")
|
||||
}
|
||||
body, err := aptosGetWithNode(node, "/v1")
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch aptos latest ledger version: %w", err)
|
||||
}
|
||||
latest := gjson.GetBytes(body, "ledger_version").Int()
|
||||
return ensureAptosConfirmations(version, latest)
|
||||
}
|
||||
|
||||
func ensureAptosConfirmations(version, latest int64) error {
|
||||
chain, err := data.GetChainByNetwork(mdb.NetworkAptos)
|
||||
if err != nil {
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestValidateManualAptosPayment(t *testing.T) {
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
overrideAptosFullnodeURLForTest(t, server.URL)
|
||||
seedTestAptosRPCNode(t, server.URL, mdb.RpcNodePurposeGeneral, mdb.RpcNodeStatusOk, 100)
|
||||
|
||||
order := &mdb.Orders{
|
||||
BaseModel: mdb.BaseModel{ID: 1, CreatedAt: *carbon.NewTime(carbon.CreateFromTimestampMilli(1699999999000))},
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||
@@ -15,26 +14,60 @@ import (
|
||||
"github.com/dromara/carbon/v2"
|
||||
)
|
||||
|
||||
func overrideAptosFullnodeURLForTest(t *testing.T, url string) {
|
||||
func seedTestAptosRPCNode(t *testing.T, url, purpose, status string, weight int) mdb.RpcNode {
|
||||
t.Helper()
|
||||
oldURL := aptosFullnodeURL
|
||||
aptosFullnodeURL = url
|
||||
aptosFixedNodeLogOnce = sync.Once{}
|
||||
t.Cleanup(func() {
|
||||
aptosFullnodeURL = oldURL
|
||||
aptosFixedNodeLogOnce = sync.Once{}
|
||||
})
|
||||
node := mdb.RpcNode{
|
||||
Network: mdb.NetworkAptos,
|
||||
Url: url,
|
||||
Type: mdb.RpcNodeTypeHttp,
|
||||
Weight: weight,
|
||||
Enabled: true,
|
||||
Purpose: purpose,
|
||||
Status: status,
|
||||
}
|
||||
if node.Purpose == "" {
|
||||
node.Purpose = mdb.RpcNodePurposeGeneral
|
||||
}
|
||||
if node.Status == "" {
|
||||
node.Status = mdb.RpcNodeStatusOk
|
||||
}
|
||||
if err := dao.Mdb.Create(&node).Error; err != nil {
|
||||
t.Fatalf("seed aptos rpc_nodes: %v", err)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func TestAptosGetLedgerVersionUsesFixedPublicNode(t *testing.T) {
|
||||
func TestAptosGetLedgerVersionUsesRpcNodes(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
data.ResetRpcFailoverForTest()
|
||||
t.Cleanup(data.ResetRpcFailoverForTest)
|
||||
|
||||
var fixedCalls int
|
||||
fixed := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fixedCalls++
|
||||
var disabledCalls int
|
||||
disabled := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
disabledCalls++
|
||||
_, _ = w.Write([]byte(`{"ledger_version":"999"}`))
|
||||
}))
|
||||
defer disabled.Close()
|
||||
disabledNode := mdb.RpcNode{
|
||||
Network: mdb.NetworkAptos,
|
||||
Url: disabled.URL,
|
||||
Type: mdb.RpcNodeTypeHttp,
|
||||
Weight: 1000,
|
||||
Enabled: false,
|
||||
Purpose: mdb.RpcNodePurposeGeneral,
|
||||
Status: mdb.RpcNodeStatusOk,
|
||||
}
|
||||
if err := dao.Mdb.Create(&disabledNode).Error; err != nil {
|
||||
t.Fatalf("seed disabled aptos rpc_nodes: %v", err)
|
||||
}
|
||||
if err := dao.Mdb.Model(&mdb.RpcNode{}).Where("id = ?", disabledNode.ID).Update("enabled", false).Error; err != nil {
|
||||
t.Fatalf("disable aptos rpc_nodes: %v", err)
|
||||
}
|
||||
|
||||
var calls int
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
if r.URL.Path != "/v1" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -42,15 +75,8 @@ func TestAptosGetLedgerVersionUsesFixedPublicNode(t *testing.T) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"ledger_version":"123"}`))
|
||||
}))
|
||||
defer fixed.Close()
|
||||
overrideAptosFullnodeURLForTest(t, fixed.URL)
|
||||
|
||||
rows := []mdb.RpcNode{
|
||||
{Network: mdb.NetworkAptos, Url: "https://old-aptos-rpc.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeBoth, Status: mdb.RpcNodeStatusOk},
|
||||
}
|
||||
if err := dao.Mdb.Create(&rows).Error; err != nil {
|
||||
t.Fatalf("seed aptos rpc_nodes: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
seedTestAptosRPCNode(t, server.URL, mdb.RpcNodePurposeGeneral, mdb.RpcNodeStatusOk, 100)
|
||||
|
||||
got, err := AptosGetLedgerVersion()
|
||||
if err != nil {
|
||||
@@ -59,12 +85,53 @@ func TestAptosGetLedgerVersionUsesFixedPublicNode(t *testing.T) {
|
||||
if got != 123 {
|
||||
t.Fatalf("ledger version = %d, want 123", got)
|
||||
}
|
||||
if fixedCalls != 1 {
|
||||
t.Fatalf("fixed node calls = %d, want 1", fixedCalls)
|
||||
if calls != 1 {
|
||||
t.Fatalf("rpc calls = %d, want 1", calls)
|
||||
}
|
||||
if disabledCalls != 0 {
|
||||
t.Fatalf("disabled rpc calls = %d, want 0", disabledCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateManualAptosPaymentUsesFixedPublicNode(t *testing.T) {
|
||||
func TestAptosGetTransactionsFallsBackToAlternateRpcNode(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
data.ResetRpcFailoverForTest()
|
||||
t.Cleanup(data.ResetRpcFailoverForTest)
|
||||
|
||||
var primaryCalls int
|
||||
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
primaryCalls++
|
||||
http.Error(w, "temporary failure", http.StatusBadGateway)
|
||||
}))
|
||||
defer primary.Close()
|
||||
var backupCalls int
|
||||
backup := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
backupCalls++
|
||||
if r.URL.Path != "/v1/transactions" || r.URL.Query().Get("start") != "10" || r.URL.Query().Get("limit") != "2" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
}))
|
||||
defer backup.Close()
|
||||
seedTestAptosRPCNode(t, primary.URL, mdb.RpcNodePurposeGeneral, mdb.RpcNodeStatusOk, 100)
|
||||
seedTestAptosRPCNode(t, backup.URL, mdb.RpcNodePurposeGeneral, mdb.RpcNodeStatusOk, 1)
|
||||
|
||||
body, err := AptosGetTransactions(10, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("AptosGetTransactions(): %v", err)
|
||||
}
|
||||
if string(body) != "[]" {
|
||||
t.Fatalf("transactions body = %s, want []", string(body))
|
||||
}
|
||||
if primaryCalls != 1 || backupCalls != 1 {
|
||||
t.Fatalf("rpc calls primary=%d backup=%d, want 1/1", primaryCalls, backupCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateManualAptosPaymentUsesManualRpcFallback(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -93,9 +160,15 @@ func TestValidateManualAptosPaymentUsesFixedPublicNode(t *testing.T) {
|
||||
t.Fatalf("normalize aptos sender store: %v", err)
|
||||
}
|
||||
|
||||
var fixedCalls int
|
||||
fixed := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fixedCalls++
|
||||
var generalCalls int
|
||||
general := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
generalCalls++
|
||||
http.Error(w, "general unavailable", http.StatusBadGateway)
|
||||
}))
|
||||
defer general.Close()
|
||||
var manualCalls int
|
||||
manual := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
manualCalls++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/v1/transactions/by_hash/"):
|
||||
@@ -106,16 +179,9 @@ func TestValidateManualAptosPaymentUsesFixedPublicNode(t *testing.T) {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer fixed.Close()
|
||||
overrideAptosFullnodeURLForTest(t, fixed.URL)
|
||||
|
||||
rows := []mdb.RpcNode{
|
||||
{Network: mdb.NetworkAptos, Url: "https://old-both.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeBoth, Status: mdb.RpcNodeStatusOk},
|
||||
{Network: mdb.NetworkAptos, Url: "https://old-manual.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusOk},
|
||||
}
|
||||
if err := dao.Mdb.Create(&rows).Error; err != nil {
|
||||
t.Fatalf("seed aptos rpc_nodes: %v", err)
|
||||
}
|
||||
defer manual.Close()
|
||||
seedTestAptosRPCNode(t, general.URL, mdb.RpcNodePurposeGeneral, mdb.RpcNodeStatusOk, 100)
|
||||
seedTestAptosRPCNode(t, manual.URL, mdb.RpcNodePurposeManualVerify, mdb.RpcNodeStatusOk, 1)
|
||||
|
||||
order := &mdb.Orders{
|
||||
BaseModel: mdb.BaseModel{ID: 1, CreatedAt: *carbon.NewTime(carbon.CreateFromTimestampMilli(1699999999000))},
|
||||
@@ -131,7 +197,7 @@ func TestValidateManualAptosPaymentUsesFixedPublicNode(t *testing.T) {
|
||||
if got != "0xabc" {
|
||||
t.Fatalf("canonical tx id = %q, want %q", got, "0xabc")
|
||||
}
|
||||
if fixedCalls != 2 {
|
||||
t.Fatalf("fixed node calls = %d, want 2", fixedCalls)
|
||||
if generalCalls != 1 || manualCalls != 2 {
|
||||
t.Fatalf("rpc calls general=%d manual=%d, want 1/2", generalCalls, manualCalls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,45 @@ func TestParseAptosTransfersUSDTAndUSDCFungibleEvents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAptosTransfersConfiguredCustomFungibleEvent(t *testing.T) {
|
||||
receive, _ := addressutil.NormalizeMoveAddress("0xa")
|
||||
custom := "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
store, _ := addressutil.NormalizeMoveAddress("0x21")
|
||||
senderStore, _ := addressutil.NormalizeMoveAddress("0x22")
|
||||
body := []byte(`[
|
||||
{
|
||||
"type":"user_transaction",
|
||||
"success":true,
|
||||
"hash":"custom",
|
||||
"version":"201",
|
||||
"timestamp":"1700000000123456",
|
||||
"events":[
|
||||
{"type":"0x1::fungible_asset::Withdraw","data":{"amount":"7500000","store":"` + senderStore + `"}},
|
||||
{"type":"0x1::fungible_asset::Deposit","data":{"amount":"7500000","store":"` + store + `"}}
|
||||
],
|
||||
"changes":[
|
||||
{"address":"` + senderStore + `","data":{"type":"0x1::fungible_asset::FungibleStore","data":{"metadata":{"inner":"` + custom + `"}}},"type":"write_resource"},
|
||||
{"address":"` + store + `","data":{"type":"0x1::fungible_asset::FungibleStore","data":{"metadata":{"inner":"` + custom + `"}}},"type":"write_resource"},
|
||||
{"address":"` + store + `","data":{"type":"0x1::object::ObjectCore","data":{"owner":"` + receive + `"}},"type":"write_resource"}
|
||||
]
|
||||
}
|
||||
]`)
|
||||
tokens := []mdb.ChainToken{
|
||||
{Network: mdb.NetworkAptos, Symbol: "MOVEUSD", ContractAddress: custom, Decimals: 6, Enabled: true},
|
||||
}
|
||||
|
||||
got, err := ParseAptosTransfers(body, receive, tokens)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseAptosTransfers: %v", err)
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("transfers len = %d, want 1: %#v", len(got), got)
|
||||
}
|
||||
if got[0].Token != "MOVEUSD" || got[0].Amount != 7.5 || got[0].TxID != "0xcustom" {
|
||||
t.Fatalf("custom transfer = %#v", got[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAptosTransfersSplitDepositsMatchSingleWithdraw(t *testing.T) {
|
||||
receive, _ := addressutil.NormalizeMoveAddress("0xa")
|
||||
receive2, _ := addressutil.NormalizeMoveAddress("0xb")
|
||||
|
||||
@@ -72,15 +72,7 @@ func ensureEnabledOrderAsset(network, token string) (*mdb.ChainToken, error) {
|
||||
if tokenRow == nil || tokenRow.ID == 0 {
|
||||
return nil, constant.SupportedAssetNotFound
|
||||
}
|
||||
if tokenRow.Network == mdb.NetworkAptos {
|
||||
switch NormalizeAptosPaymentSymbol(tokenRow.Symbol) {
|
||||
case "USDT", "USDC":
|
||||
default:
|
||||
return nil, constant.SupportedAssetNotFound
|
||||
}
|
||||
}
|
||||
contractAddress := strings.TrimSpace(tokenRow.ContractAddress)
|
||||
if tokenRow.Network == mdb.NetworkAptos && contractAddress == "" {
|
||||
if !ChainTokenReadyForPayment(*tokenRow) {
|
||||
return nil, constant.SupportedAssetNotFound
|
||||
}
|
||||
return tokenRow, nil
|
||||
|
||||
@@ -496,6 +496,29 @@ func TestCreateTransactionRejectsEnabledMisconfiguredAptosUSDT(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureEnabledOrderAssetAllowsConfiguredAptosCustomToken(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := dao.Mdb.Create(&mdb.ChainToken{
|
||||
Network: mdb.NetworkAptos,
|
||||
Symbol: "MOVEUSD",
|
||||
ContractAddress: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
Decimals: 6,
|
||||
Enabled: true,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create aptos custom token: %v", err)
|
||||
}
|
||||
|
||||
token, err := ensureEnabledOrderAsset(mdb.NetworkAptos, "MOVEUSD")
|
||||
if err != nil {
|
||||
t.Fatalf("ensureEnabledOrderAsset(): %v", err)
|
||||
}
|
||||
if token.Symbol != "MOVEUSD" {
|
||||
t.Fatalf("token symbol = %q, want MOVEUSD", token.Symbol)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTransactionSupportedTonTokenWithoutRateReturnsRateError(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
)
|
||||
|
||||
func ChainTokenReadyForPayment(token mdb.ChainToken) bool {
|
||||
if strings.TrimSpace(token.Symbol) == "" {
|
||||
return false
|
||||
}
|
||||
if chainTokenRequiresAssetID(token) && strings.TrimSpace(token.ContractAddress) == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func chainTokenRequiresAssetID(token mdb.ChainToken) bool {
|
||||
network := strings.ToLower(strings.TrimSpace(token.Network))
|
||||
symbol := strings.ToUpper(strings.TrimSpace(token.Symbol))
|
||||
switch network {
|
||||
case mdb.NetworkTron:
|
||||
return symbol != "TRX"
|
||||
case mdb.NetworkSolana:
|
||||
return symbol != "SOL"
|
||||
case mdb.NetworkTon:
|
||||
return symbol != TonNativeSymbol
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -52,8 +52,7 @@ var (
|
||||
func StartAptosLedgerScannerListener() {
|
||||
provider := serviceAptosProvider{}
|
||||
cursor := &aptosRuntimeCursor{}
|
||||
log.Sugar.Infof("[APTOS] ledger scanner starting mode=full_ledger rpc=%s chunk_size=%d workers=%d queue_limit=%d",
|
||||
service.AptosFixedFullnodeURL(),
|
||||
log.Sugar.Infof("[APTOS] ledger scanner starting mode=full_ledger chunk_size=%d workers=%d queue_limit=%d",
|
||||
aptosLedgerChunkSize,
|
||||
aptosLedgerWorkerCount,
|
||||
aptosLedgerQueueLimit,
|
||||
@@ -85,9 +84,8 @@ func runAptosLedgerScanner(ctx context.Context, provider aptosChainProvider, cur
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state.tokens = filterAptosPaymentTokens(state.tokens)
|
||||
if len(state.wallets) == 0 || len(state.tokens) == 0 {
|
||||
log.Sugar.Debug("[APTOS] no enabled wallets or USDT/USDC tokens, idling")
|
||||
log.Sugar.Debug("[APTOS] no enabled wallets or tokens, idling")
|
||||
if !sleepOrDone(ctx, interval) {
|
||||
return nil
|
||||
}
|
||||
@@ -249,17 +247,6 @@ func processAptosLedgerRange(ctx context.Context, provider aptosChainProvider, i
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterAptosPaymentTokens(tokens []mdb.ChainToken) []mdb.ChainToken {
|
||||
out := make([]mdb.ChainToken, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
switch service.NormalizeAptosPaymentSymbol(token.Symbol) {
|
||||
case "USDT", "USDC":
|
||||
out = append(out, token)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func chooseAptosIdleDelay(catchup bool, interval time.Duration) time.Duration {
|
||||
if catchup {
|
||||
return moveCatchupYield
|
||||
|
||||
@@ -81,6 +81,7 @@ func seedAptosScannerChain(t *testing.T, wallets ...string) []string {
|
||||
if err := dao.Mdb.Create(&[]mdb.ChainToken{
|
||||
{Network: mdb.NetworkAptos, Symbol: "USDC", ContractAddress: "0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b", Decimals: 6, Enabled: true},
|
||||
{Network: mdb.NetworkAptos, Symbol: "USDT", ContractAddress: "0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b", Decimals: 6, Enabled: true},
|
||||
{Network: mdb.NetworkAptos, Symbol: "MOVEUSD", ContractAddress: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Decimals: 6, Enabled: true},
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed Aptos tokens: %v", err)
|
||||
}
|
||||
@@ -101,6 +102,41 @@ func seedAptosScannerChain(t *testing.T, wallets ...string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func TestProcessAptosLedgerRangePassesConfiguredTokens(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
resetAptosScannerHooks(t)
|
||||
seedAptosScannerChain(t, "0xa")
|
||||
|
||||
state, err := loadMoveWatchState(mdb.NetworkAptos)
|
||||
if err != nil {
|
||||
t.Fatalf("load state: %v", err)
|
||||
}
|
||||
var gotSymbols []string
|
||||
parseAptosTransfersForWallets = func(_ []byte, _ map[string]struct{}, tokens []mdb.ChainToken) ([]service.MoveObservedTransfer, error) {
|
||||
for _, token := range tokens {
|
||||
gotSymbols = append(gotSymbols, token.Symbol)
|
||||
}
|
||||
sort.Strings(gotSymbols)
|
||||
return nil, nil
|
||||
}
|
||||
provider := &fakeAptosProvider{bodiesByStart: map[int64][]byte{100: []byte("[]")}}
|
||||
|
||||
err = processAptosLedgerRange(context.Background(), provider, aptosLedgerRange{start: 100, limit: 1, state: state}, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("processAptosLedgerRange(): %v", err)
|
||||
}
|
||||
want := []string{"MOVEUSD", "USDC", "USDT"}
|
||||
if len(gotSymbols) != len(want) {
|
||||
t.Fatalf("tokens = %#v, want %#v", gotSymbols, want)
|
||||
}
|
||||
for i := range want {
|
||||
if gotSymbols[i] != want[i] {
|
||||
t.Fatalf("tokens = %#v, want %#v", gotSymbols, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAptosLedgerScannerIdlesWhenChainDisabled(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
@@ -161,7 +197,6 @@ func TestProcessAptosLedgerRoundDoesNotAdvanceUnconfirmedVersion(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("load state: %v", err)
|
||||
}
|
||||
state.tokens = filterAptosPaymentTokens(state.tokens)
|
||||
|
||||
catchup, err := processAptosLedgerRound(context.Background(), provider, state, cursor, 99)
|
||||
if err != nil {
|
||||
@@ -188,7 +223,6 @@ func TestProcessAptosLedgerRoundAdvancesAfterSuccessfulRange(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("load state: %v", err)
|
||||
}
|
||||
state.tokens = filterAptosPaymentTokens(state.tokens)
|
||||
provider := &fakeAptosProvider{bodiesByStart: map[int64][]byte{100: []byte("[]")}}
|
||||
cursor := &aptosRuntimeCursor{initialized: true, lastSeenVersion: 99}
|
||||
|
||||
@@ -218,7 +252,6 @@ func TestProcessAptosLedgerRoundDoesNotAdvanceOnRangeFailure(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("load state: %v", err)
|
||||
}
|
||||
state.tokens = filterAptosPaymentTokens(state.tokens)
|
||||
provider := &fakeAptosProvider{errByStart: map[int64]error{100: errors.New("temporary rpc failure")}}
|
||||
cursor := &aptosRuntimeCursor{initialized: true, lastSeenVersion: 99}
|
||||
|
||||
@@ -240,7 +273,6 @@ func TestProcessAptosLedgerRoundProcessesMultipleChunks(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("load state: %v", err)
|
||||
}
|
||||
state.tokens = filterAptosPaymentTokens(state.tokens)
|
||||
provider := &fakeAptosProvider{
|
||||
bodiesByStart: map[int64][]byte{
|
||||
100: []byte("[]"),
|
||||
@@ -316,7 +348,6 @@ func TestProcessAptosLedgerRoundMarksMatchingUSDTOrderPaid(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("load state: %v", err)
|
||||
}
|
||||
state.tokens = filterAptosPaymentTokens(state.tokens)
|
||||
provider := &fakeAptosProvider{bodiesByStart: map[int64][]byte{101: body}}
|
||||
cursor := &aptosRuntimeCursor{initialized: true, lastSeenVersion: 100}
|
||||
|
||||
@@ -351,7 +382,6 @@ func TestProcessAptosLedgerRoundDoesNotReprocessDuplicateTransferKey(t *testing.
|
||||
if err != nil {
|
||||
t.Fatalf("load state: %v", err)
|
||||
}
|
||||
state.tokens = filterAptosPaymentTokens(state.tokens)
|
||||
provider := &fakeAptosProvider{
|
||||
bodiesByStart: map[int64][]byte{
|
||||
101: aptosFungibleTransferBody(t, "0xabc", 101, receive, usdc, "1200000"),
|
||||
|
||||
Reference in New Issue
Block a user