Skip to content
77 changes: 69 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,23 @@

私有部署,按 HTTP API 接入,几分钟内就可以开始接收 **Crypto Payments**。

### 已支持网络与代币
### 默认内置网络与代币

| 网络 | 代币 |
|------|------|
| **TRC20** (Tron) | USDT、TRX |
| **ERC20** (Ethereum) | USDT、USDC、ETH |
| **Solana** | USDT、USDC |
| **BEP20** (BSC) | USDT、USDC、BNB |
| **Polygon** | USDT、USDC |
| **ERC20** (Ethereum) | USDT、USDC |
| **Solana** | USDT、USDC、SOL |
| **BEP20** (BSC) | USDT、USDC |
| **Polygon** | USDT、USDC、USDC.e |
| **Plasma** | USDT |
| **Base** (Chain ID 8453) | USDC |
| **Arbitrum One** (Chain ID 42161) | USDC、USDT(官方合约已升级为 USDT0) |
| **TON** | TON、USDT |
| **Aptos** | USDC、USDT |
| **更多** | 持续扩展中… |

> 具体支持的链与代币以 [最新版本](https://github.com/GMWalletApp/epusdt/releases) 及 [官方文档](https://epusdt.com) 为准
> Base 默认使用 Circle 原生 USDC,不包含 USDbC;Arbitrum One 默认使用 Circle 原生 USDC 和官方 USDT/USDT0 合约,暂不支持两条链的原生 ETH。实际可用资产还取决于后台是否启用对应链、代币,以及是否配置了该链钱包地址和可用 RPC 节点,可通过 `GET /payments/gmpay/v1/config` 查询

---

Expand All @@ -75,7 +79,7 @@ Epusdt 已完成第三方安全审计。

## 核心特性

- **多链多币种** — 支持 TRC20、ERC20、BEP20、Polygon、Aptos 等主流网络
- **多链多币种** — 支持 TRON、Ethereum、Solana、BSC、Polygon、Plasma、Base、Arbitrum One、TON、Aptos 等网络
- **私有化部署** — 资金完全自主掌控
- **零依赖运行** — 单个二进制即可启动,低并发场景无需 MySQL + Redis
- **跨平台** — 支持 x86 / ARM 架构的 Windows / Linux / Mac
Expand All @@ -99,6 +103,7 @@ Epusdt 已完成第三方安全审计。
| [宝塔面板部署](https://epusdt.com/guide/installation/aapanel) | 适合宝塔用户 |
| [手动部署](https://epusdt.com/guide/installation/manual.html) | 完全手动控制 |
| [开发者 API 文档](https://epusdt.com/zh/guide/integration/gmpay.html) | 接口集成指南 |
| [仓库内:完整 API 文档](wiki/API.md) | 当前代码路由、签名、请求参数、回调与示例 |

仓库内还提供顶层脚本:

Expand All @@ -107,6 +112,62 @@ Epusdt 已完成第三方安全审计。

---

## API 暴露与认证边界

正常运行时,HTTP 端口同时承载收银台、商户支付接口和管理后台接口。部署时应通过 HTTPS 反向代理对外提供服务,并根据下表限制不需要公开的路径。

### 公开及订单访问接口

| 方法 | 路径 | 认证方式 | 用途 |
|------|------|----------|------|
| `POST` | `/` | 无 | 服务探测 |
| `GET` | `/payments/gmpay/v1/config` | 无 | 获取公开站点配置及当前可用资产 |
| `GET` | `/pay/checkout-counter/{trade_id}` | 无 | 跳转到收银台页面 |
| `GET` | `/pay/checkout-counter-resp/{trade_id}` | 无 | 获取收银台订单数据 |
| `GET` | `/pay/check-status/{trade_id}` | 无 | 查询订单状态 |
| `GET` | `/pay/return/{trade_id}` | 无 | EPay 支付完成后的商户跳转 |
| `POST` | `/pay/submit-tx-hash/{trade_id}` | `trade_id` 能力凭证 | 用户提交链上交易哈希进行补单验证 |
| `POST` | `/pay/switch-network` | `trade_id` 能力凭证 | 为订单选择或切换支付网络/通道 |

`trade_id` 可用于读取订单状态、切换支付目标或提交交易哈希,应当视为不可公开传播的能力凭证,不要写入公开日志、统计参数或第三方页面。

### 商户及支付平台接口

| 方法 | 路径 | 认证方式 |
|------|------|----------|
| `POST` | `/payments/gmpay/v1/order/create-transaction` | 商户 PID、API Key 签名及可选 IP 白名单 |
| `GET/POST` | `/payments/epay/v1/order/create-transaction/submit.php` | EPay 签名及可选 IP 白名单 |
| `POST` | `/payments/okpay/v1/notify` | OkPay 平台签名 |

Base 与 Arbitrum One 复用上述通用接口,不提供单独的链专用 API:

| 网络 | GMPay 参数 | EPay `type` 示例 |
|------|------------|------------------|
| Base | `network=base`、`token=USDC` | `USDC.base` |
| Arbitrum One | `network=arbitrum`、`token=USDC` | `USDC.arbitrum` |
| Arbitrum One | `network=arbitrum`、`token=USDT` | `USDT.arbitrum` |

### 管理后台接口

- `POST /admin/api/v1/auth/login` 和 `GET /admin/api/v1/auth/init-password-hash` 不要求 JWT。
- 其余 `/admin/api/v1/*` 接口均要求管理员 JWT,覆盖 API Key、通知渠道、链与代币、RPC、钱包、订单、仪表盘和系统设置管理。
- `GET /admin/api/v1/dashboard/rpc-stats` 是需要 JWT 的 SSE 长连接接口。

### 首次安装接口

当 `.env` 不存在或配置了 `install=true` 时,程序会先开放以下安装接口,完成安装后才启动正常业务 API:

| 方法 | 路径 | 认证方式 |
|------|------|----------|
| `GET` | `/api/install/defaults` | 无 |
| `POST` | `/api/install` | 无 |

安装服务默认监听 `:8000`,`POST /api/install` 会初始化数据库并返回初始管理员密码。首次启动必须限制在本机或可信内网完成,不要在未安装状态下直接将 `8000` 端口暴露到公网。

完整字段、签名算法、响应结构和回调示例请查看 [仓库内 API 文档](wiki/API.md)。

---

## 项目结构

```text
Expand Down Expand Up @@ -145,7 +206,7 @@ Epusdt

## 实现原理

Epusdt 通过监听多条区块链网络(TRC20、ERC20、BEP20、Polygon 等)的 API 或 RPC 节点,实时捕获钱包地址的代币入账事件,利用**金额差异**与**时效性**精确匹配交易归属:
Epusdt 通过监听多条区块链网络(TRON、Ethereum、BSC、Polygon、Base、Arbitrum One、Solana、TON、Aptos 等)的 API 或 RPC 节点,实时捕获钱包地址的代币入账事件,利用**金额差异**与**时效性**精确匹配交易归属:

```text
工作流程:
Expand Down
5 changes: 4 additions & 1 deletion src/internal/testutil/testdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func SetupTestDatabases(t testing.TB) func() {
// Seed all standard chains as enabled so IsChainEnabled checks pass.
for _, network := range []string{
mdb.NetworkTron, mdb.NetworkSolana, mdb.NetworkEthereum,
mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkTon,
mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkBase, mdb.NetworkArbitrum, mdb.NetworkTon,
} {
mainDB.Create(&mdb.Chain{Network: network, Enabled: true})
}
Expand All @@ -75,6 +75,9 @@ func SetupTestDatabases(t testing.TB) func() {
{Network: mdb.NetworkTron, Symbol: "USDT", ContractAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", Decimals: 6, Enabled: true},
{Network: mdb.NetworkEthereum, Symbol: "USDT", ContractAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7", Decimals: 6, Enabled: true},
{Network: mdb.NetworkBsc, Symbol: "USDT", ContractAddress: "0x55d398326f99059fF775485246999027B3197955", Decimals: 18, Enabled: true},
{Network: mdb.NetworkBase, Symbol: "USDC", ContractAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", Decimals: 6, Enabled: true},
{Network: mdb.NetworkArbitrum, Symbol: "USDC", ContractAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", Decimals: 6, Enabled: true},
{Network: mdb.NetworkArbitrum, Symbol: "USDT", ContractAddress: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", Decimals: 6, Enabled: true},
{Network: mdb.NetworkTon, Symbol: "TON", ContractAddress: "", Decimals: 9, Enabled: true},
{Network: mdb.NetworkTon, Symbol: "USDT", ContractAddress: "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", Decimals: 6, Enabled: true},
})
Expand Down
9 changes: 9 additions & 0 deletions src/model/dao/mdb_table_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ func seedChains() {
{Network: mdb.NetworkBsc, DisplayName: "BSC", Enabled: true, MinConfirmations: 3, ScanIntervalSec: 5},
{Network: mdb.NetworkPolygon, DisplayName: "Polygon", Enabled: true, MinConfirmations: 3, ScanIntervalSec: 5},
{Network: mdb.NetworkPlasma, DisplayName: "Plasma", Enabled: true, MinConfirmations: 1, ScanIntervalSec: 5},
{Network: mdb.NetworkBase, DisplayName: "Base", Enabled: true, MinConfirmations: 3, ScanIntervalSec: 5, Extra: `{"chain_id":8453}`},
{Network: mdb.NetworkArbitrum, DisplayName: "Arbitrum One", Enabled: true, MinConfirmations: 3, ScanIntervalSec: 5, Extra: `{"chain_id":42161}`},
{Network: mdb.NetworkTon, DisplayName: "TON", Enabled: true, MinConfirmations: 1, ScanIntervalSec: 5},
{Network: mdb.NetworkAptos, DisplayName: "Aptos", Enabled: true, MinConfirmations: 1, ScanIntervalSec: 5},
}
Expand Down Expand Up @@ -106,6 +108,11 @@ func seedChainTokens() {
{Network: mdb.NetworkPolygon, Symbol: "USDC.e", ContractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", Decimals: 6, Enabled: true},
// Plasma
{Network: mdb.NetworkPlasma, Symbol: "USDT", ContractAddress: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", Decimals: 6, Enabled: true},
// Base(Circle 原生 USDC,暂不包含跨链 USDbC)
{Network: mdb.NetworkBase, Symbol: "USDC", ContractAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", Decimals: 6, Enabled: true},
// Arbitrum One(Circle 原生 USDC,以及已升级为 USDT0 的官方 USDT 合约)
{Network: mdb.NetworkArbitrum, Symbol: "USDC", ContractAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", Decimals: 6, Enabled: true},
{Network: mdb.NetworkArbitrum, Symbol: "USDT", ContractAddress: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", Decimals: 6, Enabled: true},
// TON
{Network: mdb.NetworkTon, Symbol: "TON", ContractAddress: "", Decimals: 9, Enabled: true},
{Network: mdb.NetworkTon, Symbol: "USDT", ContractAddress: "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", Decimals: 6, Enabled: true},
Expand Down Expand Up @@ -146,6 +153,8 @@ func defaultRpcNodes() []mdb.RpcNode {
{Network: mdb.NetworkBsc, Url: "wss://bsc.drpc.org", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkPolygon, Url: "wss://polygon-bor-rpc.publicnode.com", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkPlasma, Url: "wss://rpc.plasma.to", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkBase, Url: "wss://base-rpc.publicnode.com", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkArbitrum, Url: "wss://arbitrum-one-rpc.publicnode.com", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkTon, Url: "https://ton-blockchain.github.io/global.config.json", Type: mdb.RpcNodeTypeLite, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkAptos, Url: "https://aptos-rest.publicnode.com/", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkEthereum, Url: "https://rpc.epusdt.com/ethereum", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusUnknown},
Expand Down
41 changes: 41 additions & 0 deletions src/model/dao/mdb_table_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,47 @@ func TestSeedChainTokensIncludesAptosAssets(t *testing.T) {
}
}

func TestSeedChainsAndTokensIncludeBaseAndArbitrum(t *testing.T) {
db := setupSeedTableTestDB(t, &mdb.Chain{}, &mdb.ChainToken{})
Mdb = db

seedChains()
seedChainTokens()

chains := map[string]struct {
name string
chainID string
}{
mdb.NetworkBase: {name: "Base", chainID: `"chain_id":8453`},
mdb.NetworkArbitrum: {name: "Arbitrum One", chainID: `"chain_id":42161`},
}
for network, want := range chains {
var row mdb.Chain
if err := Mdb.Where("network = ?", network).Take(&row).Error; err != nil {
t.Fatalf("load %s chain seed: %v", network, err)
}
if !row.Enabled || row.DisplayName != want.name || !strings.Contains(row.Extra, want.chainID) {
t.Fatalf("unexpected %s chain seed: %+v", network, row)
}
}

contracts := map[string]string{
mdb.NetworkBase + "/USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
mdb.NetworkArbitrum + "/USDC": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
mdb.NetworkArbitrum + "/USDT": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
}
for key, contract := range contracts {
parts := strings.Split(key, "/")
var row mdb.ChainToken
if err := Mdb.Where("network = ? AND symbol = ?", parts[0], parts[1]).Take(&row).Error; err != nil {
t.Fatalf("load token seed %s: %v", key, err)
}
if !row.Enabled || row.Decimals != 6 || !strings.EqualFold(row.ContractAddress, contract) {
t.Fatalf("unexpected token seed %s: %+v", key, row)
}
}
}

func TestSeedDefaultSettingsIncludesSystemLogLevel(t *testing.T) {
db := setupSeedSettingsTestDB(t)
Mdb = db
Expand Down
18 changes: 18 additions & 0 deletions src/model/data/order_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,21 @@ func TestAptosTransactionLockAddressUsesCanonicalKey(t *testing.T) {
t.Fatalf("aptos lock lookup = %q, want trade-aptos", gotTradeID)
}
}

func TestAddedEvmNetworkTransactionLocksAreIsolated(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
address := "0xA1B2c3D4e5F60718293aBcDeF001122334455667"
if err := LockTransaction(mdb.NetworkBase, address, "USDC", "base-trade", 1.23, time.Hour); err != nil {
t.Fatalf("lock Base: %v", err)
}
if err := LockTransaction(mdb.NetworkArbitrum, address, "USDC", "arb-trade", 1.23, time.Hour); err != nil {
t.Fatalf("lock Arbitrum: %v", err)
}
for network, want := range map[string]string{mdb.NetworkBase: "base-trade", mdb.NetworkArbitrum: "arb-trade"} {
got, err := GetTradeIdByWalletAddressAndAmountAndToken(network, address, "USDC", 1.23)
if err != nil || got != want {
t.Fatalf("%s lock = %q, %v; want %q", network, got, err, want)
}
}
}
2 changes: 1 addition & 1 deletion src/model/data/wallet_address_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
// isEVMNetwork 判断是否是 EVM 网络
func isEVMNetwork(network string) bool {
switch network {
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma:
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkBase, mdb.NetworkArbitrum:
return true
}
return false
Expand Down
17 changes: 17 additions & 0 deletions src/model/data/wallet_address_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,20 @@ func TestAddWalletAddressWithNetworkNormalizesMoveAddressVariants(t *testing.T)
t.Fatalf("add equivalent aptos wallet error = %v, want already exists", err)
}
}

func TestAddedEvmNetworksNormalizeWalletAddresses(t *testing.T) {
for _, network := range []string{mdb.NetworkBase, mdb.NetworkArbitrum} {
t.Run(network, func(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
input := "0xA1B2c3D4e5F60718293aBcDeF001122334455667"
row, err := AddWalletAddressWithNetwork(network, input)
if err != nil {
t.Fatalf("add wallet: %v", err)
}
if row.Address != strings.ToLower(input) {
t.Fatalf("wallet address = %q", row.Address)
}
})
}
}
2 changes: 2 additions & 0 deletions src/model/mdb/wallet_address_mdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const (
NetworkBsc = "binance"
NetworkPolygon = "polygon"
NetworkPlasma = "plasma"
NetworkBase = "base"
NetworkArbitrum = "arbitrum"
NetworkTon = "ton"
NetworkAptos = "aptos"
)
Expand Down
6 changes: 3 additions & 3 deletions src/model/service/manual_payment_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func validateManualOrderPaymentDefault(order *mdb.Orders, blockTransactionID str
canonicalTxID, err = validateManualTonPayment(order, txID)
case mdb.NetworkAptos:
canonicalTxID, err = ValidateManualAptosPayment(order, txID)
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma:
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkBase, mdb.NetworkArbitrum:
canonicalTxID, err = validateManualEvmPayment(order, txID)
default:
return "", fmt.Errorf("unsupported manual payment verification network: %s", order.Network)
Expand Down Expand Up @@ -125,7 +125,7 @@ func ensureManualBlockTransactionUnused(order *mdb.Orders, canonicalTxID string)

func manualBlockTransactionIDIsHex(network string) bool {
switch strings.ToLower(strings.TrimSpace(network)) {
case mdb.NetworkTron, mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkAptos:
case mdb.NetworkTron, mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkBase, mdb.NetworkArbitrum, mdb.NetworkAptos:
return true
default:
return false
Expand All @@ -151,7 +151,7 @@ func equivalentManualBlockTransactionIDs(network, canonicalTxID string) []string

add(canonicalTxID)
switch network {
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkAptos:
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkBase, mdb.NetworkArbitrum, mdb.NetworkAptos:
body := strings.TrimPrefix(strings.TrimPrefix(canonicalTxID, "0x"), "0X")
body = strings.ToLower(body)
add("0x" + body)
Expand Down
2 changes: 1 addition & 1 deletion src/model/service/order_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func normalizeOrderAddressByNetwork(network, address string) string {
network = strings.ToLower(strings.TrimSpace(network))
address = strings.TrimSpace(address)
switch network {
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma:
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkBase, mdb.NetworkArbitrum:
return strings.ToLower(address)
case mdb.NetworkTon:
if normalized, err := addressutil.NormalizeTonAddress(address); err == nil {
Expand Down
8 changes: 8 additions & 0 deletions src/model/service/task_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ func evmChainLogLabel(chainNetwork string) string {
return "POLYGON"
case mdb.NetworkPlasma:
return "PLASMA"
case mdb.NetworkBase:
return "BASE"
case mdb.NetworkArbitrum:
return "ARBITRUM"
default:
return "EVM"
}
Expand Down Expand Up @@ -350,6 +354,10 @@ func networkDisplay(n string) string {
return "Polygon"
case mdb.NetworkPlasma:
return "Plasma"
case mdb.NetworkBase:
return "Base"
case mdb.NetworkArbitrum:
return "Arbitrum One"
default:
if n == "" {
return "Tron"
Expand Down
2 changes: 2 additions & 0 deletions src/task/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ func Start() {
go StartBscWebSocketListener()
go StartPolygonWebSocketListener()
go StartPlasmaWebSocketListener()
go StartBaseWebSocketListener()
go StartArbitrumWebSocketListener()
go StartTronBlockScannerListener()
go StartTonBlockScannerListener()
go StartAptosLedgerScannerListener()
Expand Down
4 changes: 4 additions & 0 deletions src/task/listen_chain_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ func loadChainTokenContracts(network, logPrefix string) []common.Address {
if c == "" {
continue
}
if !common.IsHexAddress(c) {
log.Sugar.Warnf("%s skip invalid token contract network=%s symbol=%s contract=%q", logPrefix, network, t.Symbol, c)
continue
}
addrs = append(addrs, common.HexToAddress(c))
}
return addrs
Expand Down
Loading