github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/OrcaScraper.go (about)

     1  package scrapers
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math"
     7  	"math/big"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/diadata-org/diadata/pkg/dia"
    13  	orcaWhirlpoolIdlBind "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/orca/whirlpool"
    14  	"github.com/diadata-org/diadata/pkg/utils"
    15  
    16  	bin "github.com/gagliardetto/binary"
    17  	tokenmetadata "github.com/gagliardetto/metaplex-go/clients/token-metadata"
    18  	"github.com/gagliardetto/solana-go"
    19  	"github.com/gagliardetto/solana-go/programs/token"
    20  	"github.com/gagliardetto/solana-go/programs/tokenregistry"
    21  	"github.com/gagliardetto/solana-go/rpc"
    22  	"github.com/gagliardetto/solana-go/rpc/ws"
    23  	"github.com/gorilla/websocket"
    24  )
    25  
    26  const (
    27  	orcaSolanaHttpEndpoint           = "https://rpc.ankr.com/solana"
    28  	orcaSolanaWsEndpoint             = rpc.MainNetBeta_WS
    29  	OrcaProgWhirlpoolConfigAddr      = "2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ"
    30  	OrcaProgWhirlpoolAddr            = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"
    31  	OrcaProgWhirlpoolAccountDataSize = 653
    32  	OrcaMaxRetries                   = 5
    33  	OrcaRetryDelay                   = 3 * time.Second
    34  )
    35  
    36  // The scraper object for Orca
    37  type OrcaScraper struct {
    38  	exchangeName string
    39  
    40  	// state variables to signal events
    41  	run          bool
    42  	shutdown     chan nothing
    43  	shutdownDone chan nothing
    44  
    45  	errorLock sync.RWMutex
    46  	error     error
    47  
    48  	pairScrapers map[string]*OrcaPairScraper
    49  	chanTrades   chan *dia.Trade
    50  
    51  	RestClient *rpc.Client
    52  	WsClient   *ws.Client
    53  }
    54  
    55  // Returns a new exchange scraper
    56  func NewOrcaScraper(exchange dia.Exchange, scrape bool) *OrcaScraper {
    57  
    58  	log.Infof("init rest and ws client for %s", exchange.BlockChain.Name)
    59  	restClient := rpc.New(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_REST", orcaSolanaHttpEndpoint))
    60  	wsClient, err := ws.Connect(context.Background(), utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_WS", orcaSolanaWsEndpoint))
    61  	if err != nil {
    62  		log.Fatal("init ws client: ", err)
    63  	}
    64  
    65  	scraper := &OrcaScraper{
    66  		exchangeName: exchange.Name,
    67  		RestClient:   restClient,
    68  		WsClient:     wsClient,
    69  		shutdown:     make(chan nothing),
    70  		shutdownDone: make(chan nothing),
    71  		pairScrapers: make(map[string]*OrcaPairScraper),
    72  		chanTrades:   make(chan *dia.Trade),
    73  	}
    74  
    75  	_, err = scraper.loadMarketsMetadata()
    76  	if err != nil {
    77  		log.Error("load metadata: %s", err)
    78  	}
    79  
    80  	if scrape {
    81  		go scraper.mainLoop()
    82  	}
    83  	return scraper
    84  }
    85  
    86  // Closes any existing API connections, as well as channels of
    87  // pairScrapers from calls to ScrapePair
    88  func (s *OrcaScraper) Close() error {
    89  	s.run = false
    90  	for _, pairScraper := range s.pairScrapers {
    91  		pairScraper.closed = true
    92  	}
    93  	s.WsClient.Close()
    94  	s.RestClient.Close()
    95  
    96  	close(s.shutdown)
    97  	<-s.shutdownDone
    98  	return nil
    99  }
   100  
   101  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from the scraper
   102  func (s *OrcaScraper) ScrapePair(pair dia.ExchangePair) (ps PairScraper, err error) {
   103  	return
   104  }
   105  
   106  // Returns the list of all available trade pairs in order to pairDiscoveryService service work
   107  func (s *OrcaScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   108  	pairs, err = s.loadMarketsMetadata()
   109  	return
   110  }
   111  
   112  // Channel returns a channel that can be used to receive trades
   113  func (s *OrcaScraper) Channel() chan *dia.Trade {
   114  	return s.chanTrades
   115  }
   116  
   117  // FillSymbolData adds the name to the asset underlying @symbol on scraper
   118  func (s *OrcaScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   119  	return dia.Asset{Symbol: symbol}, nil
   120  }
   121  
   122  // NormalizePair accounts for the pair
   123  func (s *OrcaScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   124  	return pair, nil
   125  }
   126  
   127  type OrcaPairScraper struct {
   128  	parent *OrcaScraper
   129  	pair   dia.ExchangePair
   130  	closed bool
   131  }
   132  
   133  // Close stops listening for trades of the pair associated with the scraper
   134  func (ps *OrcaPairScraper) Close() error {
   135  	ps.parent.errorLock.RLock()
   136  	defer ps.parent.errorLock.RUnlock()
   137  	ps.closed = true
   138  	return nil
   139  }
   140  
   141  // Error returns an error when the channel Channel() is closed and nil otherwise
   142  func (ps *OrcaPairScraper) Error() error {
   143  	s := ps.parent
   144  	s.errorLock.RLock()
   145  	defer s.errorLock.RUnlock()
   146  	return s.error
   147  }
   148  
   149  // Pair returns the pair this scraper is subscribed to
   150  func (ps *OrcaPairScraper) Pair() dia.ExchangePair {
   151  	return ps.pair
   152  }
   153  
   154  func (s *OrcaScraper) mainLoop() {
   155  	wg := sync.WaitGroup{}
   156  
   157  	s.run = true
   158  	for s.run {
   159  
   160  		ic := make(chan map[string]nothing)
   161  
   162  		wg.Add(2)
   163  
   164  		go func() {
   165  			sub, err := s.WsClient.LogsSubscribeMentions(
   166  				solana.MustPublicKeyFromBase58(OrcaProgWhirlpoolAddr),
   167  				rpc.CommitmentFinalized,
   168  			)
   169  			if err != nil {
   170  				log.Fatal("sub: %s", err)
   171  			}
   172  			defer sub.Unsubscribe()
   173  			defer s.WsClient.Close()
   174  			log.Infof("start subscription routine")
   175  			defer log.Warnf("subscription routine end\n")
   176  			lastSlot := uint64(0)
   177  			pendingTxs := make(map[string]nothing)
   178  			for {
   179  				got, err := sub.Recv()
   180  				if err != nil {
   181  					if !websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
   182  						log.Warnf("recv expected error: %s", err)
   183  					} else {
   184  						log.Warnf("recv error: %s", err)
   185  					}
   186  					retries := 0
   187  					for retries <= OrcaMaxRetries {
   188  						retries++
   189  						log.Warnf("Recovering websocket connection %d/%d ...", retries, OrcaMaxRetries)
   190  						s.WsClient, err = ws.Connect(context.Background(), orcaSolanaWsEndpoint)
   191  						time.Sleep(OrcaRetryDelay)
   192  						if err != nil {
   193  							log.Warnf("retry failed: ", err)
   194  						} else {
   195  							sub, err = s.WsClient.LogsSubscribeMentions(
   196  								solana.MustPublicKeyFromBase58(OrcaProgWhirlpoolAddr),
   197  								rpc.CommitmentFinalized,
   198  							)
   199  							if err != nil {
   200  								log.Warnf("re-sub failed: ", err)
   201  							} else {
   202  								log.Infof("Websocket connection recovered with success.")
   203  								retries = 0
   204  								break
   205  							}
   206  						}
   207  					}
   208  				} else {
   209  					if got.Value.Err == nil {
   210  						if got.Context.Slot > lastSlot {
   211  							ic <- pendingTxs
   212  							pendingTxs = make(map[string]nothing)
   213  							lastSlot = got.Context.Slot
   214  							pendingTxs[got.Value.Signature.String()] = struct{}{}
   215  						} else if got.Context.Slot == lastSlot {
   216  							pendingTxs[got.Value.Signature.String()] = struct{}{}
   217  						} else {
   218  							log.Fatalf("invalid order for subscription: %s (curr %d, last %d)\n", got.Value.Signature.String(), got.Context.Slot, lastSlot)
   219  						}
   220  
   221  					}
   222  				}
   223  			}
   224  		}()
   225  
   226  		go func() {
   227  			log.Infof("start processing routine")
   228  			defer log.Warnf("processing routine end\n")
   229  			for v := range ic {
   230  				for len(v) > 0 {
   231  					for k := range v {
   232  
   233  						max := uint64(0)
   234  						resp, err := s.RestClient.GetTransaction(
   235  							context.TODO(),
   236  							solana.MustSignatureFromBase58(k),
   237  							&rpc.GetTransactionOpts{
   238  								MaxSupportedTransactionVersion: &max,
   239  								Commitment:                     rpc.CommitmentFinalized,
   240  								Encoding:                       solana.EncodingBase64,
   241  							},
   242  						)
   243  						if err != nil {
   244  							continue
   245  						}
   246  						if resp != nil {
   247  							tx := resp.Transaction
   248  							if tx != nil {
   249  								txParsed, err := tx.GetTransaction()
   250  								if err == nil && txParsed != nil {
   251  									if txParsed.Message.GetVersion() == 0 && !txParsed.Message.IsVersioned() {
   252  										accMetaList, err := txParsed.AccountMetaList()
   253  										if err == nil && accMetaList != nil {
   254  											for i, inst := range txParsed.Message.Instructions {
   255  												if txParsed.Message.AccountKeys[inst.ProgramIDIndex].String() == OrcaProgWhirlpoolAddr {
   256  													instDecoded, err := orcaWhirlpoolIdlBind.DecodeInstruction(accMetaList, inst.Data)
   257  													if err != nil {
   258  														log.Warnf("  %88s cannot-decode\n", k)
   259  													}
   260  													if instDecoded.TypeID == orcaWhirlpoolIdlBind.Instruction_Swap {
   261  														pair := s.pairScrapers[txParsed.Message.AccountKeys[inst.Accounts[2]].String()].pair.UnderlyingPair
   262  														baseToken := pair.BaseToken
   263  														quoteToken := pair.QuoteToken
   264  														for _, inner := range resp.Meta.InnerInstructions {
   265  															if inner.Index == uint16(i) {
   266  																var amountA, amountB, volume, price float64
   267  																for j, innerInsts := range inner.Instructions {
   268  																	innerInstData := bin.NewBorshDecoder(innerInsts.Data)
   269  																	typeIdEnconding, err := innerInstData.ReadUint8()
   270  																	if err != nil {
   271  																		log.Fatalf("cannot read inner inst param: %s", err)
   272  																	}
   273  																	innerInstParam, err := innerInstData.ReadUint64(bin.LE)
   274  																	if err != nil {
   275  																		log.Fatalf("cannot read inner inst param: %s", err)
   276  																	}
   277  																	if bin.TypeIDEncoding(uint32(typeIdEnconding)) == bin.AnchorTypeIDEncoding {
   278  																		if j == 0 {
   279  																			if *instDecoded.Impl.(*orcaWhirlpoolIdlBind.Swap).AToB {
   280  																				amountA = FormatUint64Decimals(innerInstParam, int(baseToken.Decimals))
   281  																			} else {
   282  																				amountB = FormatUint64Decimals(innerInstParam, int(quoteToken.Decimals))
   283  																			}
   284  																		} else {
   285  																			if *instDecoded.Impl.(*orcaWhirlpoolIdlBind.Swap).AToB {
   286  																				amountB = FormatUint64Decimals(innerInstParam, int(quoteToken.Decimals))
   287  																			} else {
   288  																				amountA = FormatUint64Decimals(innerInstParam, int(baseToken.Decimals))
   289  																			}
   290  																		}
   291  																	}
   292  																}
   293  																if *instDecoded.Impl.(*orcaWhirlpoolIdlBind.Swap).AToB {
   294  																	price = amountA / amountB
   295  																	volume = -amountB
   296  																} else {
   297  																	price = amountA / amountB
   298  																	volume = amountB
   299  																}
   300  																trade := &dia.Trade{
   301  																	Symbol:         quoteToken.Symbol,
   302  																	Pair:           quoteToken.Symbol + "-" + baseToken.Symbol,
   303  																	BaseToken:      baseToken,
   304  																	QuoteToken:     quoteToken,
   305  																	Price:          price,
   306  																	Volume:         volume,
   307  																	Time:           resp.BlockTime.Time(),
   308  																	ForeignTradeID: k,
   309  																	Source:         s.exchangeName,
   310  																	VerifiedPair:   true,
   311  																}
   312  																log.Infof("pair -- price -- volume: %s -- %v -- %v", trade.Pair, trade.Price, trade.Volume)
   313  																log.Info("tx hash: ", k)
   314  																s.chanTrades <- trade
   315  															}
   316  														}
   317  													}
   318  												}
   319  											}
   320  										}
   321  									}
   322  								}
   323  							}
   324  							delete(v, k)
   325  						}
   326  					}
   327  				}
   328  			}
   329  		}()
   330  
   331  		wg.Wait()
   332  
   333  	}
   334  }
   335  
   336  // Load markets and tokens metadata
   337  func (s *OrcaScraper) loadMarketsMetadata() (pairs []dia.ExchangePair, err error) {
   338  	log.Infof("loading initial data from pools ...")
   339  	start := time.Now()
   340  	poolPairs, err := s.loadMarketPools()
   341  	if err != nil {
   342  		return
   343  	}
   344  	pairs = append(pairs, poolPairs...)
   345  	log.Infof("loaded %d pairs from legacy pools in %.1fs", len(poolPairs), time.Since(start).Seconds())
   346  	start = time.Now()
   347  	whirlpoolPairs, err := s.loadMarketWhirlpools()
   348  	if err != nil {
   349  		return
   350  	}
   351  	pairs = append(pairs, whirlpoolPairs...)
   352  	log.Infof("loaded %d pairs from whirlpools in %.1fs", len(whirlpoolPairs), time.Since(start).Seconds())
   353  	return
   354  }
   355  
   356  // Get Orca market legacy pools
   357  func (s *OrcaScraper) loadMarketPools() (pairs []dia.ExchangePair, err error) {
   358  	return
   359  }
   360  
   361  // Get Orca market whirlpools
   362  func (s *OrcaScraper) loadMarketWhirlpools() (pairs []dia.ExchangePair, err error) {
   363  	hardcodedTokenMeta := GetOrcaTokensMetadata()
   364  	resp, err := s.RestClient.GetProgramAccountsWithOpts(
   365  		context.TODO(),
   366  		solana.MustPublicKeyFromBase58(OrcaProgWhirlpoolAddr),
   367  		&rpc.GetProgramAccountsOpts{
   368  			Filters: []rpc.RPCFilter{
   369  				{
   370  					DataSize: OrcaProgWhirlpoolAccountDataSize,
   371  				},
   372  			},
   373  		},
   374  	)
   375  	if err != nil {
   376  		return
   377  	}
   378  	if resp == nil {
   379  		return nil, fmt.Errorf("program account not found")
   380  	}
   381  	log.Infof("discovered %d accounts in whirlpool program, retrieving metadata ...", len(resp))
   382  	for _, progAcc := range resp {
   383  		acct := progAcc.Account
   384  		pubKey := progAcc.Pubkey.String()
   385  		if acct.Owner.String() == OrcaProgWhirlpoolAddr {
   386  			d := bin.NewBorshDecoder(acct.Data.GetBinary())
   387  			var w orcaWhirlpoolIdlBind.Whirlpool
   388  			d.Decode(&w)
   389  			// Blacklist XXX/USDC, ATLAS/USDC, SHIB/USDC
   390  			if pubKey == "FfBeru58Q7hjqHq9T2Trw1BeyjE1YwHsx9MivKUwoTLQ" || pubKey == "9vqFu6v9CcVDaSx2oRD3jo8H5gqkE2urYQgpT16V1BTa" || pubKey == "DahhciLA89UkZoqrqVWL2nojwPLmSVkXQGTiEhAtkaFa" {
   391  				continue
   392  			}
   393  			if w.WhirlpoolsConfig.String() == OrcaProgWhirlpoolConfigAddr {
   394  				var tokenA, tokenB dia.Asset
   395  
   396  				// Get token A mint data and metadata
   397  				if mintData, err := s.getTokenMintData(w.TokenMintA.String()); err == nil {
   398  					if mintData.IsInitialized {
   399  						tokenA.Decimals = mintData.Decimals
   400  					}
   401  				} else {
   402  					return nil, err
   403  				}
   404  				if metadata, err := s.getTokenMetadata(w.TokenMintA.String()); err != nil {
   405  					if v, ok := hardcodedTokenMeta[w.TokenMintA.String()]; ok {
   406  						tokenA.Symbol = v.(OrcaTokenMetadata).GetSymbol()
   407  						tokenA.Name = v.(OrcaTokenMetadata).GetName()
   408  					} else {
   409  						log.Warnf("token metadata not found for %s: %s", w.TokenMintA.String(), err)
   410  						if strings.Contains(err.Error(), "not found") {
   411  							err = nil
   412  						}
   413  						continue
   414  					}
   415  				} else {
   416  					tokenA.Symbol = strings.TrimRight(metadata.Data.Symbol, "\x00")
   417  					tokenA.Name = strings.TrimRight(metadata.Data.Name, "\x00")
   418  				}
   419  				tokenA.Address = w.TokenMintA.String()
   420  				tokenA.Blockchain = "Solana"
   421  
   422  				// Get token B mint data and metadata
   423  				if mintData, err := s.getTokenMintData(w.TokenMintB.String()); err == nil {
   424  					tokenB.Decimals = mintData.Decimals
   425  				} else {
   426  					return nil, err
   427  				}
   428  				if metadata, err := s.getTokenMetadata(w.TokenMintB.String()); err != nil {
   429  					if v, ok := hardcodedTokenMeta[w.TokenMintB.String()]; ok {
   430  						tokenB.Symbol = v.(OrcaTokenMetadata).GetSymbol()
   431  						tokenB.Name = v.(OrcaTokenMetadata).GetName()
   432  					} else {
   433  						log.Warnf("token metadata not found for %s: %s", w.TokenMintB.String(), err)
   434  						if strings.Contains(err.Error(), "not found") {
   435  							err = nil
   436  						}
   437  						continue
   438  					}
   439  				} else {
   440  					tokenB.Symbol = strings.TrimRight(metadata.Data.Symbol, "\x00")
   441  					tokenB.Name = strings.TrimRight(metadata.Data.Name, "\x00")
   442  				}
   443  				tokenB.Address = w.TokenMintB.String()
   444  				tokenB.Blockchain = "Solana"
   445  
   446  				log.Infof("whirlpool loaded: %44s (%10s / %10s)\n", pubKey, tokenA.Symbol, tokenB.Symbol)
   447  				pair := dia.ExchangePair{
   448  					Symbol:      tokenA.Symbol,
   449  					Verified:    true,
   450  					ForeignName: pubKey,
   451  					Exchange:    s.exchangeName,
   452  					UnderlyingPair: dia.Pair{
   453  						BaseToken:  tokenA,
   454  						QuoteToken: tokenB,
   455  					},
   456  				}
   457  				pairs = append(pairs, pair)
   458  				s.pairScrapers[pubKey] = &OrcaPairScraper{
   459  					parent: s,
   460  					pair:   pair,
   461  					closed: false,
   462  				}
   463  			}
   464  		}
   465  	}
   466  	return
   467  }
   468  
   469  // Get Solana token mint data
   470  func (s *OrcaScraper) getTokenMintData(account string) (mint token.Mint, err error) {
   471  	resp, err := s.RestClient.GetAccountInfoWithOpts(
   472  		context.TODO(),
   473  		solana.MustPublicKeyFromBase58(account),
   474  		&rpc.GetAccountInfoOpts{},
   475  	)
   476  	if err != nil {
   477  		return
   478  	}
   479  	d := bin.NewBorshDecoder(resp.Value.Data.GetBinary())
   480  	err = d.Decode(&mint)
   481  	if err != nil {
   482  		return
   483  	}
   484  	return
   485  }
   486  
   487  // Get Solana token metadata
   488  func (s *OrcaScraper) getTokenMetadata(account string) (metadata tokenmetadata.Metadata, err error) {
   489  	accMint := solana.MustPublicKeyFromBase58(account)
   490  	tMeta, err := tokenregistry.GetTokenRegistryEntry(context.TODO(), s.RestClient, accMint)
   491  	if err != nil {
   492  		metaAddress, _, err := solana.FindTokenMetadataAddress(accMint)
   493  		if err != nil {
   494  			return metadata, err
   495  		}
   496  		resp, err := s.RestClient.GetAccountInfo(
   497  			context.TODO(),
   498  			metaAddress,
   499  		)
   500  		if err != nil {
   501  			return metadata, err
   502  		}
   503  		d := bin.NewBorshDecoder(resp.Value.Data.GetBinary())
   504  		err = d.Decode(&metadata)
   505  		if err != nil {
   506  			return metadata, err
   507  		}
   508  		return metadata, nil
   509  	}
   510  	return tokenmetadata.Metadata{Data: tokenmetadata.Data{Symbol: tMeta.Symbol.String()}}, nil
   511  }
   512  
   513  type OrcaTokenMetadata interface {
   514  	GetName() string
   515  	GetSymbol() string
   516  }
   517  
   518  func (t *orcaTokenMetadata) GetName() string {
   519  	return t.Name
   520  }
   521  
   522  func (t *orcaTokenMetadata) GetSymbol() string {
   523  	return t.Symbol
   524  }
   525  
   526  type orcaTokenMetadata struct {
   527  	Name   string
   528  	Symbol string
   529  }
   530  
   531  func GetOrcaTokensMetadata() map[string]interface{} {
   532  	tokenMetadata := make(map[string]interface{})
   533  	tokenMetadata["zebeczgi5fSEtbpfQKVZKCJ3WgYXxjkMUkNNx7fLKAF"] = &orcaTokenMetadata{Name: "ZEBEC", Symbol: "ZBC"}
   534  	tokenMetadata["GEJpt3Wjmr628FqXxTgxMce1pLntcPV4uFi8ksxMyPQh"] = &orcaTokenMetadata{Name: "daoSOL Token", Symbol: "daoSOL"}
   535  	tokenMetadata["CT1iZ7MJzm8Riy6MTgVht2PowGetEWrnq1SfmUjKvz8c"] = &orcaTokenMetadata{Name: "Balloonsville Solvent Droplet", Symbol: "svtBV"}
   536  	tokenMetadata["7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn"] = &orcaTokenMetadata{Name: "JPOOL Solana Token", Symbol: "JSOL"}
   537  	tokenMetadata["USDH1SM1ojwWUga67PGrgFWUHibbjqMvuMaDkRJTgkX"] = &orcaTokenMetadata{Name: "USDH Hubble Stablecoin", Symbol: "USDH"}
   538  	tokenMetadata["6naWDMGNWwqffJnnXFLBCLaYu1y5U9Rohe5wwJPHvf1p"] = &orcaTokenMetadata{Name: "SCRAP", Symbol: "SCRAP"}
   539  	tokenMetadata["SLNDpmoWTVADgEdndyvWzroNL7zSi1dF9PC3xHGtPwp"] = &orcaTokenMetadata{Name: "Solend", Symbol: "SLND"}
   540  	tokenMetadata["EiasWmzy9MrkyekABHLfFRkGhRakaWNvmQ8h5DV86zyn"] = &orcaTokenMetadata{Name: "Visionary Studios Solvent Droplet", Symbol: "svtVSNRY"}
   541  	tokenMetadata["DUSTawucrTsGU8hcqRdHDCbuYhCPADMLM2VcCb8VnFnQ"] = &orcaTokenMetadata{Name: "DUST Protocol", Symbol: "DUST"}
   542  	tokenMetadata["BoeDfSFRyaeuaLP97dhxkHnsn7hhhes3w3X8GgQj5obK"] = &orcaTokenMetadata{Name: "Famous Fox Federation Solvent Droplet", Symbol: "svtFFF"}
   543  	tokenMetadata["ANAxByE6G2WjFp7A4NqtWYXb3mgruyzZYg3spfxe6Lbo"] = &orcaTokenMetadata{Name: "ANA", Symbol: "ANA"}
   544  	tokenMetadata["9iLH8T7zoWhY7sBmj1WK9ENbWdS1nL8n9wAxaeRitTa6"] = &orcaTokenMetadata{Name: "Hedge USD", Symbol: "USH"}
   545  	tokenMetadata["HBB111SCo9jkCejsZfz8Ec8nH7T6THF8KEKSnvwT6XK6"] = &orcaTokenMetadata{Name: "Hubble Protocol Token", Symbol: "HBB"}
   546  	tokenMetadata["52GzcLDMfBveMRnWXKX7U3Pa5Lf7QLkWWvsJRDjWDBSk"] = &orcaTokenMetadata{Name: "NGN Coin", Symbol: "NGNC"}
   547  	tokenMetadata["5PmpMzWjraf3kSsGEKtqdUsCoLhptg4yriZ17LKKdBBy"] = &orcaTokenMetadata{Name: "Hedge Token", Symbol: "HDG"}
   548  	tokenMetadata["9tzZzEHsKnwFL1A3DyFJwj36KnZj3gZ7g4srWp9YTEoh"] = &orcaTokenMetadata{Name: "ARB Protocol", Symbol: "ARB"}
   549  	tokenMetadata["AG5j4hhrd1ReYi7d1JsZL8ZpcoHdjXvc8sdpWF74RaQh"] = &orcaTokenMetadata{Name: "Okay Bears Solvent Droplet", Symbol: "svtOKAY"}
   550  	tokenMetadata["7kbnvuGBxxj8AG9qp8Scn56muWGaRaFqxg1FsRp3PaFT"] = &orcaTokenMetadata{Name: "UXD Stablecoin", Symbol: "UXD"}
   551  	tokenMetadata["SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y"] = &orcaTokenMetadata{Name: "Shadow Token", Symbol: "SHDW"}
   552  	tokenMetadata["GENEtH5amGSi8kHAtQoezp1XEXwZJ8vcuePYnXdKrMYz"] = &orcaTokenMetadata{Name: "Genopets", Symbol: "GENE"}
   553  	tokenMetadata["EsPKhGTMf3bGoy4Qm7pCv3UCcWqAmbC1UGHBTDxRjjD4"] = &orcaTokenMetadata{Name: "FTM (Allbridge from Fantom)", Symbol: "FTM"}
   554  	tokenMetadata["bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1"] = &orcaTokenMetadata{Name: "BlazeStake Staked SOL (bSOL)", Symbol: "bSOL"}
   555  	tokenMetadata["SuperbZyz7TsSdSoFAZ6RYHfAWe9NmjXBLVQpS8hqdx"] = &orcaTokenMetadata{Name: "SuperBonds Token", Symbol: "SB"}
   556  	tokenMetadata["8W2ZFYag9zTdnVpiyR4sqDXszQfx2jAZoMcvPtCSQc7D"] = &orcaTokenMetadata{Name: "The Catalina Whale Mixer Solvent Droplet", Symbol: "svtCWM"}
   557  	tokenMetadata["PsyFiqqjiv41G7o5SMRzDJCu4psptThNR2GtfeGHfSq"] = &orcaTokenMetadata{Name: "PsyOptions", Symbol: "PSY"}
   558  	tokenMetadata["GePFQaZKHcWE5vpxHfviQtH5jgxokSs51Y5Q4zgBiMDs"] = &orcaTokenMetadata{Name: "Jungle DeFi", Symbol: "JFI"}
   559  	tokenMetadata["METAmTMXwdb8gYzyCPfXXFmZZw4rUsXX58PNsDg7zjL"] = &orcaTokenMetadata{Name: "Solice", Symbol: "SLC"}
   560  	tokenMetadata["USDrbBQwQbQ2oWHUPfA8QBHcyVxKUq1xHyXsSLKdUq2"] = &orcaTokenMetadata{Name: "Ratio stable Token", Symbol: "USDr"}
   561  	tokenMetadata["4MSMKZwGnkT8qxK8LsdH28Uu8UfKRT2aNaGTU8TEMuHz"] = &orcaTokenMetadata{Name: "Genopets Genesis - Solvent Droplet", Symbol: "svtGENE"}
   562  	tokenMetadata["F3nefJBcejYbtdREjui1T9DPh5dBgpkKq7u2GAAMXs5B"] = &orcaTokenMetadata{Name: "ALL ART", Symbol: "AART"}
   563  	tokenMetadata["BKipkearSqAUdNKa1WDstvcMjoPsSKBuNyvKDQDDu9WE"] = &orcaTokenMetadata{Name: "Hawksight", Symbol: "HAWK"}
   564  	tokenMetadata["CowKesoLUaHSbAMaUxJUj7eodHHsaLsS65cy8NFyRDGP"] = &orcaTokenMetadata{Name: "Cash Cow", Symbol: "COW"}
   565  	tokenMetadata["Ez2zVjw85tZan1ycnJ5PywNNxR6Gm4jbXQtZKyQNu3Lv"] = &orcaTokenMetadata{Name: "Fluid USDC", Symbol: "fUSDC"}
   566  	tokenMetadata["AFbX8oGjGpmVFywbVouvhQSRmiW2aR1mohfahi4Y2AdB"] = &orcaTokenMetadata{Name: "GST", Symbol: "GST"}
   567  	tokenMetadata["svtMpL5eQzdmB3uqK9NXaQkq8prGZoKQFNVJghdWCkV"] = &orcaTokenMetadata{Name: "Solvent", Symbol: "SVT"}
   568  	tokenMetadata["6F5A4ZAtQfhvi3ZxNex9E1UN5TK7VM2enDCYG1sx1AXT"] = &orcaTokenMetadata{Name: "Degenerate Ape Academy Solvent Droplet", Symbol: "svtDAPE"}
   569  	tokenMetadata["UXPhBoR3qG4UCiGNJfV7MqhHyFqKN68g45GoYvAeL2M"] = &orcaTokenMetadata{Name: "UXP Governance Token", Symbol: "UXP"}
   570  	tokenMetadata["FANTafPFBAt93BNJVpdu25pGPmca3RfwdsDsRrT3LX1r"] = &orcaTokenMetadata{Name: "Phantasia", Symbol: "FANT"}
   571  	tokenMetadata["Bp6k6xacSc4KJ5Bmk9D5xfbw8nN42ZHtPAswEPkNze6U"] = &orcaTokenMetadata{Name: "Pesky Penguins Solvent Droplet", Symbol: "svtPSK"}
   572  	tokenMetadata["2zzC22UBgJGCYPdFyo7GDwz7YHq5SozJc1nnBqLU8oZb"] = &orcaTokenMetadata{Name: "1SPACE", Symbol: "1SP"}
   573  	tokenMetadata["EmLJ8cNEsUtboiV2eiD6VgaEscSJ6zu3ELhqixUP4J56"] = &orcaTokenMetadata{Name: "Thugbirdz - Solvent Droplet", Symbol: "svtTHUGZ"}
   574  	tokenMetadata["9acdc5M9F9WVM4nVZ2gPtVvkeYiWenmzLW9EsTkKdsUJ"] = &orcaTokenMetadata{Name: "Gooney Toons Solvent Droplet", Symbol: "svtGOON"}
   575  	tokenMetadata["GNCjk3FmPPgZTkbQRSxr6nCvLtYMbXKMnRxg8BgJs62e"] = &orcaTokenMetadata{Name: "CELO (Allbridge from Celo)", Symbol: "CELO"}
   576  	tokenMetadata["DXA9itWDGmGgqqUoHnBhw6CjvJKMUmTMKB17hBuoYkfQ"] = &orcaTokenMetadata{Name: "Honey Genesis Bee Solvent Droplet", Symbol: "svtHNYG"}
   577  	tokenMetadata["HYtdDGdMFqBrtyUe5z74bKCtH2WUHZiWRicjNVaHSfkg"] = &orcaTokenMetadata{Name: "Aurory - Solvent Droplet", Symbol: "svtAURY"}
   578  	tokenMetadata["G9tt98aYSznRk7jWsfuz9FnTdokxS6Brohdo9hSmjTRB"] = &orcaTokenMetadata{Name: "PUFF", Symbol: "PUFF"}
   579  	tokenMetadata["8vkTew1mT8w5NapTqpAoNUNHW2MSnAGVNeu8QPmumSJM"] = &orcaTokenMetadata{Name: "Playground Waves Solvent Droplet", Symbol: "svtWAVE"}
   580  	tokenMetadata["PRSMNsEPqhGVCH1TtWiJqPjJyh2cKrLostPZTNy1o5x"] = &orcaTokenMetadata{Name: "PRISM", Symbol: "PRISM"}
   581  	tokenMetadata["seedEDBqu63tJ7PFqvcbwvThrYUkQeqT6NLf81kLibs"] = &orcaTokenMetadata{Name: "Seeded Network", Symbol: "SEEDED"}
   582  	tokenMetadata["FoXyMu5xwXre7zEoSvzViRk3nGawHUp9kUh97y2NDhcq"] = &orcaTokenMetadata{Name: "Famous Fox Federation", Symbol: "FOXY"}
   583  	tokenMetadata["BDrL8huis6S5tpmozaAaT5zhE5A7ZBAB2jMMvpKEeF8A"] = &orcaTokenMetadata{Name: "NOVA FINANCE", Symbol: "NOVA"}
   584  	tokenMetadata["3GQqCi9cuGhAH4VwkmWD32gFHHJhxujurzkRCQsjxLCT"] = &orcaTokenMetadata{Name: "Galactic Geckos Space Garage Solvent Droplet", Symbol: "svtGGSG"}
   585  	tokenMetadata["DCgRa2RR7fCsD63M3NgHnoQedMtwH1jJCwZYXQqk9x3v"] = &orcaTokenMetadata{Name: "DeGods Solvent Droplet", Symbol: "svtDGOD"}
   586  	tokenMetadata["F8Wh3zT1ydxPYfQ3p1oo9SCJbjedqDsaC1WaBwh64NHA"] = &orcaTokenMetadata{Name: "Serum Surfers Droplet", Symbol: "SSURF"}
   587  	tokenMetadata["Fm9rHUTF5v3hwMLbStjZXqNBBoZyGriQaFM6sTFz3K8A"] = &orcaTokenMetadata{Name: "MonkeyBucks", Symbol: "MBS"}
   588  	tokenMetadata["4h41QKUkQPd2pCAFXNNgZUyGUxQ6E7fMexaZZHziCvhh"] = &orcaTokenMetadata{Name: "The Suites Token", Symbol: "SUITE"}
   589  	tokenMetadata["7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx"] = &orcaTokenMetadata{Name: "GMT", Symbol: "GMT"}
   590  	tokenMetadata["ratioMVg27rSZbSvBopUvsdrGUzeALUfFma61mpxc8J"] = &orcaTokenMetadata{Name: "Ratio Governance Token", Symbol: "RATIO"}
   591  	tokenMetadata["3b9wtU4VP6qSUDL6NidwXxK6pMvYLFUTBR1QHWCtYKTS"] = &orcaTokenMetadata{Name: "Playground Epochs Solvent Droplet", Symbol: "svtEPOCH"}
   592  	tokenMetadata["FoRGERiW7odcCBGU1bztZi16osPBHjxharvDathL5eds"] = &orcaTokenMetadata{Name: "FORGE", Symbol: "FORGE"}
   593  	tokenMetadata["4wGimtLPQhbRT1cmKFJ7P7jDTgBqDnRBWsFXEhLoUep2"] = &orcaTokenMetadata{Name: "Lifinity Flares Solvent Droplet", Symbol: "svtFLARE"}
   594  	tokenMetadata["SNSNkV9zfG5ZKWQs6x4hxvBRV6s8SqMfSGCtECDvdMd"] = &orcaTokenMetadata{Name: "SynesisOne", Symbol: "SNS"}
   595  	tokenMetadata["9WMwGcY6TcbSfy9XPpQymY3qNEsvEaYL3wivdwPG2fpp"] = &orcaTokenMetadata{Name: "Jelly", Symbol: "JELLY"}
   596  	tokenMetadata["Ca5eaXbfQQ6gjZ5zPVfybtDpqWndNdACtKVtxxNHsgcz"] = &orcaTokenMetadata{Name: "Solana Monkey Business Solvent Droplet", Symbol: "svtSMB"}
   597  	tokenMetadata["5Wsd311hY8NXQhkt9cWHwTnqafk7BGEbLu8Py3DSnPAr"] = &orcaTokenMetadata{Name: "Compendium Finance", Symbol: "CMFI"}
   598  	tokenMetadata["GWsZd8k85q2ie9SNycVSLeKkX7HLZfSsgx6Jdat9cjY1"] = &orcaTokenMetadata{Name: "Pollen Coin", Symbol: "PCN"}
   599  
   600  	return tokenMetadata
   601  }
   602  
   603  // Format a uint64 to a float64 with the given number of decimals
   604  func FormatUint64Decimals(value uint64, decimals int) (valueFormatted float64) {
   605  	balance, _ := new(big.Float).Quo(big.NewFloat(0).SetUint64(value), big.NewFloat(math.Pow10(decimals))).Float64()
   606  	return balance
   607  }