initial commit
This commit is contained in:
commit
5410033aa9
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
|
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
9
.idea/pancakeswapnft.iml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
7
collection.go
Normal file
@ -0,0 +1,7 @@
|
||||
package pancakeswapnft
|
||||
|
||||
type Collection struct {
|
||||
Id string `json:"id"`
|
||||
FirstTokenId int
|
||||
TokenCount int
|
||||
}
|
118
floor.go
Normal file
118
floor.go
Normal 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
12
go.mod
Normal 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
36
go.sum
Normal 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
306
nft.go
Normal 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
202
squad.go
Normal 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
199
transaction.go
Normal 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
38
user.go
Normal 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
100
userservice.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user