Skip to content

Commit a572056

Browse files
committed
feat: implement token lists management with fetcher and parser support
- Added new package `tokenlists` for managing token lists. - Implemented `tokensList` struct for thread-safe state management. - Introduced `refreshWorker` for background updates of token lists. - Created fetchers for remote token lists and individual token lists. - Added parsers for CoinGecko, Uniswap and Status list token formats. - Included configuration options for token lists management. - Implemented validation for token lists against JSON schemas. - Added comprehensive tests for all new functionalities.
1 parent 59cdec5 commit a572056

37 files changed

+6769
-0
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ go get github.com/status-im/go-wallet-sdk
2222
- Complete RPC method coverage (eth_*, net_*, web3_*)
2323
- Go-ethereum ethclient compatible interface for easy migration
2424

25+
### Token Lists Management
26+
- **`pkg/tokenlists`**: Comprehensive token list management with privacy-aware fetching
27+
- Multi-source support (Status, Uniswap, CoinGecko, custom sources)
28+
- Privacy-aware automatic refresh with ETag support
29+
- Cross-chain token management and validation
30+
- Extensible parser system for custom token list formats
31+
- Thread-safe concurrent access with proper synchronization
32+
2533
### Common Utilities
2634
- **`pkg/common`**: Shared utilities and constants used across the SDK
2735

@@ -56,6 +64,7 @@ go-wallet-sdk/
5664
├── pkg/ # Core SDK packages
5765
│ ├── balance/ # Balance-related functionality
5866
│ ├── ethclient/ # Ethereum client with full RPC support
67+
│ ├── tokenlists/ # Token list management and fetching
5968
│ └── common/ # Shared utilities
6069
├── examples/ # Usage examples
6170
└── README.md # This file
@@ -65,6 +74,7 @@ go-wallet-sdk/
6574

6675
- [Balance Fetcher](pkg/balance/fetcher/README.md) - Balance fetching functionality
6776
- [Ethereum Client](pkg/ethclient/README.md) - Complete Ethereum RPC client
77+
- [Token Lists](pkg/tokenlists/README.md) - Token list management and fetching
6878
- [Web Example](examples/balance-fetcher-web/README.md) - Complete web application
6979

7080
## Contributing

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ require (
88
github.com/brianvoe/gofakeit/v7 v7.3.0
99
github.com/ethereum/go-ethereum v1.16.0
1010
github.com/stretchr/testify v1.10.0
11+
github.com/xeipuuv/gojsonschema v1.2.0
1112
go.uber.org/mock v0.5.2
13+
go.uber.org/zap v1.27.0
1214
)
1315

1416
require (
@@ -84,7 +86,10 @@ require (
8486
github.com/tklauser/go-sysconf v0.3.12 // indirect
8587
github.com/tklauser/numcpus v0.6.1 // indirect
8688
github.com/urfave/cli/v2 v2.27.5 // indirect
89+
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
90+
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
8791
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
92+
go.uber.org/multierr v1.10.0 // indirect
8893
golang.org/x/crypto v0.36.0 // indirect
8994
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
9095
golang.org/x/sync v0.12.0 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,25 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
230230
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
231231
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
232232
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
233+
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
234+
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
235+
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
236+
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
237+
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
238+
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
233239
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
234240
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
235241
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
236242
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
237243
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
244+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
245+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
238246
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
239247
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
248+
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
249+
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
250+
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
251+
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
240252
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
241253
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
242254
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

pkg/common/chainid.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
package common
22

3+
const (
4+
EthereumNativeCrossChainID = "eth-native"
5+
EthereumNativeSymbol = "ETH"
6+
EthereumNativeName = "Ethereum"
7+
8+
BinanceSmartChainNativeCrossChainID = "bsc-native"
9+
BinanceSmartChainNativeSymbol = "BNB"
10+
BinanceSmartChainNativeName = "BNB"
11+
)
12+
313
type ChainID = uint64
414

515
const (
@@ -16,3 +26,17 @@ const (
1626
BaseSepolia ChainID = 84532
1727
StatusNetworkSepolia ChainID = 1660990954
1828
)
29+
30+
var AllChains = []ChainID{
31+
EthereumMainnet,
32+
EthereumSepolia,
33+
OptimismMainnet,
34+
OptimismSepolia,
35+
ArbitrumMainnet,
36+
ArbitrumSepolia,
37+
BSCMainnet,
38+
BSCTestnet,
39+
BaseMainnet,
40+
BaseSepolia,
41+
StatusNetworkSepolia,
42+
}

pkg/tokenlists/README.md

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
# TokenLists Package
2+
3+
The `tokenlists` package provides a comprehensive solution for managing and fetching token lists from various sources in a privacy-aware manner. It supports multiple token list formats, automatic refresh capabilities, and cross-chain token management.
4+
5+
## Features
6+
7+
- **Multi-source Support**: Fetch token lists from Status, Uniswap, CoinGecko, and custom sources
8+
- **Privacy-aware**: Respects privacy settings to prevent unwanted network requests
9+
- **Automatic Refresh**: Configurable automatic refresh intervals with ETag support
10+
- **Cross-chain Support**: Manage tokens across multiple blockchain networks
11+
- **Extensible**: Plugin-based parser system for custom token list formats
12+
- **Thread-safe**: Concurrent access support with proper synchronization
13+
- **Caching**: Built-in content caching with ETag support for efficient updates
14+
15+
## Quick Start
16+
17+
```go
18+
package main
19+
20+
import (
21+
"context"
22+
"log"
23+
"time"
24+
25+
"github.com/status-im/go-wallet-sdk/pkg/tokenlists"
26+
"go.uber.org/zap"
27+
)
28+
29+
func main() {
30+
// Create configuration
31+
config := tokenlists.DefaultConfig().
32+
WithChains([]uint64{1, 10, 137}). // Ethereum, Polygon, BSC
33+
WithRemoteListOfTokenListsURL("https://example.com/token-lists.json").
34+
WithAutoRefreshInterval(30*time.Minute, 3*time.Minute).
35+
WithLogger(zap.NewNop())
36+
37+
// Create token list manager
38+
tl, err := tokenlists.NewTokensList(config)
39+
if err != nil {
40+
log.Fatal(err)
41+
}
42+
43+
// Start the service
44+
ctx := context.Background()
45+
notifyCh := make(chan struct{}, 1)
46+
47+
if err := tl.Start(ctx, notifyCh); err != nil {
48+
log.Fatal(err)
49+
}
50+
defer tl.Stop(ctx)
51+
52+
// Wait for initial fetch
53+
select {
54+
case <-notifyCh:
55+
log.Println("Token lists updated")
56+
case <-time.After(10 * time.Second):
57+
log.Println("Timeout waiting for token lists")
58+
}
59+
60+
// Get all unique tokens
61+
tokens := tl.UniqueTokens()
62+
log.Printf("Found %d unique tokens", len(tokens))
63+
64+
// Get tokens for a specific chain
65+
ethereumTokens := tl.GetTokensByChain(1)
66+
log.Printf("Found %d tokens on Ethereum", len(ethereumTokens))
67+
}
68+
```
69+
70+
## Configuration
71+
72+
The package uses a builder pattern for configuration:
73+
74+
```go
75+
config := &tokenlists.Config{
76+
// Required fields
77+
MainList: []byte(`{"tokens": []}`),
78+
MainListID: "status",
79+
Chains: []uint64{1, 10, 137},
80+
81+
// Optional fields with defaults
82+
RemoteListOfTokenListsURL: "https://example.com/lists.json",
83+
AutoRefreshInterval: 30 * time.Minute,
84+
AutoRefreshCheckInterval: 3 * time.Minute,
85+
86+
// Custom components
87+
PrivacyGuard: tokenlists.NewDefaultPrivacyGuard(false),
88+
LastTokenListsUpdateTimeStore: tokenlists.NewDefaultLastTokenListsUpdateTimeStore(),
89+
ContentStore: tokenlists.NewDefaultContentStore(),
90+
Parsers: make(map[string]tokenlists.Parser),
91+
}
92+
```
93+
94+
### Configuration Options
95+
96+
| Option | Type | Default | Description |
97+
|--------|------|---------|-------------|
98+
| `MainList` | `[]byte` | - | Initial token list data |
99+
| `MainListID` | `string` | - | Identifier for the main token list |
100+
| `Chains` | `[]uint64` | - | Supported blockchain chain IDs |
101+
| `RemoteListOfTokenListsURL` | `string` | - | URL to fetch list of token lists |
102+
| `AutoRefreshInterval` | `time.Duration` | 30 min | How often to refresh token lists |
103+
| `AutoRefreshCheckInterval` | `time.Duration` | 3 min | How often to check if refresh is needed |
104+
| `PrivacyGuard` | `PrivacyGuard` | `NewDefaultPrivacyGuard(false)` | Privacy mode controller |
105+
| `ContentStore` | `ContentStore` | `NewDefaultContentStore()` | Content caching store |
106+
| `Parsers` | `map[string]Parser` | Built-in parsers | Token list format parsers |
107+
108+
## API Reference
109+
110+
### TokensList Interface
111+
112+
```go
113+
type TokensList interface {
114+
// Lifecycle management
115+
Start(ctx context.Context, notifyCh chan struct{}) error
116+
Stop(ctx context.Context) error
117+
118+
LastRefreshTime() (time.Time, error)
119+
RefreshNow(ctx context.Context) error
120+
121+
// Privacy management
122+
PrivacyModeUpdated(ctx context.Context) error
123+
124+
// Token queries
125+
UniqueTokens() []*Token
126+
GetTokenByChainAddress(chainID uint64, addr common.Address) (*Token, bool)
127+
GetTokensByChain(chainID uint64) []*Token
128+
129+
// Token list queries
130+
TokenLists() []*TokenList
131+
TokenList(id string) (*TokenList, bool)
132+
}
133+
```
134+
135+
### Token Structure
136+
137+
```go
138+
type Token struct {
139+
CrossChainID string `json:"crossChainId"`
140+
ChainID uint64 `json:"chainId"`
141+
Address common.Address `json:"address"`
142+
Decimals uint `json:"decimals"`
143+
Name string `json:"name"`
144+
Symbol string `json:"symbol"`
145+
LogoURI string `json:"logoUri"`
146+
Color string `json:"color"`
147+
CommunityID bool `json:"community"`
148+
CustomToken bool `json:"custom"`
149+
}
150+
```
151+
152+
### TokenList Structure
153+
154+
```go
155+
type TokenList struct {
156+
Name string `json:"name"`
157+
Timestamp string `json:"timestamp"`
158+
FetchedTimestamp string `json:"fetchedTimestamp"`
159+
Source string `json:"source"`
160+
Version Version `json:"version"`
161+
Tags map[string]interface{} `json:"tags"`
162+
LogoURI string `json:"logoURI"`
163+
Keywords []string `json:"keywords"`
164+
Tokens []*Token `json:"tokens"`
165+
}
166+
```
167+
168+
## Privacy Mode
169+
170+
The package respects privacy settings to prevent unwanted network requests:
171+
172+
```go
173+
// Enable privacy mode
174+
config := tokenlists.DefaultConfig().
175+
WithPrivacyGuard(tokenlists.NewDefaultPrivacyGuard(true))
176+
177+
// Privacy mode prevents:
178+
// - Automatic token list fetching
179+
// - RefreshNow() calls from making network requests
180+
// - Background refresh worker from running
181+
```
182+
183+
## Supported Token List Formats
184+
185+
### Status Token List
186+
- **Parser**: `StatusTokenListParser`
187+
- **Format**: Status-specific JSON format
188+
189+
### Standard Token List Formats (uniswap, platform specific coingecko list and others use this format)
190+
- **Parser**: `StandardTokenListParser`
191+
- **Format**: Standard Token List format
192+
193+
### CoinGecko All Token List (doesn't contain decimals)
194+
- **Parser**: `CoinGeckoAllTokensParser`
195+
- **Format**: CoinGecko API format with chain mapping
196+
197+
## Custom Parsers (if the list doesn't follow the standard token list format)
198+
199+
Implement the `Parser` interface to support custom token list formats:
200+
201+
```go
202+
type CustomParser struct{}
203+
204+
func (p *CustomParser) Parse(raw []byte, sourceURL string, fetchedAt time.Time) (*TokenList, error) {
205+
// Parse your custom format
206+
var customFormat struct {
207+
Name string `json:"name"`
208+
Tokens []*Token `json:"tokens"`
209+
}
210+
211+
if err := json.Unmarshal(raw, &customFormat); err != nil {
212+
return nil, err
213+
}
214+
215+
return &TokenList{
216+
Name: customFormat.Name,
217+
Timestamp: time.Now().Format(time.RFC3339),
218+
FetchedTimestamp: fetchedAt.Format(time.RFC3339),
219+
Source: sourceURL,
220+
Tokens: customFormat.Tokens,
221+
}, nil
222+
}
223+
224+
// Register custom parser
225+
config := tokenlists.DefaultConfig().
226+
WithParsers(map[string]tokenlists.Parser{
227+
"custom": &CustomParser{},
228+
})
229+
```
230+
231+
## Error Handling
232+
233+
The package provides comprehensive error handling:
234+
235+
```go
236+
tl, err := tokenlists.NewTokensList(config)
237+
if err != nil {
238+
// Handle configuration errors
239+
log.Fatal(err)
240+
}
241+
242+
if err := tl.Start(ctx, notifyCh); err != nil {
243+
// Handle startup errors
244+
log.Fatal(err)
245+
}
246+
247+
// Handle refresh errors via notification channel
248+
go func() {
249+
for range notifyCh {
250+
// Token lists updated successfully
251+
log.Println("Token lists refreshed")
252+
}
253+
}()
254+
```
255+
256+
## Testing
257+
258+
The package includes comprehensive tests:
259+
260+
```bash
261+
# Run all tests
262+
go test ./pkg/tokenlists/...
263+
264+
# Run specific test
265+
go test ./pkg/tokenlists -run TestTokensList_RefreshNow
266+
267+
# Run with verbose output
268+
go test ./pkg/tokenlists/... -v
269+
```

0 commit comments

Comments
 (0)