initial commit
This commit is contained in:
commit
5410033aa9
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
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
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
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
Normal file
8
.idea/modules.xml
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
Normal file
9
.idea/pancakeswapnft.iml
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
Normal file
6
.idea/vcs.xml
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