2021-11-25 07:04:16 +00:00
|
|
|
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 {
|
2021-12-22 06:38:57 +00:00
|
|
|
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
|
2021-11-25 07:04:16 +00:00
|
|
|
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 {
|
2021-12-22 06:38:57 +00:00
|
|
|
collection string
|
|
|
|
graphClient *graphql.Client
|
|
|
|
squadservice *SquadService
|
|
|
|
floorService *FloorService
|
|
|
|
profileService *ProfileService
|
2021-11-25 07:04:16 +00:00
|
|
|
|
2021-11-25 07:06:40 +00:00
|
|
|
bscaddress string
|
|
|
|
|
2021-11-25 07:04:16 +00:00
|
|
|
nfts []Nft
|
|
|
|
floor float64
|
|
|
|
nftslock *sync.RWMutex
|
|
|
|
}
|
|
|
|
|
2021-12-22 06:38:57 +00:00
|
|
|
func NewMarketService(collection string, graphClient *graphql.Client, squadservice *SquadService, floorService *FloorService, profileService *ProfileService, bscadress string) *MarketService {
|
2021-11-25 07:04:16 +00:00
|
|
|
return &MarketService{
|
2021-12-22 06:38:57 +00:00
|
|
|
collection: collection,
|
|
|
|
graphClient: graphClient,
|
|
|
|
squadservice: squadservice,
|
|
|
|
floorService: floorService,
|
|
|
|
profileService: profileService,
|
2021-11-25 07:04:16 +00:00
|
|
|
|
2021-11-25 07:06:40 +00:00
|
|
|
bscaddress: bscadress,
|
|
|
|
|
2021-11-25 07:04:16 +00:00
|
|
|
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))
|
2021-11-25 07:06:40 +00:00
|
|
|
res.IsNotMine = res.CurrentSeller != s.bscaddress
|
2021-12-22 06:56:26 +00:00
|
|
|
res.CurrentSellerUsername = s.profileService.GetUsername(res.CurrentSeller)
|
2021-12-22 06:38:57 +00:00
|
|
|
|
2021-11-25 07:04:16 +00:00
|
|
|
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
|
|
|
|
}
|
2021-11-25 07:06:40 +00:00
|
|
|
nft.IsNotMine = nft.CurrentSeller != s.bscaddress
|
2021-12-22 06:56:26 +00:00
|
|
|
nft.CurrentSellerUsername = s.profileService.GetUsername(nft.CurrentSeller)
|
2021-11-25 07:04:16 +00:00
|
|
|
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
|
|
|
|
}
|