package pancakeswapnft import ( "context" "fmt" "sort" "strconv" "sync" "time" "github.com/dustin/go-humanize" "github.com/machinebox/graphql" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) type AskOrder struct { Id string `json:"id"` Block string `json:"block"` AskPrice float64 `json:"askPrice,string"` Timestamp int64 `json:"timestamp,string"` Seller User `json:"seller"` Nft Nft `json:"nft"` Collection Collection `json:"collection"` OrderType string `json:"orderType"` } type Nft struct { Id string `json:"id"` TokenID int64 `json:"tokenId,string"` OtherId int64 `json:"otherId,string"` Collection Collection `json:"collection"` MetadataUrl string `json:"metadataUrl"` UpdatedAt int64 `json:"updatedAt,string"` CurrentAskPrice float64 `json:"currentAskPrice,string"` CurrentSeller string `json:"currentSeller"` CurrentSellerUsername string LatestTradedPriceInBNB float64 `json:"latestTradedPriceInBNB,string"` TradeVolumeBNB float64 `json:"tradeVolumeBNB,string"` TotalTrades int64 `json:"totalTrades,string"` TransactionHistory []Transaction `json:"transactionHistory"` AskHistory []AskOrder `json:"askHistory"` IsTradable bool `json:"IsTradable"` Squad Squad Score float64 Url string TransactionCount int UpdatedTimeString string IsRecent bool IsVeryRecent bool IsNotMine bool } type MarketService struct { collection string graphClient *graphql.Client squadservice *SquadService floorService *FloorService profileService *ProfileService bscaddress string nfts []Nft floor float64 nftslock *sync.RWMutex } func NewMarketService(collection string, graphClient *graphql.Client, squadservice *SquadService, floorService *FloorService, profileService *ProfileService, bscadress string) *MarketService { return &MarketService{ collection: collection, graphClient: graphClient, squadservice: squadservice, floorService: floorService, profileService: profileService, bscaddress: bscadress, nfts: nil, floor: 0, nftslock: &sync.RWMutex{}, } } func (s *MarketService) Start(ctx context.Context) chan struct{} { done := make(chan struct{}) t := time.NewTicker(5 * time.Second) go func() { defer close(done) err := s.updateMarket(ctx) if err != nil { log.Error().Err(err).Msg("updating market") } for { select { case <-ctx.Done(): return case <-t.C: err := s.updateMarket(ctx) if err != nil { log.Error().Err(err).Msg("updating market") } } } }() return done } func (s *MarketService) updateMarket(ctx context.Context) error { nfts, floor, err := s.getFirstNPages(ctx, 5) if err != nil { return errors.Wrap(err, "get first pages") } s.setNftCache(nfts, floor) return nil } func (s *MarketService) GetNftCache() ([]Nft, float64) { s.nftslock.RLock() defer s.nftslock.RUnlock() return s.nfts, s.floor } func (s *MarketService) setNftCache(nfts []Nft, floor float64) { s.nftslock.Lock() defer s.nftslock.Unlock() s.nfts = nfts s.floor = floor } func (s *MarketService) GetNft(ctx context.Context, collection string, tokenId int) (Nft, error) { req := graphql.NewRequest(` query ($where: NFT_filter) { nfts(where: $where) { id tokenId otherId collection { id } metadataUrl currentAskPrice currentSeller updatedAt latestTradedPriceInBNB tradeVolumeBNB totalTrades transactionHistory { id block timestamp askPrice netPrice buyer { id } seller { id } withBNB } askHistory { id block askPrice timestamp seller { id } orderType } } }`) req.Var("where", struct { Collection string `json:"collection"` TokenId string `json:"tokenId"` }{ Collection: collection, TokenId: strconv.Itoa(tokenId), }) var respData struct { Nfts []Nft `json:"nfts"` } if err := s.graphClient.Run(ctx, req, &respData); err != nil { return Nft{}, errors.Wrap(err, "querying graphql") } if len(respData.Nfts) <= 0 { return Nft{}, errors.Errorf("no nft found for id %d", tokenId) } res := respData.Nfts[0] sq, err := s.squadservice.GetSquad(res.TokenID) if err != nil { return Nft{}, errors.Wrap(err, "getting squad") } res.Squad = sq res.Score = 10 / ((float64(sq.Rank) / float64(10000)) * res.CurrentAskPrice) res.Url = fmt.Sprintf("https://pancakeswap.finance/nfts/collections/%s/%d", collection, res.TokenID) res.TransactionCount = len(res.TransactionHistory) sort.Slice(res.TransactionHistory, func(i, j int) bool { return res.TransactionHistory[i].Timestamp > res.TransactionHistory[j].Timestamp }) res.UpdatedTimeString = humanize.Time(time.Unix(res.UpdatedAt, 0)) res.IsRecent = time.Unix(res.UpdatedAt, 0).After(time.Now().Add(-1 * time.Hour)) res.IsVeryRecent = time.Unix(res.UpdatedAt, 0).After(time.Now().Add(-5 * time.Minute)) res.IsNotMine = res.CurrentSeller != s.bscaddress res.CurrentSellerUsername = s.profileService.GetUsername(res.CurrentSeller) return res, nil } func (s *MarketService) getPage(ctx context.Context, collection string, pagenum int) ([]Nft, float64, error) { req := graphql.NewRequest(` query ($first: Int, $skip: Int!, $where: NFT_filter, $orderBy: NFT_orderBy, $orderDirection: OrderDirection) { nfts(where: $where, first: $first, orderBy: $orderBy, orderDirection: $orderDirection, skip: $skip) { tokenId currentAskPrice currentSeller updatedAt transactionHistory { timestamp askPrice } } }`) req.Var("first", 1000) req.Var("orderBy", "currentAskPrice") req.Var("orderDirection", "asc") req.Var("skip", (pagenum-1)*1000) req.Var("where", struct { Collection string `json:"collection"` IsTradable bool `json:"isTradable"` }{ Collection: collection, IsTradable: true, }) var respData struct { Nfts []Nft `json:"nfts"` } if err := s.graphClient.Run(ctx, req, &respData); err != nil { return nil, 0, errors.Wrap(err, "querying graphql") } var res []Nft floor := float64(0) for _, nft := range respData.Nfts { squad, err := s.squadservice.GetSquad(nft.TokenID) if err != nil { log.Error().Err(err).Msgf("getting squad for tokenid %d", nft.TokenID) continue } nft.Squad = squad nft.Score = nft.Squad.GetScoreByPrice(s.floorService, nft.CurrentAskPrice) nft.Url = fmt.Sprintf("https://pancakeswap.finance/nfts/collections/%s/%d", collection, nft.TokenID) nft.TransactionCount = len(nft.TransactionHistory) sort.Slice(nft.TransactionHistory, func(i, j int) bool { return nft.TransactionHistory[i].Timestamp > nft.TransactionHistory[j].Timestamp }) nft.UpdatedTimeString = humanize.Time(time.Unix(nft.UpdatedAt, 0)) nft.IsRecent = time.Unix(nft.UpdatedAt, 0).After(time.Now().Add(-1 * time.Hour)) nft.IsVeryRecent = time.Unix(nft.UpdatedAt, 0).After(time.Now().Add(-5 * time.Minute)) if floor == 0 || nft.CurrentAskPrice < floor { floor = nft.CurrentAskPrice } nft.IsNotMine = nft.CurrentSeller != s.bscaddress nft.CurrentSellerUsername = s.profileService.GetUsername(nft.CurrentSeller) res = append(res, nft) } return res, floor, nil } func (s *MarketService) getFirstNPages(ctx context.Context, n int) ([]Nft, float64, error) { var res []Nft var floor float64 for i := 1; i <= n; i++ { page, tmpfloor, err := s.getPage(ctx, s.collection, i) if err != nil { return nil, 0, errors.Wrapf(err, "getting page %d", i) } res = append(res, page...) if floor == 0 || tmpfloor < floor { floor = tmpfloor } } return res, floor, nil } func (s *MarketService) getAll(ctx context.Context) ([]Nft, float64, error) { var res []Nft var floor float64 currentpage := 1 for { page, tmpfloor, err := s.getPage(ctx, s.collection, currentpage) if err != nil { return nil, 0, errors.Wrapf(err, "getting page %d", currentpage) } if len(page) == 0 { break } res = append(res, page...) if floor == 0 || tmpfloor < floor { floor = tmpfloor } currentpage++ } return res, floor, nil }