mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 10:16:15 +00:00
feat: add ARC Testnet
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const (
|
|||||||
NetworkBsc = "bsc"
|
NetworkBsc = "bsc"
|
||||||
NetworkPolygon = "polygon"
|
NetworkPolygon = "polygon"
|
||||||
NetworkPlasma = "plasma"
|
NetworkPlasma = "plasma"
|
||||||
|
NetworkArc = "arc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user