203 lines
5.1 KiB
Go
203 lines
5.1 KiB
Go
|
package pancakeswapnft
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
"github.com/rs/zerolog/log"
|
||
|
)
|
||
|
|
||
|
type Squad struct {
|
||
|
TokenId int64 `json:"tokenId,string"`
|
||
|
Name string `json:"name"`
|
||
|
Description string `json:"description"`
|
||
|
Image struct {
|
||
|
Original string `json:"original"`
|
||
|
Thumbnail string `json:"thumbnail"`
|
||
|
Mp4 string `json:"mp4"`
|
||
|
Webm string `json:"webm"`
|
||
|
Gif string `json:"gif"`
|
||
|
} `json:"image"`
|
||
|
CreatedAt time.Time `json:"createdAt"`
|
||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||
|
Attributes []struct {
|
||
|
TraitType string `json:"traitType"`
|
||
|
Value string `json:"value"`
|
||
|
DisplayType string `json:"displayType"`
|
||
|
} `json:"attributes"`
|
||
|
Collection struct {
|
||
|
Name string `json:"name"`
|
||
|
} `json:"collection"`
|
||
|
RarityScore float64 `json:"rarity_score"`
|
||
|
Rank int `json:"rank"`
|
||
|
TraitCount int `json:"trait_count"`
|
||
|
}
|
||
|
|
||
|
type SquadService struct {
|
||
|
Collection Collection
|
||
|
squads map[int64]Squad
|
||
|
squadslock *sync.RWMutex
|
||
|
}
|
||
|
|
||
|
func NewSquadService(collection Collection) *SquadService {
|
||
|
return &SquadService{
|
||
|
Collection: collection,
|
||
|
squads: make(map[int64]Squad),
|
||
|
squadslock: &sync.RWMutex{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *SquadService) LoadCacheFromFile(filename string) error {
|
||
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||
|
err := s.LoadCacheFromWeb(s.Collection.FirstTokenId, s.Collection.TokenCount)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "loading cache from web")
|
||
|
}
|
||
|
err = s.SaveCacheToFile(filename)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "saving cache to file")
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
f, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm)
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "opening file %s", filename)
|
||
|
}
|
||
|
var res map[int64]Squad
|
||
|
err = json.NewDecoder(f).Decode(&res)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "decoding json")
|
||
|
}
|
||
|
s.squadslock.Lock()
|
||
|
s.squads = res
|
||
|
defer s.squadslock.Unlock()
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *SquadService) SaveCacheToFile(filepath string) error {
|
||
|
file, _ := os.OpenFile(filepath, os.O_CREATE, os.ModePerm)
|
||
|
defer file.Close()
|
||
|
s.squadslock.RLock()
|
||
|
defer s.squadslock.RUnlock()
|
||
|
err := json.NewEncoder(file).Encode(s.squads)
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "encoding to json & writing to file %s", filepath)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *SquadService) getSquadFromWeb(id int) (Squad, error) {
|
||
|
resp, err := http.Get(fmt.Sprintf("https://nft.pancakeswap.com/api/v1/collections/%s/tokens/%d", s.Collection.Id, id))
|
||
|
if err != nil {
|
||
|
return Squad{}, err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
var res struct {
|
||
|
Data Squad `json:"data"`
|
||
|
}
|
||
|
err = json.NewDecoder(resp.Body).Decode(&res)
|
||
|
if err != nil {
|
||
|
return Squad{}, err
|
||
|
}
|
||
|
return res.Data, nil
|
||
|
}
|
||
|
|
||
|
func (s *SquadService) getRarityData() (map[string]map[string]int, error) {
|
||
|
resp, err := http.Get(fmt.Sprintf("https://nft.pancakeswap.com/api/v1/collections/%s/distribution", s.Collection.Id))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
var res struct {
|
||
|
Data map[string]map[string]int `json:"data"`
|
||
|
}
|
||
|
err = json.NewDecoder(resp.Body).Decode(&res)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return res.Data, nil
|
||
|
}
|
||
|
|
||
|
func (s *SquadService) LoadCacheFromWeb(starting int, count int) error {
|
||
|
squadcount := 10000
|
||
|
var squads []*Squad
|
||
|
for i := starting; i <= count+starting-1; i++ {
|
||
|
log.Printf("getting %d", i)
|
||
|
s, err := s.getSquadFromWeb(i)
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "getting squad %d", i)
|
||
|
}
|
||
|
squads = append(squads, &s)
|
||
|
}
|
||
|
|
||
|
raritydata, err := s.getRarityData()
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "getting rarity data from api")
|
||
|
}
|
||
|
|
||
|
raritydata["traitcount"] = make(map[string]int)
|
||
|
for _, squad := range squads {
|
||
|
traitcount := 0
|
||
|
for _, attribute := range squad.Attributes {
|
||
|
if attribute.Value != "None" {
|
||
|
traitcount++
|
||
|
}
|
||
|
}
|
||
|
raritydata["traitcount"][strconv.Itoa(traitcount)]++
|
||
|
squad.TraitCount = traitcount
|
||
|
if traitcount == 0 {
|
||
|
squadcount--
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for _, squad := range squads {
|
||
|
rarityScore := float64(0)
|
||
|
for _, attribute := range squad.Attributes {
|
||
|
rarityScore += 1 / (float64(raritydata[attribute.TraitType][attribute.Value]) / float64(squadcount))
|
||
|
}
|
||
|
rarityScore += 1 / (float64(raritydata["traitcount"][strconv.Itoa(squad.TraitCount)]) / float64(squadcount))
|
||
|
squad.RarityScore = rarityScore
|
||
|
}
|
||
|
|
||
|
sort.Slice(squads, func(i, j int) bool {
|
||
|
return squads[i].RarityScore > squads[j].RarityScore
|
||
|
})
|
||
|
|
||
|
for i, squad := range squads {
|
||
|
squad.Rank = i + 1
|
||
|
}
|
||
|
s.squadslock.Lock()
|
||
|
defer s.squadslock.Unlock()
|
||
|
for _, squad := range squads {
|
||
|
s.squads[squad.TokenId] = *squad
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *SquadService) GetSquad(id int64) (Squad, error) {
|
||
|
s.squadslock.RLock()
|
||
|
defer s.squadslock.RUnlock()
|
||
|
if s, ok := s.squads[id]; !ok {
|
||
|
return Squad{}, errors.Errorf("squad %d not found", id)
|
||
|
} else {
|
||
|
return s, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *Squad) GetScoreByPrice(floorService *FloorService, price float64) float64 {
|
||
|
return 1 + (4 / ((price - (floorService.GetFloor() - 1.5)) * float64(s.Rank) / float64(10000)))
|
||
|
}
|
||
|
|
||
|
func (s *Squad) GetPriceByScore(floorService *FloorService, score float64) float64 {
|
||
|
return (floorService.GetFloor() - 1.5) + (4 / ((score - 1) * float64(s.Rank) / float64(10000)))
|
||
|
}
|