initial commit

This commit is contained in:
Laurent Le Houerou 2021-11-25 11:04:16 +04:00
commit 5410033aa9
14 changed files with 1059 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pancakeswapnft.iml" filepath="$PROJECT_DIR$/.idea/pancakeswapnft.iml" />
</modules>
</component>
</project>

9
.idea/pancakeswapnft.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

7
collection.go Normal file
View File

@ -0,0 +1,7 @@
package pancakeswapnft
type Collection struct {
Id string `json:"id"`
FirstTokenId int
TokenCount int
}

118
floor.go Normal file
View File

@ -0,0 +1,118 @@
package pancakeswapnft
import (
"context"
"sync"
"time"
"github.com/machinebox/graphql"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
type FloorService struct {
Done chan struct{}
collection string
graphClient *graphql.Client
floors map[time.Time]float64
lock *sync.RWMutex
cacheDuration time.Duration
}
func NewFloorService(ctx context.Context, collection string, graphClient *graphql.Client, cacheDuration time.Duration) *FloorService {
s := &FloorService{
Done: make(chan struct{}),
collection: collection,
graphClient: graphClient,
floors: make(map[time.Time]float64),
lock: &sync.RWMutex{},
cacheDuration: cacheDuration,
}
s.start(ctx)
return s
}
func (s *FloorService) start(ctx context.Context) {
t := time.NewTicker(1 * time.Minute)
err := s.watchfloor(ctx)
if err != nil {
log.Error().Err(err).Msg("watching floor")
}
go func() {
defer close(s.Done)
for {
select {
case <-ctx.Done():
return
case <-t.C:
err := s.watchfloor(ctx)
if err != nil {
log.Error().Err(err).Msg("watching floor")
}
}
}
}()
}
func (s *FloorService) watchfloor(ctx context.Context) 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) {
currentAskPrice
}
}`)
req.Var("first", 1)
req.Var("orderBy", "currentAskPrice")
req.Var("orderDirection", "asc")
req.Var("skip", 0)
req.Var("where", struct {
Collection string `json:"collection"`
IsTradable bool `json:"isTradable"`
}{
Collection: s.collection,
IsTradable: true,
})
var respData struct {
Nfts []Nft `json:"nfts"`
}
if err := s.graphClient.Run(ctx, req, &respData); err != nil {
return errors.Wrap(err, "querying graphql")
}
if len(respData.Nfts) <= 0 {
return errors.Errorf("no tradable nft found")
}
floor := respData.Nfts[0].CurrentAskPrice
s.lock.Lock()
newfloors := make(map[time.Time]float64)
for t, f := range s.floors {
if t.After(time.Now().Add(-s.cacheDuration)) {
newfloors[t] = f
}
}
newfloors[time.Now()] = floor
s.floors = newfloors
s.lock.Unlock()
return nil
}
func (s *FloorService) GetFloor() float64 {
s.lock.RLock()
defer s.lock.RUnlock()
var tot float64
for _, f := range s.floors {
tot += f
}
res := tot / float64(len(s.floors))
if res < 0.001 {
res = 3
}
return res
}

12
go.mod Normal file
View File

@ -0,0 +1,12 @@
module git.lehouerou.net/laurent/pancakeswapnft
go 1.17
require (
github.com/dustin/go-humanize v1.0.0
github.com/machinebox/graphql v0.2.2
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.26.0
)
require github.com/matryer/is v1.4.0 // indirect

36
go.sum Normal file
View File

@ -0,0 +1,36 @@
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo=
github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE=
github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

306
nft.go Normal file
View File

@ -0,0 +1,306 @@
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"`
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
nfts []Nft
floor float64
nftslock *sync.RWMutex
}
func NewMarketService(collection string, graphClient *graphql.Client, squadservice *SquadService, floorService *FloorService) *MarketService {
return &MarketService{
collection: collection,
graphClient: graphClient,
squadservice: squadservice,
floorService: floorService,
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 != "0x73d74055cc40ee8c62978a37a3b938f6e62739ed"
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 != "0x73d74055cc40ee8c62978a37a3b938f6e62739ed"
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
}

202
squad.go Normal file
View File

@ -0,0 +1,202 @@
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)))
}

199
transaction.go Normal file
View File

@ -0,0 +1,199 @@
package pancakeswapnft
import (
"context"
"fmt"
"github.com/dustin/go-humanize"
"sort"
"strconv"
"time"
"github.com/machinebox/graphql"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
type Transaction struct {
Id string `json:"id"`
Block string `json:"block"`
Timestamp int64 `json:"timestamp,string"`
Collection Collection `json:"collection"`
Nft Nft `json:"nft"`
AskPrice float64 `json:"askPrice,string"`
NetPrice float64 `json:"netPrice,string"`
Buyer User `json:"buyer"`
Seller User `json:"seller"`
WithBNB bool `json:"withBNB"`
Time time.Time
Squad Squad
Score float64
Url string
TimeDescription string
}
type TransactionService struct {
graphClient *graphql.Client
squadservice *SquadService
floorService *FloorService
}
func NewTransactionService(graphClient *graphql.Client, squadservice *SquadService, floorService *FloorService) *TransactionService {
return &TransactionService{
graphClient: graphClient,
squadservice: squadservice,
floorService: floorService,
}
}
type WhereOptions struct {
Collection string `json:"collection,omitempty"`
TimestampLte string `json:"timestamp_lte,omitempty"`
}
func (s *TransactionService) GetLightPageByLimit(ctx context.Context, pagenumber int, limit int, opts WhereOptions) ([]Transaction, error) {
req := graphql.NewRequest(`
query getNftsMarketData($first: Int, $skip: Int!, $where: Transaction_filter, $orderBy: Transaction_orderBy, $orderDirection: OrderDirection) {
transactions(where: $where, first: $first, orderBy: $orderBy, orderDirection: $orderDirection, skip: $skip) {
timestamp
askPrice
nft {
tokenId
}
buyer {
id
}
seller {
id
}
}
}`)
req.Var("first", limit)
req.Var("orderBy", "timestamp")
req.Var("orderDirection", "desc")
req.Var("skip", (pagenumber-1)*1000)
req.Var("where", opts)
var respData struct {
Transactions []*Transaction `json:"transactions"`
}
if err := s.graphClient.Run(ctx, req, &respData); err != nil {
return nil, errors.Wrap(err, "requesting graphql")
}
for _, transaction := range respData.Transactions {
squad, err := s.squadservice.GetSquad(transaction.Nft.TokenID)
if err != nil {
log.Error().Err(err).Msgf("getting squad for tokenid %d", transaction.Nft.TokenID)
continue
}
transaction.Squad = squad
transaction.Score = transaction.Squad.GetScoreByPrice(s.floorService, transaction.AskPrice)
transaction.Url = fmt.Sprintf("https://pancakeswap.finance/nfts/collections/%s/%d", opts.Collection, transaction.Nft.TokenID)
transaction.Time = time.Unix(transaction.Timestamp, 0)
transaction.TimeDescription = humanize.Time(transaction.Time)
}
var res []Transaction
for _, transaction := range respData.Transactions {
res = append(res, *transaction)
}
return res, nil
}
func (s *TransactionService) GetPageByLimit(ctx context.Context, pagenumber int, limit int, opts WhereOptions) ([]Transaction, error) {
req := graphql.NewRequest(`
query getNftsMarketData($first: Int, $skip: Int!, $where: Transaction_filter, $orderBy: Transaction_orderBy, $orderDirection: OrderDirection) {
transactions(where: $where, first: $first, orderBy: $orderBy, orderDirection: $orderDirection, skip: $skip) {
id
block
timestamp
askPrice
netPrice
nft {
tokenId
}
collection {
id
}
buyer {
id
}
seller {
id
}
withBNB
}
}`)
req.Var("first", limit)
req.Var("orderBy", "timestamp")
req.Var("orderDirection", "desc")
req.Var("skip", (pagenumber-1)*1000)
req.Var("where", opts)
var respData struct {
Transactions []*Transaction `json:"transactions"`
}
if err := s.graphClient.Run(ctx, req, &respData); err != nil {
return nil, errors.Wrap(err, "requesting graphql")
}
for _, transaction := range respData.Transactions {
s, err := s.squadservice.GetSquad(transaction.Nft.TokenID)
if err != nil {
log.Error().Err(err).Msgf("getting squad for tokenid %d", transaction.Nft.TokenID)
continue
}
transaction.Squad = s
transaction.Score = 10 / ((float64(s.Rank) / float64(10000)) * transaction.AskPrice)
transaction.Url = fmt.Sprintf("https://pancakeswap.finance/nfts/collections/%s/%d", opts.Collection, transaction.Nft.TokenID)
transaction.Time = time.Unix(transaction.Timestamp, 0)
transaction.TimeDescription = humanize.Time(transaction.Time)
}
var res []Transaction
for _, transaction := range respData.Transactions {
res = append(res, *transaction)
}
return res, nil
}
func (s *TransactionService) GetAll(ctx context.Context, collection string) ([]Transaction, error) {
var res []Transaction
currentpage := 1
whereoptions := WhereOptions{
Collection: collection,
TimestampLte: "",
}
now := time.Now()
tmpmap := make(map[string]Transaction)
loop:
for {
log.Debug().Msgf("timestamp : %s", whereoptions.TimestampLte)
page, err := s.GetPageByLimit(ctx, currentpage, 1000, whereoptions)
if err != nil {
return nil, errors.Wrapf(err, "getting page %d", currentpage)
}
if len(page) == 0 {
break
}
for _, transaction := range page {
if transaction.Time.Before(now.Add(-7 * 24 * time.Hour)) {
break loop
}
tmpmap[transaction.Id] = transaction
}
currentpage++
whereoptions.TimestampLte = strconv.Itoa(int(page[len(page)-1].Timestamp))
}
for _, transaction := range tmpmap {
res = append(res, transaction)
}
sort.Slice(res, func(i, j int) bool {
return res[i].Timestamp > res[j].Timestamp
})
return res, nil
}

38
user.go Normal file
View File

@ -0,0 +1,38 @@
package pancakeswapnft
import (
"time"
)
type User struct {
Address string `json:"id"`
NumberTokensListed int64 `json:"numberTokensListed,string"`
NumberTokensPurchased int64 `json:"numberTokensPurchased,string"`
NumberTokensSold int64 `json:"numberTokensSold,string"`
TotalVolumeInBNBTokensPurchased float64 `json:"totalVolumeInBNBTokensPurchased,string"`
TotalVolumeInBNBTokensSold float64 `json:"totalVolumeInBNBTokensSold,string"`
TotalFeesCollectedInBNB float64 `json:"totalFeesCollectedInBNB,string"`
BuyTradeHistory []Transaction `json:"buyTradeHistory"`
SellTradeHistory []Transaction `json:"sellTradeHistory"`
AskOrderHistory []AskOrder `json:"askOrderHistory"`
AverageTokenPriceInBNBPurchased float64 `json:"averageTokenPriceInBNBPurchased,string"`
AverageTokenPriceInBNBSold float64 `json:"averageTokenPriceInBNBSold,string"`
}
func (u *User) GetPnl() float64 {
var res float64
for _, transaction := range u.SellTradeHistory {
res += transaction.NetPrice
}
for _, buy := range u.BuyTradeHistory {
buyTime := time.Unix(buy.Timestamp, 0)
for _, sell := range u.SellTradeHistory {
sellTime := time.Unix(sell.Timestamp, 0)
if sell.Nft.TokenID == buy.Nft.TokenID && sellTime.After(buyTime) {
res -= buy.AskPrice
}
}
}
return res
}

100
userservice.go Normal file
View File

@ -0,0 +1,100 @@
package pancakeswapnft
import (
"context"
"github.com/machinebox/graphql"
"github.com/pkg/errors"
"strings"
)
const (
userQuery = `
query ($where: User_filter) {
users(where: $where) {
id
numberTokensListed
numberTokensPurchased
numberTokensSold
totalVolumeInBNBTokensPurchased
totalVolumeInBNBTokensSold
totalFeesCollectedInBNB
buyTradeHistory {
id
block
timestamp
askPrice
netPrice
buyer {
id
}
seller {
id
}
nft {
tokenId
}
withBNB
}
sellTradeHistory {
id
block
timestamp
askPrice
netPrice
buyer {
id
}
seller {
id
}
nft {
tokenId
}
withBNB
}
askOrderHistory {
id
block
askPrice
timestamp
seller {
id
}
orderType
}
averageTokenPriceInBNBPurchased
averageTokenPriceInBNBSold
}
}`
)
type UserService struct {
graphClient *graphql.Client
}
func NewUserService(graphclient *graphql.Client) *UserService {
return &UserService{
graphClient: graphclient,
}
}
func (s *UserService) GetUser(ctx context.Context, address string) (User, error) {
req := graphql.NewRequest(userQuery)
req.Var("where", struct {
Id string `json:"id"`
}{
Id: strings.ToLower(address),
})
var respData struct {
Users []User `json:"users"`
}
if err := s.graphClient.Run(ctx, req, &respData); err != nil {
return User{}, errors.Wrap(err, "requesting graphql")
}
if len(respData.Users) == 0 {
return User{}, errors.Errorf("no user found with id %s", address)
}
return respData.Users[0], nil
}