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 }