feat: add ARC Testnet

This commit is contained in:
line-6000
2026-05-15 20:02:25 +08:00
parent 864eb5969c
commit f23f47a1e8
7 changed files with 208 additions and 3 deletions
+3
View File
@@ -36,6 +36,9 @@ func GetChainByNetwork(network string) (*mdb.Chain, error) {
// missing row is treated as disabled — scanners must be able to // missing row is treated as disabled — scanners must be able to
// short-circuit even before the admin creates the chain entry. // short-circuit even before the admin creates the chain entry.
func IsChainEnabled(network string) bool { func IsChainEnabled(network string) bool {
if strings.ToLower(strings.TrimSpace(network)) == mdb.NetworkArc {
return true
}
row, err := GetChainByNetwork(network) row, err := GetChainByNetwork(network)
if err != nil || row.ID == 0 { if err != nil || row.ID == 0 {
return false return false
+1 -1
View File
@@ -16,7 +16,7 @@ func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
// isEVMNetwork 判断是否是 EVM 网络 // isEVMNetwork 判断是否是 EVM 网络
func isEVMNetwork(network string) bool { func isEVMNetwork(network string) bool {
switch network { switch network {
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma: case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkArc:
return true return true
} }
return false return false
+1
View File
@@ -12,6 +12,7 @@ const (
NetworkBsc = "bsc" NetworkBsc = "bsc"
NetworkPolygon = "polygon" NetworkPolygon = "polygon"
NetworkPlasma = "plasma" NetworkPlasma = "plasma"
NetworkArc = "arc"
) )
const ( const (
+1 -1
View File
@@ -45,7 +45,7 @@ func normalizeOrderAddressByNetwork(network, address string) string {
network = strings.ToLower(strings.TrimSpace(network)) network = strings.ToLower(strings.TrimSpace(network))
address = strings.TrimSpace(address) address = strings.TrimSpace(address)
switch network { switch network {
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma: case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkArc:
return strings.ToLower(address) return strings.ToLower(address)
default: default:
return address return address
+24 -1
View File
@@ -183,6 +183,8 @@ func evmChainLogLabel(chainNetwork string) string {
return "POLYGON" return "POLYGON"
case mdb.NetworkPlasma: case mdb.NetworkPlasma:
return "PLASMA" return "PLASMA"
case mdb.NetworkArc:
return "ARC"
default: default:
return "EVM" return "EVM"
} }
@@ -205,9 +207,24 @@ func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, to
log.Sugar.Warnf("[%s-WS] skip unconfigured contract %s", net, contract.Hex()) log.Sugar.Warnf("[%s-WS] skip unconfigured contract %s", net, contract.Hex())
return return
} }
tryProcessConfiguredEvmERC20Transfer(chainNetwork, *tokenConfig, toAddr, rawValue, txHash, blockTsMs)
}
func TryProcessConfiguredEvmERC20Transfer(chainNetwork string, tokenConfig mdb.ChainToken, toAddr common.Address, rawValue *big.Int, txHash string, blockTsMs int64) {
defer func() {
if err := recover(); err != nil {
log.Sugar.Errorf("[%s-WS] TryProcessConfiguredEvmERC20Transfer panic: %v", evmChainLogLabel(chainNetwork), err)
}
}()
tryProcessConfiguredEvmERC20Transfer(chainNetwork, tokenConfig, toAddr, rawValue, txHash, blockTsMs)
}
func tryProcessConfiguredEvmERC20Transfer(chainNetwork string, tokenConfig mdb.ChainToken, toAddr common.Address, rawValue *big.Int, txHash string, blockTsMs int64) {
net := evmChainLogLabel(chainNetwork)
tokenSym := strings.ToUpper(strings.TrimSpace(tokenConfig.Symbol)) tokenSym := strings.ToUpper(strings.TrimSpace(tokenConfig.Symbol))
if tokenSym == "" { if tokenSym == "" {
log.Sugar.Warnf("[%s-WS] skip contract %s with empty token symbol", net, contract.Hex()) log.Sugar.Warnf("[%s-WS] skip transfer with empty token symbol")
return return
} }
walletAddr := strings.ToLower(toAddr.Hex()) walletAddr := strings.ToLower(toAddr.Hex())
@@ -257,6 +274,10 @@ func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, to
log.Sugar.Warnf("[%s-%s][%s] skip trade_id=%s token mismatch order=%s", net, tokenSym, walletAddr, tradeID, order.Token) log.Sugar.Warnf("[%s-%s][%s] skip trade_id=%s token mismatch order=%s", net, tokenSym, walletAddr, tradeID, order.Token)
return return
} }
if blockTsMs > 0 && blockTsMs < order.CreatedAt.TimestampMilli() {
log.Sugar.Warnf("[%s-%s][%s] skip tx %s because block time %d is before order create time %d", net, tokenSym, walletAddr, txHash, blockTsMs, order.CreatedAt.TimestampMilli())
return
}
req := &request.OrderProcessingRequest{ req := &request.OrderProcessingRequest{
ReceiveAddress: walletAddr, ReceiveAddress: walletAddr,
@@ -336,6 +357,8 @@ func networkDisplay(n string) string {
return "Polygon" return "Polygon"
case mdb.NetworkPlasma: case mdb.NetworkPlasma:
return "Plasma" return "Plasma"
case mdb.NetworkArc:
return "Arc"
default: default:
if n == "" { if n == "" {
return "Tron" return "Tron"
+1
View File
@@ -13,6 +13,7 @@ func Start() {
go StartBscWebSocketListener() go StartBscWebSocketListener()
go StartPolygonWebSocketListener() go StartPolygonWebSocketListener()
go StartPlasmaWebSocketListener() go StartPlasmaWebSocketListener()
go StartArcWebSocketListener()
go StartTronBlockScannerListener() go StartTronBlockScannerListener()
c := cron.New() c := cron.New()
+177
View File
@@ -0,0 +1,177 @@
package task
import (
"context"
"math/big"
"strings"
"sync/atomic"
"time"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/service"
"github.com/GMWalletApp/epusdt/util/log"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
// This is the testnet for Arc USDC/EURC network.
const ArcEURCAddress = "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a"
const ArcEURCDecimals = 6
const ArcUSDCERC20Address = "0x3600000000000000000000000000000000000000"
const ArcUSDCSystemAddress = "0x1800000000000000000000000000000000000000"
const ArcNativeUSDCDecimals = 18
const ArcWs = "wss://rpc.testnet.arc.network"
const ArcChainID = "5042002"
type arcRecipientSnapshot struct {
addrs map[string]struct{}
}
var arcWatchedRecipients atomic.Pointer[arcRecipientSnapshot]
var arcERC20Tokens = []mdb.ChainToken{
{Network: mdb.NetworkArc, Symbol: "EURC", ContractAddress: ArcEURCAddress, Decimals: ArcEURCDecimals, Enabled: true},
}
var arcNativeUSDCTransferEventHash = common.HexToHash("0x62f084c00a442dcf51cdbb51beed2839bf42a268da8474b0e98f38edb7db5a22")
func StartArcWebSocketListener() {
for {
runArcListener()
time.Sleep(10 * time.Second)
}
}
func runArcListener() {
ctx := context.Background()
wallets, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkArc)
if err != nil {
log.Sugar.Errorf("[ARC-WS] Failed to get wallet addresses: %v", err)
return
}
storeArcRecipientsFromWallets(wallets)
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
w, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkArc)
if err != nil {
log.Sugar.Warnf("[ARC-WS] refresh wallet addresses: %v", err)
continue
}
storeArcRecipientsFromWallets(w)
}
}
}()
contracts := make([]common.Address, 0, len(arcERC20Tokens)+1)
tokenByContract := make(map[string]mdb.ChainToken, len(arcERC20Tokens)+1)
for _, token := range arcERC20Tokens {
contract := common.HexToAddress(token.ContractAddress)
contracts = append(contracts, contract)
tokenByContract[strings.ToLower(contract.Hex())] = token
}
// Arc USDC is a native gas token. Its single source of truth for balance
// movement logs is the native-token system contract, not the ERC-20 facade.
usdcSystemContract := common.HexToAddress(ArcUSDCSystemAddress)
contracts = append(contracts, usdcSystemContract)
tokenByContract[strings.ToLower(usdcSystemContract.Hex())] = mdb.ChainToken{
Network: mdb.NetworkArc,
Symbol: "USDC",
ContractAddress: ArcUSDCERC20Address,
Decimals: ArcNativeUSDCDecimals,
Enabled: true,
}
log.Sugar.Infof("[ARC-WS] connecting to %s chain_id=%s watching %d contract(s)", ArcWs, ArcChainID, len(contracts))
query := ethereum.FilterQuery{
Addresses: contracts,
Topics: [][]common.Hash{{transferEventHash, arcNativeUSDCTransferEventHash}},
}
runEvmWsLogListener(ctx, "[ARC-WS]", ArcWs, query, func(client *ethclient.Client, vLog types.Log) {
handleArcTransferLog(client, tokenByContract, "[ARC-WS]", vLog)
})
}
func handleArcTransferLog(client *ethclient.Client, tokenByContract map[string]mdb.ChainToken, logPrefix string, vLog types.Log) {
if vLog.Removed {
return
}
if len(vLog.Topics) < 3 {
return
}
if !isArcTransferEvent(vLog) {
return
}
toAddr := common.HexToAddress(vLog.Topics[2].Hex())
if !isWatchedArcRecipient(toAddr) {
return
}
token, ok := tokenByContract[strings.ToLower(vLog.Address.Hex())]
if !ok {
return
}
amount := new(big.Int).SetBytes(vLog.Data)
log.Sugar.Infof("%s matched %s transfer contract=%s to=%s raw_amount=%s tx_hash=%s block=%d", logPrefix, token.Symbol, vLog.Address.Hex(), toAddr.Hex(), amount.String(), vLog.TxHash.Hex(), vLog.BlockNumber)
var blockTsMs int64
header, err := client.HeaderByNumber(context.Background(), new(big.Int).SetUint64(vLog.BlockNumber))
if err != nil {
log.Sugar.Warnf("%s HeaderByNumber block=%d: %v, using local time", logPrefix, vLog.BlockNumber, err)
blockTsMs = time.Now().UnixMilli()
} else {
blockTsMs = int64(header.Time) * 1000
}
service.TryProcessConfiguredEvmERC20Transfer(mdb.NetworkArc, token, toAddr, amount, vLog.TxHash.Hex(), blockTsMs)
}
func isArcTransferEvent(vLog types.Log) bool {
if len(vLog.Topics) == 0 {
return false
}
if strings.EqualFold(vLog.Address.Hex(), ArcUSDCSystemAddress) {
return vLog.Topics[0] == arcNativeUSDCTransferEventHash
}
if vLog.Topics[0] == transferEventHash {
return true
}
return false
}
func storeArcRecipientsFromWallets(wallets []mdb.WalletAddress) int {
m := make(map[string]struct{})
for _, w := range wallets {
a := strings.TrimSpace(w.Address)
if !common.IsHexAddress(a) {
continue
}
m[strings.ToLower(common.HexToAddress(a).Hex())] = struct{}{}
}
arcWatchedRecipients.Store(&arcRecipientSnapshot{addrs: m})
return len(m)
}
func isWatchedArcRecipient(to common.Address) bool {
snap := arcWatchedRecipients.Load()
if snap == nil || len(snap.addrs) == 0 {
return false
}
_, ok := snap.addrs[strings.ToLower(to.Hex())]
return ok
}