Skip to content

Commit 82299f4

Browse files
committed
feat: market data fetching for test tokens
Since CoinGecko doesn't have data for test tokens new methods introduced in TokenManagerInterface for fetching market data, for both mainnet and test tokens. Mapping tokens in CoinGecko client updated.
1 parent ccfa813 commit 82299f4

File tree

6 files changed

+175
-28
lines changed

6 files changed

+175
-28
lines changed

services/wallet/common/const.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const (
2424
HopSymbol = "HOP"
2525
DaiSymbol = "DAI"
2626
BNBSymbol = "BNB"
27+
28+
StatusMainnetTokenCrossChainID = "status"
29+
StatusTestTokenCrossChainID = "status-test-token"
2730
)
2831

2932
// ProviderID represents the internal ID of a blockchain provider

services/wallet/market/market.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ type TokenMarketCache MarketValuesPerCurrencyAndToken
4242
type TokenPriceCache DataPerTokenAndCurrency
4343

4444
type TokenManagerInterface interface {
45-
GetTokensByKeys(tokensKeys []string) ([]*tokentypes.Token, error)
46-
GetTokensForActiveNetworksMode() ([]*tokentypes.Token, error)
45+
GetTokensForFetchingMarketData() ([]*tokentypes.Token, error)
46+
GetTokensByKeysForFetchingMarketData(tokenKeys []string) ([]*tokentypes.Token, error)
4747
GetTokenByKey(tokenKey string) (*tokentypes.Token, error)
4848
}
4949

@@ -123,9 +123,9 @@ func (pm *Manager) makeCall(providers []thirdparty.MarketDataProvider, f func(pr
123123

124124
func (pm *Manager) getTokensByKeys(tokensKeys []string) (tokens []*tokentypes.Token, err error) {
125125
if len(tokensKeys) > 0 {
126-
tokens, err = pm.tokenManager.GetTokensByKeys(tokensKeys)
126+
tokens, err = pm.tokenManager.GetTokensByKeysForFetchingMarketData(tokensKeys)
127127
} else {
128-
tokens, err = pm.tokenManager.GetTokensForActiveNetworksMode()
128+
tokens, err = pm.tokenManager.GetTokensForFetchingMarketData()
129129
}
130130
return
131131
}

services/wallet/market/market_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ import (
2222
// MockTokenManager implements TokenManagerInterface for testing
2323
type MockTokenManager struct{}
2424

25+
func (m *MockTokenManager) GetTokensForFetchingMarketData() ([]*tokentypes.Token, error) {
26+
return m.GetTokensForActiveNetworksMode()
27+
}
28+
29+
func (m *MockTokenManager) GetTokensByKeysForFetchingMarketData(tokenKeys []string) ([]*tokentypes.Token, error) {
30+
return m.GetTokensByKeys(tokenKeys)
31+
}
32+
2533
func (m *MockTokenManager) GetTokensByKeys(tokenKeys []string) ([]*tokentypes.Token, error) {
2634
var tokens []*tokentypes.Token
2735
for _, key := range tokenKeys {

services/wallet/thirdparty/market/coingecko/client.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/status-im/go-wallet-sdk/pkg/tokens/types"
1515

1616
"github.com/status-im/status-go/pkg/security"
17+
walletcommon "github.com/status-im/status-go/services/wallet/common"
1718
"github.com/status-im/status-go/services/wallet/thirdparty"
1819
"github.com/status-im/status-go/services/wallet/thirdparty/utils"
1920
tokentypes "github.com/status-im/status-go/services/wallet/token/types"
@@ -128,11 +129,47 @@ func (c *Client) mapTokensToIds(tokens []*tokentypes.Token) (map[string][]string
128129
mappedTokens := make(map[string][]string) // map[coingeckoId][]tokenKey
129130
unmappedTokens := make([]string, 0)
130131

132+
// Coingecko doesn't provide prices for test tokens, so we need to map them to the mainnet tokens.
133+
// We can do that only for test tokens that have a cross chain id.
134+
tokenKeysByCrossChainID := make(map[string][]string)
135+
for _, token := range tokens {
136+
// Skip tokens that don't have a cross chain id
137+
if token.CrossChainID == "" {
138+
continue
139+
}
140+
// Skip tokens that are on test networks
141+
if !walletcommon.ChainID(token.ChainID).IsMainnet() {
142+
continue
143+
}
144+
tokenKeysByCrossChainID[token.CrossChainID] = append(tokenKeysByCrossChainID[token.CrossChainID], token.Key())
145+
}
146+
131147
for _, token := range tokens {
132148
coingeckoToken, ok := coingeckoTokensByTokenKey[token.Key()]
133149
if !ok {
134-
unmappedTokens = append(unmappedTokens, token.Key())
135-
continue
150+
if walletcommon.ChainID(token.ChainID).IsMainnet() {
151+
unmappedTokens = append(unmappedTokens, token.Key())
152+
continue
153+
}
154+
155+
// Check by cross chain id if any of the test tokens have a coingecko token
156+
crossChainID := token.CrossChainID
157+
// Sepecial handling for status test token STT, cause even it's the same token belongs to different group and has different symbol SNT/STT.
158+
if crossChainID == walletcommon.StatusTestTokenCrossChainID {
159+
crossChainID = walletcommon.StatusMainnetTokenCrossChainID
160+
}
161+
162+
for _, tokenKey := range tokenKeysByCrossChainID[crossChainID] {
163+
coingeckoToken, ok = coingeckoTokensByTokenKey[tokenKey]
164+
if ok {
165+
break
166+
}
167+
}
168+
169+
if !ok {
170+
unmappedTokens = append(unmappedTokens, token.Key())
171+
continue
172+
}
136173
}
137174
mappedTokens[coingeckoToken.ID] = append(mappedTokens[coingeckoToken.ID], token.Key())
138175
}

services/wallet/thirdparty/market/coingecko/client_test.go

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -266,43 +266,57 @@ func TestFetchMarketValues(t *testing.T) {
266266
tokens := []*tokentypes.Token{
267267
{
268268
Token: &types.Token{
269-
Name: "USDC",
270-
Symbol: "USDC",
271-
ChainID: common.EthereumMainnet,
272-
Address: gethcommon.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"),
269+
CrossChainID: "usd-coin",
270+
Name: "USDC",
271+
Symbol: "USDC",
272+
ChainID: common.EthereumMainnet,
273+
Address: gethcommon.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"),
273274
},
274275
},
275276
{
276277
Token: &types.Token{
277-
Name: "Status",
278-
Symbol: "SNT",
279-
ChainID: common.EthereumMainnet,
280-
Address: gethcommon.HexToAddress("0x744d70fdbe2ba4cf95131626614a1763df805b9e"),
278+
CrossChainID: "usd-coin",
279+
Name: "USDC",
280+
Symbol: "USDC",
281+
ChainID: common.EthereumSepolia,
282+
Address: gethcommon.HexToAddress("0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"),
281283
},
282284
},
283285
{
284286
Token: &types.Token{
285-
Name: "Dai",
286-
Symbol: "DAI",
287-
ChainID: common.EthereumMainnet,
288-
Address: gethcommon.HexToAddress("0x6b175474e89094c44da98b954eedeac495271d0f"),
287+
CrossChainID: "status",
288+
Name: "Status",
289+
Symbol: "SNT",
290+
ChainID: common.EthereumMainnet,
291+
Address: gethcommon.HexToAddress("0x744d70fdbe2ba4cf95131626614a1763df805b9e"),
292+
},
293+
},
294+
{
295+
Token: &types.Token{
296+
CrossChainID: "dai",
297+
Name: "Dai",
298+
Symbol: "DAI",
299+
ChainID: common.EthereumMainnet,
300+
Address: gethcommon.HexToAddress("0x6b175474e89094c44da98b954eedeac495271d0f"),
289301
},
290302
},
291303
}
292304
prices, err := geckoClient.FetchTokenMarketValues(tokens, "USD")
293305
require.NoError(t, err)
294306
require.Len(t, prices, len(tokens))
295307

296-
usdcPrice := prices[tokens[0].Key()]
297-
require.InDelta(t, 72388338754.0, usdcPrice.MKTCAP, 1e-6)
298-
require.InDelta(t, 0.999835, usdcPrice.HIGHDAY, 1e-10)
299-
require.InDelta(t, 0.999619, usdcPrice.LOWDAY, 1e-10)
300-
require.InDelta(t, 0.00874017695509919, usdcPrice.CHANGEPCTHOUR, 1e-10)
301-
require.InDelta(t, 6.0e-05, usdcPrice.CHANGEPCTDAY, 1e-5)
302-
require.InDelta(t, 6.0e-05, usdcPrice.CHANGEPCT24HOUR, 1e-5)
303-
require.InDelta(t, 6.21125e-07, usdcPrice.CHANGE24HOUR, 1e-10)
308+
for _, index := range []int{0, 1} {
309+
usdcPrice := prices[tokens[index].Key()]
310+
require.InDelta(t, 72388338754.0, usdcPrice.MKTCAP, 1e-6)
311+
require.InDelta(t, 0.999835, usdcPrice.HIGHDAY, 1e-10)
312+
require.InDelta(t, 0.999619, usdcPrice.LOWDAY, 1e-10)
313+
require.InDelta(t, 0.00874017695509919, usdcPrice.CHANGEPCTHOUR, 1e-10)
314+
require.InDelta(t, 6.0e-05, usdcPrice.CHANGEPCTDAY, 1e-5)
315+
require.InDelta(t, 6.0e-05, usdcPrice.CHANGEPCT24HOUR, 1e-5)
316+
require.InDelta(t, 6.21125e-07, usdcPrice.CHANGE24HOUR, 1e-10)
317+
}
304318

305-
sntPrice := prices[tokens[1].Key()]
319+
sntPrice := prices[tokens[2].Key()]
306320
require.InDelta(t, 103479828.0, sntPrice.MKTCAP, 1e-6)
307321
require.InDelta(t, 0.02612809, sntPrice.HIGHDAY, 1e-8)
308322
require.InDelta(t, 0.02565221, sntPrice.LOWDAY, 1e-8)
@@ -311,5 +325,5 @@ func TestFetchMarketValues(t *testing.T) {
311325
require.InDelta(t, 0.14647083029811012, sntPrice.CHANGEPCT24HOUR, 1e-6)
312326
require.InDelta(t, 3.819e-05, sntPrice.CHANGE24HOUR, 1e-8)
313327

314-
require.Equal(t, prices[tokens[2].Key()], thirdparty.TokenMarketValues{})
328+
require.Equal(t, prices[tokens[3].Key()], thirdparty.TokenMarketValues{})
315329
}

services/wallet/token/token.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,91 @@ func (tm *Manager) GetTokensForActiveNetworksMode() ([]*tokentypes.Token, error)
388388
return tm.GetTokensByChains(chainIDs)
389389
}
390390

391+
// GetTokensForFetchingMarketData returns all unique tokens for fetching market data from Coingecko (doesn't affect CryptoCompare cause it maps tokens differently, by symbol)
392+
// Special handling for test tokens, for fetching market data from Coingecko, cause their API doesn't support test tokens
393+
// Corresponding mainnet tokens are needed to fetch market data for test tokens.
394+
// Returns list of test tokens and list of mainnet tokens that have a cross chain id set.
395+
func (tm *Manager) GetTokensForFetchingMarketData() ([]*tokentypes.Token, error) {
396+
testnetMode, err := tm.settings.GetTestNetworksEnabled()
397+
if err != nil {
398+
return nil, err
399+
}
400+
401+
// If not in testnet mode, use the regular tokens
402+
if !testnetMode {
403+
return tm.GetTokensForActiveNetworksMode()
404+
}
405+
406+
// Test tokens handling...
407+
// Use all test tokens and add to the list only mainnet tokens that have a cross chain id set
408+
allWsdkTokens := tm.tokensManager.UniqueTokens()
409+
tokens := make([]*tokentypes.Token, 0)
410+
for _, token := range allWsdkTokens {
411+
if !walletcommon.ChainID(token.ChainID).IsMainnet() {
412+
tokens = append(tokens, &tokentypes.Token{Token: token})
413+
} else if token.CrossChainID != "" {
414+
tokens = append(tokens, &tokentypes.Token{Token: token})
415+
}
416+
}
417+
418+
return tokens, nil
419+
}
420+
421+
// GetTokensByKeysForFetchingMarketData returns all unique tokens for fetching market data from Coingecko (doesn't affect CryptoCompare cause it maps tokens differently, by symbol)
422+
// Special handling for test tokens, for fetching market data from Coingecko, cause their API doesn't support test tokens
423+
// Corresponding mainnet tokens are needed to fetch market data for test tokens.
424+
// Returns list of test tokens that match the given token keys and corresponding mainnet tokens for those test tokens that have a cross chain id set.
425+
func (tm *Manager) GetTokensByKeysForFetchingMarketData(tokenKeys []string) ([]*tokentypes.Token, error) {
426+
testnetMode, err := tm.settings.GetTestNetworksEnabled()
427+
if err != nil {
428+
return nil, err
429+
}
430+
431+
// If not in testnet mode, use the regular tokens
432+
if !testnetMode {
433+
return tm.GetTokensByKeys(tokenKeys)
434+
}
435+
436+
// Test tokens handling...
437+
// Use corresponding mainnet tokens for the test tokens that contains the same cross chain id
438+
mainnetTokenKeysByCrossChainIDs := make(map[string][]string, 0) // keeps token keys of all mainnet tokens by cross chain id
439+
allWsdkTokens := tm.tokensManager.UniqueTokens()
440+
tokens := make([]*tokentypes.Token, 0)
441+
for _, token := range allWsdkTokens {
442+
if token.CrossChainID != "" && walletcommon.ChainID(token.ChainID).IsMainnet() {
443+
mainnetTokenKeysByCrossChainIDs[token.CrossChainID] = append(mainnetTokenKeysByCrossChainIDs[token.CrossChainID], token.Key())
444+
}
445+
if !slices.Contains(tokenKeys, token.Key()) {
446+
continue
447+
}
448+
tokens = append(tokens, &tokentypes.Token{Token: token})
449+
}
450+
451+
mainnetTokenKeys := make([]string, 0) // keeps token keys of mainnet tokens that have the same cross chain id as the test tokens
452+
for _, token := range tokens {
453+
crossChainID := token.CrossChainID
454+
if crossChainID == "" {
455+
continue
456+
}
457+
// Special handling for status test token STT, cause even it's the same token belongs to different group and has different symbol SNT/STT.
458+
if crossChainID == walletcommon.StatusTestTokenCrossChainID {
459+
crossChainID = walletcommon.StatusMainnetTokenCrossChainID
460+
}
461+
if _, ok := mainnetTokenKeysByCrossChainIDs[crossChainID]; !ok {
462+
continue
463+
}
464+
mainnetTokenKeys = append(mainnetTokenKeys, mainnetTokenKeysByCrossChainIDs[crossChainID]...)
465+
}
466+
467+
mainnetTokens, err := tm.GetTokensByKeys(mainnetTokenKeys)
468+
if err != nil {
469+
return nil, err
470+
}
471+
tokens = append(tokens, mainnetTokens...)
472+
473+
return tokens, nil
474+
}
475+
391476
func (tm *Manager) GetAllTokens() ([]*tokentypes.Token, error) {
392477
wsdkTokens := tm.tokensManager.UniqueTokens()
393478
tokens := make([]*tokentypes.Token, 0)

0 commit comments

Comments
 (0)