fix(aptos): drive token support and RPC routing from config

This commit is contained in:
line-6000
2026-06-20 14:17:42 +08:00
parent de96a5d3e7
commit 8216c91739
11 changed files with 355 additions and 121 deletions
@@ -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
View File
@@ -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))},
+106 -40
View File
@@ -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)
}
}
+39
View File
@@ -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")
+1 -9
View File
@@ -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
+23
View File
@@ -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()
+32
View File
@@ -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
}
}
+2 -15
View File
@@ -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
+36 -6
View File
@@ -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"),