Skip to content

Commit

Permalink
Merge pull request #226 from InjectiveLabs/feat/load_official_tokens_…
Browse files Browse the repository at this point in the history
…list

feat/load_official_tokens_list
  • Loading branch information
aarmoa committed Jun 6, 2024
2 parents 68f05f6 + 0a1185e commit b0aed65
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 31 deletions.
2 changes: 2 additions & 0 deletions client/chain/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func createClient(senderAddress cosmtypes.AccAddress, cosmosKeyring keyring.Keyr
}

clientCtx = clientCtx.WithNodeURI(network.TmEndpoint).WithClient(tmClient)
// configure Keyring as nil to avoid the account initialization request when running unit tests
clientCtx.Keyring = nil

chainClient, err := NewChainClient(
clientCtx,
Expand Down
51 changes: 24 additions & 27 deletions client/chain/markets_assistant.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ import (
var legacyMarketAssistantLazyInitialization sync.Once
var legacyMarketAssistant MarketsAssistant

type TokenMetadata interface {
GetName() string
GetAddress() string
GetSymbol() string
GetLogo() string
GetDecimals() int32
GetUpdatedAt() int64
}

type MarketsAssistant struct {
tokensBySymbol map[string]core.Token
tokensByDenom map[string]core.Token
Expand Down Expand Up @@ -140,6 +149,17 @@ func NewMarketsAssistant(networkName string) (MarketsAssistant, error) {

func NewMarketsAssistantInitializedFromChain(ctx context.Context, exchangeClient exchange.ExchangeClient) (MarketsAssistant, error) {
assistant := newMarketsAssistant()

officialTokens, err := core.LoadTokens(exchangeClient.GetNetwork().OfficialTokensListUrl)
if err == nil {
for _, tokenMetadata := range officialTokens {
if tokenMetadata.Denom != "" {
// add tokens to the assistant ensuring all of them get assigned a unique symbol
tokenRepresentation(tokenMetadata.GetSymbol(), tokenMetadata, tokenMetadata.Denom, &assistant)
}
}
}

spotMarketsRequest := spotExchangePB.MarketsRequest{
MarketStatus: "active",
}
Expand All @@ -160,8 +180,8 @@ func NewMarketsAssistantInitializedFromChain(ctx context.Context, exchangeClient
quoteTokenSymbol = marketInfo.GetQuoteTokenMeta().GetSymbol()
}

baseToken := spotTokenRepresentation(baseTokenSymbol, marketInfo.GetBaseTokenMeta(), marketInfo.GetBaseDenom(), &assistant)
quoteToken := spotTokenRepresentation(quoteTokenSymbol, marketInfo.GetQuoteTokenMeta(), marketInfo.GetQuoteDenom(), &assistant)
baseToken := tokenRepresentation(baseTokenSymbol, marketInfo.GetBaseTokenMeta(), marketInfo.GetBaseDenom(), &assistant)
quoteToken := tokenRepresentation(quoteTokenSymbol, marketInfo.GetQuoteTokenMeta(), marketInfo.GetQuoteDenom(), &assistant)

makerFeeRate := decimal.RequireFromString(marketInfo.GetMakerFeeRate())
takerFeeRate := decimal.RequireFromString(marketInfo.GetTakerFeeRate())
Expand Down Expand Up @@ -199,7 +219,7 @@ func NewMarketsAssistantInitializedFromChain(ctx context.Context, exchangeClient
if len(marketInfo.GetQuoteTokenMeta().GetSymbol()) > 0 {
quoteTokenSymbol := marketInfo.GetQuoteTokenMeta().GetSymbol()

quoteToken := derivativeTokenRepresentation(quoteTokenSymbol, marketInfo.GetQuoteTokenMeta(), marketInfo.GetQuoteDenom(), &assistant)
quoteToken := tokenRepresentation(quoteTokenSymbol, marketInfo.GetQuoteTokenMeta(), marketInfo.GetQuoteDenom(), &assistant)

initialMarginRatio := decimal.RequireFromString(marketInfo.GetInitialMarginRatio())
maintenanceMarginRatio := decimal.RequireFromString(marketInfo.GetMaintenanceMarginRatio())
Expand Down Expand Up @@ -265,30 +285,7 @@ func uniqueSymbol(symbol string, denom string, tokenMetaSymbol string, tokenMeta
return uniqueSymbol
}

func spotTokenRepresentation(symbol string, tokenMeta *spotExchangePB.TokenMeta, denom string, assistant *MarketsAssistant) core.Token {
_, isPresent := assistant.tokensByDenom[denom]

if !isPresent {
uniqueSymbol := uniqueSymbol(symbol, denom, tokenMeta.GetSymbol(), tokenMeta.GetName(), *assistant)

newToken := core.Token{
Name: tokenMeta.GetName(),
Symbol: symbol,
Denom: denom,
Address: tokenMeta.GetAddress(),
Decimals: tokenMeta.GetDecimals(),
Logo: tokenMeta.GetLogo(),
Updated: tokenMeta.GetUpdatedAt(),
}

assistant.tokensByDenom[denom] = newToken
assistant.tokensBySymbol[uniqueSymbol] = newToken
}

return assistant.tokensByDenom[denom]
}

func derivativeTokenRepresentation(symbol string, tokenMeta *derivativeExchangePB.TokenMeta, denom string, assistant *MarketsAssistant) core.Token {
func tokenRepresentation(symbol string, tokenMeta TokenMetadata, denom string, assistant *MarketsAssistant) core.Token {
_, isPresent := assistant.tokensByDenom[denom]

if !isPresent {
Expand Down
24 changes: 24 additions & 0 deletions client/chain/markets_assistant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package chain

import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/InjectiveLabs/sdk-go/client/common"

"github.com/InjectiveLabs/sdk-go/client/exchange"
derivativeExchangePB "github.com/InjectiveLabs/sdk-go/exchange/derivative_exchange_rpc/pb"
spotExchangePB "github.com/InjectiveLabs/sdk-go/exchange/spot_exchange_rpc/pb"
Expand All @@ -13,7 +17,17 @@ import (
)

func TestMarketAssistantCreationUsingMarketsFromExchange(t *testing.T) {
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("[]"))
}))
defer httpServer.Close()

network := common.NewNetwork()
network.OfficialTokensListUrl = httpServer.URL

mockExchange := exchange.MockExchangeClient{}
mockExchange.Network = network
var spotMarketInfos []*spotExchangePB.SpotMarketInfo
var derivativeMarketInfos []*derivativeExchangePB.DerivativeMarketInfo
injUsdtSpotMarketInfo := createINJUSDTSpotMarketInfo()
Expand Down Expand Up @@ -74,7 +88,17 @@ func TestMarketAssistantCreationUsingMarketsFromExchange(t *testing.T) {
}

func TestMarketAssistantCreationWithAllTokens(t *testing.T) {
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("[]"))
}))
defer httpServer.Close()

network := common.NewNetwork()
network.OfficialTokensListUrl = httpServer.URL

mockExchange := exchange.MockExchangeClient{}
mockExchange.Network = network
mockChain := MockChainClient{}
smartDenomMetadata := createSmartDenomMetadata()

Expand Down
16 changes: 13 additions & 3 deletions client/common/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import (
)

const (
SessionRenewalOffset = 2 * time.Minute
MainnetTokensListUrl = "https://github.com/InjectiveLabs/injective-lists/raw/master/tokens/mainnet.json"
TestnetTokensListUrl = "https://github.com/InjectiveLabs/injective-lists/raw/master/tokens/testnet.json"
DevnetTokensListUrl = "https://github.com/InjectiveLabs/injective-lists/raw/master/tokens/devnet.json"
)

func cookieByName(cookies []*http.Cookie, key string) *http.Cookie {
Expand Down Expand Up @@ -199,6 +201,7 @@ type Network struct {
ChainCookieAssistant CookieAssistant
ExchangeCookieAssistant CookieAssistant
ExplorerCookieAssistant CookieAssistant
OfficialTokensListUrl string
}

func LoadNetwork(name string, node string) Network {
Expand All @@ -218,6 +221,7 @@ func LoadNetwork(name string, node string) Network {
ChainCookieAssistant: &DisabledCookieAssistant{},
ExchangeCookieAssistant: &DisabledCookieAssistant{},
ExplorerCookieAssistant: &DisabledCookieAssistant{},
OfficialTokensListUrl: MainnetTokensListUrl,
}

case "devnet-1":
Expand All @@ -234,6 +238,7 @@ func LoadNetwork(name string, node string) Network {
ChainCookieAssistant: &DisabledCookieAssistant{},
ExchangeCookieAssistant: &DisabledCookieAssistant{},
ExplorerCookieAssistant: &DisabledCookieAssistant{},
OfficialTokensListUrl: DevnetTokensListUrl,
}
case "devnet":
return Network{
Expand All @@ -249,6 +254,7 @@ func LoadNetwork(name string, node string) Network {
ChainCookieAssistant: &DisabledCookieAssistant{},
ExchangeCookieAssistant: &DisabledCookieAssistant{},
ExplorerCookieAssistant: &DisabledCookieAssistant{},
OfficialTokensListUrl: DevnetTokensListUrl,
}
case "testnet":
validNodes := []string{"lb", "sentry"}
Expand Down Expand Up @@ -303,6 +309,7 @@ func LoadNetwork(name string, node string) Network {
ChainCookieAssistant: chainCookieAssistant,
ExchangeCookieAssistant: exchangeCookieAssistant,
ExplorerCookieAssistant: explorerCookieAssistant,
OfficialTokensListUrl: TestnetTokensListUrl,
}
case "mainnet":
validNodes := []string{"lb"}
Expand Down Expand Up @@ -342,10 +349,12 @@ func LoadNetwork(name string, node string) Network {
ChainCookieAssistant: chainCookieAssistant,
ExchangeCookieAssistant: exchangeCookieAssistant,
ExplorerCookieAssistant: explorerCookieAssistant,
OfficialTokensListUrl: MainnetTokensListUrl,
}
}

return Network{}
default:
panic(fmt.Sprintf("invalid network %s", name))
}
}

// NewNetwork returns a new Network instance with all cookie assistants disabled.
Expand All @@ -355,6 +364,7 @@ func NewNetwork() Network {
ChainCookieAssistant: &DisabledCookieAssistant{},
ExchangeCookieAssistant: &DisabledCookieAssistant{},
ExplorerCookieAssistant: &DisabledCookieAssistant{},
OfficialTokensListUrl: MainnetTokensListUrl,
}
}

Expand Down
67 changes: 67 additions & 0 deletions client/core/tokens_file_loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package core

import (
"encoding/json"
"fmt"
"net/http"
)

type TokenMetadata struct {
Address string `json:"address"`
IsNative bool `json:"isNative"`
TokenVerification string `json:"tokenVerification"`
Decimals int32 `json:"decimals"`
CoinGeckoId string `json:"coinGeckoId"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Logo string `json:"logo"`
Creator string `json:"creator"`
Denom string `json:"denom"`
TokenType string `json:"tokenType"`
ExternalLogo string `json:"externalLogo"`
}

func (tm TokenMetadata) GetName() string {
return tm.Name
}

func (tm TokenMetadata) GetAddress() string {
return tm.Address
}

func (tm TokenMetadata) GetSymbol() string {
return tm.Symbol
}

func (tm TokenMetadata) GetLogo() string {
return tm.Logo
}

func (tm TokenMetadata) GetDecimals() int32 {
return tm.Decimals
}

func (tm TokenMetadata) GetUpdatedAt() int64 {
return -1
}

// LoadTokens loads tokens from the given file URL
func LoadTokens(tokensFileUrl string) ([]TokenMetadata, error) {
var tokensMetadata []TokenMetadata
response, err := http.Get(tokensFileUrl)
if err != nil {
return tokensMetadata, err
}
if 400 <= response.StatusCode {
return tokensMetadata, fmt.Errorf("failed to load tokens from %s: %s", tokensFileUrl, response.Status)
}
defer response.Body.Close()

decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&tokensMetadata)
if err != nil {
return make([]TokenMetadata, 0), err
}

return tokensMetadata, nil
}
74 changes: 74 additions & 0 deletions client/core/tokens_file_loader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package core

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"gotest.tools/v3/assert"
)

func TestLoadTokensFromUrl(t *testing.T) {
tokensMetadata := make([]TokenMetadata, 2)
tokensMetadata = append(tokensMetadata, TokenMetadata{
Address: "",
IsNative: true,
Decimals: 9,
Symbol: "SOL",
Name: "Solana",
Logo: "https://imagedelivery.net/DYKOWp0iCc0sIkF-2e4dNw/2aa4deed-fa31-4d1a-ba0a-d698b84f3800/public",
Creator: "inj15jeczm4mqwtc9lk4c0cyynndud32mqd4m9xnmu",
CoinGeckoId: "solana",
Denom: "",
TokenType: "spl",
TokenVerification: "verified",
ExternalLogo: "solana.png",
},
)
tokensMetadata = append(tokensMetadata, TokenMetadata{
Address: "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270",
IsNative: false,
Decimals: 18,
Symbol: "WMATIC",
Name: "Wrapped Matic",
Logo: "https://imagedelivery.net/DYKOWp0iCc0sIkF-2e4dNw/0d061e1e-a746-4b19-1399-8187b8bb1700/public",
Creator: "inj169ed97mcnf8ay6rgvskn95n6tyt46uwvy5qgs0",
CoinGeckoId: "wmatic",
Denom: "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270",
TokenType: "evm",
TokenVerification: "verified",
ExternalLogo: "polygon.png",
},
)

metadataString, err := json.Marshal(tokensMetadata)
assert.NilError(t, err)

httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write(metadataString)
}))
defer httpServer.Close()

loadedTokens, err := LoadTokens(httpServer.URL)
assert.NilError(t, err)

assert.Equal(t, len(loadedTokens), len(tokensMetadata))

for i, metadata := range tokensMetadata {
assert.Equal(t, loadedTokens[i], metadata)
}
}

func TestLoadTokensFromUrlReturnsNoTokensWhenRequestErrorHappens(t *testing.T) {
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer httpServer.Close()

loadedTokens, err := LoadTokens(httpServer.URL)
assert.Error(t, err, fmt.Sprintf("failed to load tokens from %s: %v %s", httpServer.URL, http.StatusNotFound, http.StatusText(http.StatusNotFound)))
assert.Equal(t, len(loadedTokens), 0)
}
5 changes: 5 additions & 0 deletions client/exchange/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type ExchangeClient interface {
GetInfo(ctx context.Context, req *metaPB.InfoRequest) (*metaPB.InfoResponse, error)
GetVersion(ctx context.Context, req *metaPB.VersionRequest) (*metaPB.VersionResponse, error)
Ping(ctx context.Context, req *metaPB.PingRequest) (*metaPB.PingResponse, error)
GetNetwork() common.Network
Close()
}

Expand Down Expand Up @@ -961,6 +962,10 @@ func (c *exchangeClient) StreamAccountPortfolio(ctx context.Context, accountAddr
return stream, nil
}

func (c *exchangeClient) GetNetwork() common.Network {
return c.network
}

func (c *exchangeClient) Close() {
c.conn.Close()
}
Loading

0 comments on commit b0aed65

Please sign in to comment.