initial commit
This commit is contained in:
commit
b99d855805
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.vscode/
|
132
api.go
Normal file
132
api.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package sorare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/config"
|
||||||
|
"git.lehouerou.net/laurent/sorare/football"
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/mutations"
|
||||||
|
"git.lehouerou.net/laurent/sorare/subscriptions"
|
||||||
|
"git.lehouerou.net/laurent/sorare/tokens"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseUrl = "https://api.sorare.com/federation/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Sorare struct {
|
||||||
|
httpClient *retryablehttp.Client
|
||||||
|
|
||||||
|
Client *graphql.Client
|
||||||
|
|
||||||
|
Config *config.Config
|
||||||
|
Football *football.Football
|
||||||
|
Users *Users
|
||||||
|
Country *graphql.Query[Country, graphql.SlugParams]
|
||||||
|
Countries *graphql.Query[[]Country, graphql.SlugsParams]
|
||||||
|
Tokens *tokens.Tokens
|
||||||
|
ShopItems *graphql.PaginatedQuery[ShopItem, ShopItemsParams]
|
||||||
|
NbaPlayer *graphql.Query[NbaPlayer, graphql.SlugParams]
|
||||||
|
NbaPlayers *graphql.Query[[]NbaPlayer, graphql.SlugsParams]
|
||||||
|
|
||||||
|
Mutations *mutations.Mutations
|
||||||
|
Subscriptions *subscriptions.Subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Sorare {
|
||||||
|
httpClient := retryablehttp.NewClient()
|
||||||
|
httpClient.Logger = nil
|
||||||
|
c := graphql.NewClient(httpClient.StandardClient(), baseUrl)
|
||||||
|
return &Sorare{
|
||||||
|
httpClient: httpClient,
|
||||||
|
Client: c,
|
||||||
|
Config: config.NewConfig(c),
|
||||||
|
Football: football.NewFootball(c),
|
||||||
|
Users: NewUsers(c),
|
||||||
|
Country: graphql.NewQuery[Country, graphql.SlugParams](c, "country", nil),
|
||||||
|
Countries: graphql.NewQuery[[]Country, graphql.SlugsParams](c, "countries", nil),
|
||||||
|
Tokens: tokens.NewTokens(c),
|
||||||
|
ShopItems: graphql.NewPaginatedQuery[ShopItem, ShopItemsParams](c, "shopItems", nil),
|
||||||
|
NbaPlayer: graphql.NewQuery[NbaPlayer, graphql.SlugParams](c, "nbaPlayer", nil),
|
||||||
|
NbaPlayers: graphql.NewQuery[[]NbaPlayer, graphql.SlugsParams](c, "nbaPlayers", nil),
|
||||||
|
|
||||||
|
Mutations: mutations.NewMutations(c),
|
||||||
|
Subscriptions: subscriptions.NewSubscriptions(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sorare) Debug() *Sorare {
|
||||||
|
s.httpClient.RequestLogHook = func(l retryablehttp.Logger, req *http.Request, attempt int) {
|
||||||
|
if req.Body != nil {
|
||||||
|
b, err := io.ReadAll(req.Body)
|
||||||
|
if err == nil {
|
||||||
|
req.Body = io.NopCloser(bytes.NewBuffer(b))
|
||||||
|
}
|
||||||
|
log.Printf("Request: %s %s\n%s\n", req.Method, req.URL, string(b))
|
||||||
|
} else {
|
||||||
|
log.Printf("Request: %s %s\n", req.Method, req.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.httpClient.ResponseLogHook = func(l retryablehttp.Logger, resp *http.Response) {
|
||||||
|
if resp.Body != nil {
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
if err == nil {
|
||||||
|
resp.Body = io.NopCloser(bytes.NewBuffer(b))
|
||||||
|
}
|
||||||
|
var jsonbody bytes.Buffer
|
||||||
|
err = json.Indent(&jsonbody, b, "", " ")
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("Response: %s\n%s\n", resp.Status, jsonbody.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Response: %s\n%s\n", resp.Status, string(b))
|
||||||
|
} else {
|
||||||
|
log.Printf("Response: %s\n", resp.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Subscriptions.SetDebug(true)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sorare) SetJWTToken(token graphql.JwtToken, audience string) {
|
||||||
|
s.Client.SetJWTToken(token, audience)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sorare) GetCurrentToken() graphql.JwtToken {
|
||||||
|
return s.Client.GetCurrentToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sorare) Authenticate(
|
||||||
|
ctx context.Context,
|
||||||
|
email string,
|
||||||
|
password string,
|
||||||
|
audience string,
|
||||||
|
otp string,
|
||||||
|
) error {
|
||||||
|
return s.Client.Authenticate(ctx, email, password, audience, otp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sorare) Query(
|
||||||
|
ctx context.Context,
|
||||||
|
query interface{},
|
||||||
|
variables map[string]interface{},
|
||||||
|
) error {
|
||||||
|
return s.Client.Query(ctx, query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sorare) GetRawQuery(query interface{}, variables map[string]interface{}) (string, error) {
|
||||||
|
return s.Client.ConstructRawQuery(query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sorare) SetDefaultSettlementCurrencies(currencies []types.SupportedCurrency) {
|
||||||
|
s.Mutations.SetDefaultSettlementCurrencies(currencies)
|
||||||
|
}
|
35
bcrypt/base64.go
Normal file
35
bcrypt/base64.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package bcrypt
|
||||||
|
|
||||||
|
import "encoding/base64"
|
||||||
|
|
||||||
|
const alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
||||||
|
var bcEncoding = base64.NewEncoding(alphabet)
|
||||||
|
|
||||||
|
func base64Encode(src []byte) []byte {
|
||||||
|
n := bcEncoding.EncodedLen(len(src))
|
||||||
|
dst := make([]byte, n)
|
||||||
|
bcEncoding.Encode(dst, src)
|
||||||
|
for dst[n-1] == '=' {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
return dst[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func base64Decode(src []byte) ([]byte, error) {
|
||||||
|
numOfEquals := 4 - (len(src) % 4)
|
||||||
|
for i := 0; i < numOfEquals; i++ {
|
||||||
|
src = append(src, '=')
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := make([]byte, bcEncoding.DecodedLen(len(src)))
|
||||||
|
n, err := bcEncoding.Decode(dst, src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dst[:n], nil
|
||||||
|
}
|
325
bcrypt/bcrypt.go
Normal file
325
bcrypt/bcrypt.go
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package bcrypt implements Provos and Mazières's bcrypt adaptive hashing
|
||||||
|
// algorithm. See http://www.usenix.org/event/usenix99/provos/provos.pdf
|
||||||
|
package bcrypt // import "golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
// The code is a port of Provos and Mazières's C implementation.
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/blowfish"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MinCost int = 4 // the minimum allowable cost as passed in to GenerateFromPassword
|
||||||
|
MaxCost int = 31 // the maximum allowable cost as passed in to GenerateFromPassword
|
||||||
|
DefaultCost int = 10 // the cost that will actually be set if a cost below MinCost is passed into GenerateFromPassword
|
||||||
|
)
|
||||||
|
|
||||||
|
// The error returned from CompareHashAndPassword when a password and hash do
|
||||||
|
// not match.
|
||||||
|
var ErrMismatchedHashAndPassword = errors.New(
|
||||||
|
"crypto/bcrypt: hashedPassword is not the hash of the given password",
|
||||||
|
)
|
||||||
|
|
||||||
|
// The error returned from CompareHashAndPassword when a hash is too short to
|
||||||
|
// be a bcrypt hash.
|
||||||
|
var ErrHashTooShort = errors.New("crypto/bcrypt: hashedSecret too short to be a bcrypted password")
|
||||||
|
|
||||||
|
// The error returned from CompareHashAndPassword when a hash was created with
|
||||||
|
// a bcrypt algorithm newer than this implementation.
|
||||||
|
type HashVersionTooNewError byte
|
||||||
|
|
||||||
|
func (hv HashVersionTooNewError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"crypto/bcrypt: bcrypt algorithm version '%c' requested is newer than current version '%c'",
|
||||||
|
byte(hv),
|
||||||
|
majorVersion,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The error returned from CompareHashAndPassword when a hash starts with something other than '$'
|
||||||
|
type InvalidHashPrefixError byte
|
||||||
|
|
||||||
|
func (ih InvalidHashPrefixError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"crypto/bcrypt: bcrypt hashes must start with '$', but hashedSecret started with '%c'",
|
||||||
|
byte(ih),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvalidCostError int
|
||||||
|
|
||||||
|
func (ic InvalidCostError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"crypto/bcrypt: cost %d is outside allowed range (%d,%d)",
|
||||||
|
int(ic),
|
||||||
|
MinCost,
|
||||||
|
MaxCost,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
majorVersion = '2'
|
||||||
|
minorVersion = 'a'
|
||||||
|
maxSaltSize = 16
|
||||||
|
maxCryptedHashSize = 23
|
||||||
|
encodedSaltSize = 22
|
||||||
|
encodedHashSize = 31
|
||||||
|
minHashSize = 59
|
||||||
|
)
|
||||||
|
|
||||||
|
// magicCipherData is an IV for the 64 Blowfish encryption calls in
|
||||||
|
// bcrypt(). It's the string "OrpheanBeholderScryDoubt" in big-endian bytes.
|
||||||
|
var magicCipherData = []byte{
|
||||||
|
0x4f, 0x72, 0x70, 0x68,
|
||||||
|
0x65, 0x61, 0x6e, 0x42,
|
||||||
|
0x65, 0x68, 0x6f, 0x6c,
|
||||||
|
0x64, 0x65, 0x72, 0x53,
|
||||||
|
0x63, 0x72, 0x79, 0x44,
|
||||||
|
0x6f, 0x75, 0x62, 0x74,
|
||||||
|
}
|
||||||
|
|
||||||
|
type hashed struct {
|
||||||
|
hash []byte
|
||||||
|
salt []byte
|
||||||
|
cost int // allowed range is MinCost to MaxCost
|
||||||
|
major byte
|
||||||
|
minor byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrPasswordTooLong is returned when the password passed to
|
||||||
|
// GenerateFromPassword is too long (i.e. > 72 bytes).
|
||||||
|
var ErrPasswordTooLong = errors.New("bcrypt: password length exceeds 72 bytes")
|
||||||
|
|
||||||
|
// GenerateFromPassword returns the bcrypt hash of the password at the given
|
||||||
|
// cost. If the cost given is less than MinCost, the cost will be set to
|
||||||
|
// DefaultCost, instead. Use CompareHashAndPassword, as defined in this package,
|
||||||
|
// to compare the returned hashed password with its cleartext version.
|
||||||
|
// GenerateFromPassword does not accept passwords longer than 72 bytes, which
|
||||||
|
// is the longest password bcrypt will operate on.
|
||||||
|
func GenerateFromPassword(password []byte, cost int) ([]byte, error) {
|
||||||
|
if len(password) > 72 {
|
||||||
|
return nil, ErrPasswordTooLong
|
||||||
|
}
|
||||||
|
p, err := newFromPassword(password, cost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.Hash(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareHashAndPassword compares a bcrypt hashed password with its possible
|
||||||
|
// plaintext equivalent. Returns nil on success, or an error on failure.
|
||||||
|
func CompareHashAndPassword(hashedPassword, password []byte) error {
|
||||||
|
p, err := newFromHash(hashedPassword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
otherHash, err := bcrypt(password, p.cost, p.salt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
otherP := &hashed{otherHash, p.salt, p.cost, p.major, p.minor}
|
||||||
|
if subtle.ConstantTimeCompare(p.Hash(), otherP.Hash()) == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrMismatchedHashAndPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cost returns the hashing cost used to create the given hashed
|
||||||
|
// password. When, in the future, the hashing cost of a password system needs
|
||||||
|
// to be increased in order to adjust for greater computational power, this
|
||||||
|
// function allows one to establish which passwords need to be updated.
|
||||||
|
func Cost(hashedPassword []byte) (int, error) {
|
||||||
|
p, err := newFromHash(hashedPassword)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return p.cost, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFromPassword(password []byte, cost int) (*hashed, error) {
|
||||||
|
if cost < MinCost {
|
||||||
|
cost = DefaultCost
|
||||||
|
}
|
||||||
|
p := new(hashed)
|
||||||
|
p.major = majorVersion
|
||||||
|
p.minor = minorVersion
|
||||||
|
|
||||||
|
err := checkCost(cost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.cost = cost
|
||||||
|
|
||||||
|
unencodedSalt := make([]byte, maxSaltSize)
|
||||||
|
_, err = io.ReadFull(rand.Reader, unencodedSalt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.salt = base64Encode(unencodedSalt)
|
||||||
|
hash, err := bcrypt(password, p.cost, p.salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.hash = hash
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFromHash(hashedSecret []byte) (*hashed, error) {
|
||||||
|
if len(hashedSecret) < minHashSize {
|
||||||
|
return nil, ErrHashTooShort
|
||||||
|
}
|
||||||
|
p := new(hashed)
|
||||||
|
n, err := p.decodeVersion(hashedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hashedSecret = hashedSecret[n:]
|
||||||
|
n, err = p.decodeCost(hashedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hashedSecret = hashedSecret[n:]
|
||||||
|
|
||||||
|
// The "+2" is here because we'll have to append at most 2 '=' to the salt
|
||||||
|
// when base64 decoding it in expensiveBlowfishSetup().
|
||||||
|
p.salt = make([]byte, encodedSaltSize, encodedSaltSize+2)
|
||||||
|
copy(p.salt, hashedSecret[:encodedSaltSize])
|
||||||
|
|
||||||
|
hashedSecret = hashedSecret[encodedSaltSize:]
|
||||||
|
p.hash = make([]byte, len(hashedSecret))
|
||||||
|
copy(p.hash, hashedSecret)
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bcrypt(password []byte, cost int, salt []byte) ([]byte, error) {
|
||||||
|
cipherData := make([]byte, len(magicCipherData))
|
||||||
|
copy(cipherData, magicCipherData)
|
||||||
|
|
||||||
|
c, err := expensiveBlowfishSetup(password, uint32(cost), salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 24; i += 8 {
|
||||||
|
for j := 0; j < 64; j++ {
|
||||||
|
c.Encrypt(cipherData[i:i+8], cipherData[i:i+8])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bug compatibility with C bcrypt implementations. We only encode 23 of
|
||||||
|
// the 24 bytes encrypted.
|
||||||
|
hsh := base64Encode(cipherData[:maxCryptedHashSize])
|
||||||
|
return hsh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expensiveBlowfishSetup(key []byte, cost uint32, salt []byte) (*blowfish.Cipher, error) {
|
||||||
|
csalt, err := base64Decode(salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bug compatibility with C bcrypt implementations. They use the trailing
|
||||||
|
// NULL in the key string during expansion.
|
||||||
|
// We copy the key to prevent changing the underlying array.
|
||||||
|
ckey := append(key[:len(key):len(key)], 0)
|
||||||
|
|
||||||
|
c, err := blowfish.NewSaltedCipher(ckey, csalt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var i, rounds uint64
|
||||||
|
rounds = 1 << cost
|
||||||
|
for i = 0; i < rounds; i++ {
|
||||||
|
blowfish.ExpandKey(ckey, c)
|
||||||
|
blowfish.ExpandKey(csalt, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *hashed) Hash() []byte {
|
||||||
|
arr := make([]byte, 60)
|
||||||
|
arr[0] = '$'
|
||||||
|
arr[1] = p.major
|
||||||
|
n := 2
|
||||||
|
if p.minor != 0 {
|
||||||
|
arr[2] = p.minor
|
||||||
|
n = 3
|
||||||
|
}
|
||||||
|
arr[n] = '$'
|
||||||
|
n++
|
||||||
|
copy(arr[n:], []byte(fmt.Sprintf("%02d", p.cost)))
|
||||||
|
n += 2
|
||||||
|
arr[n] = '$'
|
||||||
|
n++
|
||||||
|
copy(arr[n:], p.salt)
|
||||||
|
n += encodedSaltSize
|
||||||
|
copy(arr[n:], p.hash)
|
||||||
|
n += encodedHashSize
|
||||||
|
return arr[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *hashed) decodeVersion(sbytes []byte) (int, error) {
|
||||||
|
if sbytes[0] != '$' {
|
||||||
|
return -1, InvalidHashPrefixError(sbytes[0])
|
||||||
|
}
|
||||||
|
if sbytes[1] > majorVersion {
|
||||||
|
return -1, HashVersionTooNewError(sbytes[1])
|
||||||
|
}
|
||||||
|
p.major = sbytes[1]
|
||||||
|
n := 3
|
||||||
|
if sbytes[2] != '$' {
|
||||||
|
p.minor = sbytes[2]
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sbytes should begin where decodeVersion left off.
|
||||||
|
func (p *hashed) decodeCost(sbytes []byte) (int, error) {
|
||||||
|
cost, err := strconv.Atoi(string(sbytes[0:2]))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
err = checkCost(cost)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
p.cost = cost
|
||||||
|
return 3, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *hashed) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"&{hash: %#v, salt: %#v, cost: %d, major: %c, minor: %c}",
|
||||||
|
string(p.hash),
|
||||||
|
p.salt,
|
||||||
|
p.cost,
|
||||||
|
p.major,
|
||||||
|
p.minor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCost(cost int) error {
|
||||||
|
if cost < MinCost || cost > MaxCost {
|
||||||
|
return InvalidCostError(cost)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
265
bcrypt/bcrypt_test.go
Normal file
265
bcrypt/bcrypt_test.go
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package bcrypt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBcryptingIsEasy(t *testing.T) {
|
||||||
|
pass := []byte("mypassword")
|
||||||
|
hp, err := GenerateFromPassword(pass, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateFromPassword error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if CompareHashAndPassword(hp, pass) != nil {
|
||||||
|
t.Errorf("%v should hash %s correctly", hp, pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
notPass := "notthepass"
|
||||||
|
err = CompareHashAndPassword(hp, []byte(notPass))
|
||||||
|
if err != ErrMismatchedHashAndPassword {
|
||||||
|
t.Errorf("%v and %s should be mismatched", hp, notPass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBcryptingIsCorrect(t *testing.T) {
|
||||||
|
pass := []byte("allmine")
|
||||||
|
salt := []byte("XajjQvNhvvRt5GSeFk1xFe")
|
||||||
|
expectedHash := []byte("$2a$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga")
|
||||||
|
|
||||||
|
hash, err := bcrypt(pass, 10, salt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bcrypt blew up: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.HasSuffix(expectedHash, hash) {
|
||||||
|
t.Errorf("%v should be the suffix of %v", hash, expectedHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err := newFromHash(expectedHash)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to parse %s: %v", string(expectedHash), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is not the safe way to compare these hashes. We do this only for
|
||||||
|
// testing clarity. Use bcrypt.CompareHashAndPassword()
|
||||||
|
if err == nil && !bytes.Equal(expectedHash, h.Hash()) {
|
||||||
|
t.Errorf("Parsed hash %v should equal %v", h.Hash(), expectedHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVeryShortPasswords(t *testing.T) {
|
||||||
|
key := []byte("k")
|
||||||
|
salt := []byte("XajjQvNhvvRt5GSeFk1xFe")
|
||||||
|
_, err := bcrypt(key, 10, salt)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("One byte key resulted in error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTooLongPasswordsWork(t *testing.T) {
|
||||||
|
salt := []byte("XajjQvNhvvRt5GSeFk1xFe")
|
||||||
|
// One byte over the usual 56 byte limit that blowfish has
|
||||||
|
tooLongPass := []byte("012345678901234567890123456789012345678901234567890123456")
|
||||||
|
tooLongExpected := []byte("$2a$10$XajjQvNhvvRt5GSeFk1xFe5l47dONXg781AmZtd869sO8zfsHuw7C")
|
||||||
|
hash, err := bcrypt(tooLongPass, 10, salt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bcrypt blew up on long password: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.HasSuffix(tooLongExpected, hash) {
|
||||||
|
t.Errorf("%v should be the suffix of %v", hash, tooLongExpected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvalidHashTest struct {
|
||||||
|
err error
|
||||||
|
hash []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidTests = []InvalidHashTest{
|
||||||
|
{ErrHashTooShort, []byte("$2a$10$fooo")},
|
||||||
|
{ErrHashTooShort, []byte("$2a")},
|
||||||
|
{
|
||||||
|
HashVersionTooNewError('3'),
|
||||||
|
[]byte("$3a$10$sssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvalidHashPrefixError('%'),
|
||||||
|
[]byte("%2a$10$sssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"),
|
||||||
|
},
|
||||||
|
{InvalidCostError(32), []byte("$2a$32$sssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh")},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidHashErrors(t *testing.T) {
|
||||||
|
check := func(name string, expected, err error) {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("%s: Should have returned an error", name)
|
||||||
|
}
|
||||||
|
if err != nil && err != expected {
|
||||||
|
t.Errorf("%s gave err %v but should have given %v", name, err, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, iht := range invalidTests {
|
||||||
|
_, err := newFromHash(iht.hash)
|
||||||
|
check("newFromHash", iht.err, err)
|
||||||
|
err = CompareHashAndPassword(iht.hash, []byte("anything"))
|
||||||
|
check("CompareHashAndPassword", iht.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnpaddedBase64Encoding(t *testing.T) {
|
||||||
|
original := []byte{101, 201, 101, 75, 19, 227, 199, 20, 239, 236, 133, 32, 30, 109, 243, 30}
|
||||||
|
encodedOriginal := []byte("XajjQvNhvvRt5GSeFk1xFe")
|
||||||
|
|
||||||
|
encoded := base64Encode(original)
|
||||||
|
|
||||||
|
if !bytes.Equal(encodedOriginal, encoded) {
|
||||||
|
t.Errorf("Encoded %v should have equaled %v", encoded, encodedOriginal)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := base64Decode(encodedOriginal)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("base64Decode blew up: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(decoded, original) {
|
||||||
|
t.Errorf("Decoded %v should have equaled %v", decoded, original)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCost(t *testing.T) {
|
||||||
|
suffix := "XajjQvNhvvRt5GSeFk1xFe5l47dONXg781AmZtd869sO8zfsHuw7C"
|
||||||
|
for _, vers := range []string{"2a", "2"} {
|
||||||
|
for _, cost := range []int{4, 10} {
|
||||||
|
s := fmt.Sprintf("$%s$%02d$%s", vers, cost, suffix)
|
||||||
|
h := []byte(s)
|
||||||
|
actual, err := Cost(h)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cost, error: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if actual != cost {
|
||||||
|
t.Errorf("Cost, expected: %d, actual: %d", cost, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := Cost([]byte("$a$a$" + suffix))
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Cost, malformed but no error returned")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCostValidationInHash(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pass := []byte("mypassword")
|
||||||
|
|
||||||
|
for c := 0; c < MinCost; c++ {
|
||||||
|
p, _ := newFromPassword(pass, c)
|
||||||
|
if p.cost != DefaultCost {
|
||||||
|
t.Errorf(
|
||||||
|
"newFromPassword should default costs below %d to %d, but was %d",
|
||||||
|
MinCost,
|
||||||
|
DefaultCost,
|
||||||
|
p.cost,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p, _ := newFromPassword(pass, 14)
|
||||||
|
if p.cost != 14 {
|
||||||
|
t.Errorf("newFromPassword should default cost to 14, but was %d", p.cost)
|
||||||
|
}
|
||||||
|
|
||||||
|
hp, _ := newFromHash(p.Hash())
|
||||||
|
if p.cost != hp.cost {
|
||||||
|
t.Errorf("newFromHash should maintain the cost at %d, but was %d", p.cost, hp.cost)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := newFromPassword(pass, 32)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("newFromPassword: should return a cost error")
|
||||||
|
}
|
||||||
|
if err != InvalidCostError(32) {
|
||||||
|
t.Errorf("newFromPassword: should return cost error, got %#v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCostReturnsWithLeadingZeroes(t *testing.T) {
|
||||||
|
hp, _ := newFromPassword([]byte("abcdefgh"), 7)
|
||||||
|
cost := hp.Hash()[4:7]
|
||||||
|
expected := []byte("07$")
|
||||||
|
|
||||||
|
if !bytes.Equal(expected, cost) {
|
||||||
|
t.Errorf(
|
||||||
|
"single digit costs in hash should have leading zeros: was %v instead of %v",
|
||||||
|
cost,
|
||||||
|
expected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMinorNotRequired(t *testing.T) {
|
||||||
|
noMinorHash := []byte("$2$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga")
|
||||||
|
h, err := newFromHash(noMinorHash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("No minor hash blew up: %s", err)
|
||||||
|
}
|
||||||
|
if h.minor != 0 {
|
||||||
|
t.Errorf("Should leave minor version at 0, but was %d", h.minor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(noMinorHash, h.Hash()) {
|
||||||
|
t.Errorf("Should generate hash %v, but created %v", noMinorHash, h.Hash())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEqual(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
passwd := []byte("somepasswordyoulike")
|
||||||
|
hash, _ := GenerateFromPassword(passwd, DefaultCost)
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
CompareHashAndPassword(hash, passwd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDefaultCost(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
passwd := []byte("mylongpassword1234")
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
GenerateFromPassword(passwd, DefaultCost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// See Issue https://github.com/golang/go/issues/20425.
|
||||||
|
func TestNoSideEffectsFromCompare(t *testing.T) {
|
||||||
|
source := []byte("passw0rd123456")
|
||||||
|
password := source[:len(source)-6]
|
||||||
|
token := source[len(source)-6:]
|
||||||
|
want := make([]byte, len(source))
|
||||||
|
copy(want, source)
|
||||||
|
|
||||||
|
wantHash := []byte("$2a$10$LK9XRuhNxHHCvjX3tdkRKei1QiCDUKrJRhZv7WWZPuQGRUM92rOUa")
|
||||||
|
_ = CompareHashAndPassword(wantHash, password)
|
||||||
|
|
||||||
|
got := bytes.Join([][]byte{password, token}, []byte(""))
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Errorf("got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPasswordTooLong(t *testing.T) {
|
||||||
|
_, err := GenerateFromPassword(make([]byte, 73), 1)
|
||||||
|
if err != ErrPasswordTooLong {
|
||||||
|
t.Errorf("unexpected error: got %q, want %q", err, ErrPasswordTooLong)
|
||||||
|
}
|
||||||
|
}
|
36
bcrypt/patch.go
Normal file
36
bcrypt/patch.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package bcrypt
|
||||||
|
|
||||||
|
func newFromPasswordAndSalt(password, unencodedSalt []byte, cost int) (*hashed, error) {
|
||||||
|
if cost < MinCost {
|
||||||
|
cost = DefaultCost
|
||||||
|
}
|
||||||
|
p := new(hashed)
|
||||||
|
p.major = majorVersion
|
||||||
|
p.minor = minorVersion
|
||||||
|
|
||||||
|
err := checkCost(cost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.cost = cost
|
||||||
|
|
||||||
|
p.salt = unencodedSalt
|
||||||
|
hash, err := bcrypt(password, p.cost, p.salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.hash = hash
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This function is necessary because mbcrypt needs to control the salt (like you can with other KDFs).
|
||||||
|
All other bcrypt source remains unmodified from the original x/crypto/bcrypt.
|
||||||
|
*/
|
||||||
|
func GenerateFromPasswordAndSalt(password, rawSalt []byte, cost int) ([]byte, error) {
|
||||||
|
p, err := newFromPasswordAndSalt(password, rawSalt, cost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.Hash(), nil
|
||||||
|
}
|
40
cmd/console/main.go
Normal file
40
cmd/console/main.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare"
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
token := os.Getenv("SORARE_TOKEN")
|
||||||
|
audience := os.Getenv("SORARE_AUDIENCE")
|
||||||
|
if token == "" {
|
||||||
|
log.Fatal("No token provided")
|
||||||
|
}
|
||||||
|
if audience == "" {
|
||||||
|
log.Fatal("No audience provided")
|
||||||
|
}
|
||||||
|
api := sorare.New()
|
||||||
|
api.SetJWTToken(
|
||||||
|
graphql.JwtToken{
|
||||||
|
Token: token,
|
||||||
|
ExpiredAt: time.Time{},
|
||||||
|
},
|
||||||
|
audience,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cu, err := api.Country.Get(ctx, graphql.SlugParams{Slug: "fr"})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(cu)
|
||||||
|
}
|
71
config/config.go
Normal file
71
config/config.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
c *graphql.Client
|
||||||
|
|
||||||
|
exchangeRate *graphql.Query[exchangeRate, graphql.EmptyParams]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig(c *graphql.Client) *Config {
|
||||||
|
return &Config{
|
||||||
|
c: c,
|
||||||
|
exchangeRate: graphql.NewQuery[exchangeRate, graphql.EmptyParams](
|
||||||
|
c,
|
||||||
|
"exchangeRate",
|
||||||
|
[]string{"config"},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type exchangeRate struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Time time.Time `graphql:"time"`
|
||||||
|
Rates json.RawMessage `graphql:"rates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateForCurrency struct {
|
||||||
|
Eur decimal.Decimal `json:"eur"`
|
||||||
|
Usd decimal.Decimal `json:"usd"`
|
||||||
|
Gbp decimal.Decimal `json:"gbp"`
|
||||||
|
}
|
||||||
|
type Rates struct {
|
||||||
|
Eth RateForCurrency `json:"eth"`
|
||||||
|
Wei RateForCurrency `json:"wei"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExchangeRate struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Rates Rates `json:"rates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRates(rates json.RawMessage) Rates {
|
||||||
|
var r Rates
|
||||||
|
err := json.Unmarshal(rates, &r)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ExchangeRate(ctx context.Context) (*ExchangeRate, error) {
|
||||||
|
raw, err := c.exchangeRate.Get(ctx, graphql.EmptyParams{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ExchangeRate{
|
||||||
|
Id: raw.Id.String(),
|
||||||
|
Time: raw.Time,
|
||||||
|
Rates: parseRates(raw.Rates),
|
||||||
|
}, nil
|
||||||
|
}
|
15
country.go
Normal file
15
country.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package sorare
|
||||||
|
|
||||||
|
import "git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
|
||||||
|
type Country struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Code string `graphql:"code"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
Name string `graphql:"name"`
|
||||||
|
ThreeLetterCode string `graphql:"threeLetterCode"`
|
||||||
|
FlagFlat64Url string `graphql:"flagFlat64:flagUrl(shape:\"flat\", size:64)"`
|
||||||
|
FlagFlat32Url string `graphql:"flagFlat32:flagUrl(shape:\"flat\", size:32)"`
|
||||||
|
FlagRound64Url string `graphql:"flagRound64:flagUrl(shape:\"round\", size:64)"`
|
||||||
|
FlagRound32Url string `graphql:"flatRound32:flagUrl(shape:\"round\", size:32)"`
|
||||||
|
}
|
42
football/card.go
Normal file
42
football/card.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/tokens"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Card struct {
|
||||||
|
Power decimal.Decimal `graphql:"power"`
|
||||||
|
PowerMalusAfterTransfer decimal.Decimal `graphql:"powerMalusAfterTransfer"`
|
||||||
|
RivalsPower decimal.Decimal `graphql:"rivalsPower"`
|
||||||
|
Grade int `graphql:"grade"`
|
||||||
|
GradeAfterTransfer int `graphql:"gradeAfterTransfer"`
|
||||||
|
Xp int `graphql:"xp"`
|
||||||
|
Token tokens.Token `graphql:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CardsParams struct {
|
||||||
|
Age *int `graphql:"age"`
|
||||||
|
AssetIds []string `graphql:"assetIds"`
|
||||||
|
CustomCardEditionName *string `graphql:"customCardEditionName"`
|
||||||
|
Owned *bool `graphql:"owned"`
|
||||||
|
OwnedSinceAfter *types.ISO8601DateTime `graphql:"ownedSinceAfter"`
|
||||||
|
PlayerSlugs []string `graphql:"playerSlugs"`
|
||||||
|
Positions []types.Position `graphql:"positions"`
|
||||||
|
Rarities []types.Rarity `graphql:"rarities"`
|
||||||
|
SerialNumber *int `graphql:"serialNumber"`
|
||||||
|
ShirtNumber *int `graphql:"shirtNumber"`
|
||||||
|
Slugs []string `graphql:"slugs"`
|
||||||
|
TeamSlugs []string `graphql:"teamSlugs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Football) NewClubCardsQuery(clubSlug string) *graphql.PaginatedQuery[Card, CardsParams] {
|
||||||
|
return graphql.NewPaginatedQuery[Card, CardsParams](
|
||||||
|
f.c,
|
||||||
|
"cards",
|
||||||
|
[]string{"football", "club(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", clubSlug)
|
||||||
|
}
|
42
football/club.go
Normal file
42
football/club.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Club struct {
|
||||||
|
Team
|
||||||
|
DomesticLeague struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"domesticLeague"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Football) NewClubActivePlayersQuery(
|
||||||
|
clubSlug string,
|
||||||
|
) *graphql.PaginatedQuery[Player, graphql.EmptyParams] {
|
||||||
|
return graphql.NewPaginatedQuery[Player, graphql.EmptyParams](
|
||||||
|
f.c,
|
||||||
|
"activePlayers",
|
||||||
|
[]string{"football", "club(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", clubSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Football) NewClubPlayersQuery(
|
||||||
|
clubSlug string,
|
||||||
|
) *graphql.PaginatedQuery[Player, graphql.EmptyParams] {
|
||||||
|
return graphql.NewPaginatedQuery[Player, graphql.EmptyParams](
|
||||||
|
f.c,
|
||||||
|
"players",
|
||||||
|
[]string{"football", "club(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", clubSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Football) NewClubRecentDeparturesQuery(
|
||||||
|
clubSlug string,
|
||||||
|
) *graphql.PaginatedQuery[Player, graphql.EmptyParams] {
|
||||||
|
return graphql.NewPaginatedQuery[Player, graphql.EmptyParams](
|
||||||
|
f.c,
|
||||||
|
"recentDepartures",
|
||||||
|
[]string{"football", "club(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", clubSlug)
|
||||||
|
}
|
45
football/competition.go
Normal file
45
football/competition.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Competition struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
DisplayName string `graphql:"displayName"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
Country struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"country"`
|
||||||
|
Format string `graphql:"format"`
|
||||||
|
Type string `graphql:"type"`
|
||||||
|
PictureUrl string `graphql:"pictureUrl"`
|
||||||
|
LogoUrl string `graphql:"logoUrl"`
|
||||||
|
BackgroundColor string `graphql:"backgroundColor"`
|
||||||
|
BackgroundPictureUrl string `graphql:"backgroundPictureUrl"`
|
||||||
|
OpenForGameStats bool `graphql:"openForGameStats"`
|
||||||
|
Released bool `graphql:"released"`
|
||||||
|
Seasons []struct {
|
||||||
|
StartYear int `graphql:"startYear"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Football) NewCompetitionClubs(
|
||||||
|
competitionSlug string,
|
||||||
|
) *graphql.PaginatedQuery[Club, graphql.EmptyParams] {
|
||||||
|
return graphql.NewPaginatedQuery[Club, graphql.EmptyParams](
|
||||||
|
f.c,
|
||||||
|
"clubs",
|
||||||
|
[]string{"football", "competition(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", competitionSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Football) NewCompetitionPlayersByLastFiveAverage(
|
||||||
|
competitionSlug string,
|
||||||
|
) *graphql.PaginatedQuery[Player, graphql.EmptyParams] {
|
||||||
|
return graphql.NewPaginatedQuery[Player, graphql.EmptyParams](
|
||||||
|
f.c,
|
||||||
|
"playersByLastFiveAverage",
|
||||||
|
[]string{"football", "competition(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", competitionSlug)
|
||||||
|
}
|
113
football/football.go
Normal file
113
football/football.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Football struct {
|
||||||
|
c *graphql.Client
|
||||||
|
|
||||||
|
AllCards *graphql.PaginatedQuery[Card, CardsParams]
|
||||||
|
Card *graphql.Query[Card, graphql.SlugParams]
|
||||||
|
CardByAssetId *graphql.Query[Card, graphql.AssetIdParams]
|
||||||
|
CardByBlockchainId *graphql.Query[Card, graphql.BlockchainIdParams]
|
||||||
|
Cards *graphql.Query[[]Card, CardsParams]
|
||||||
|
Club *graphql.Query[Club, graphql.SlugParams]
|
||||||
|
Clubs *graphql.Query[[]Club, graphql.SlugsParams]
|
||||||
|
ClubsReady *graphql.Query[[]Club, graphql.EmptyParams]
|
||||||
|
Competition *graphql.Query[Competition, graphql.SlugParams]
|
||||||
|
Game *graphql.Query[GameWithFormation, graphql.IdParams]
|
||||||
|
MyLiveGames *graphql.Query[[]Game, graphql.EmptyParams]
|
||||||
|
NationalTeam *graphql.Query[NationalTeam, graphql.SlugParams]
|
||||||
|
NationalTeams *graphql.Query[[]NationalTeam, graphql.SlugsParams]
|
||||||
|
Player *graphql.Query[Player, graphql.SlugParams]
|
||||||
|
Players *graphql.Query[[]Player, graphql.SlugsParams]
|
||||||
|
Season *graphql.Query[Season, SeasonParams]
|
||||||
|
|
||||||
|
So5 *So5
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFootball(c *graphql.Client) *Football {
|
||||||
|
return &Football{
|
||||||
|
c: c,
|
||||||
|
|
||||||
|
AllCards: graphql.NewPaginatedQuery[Card, CardsParams](
|
||||||
|
c,
|
||||||
|
"allCards",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
Card: graphql.NewQuery[Card, graphql.SlugParams](
|
||||||
|
c,
|
||||||
|
"card",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
CardByAssetId: graphql.NewQuery[Card, graphql.AssetIdParams](
|
||||||
|
c,
|
||||||
|
"cardByAssetId",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
CardByBlockchainId: graphql.NewQuery[Card, graphql.BlockchainIdParams](
|
||||||
|
c,
|
||||||
|
"cardByBlockchainId",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
Cards: graphql.NewQuery[[]Card, CardsParams](c, "cards", []string{"football"}),
|
||||||
|
Club: graphql.NewQuery[Club, graphql.SlugParams](
|
||||||
|
c,
|
||||||
|
"club",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
Clubs: graphql.NewQuery[[]Club, graphql.SlugsParams](
|
||||||
|
c,
|
||||||
|
"clubs",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
ClubsReady: graphql.NewQuery[[]Club, graphql.EmptyParams](
|
||||||
|
c,
|
||||||
|
"clubsReady",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
Competition: graphql.NewQuery[Competition, graphql.SlugParams](
|
||||||
|
c,
|
||||||
|
"competition",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
Game: graphql.NewQuery[GameWithFormation, graphql.IdParams](
|
||||||
|
c,
|
||||||
|
"game",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
MyLiveGames: graphql.NewQuery[[]Game, graphql.EmptyParams](
|
||||||
|
c,
|
||||||
|
"myLiveGames",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
NationalTeam: graphql.NewQuery[NationalTeam, graphql.SlugParams](
|
||||||
|
c,
|
||||||
|
"nationalTeam",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
NationalTeams: graphql.NewQuery[[]NationalTeam, graphql.SlugsParams](
|
||||||
|
c,
|
||||||
|
"nationalTeams",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
Player: graphql.NewQuery[Player, graphql.SlugParams](
|
||||||
|
c,
|
||||||
|
"player",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
Players: graphql.NewQuery[[]Player, graphql.SlugsParams](
|
||||||
|
c,
|
||||||
|
"players",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
Season: graphql.NewQuery[Season, SeasonParams](
|
||||||
|
c,
|
||||||
|
"season",
|
||||||
|
[]string{"football"},
|
||||||
|
),
|
||||||
|
|
||||||
|
So5: NewSo5(c),
|
||||||
|
}
|
||||||
|
}
|
86
football/game.go
Normal file
86
football/game.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GameWithFormation struct {
|
||||||
|
Game
|
||||||
|
AwayFormation struct {
|
||||||
|
Bench []struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"bench"`
|
||||||
|
StartingLineup [][]struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"startingLineup"`
|
||||||
|
} `graphql:"awayFormation"`
|
||||||
|
HomeFormation struct {
|
||||||
|
Bench []struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"bench"`
|
||||||
|
StartingLineup [][]struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"startingLineup"`
|
||||||
|
} `graphql:"homeFormation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
|
||||||
|
AwayGoals int `graphql:"awayGoals"`
|
||||||
|
AwayTeam struct {
|
||||||
|
TypeName string `graphql:"__typename"`
|
||||||
|
Team struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"... on TeamInterface"`
|
||||||
|
} `graphql:"awayTeam"`
|
||||||
|
Competition struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"competition"`
|
||||||
|
CoverageStatus string `graphql:"coverageStatus"`
|
||||||
|
Date time.Time `graphql:"date"`
|
||||||
|
ExtraTimeScoreAway int `graphql:"extraTimeScoreAway"`
|
||||||
|
ExtraTimeScoreHome int `graphql:"extraTimeScoreHome"`
|
||||||
|
|
||||||
|
HomeGoals int `graphql:"homeGoals"`
|
||||||
|
HomeTeam struct {
|
||||||
|
TypeName string `graphql:"__typename"`
|
||||||
|
Team struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"... on TeamInterface"`
|
||||||
|
} `graphql:"homeTeam"`
|
||||||
|
LowCoverage bool `graphql:"lowCoverage"`
|
||||||
|
Minute int `graphql:"minute"`
|
||||||
|
PenaltyScoreAway int `graphql:"penaltyScoreAway"`
|
||||||
|
PenaltyScoreHome int `graphql:"penaltyScoreHome"`
|
||||||
|
PeriodType string `graphql:"periodType"`
|
||||||
|
Scored bool `graphql:"scored"`
|
||||||
|
So5Fixture struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"so5Fixture"`
|
||||||
|
Status string `graphql:"status"`
|
||||||
|
Winner struct {
|
||||||
|
TypeName string `graphql:"__typename"`
|
||||||
|
Team struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"... on TeamInterface"`
|
||||||
|
} `graphql:"winner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClubGamesParams struct {
|
||||||
|
StartDate types.ISO8601DateTime `graphql:"startDate"`
|
||||||
|
EndDate types.ISO8601DateTime `graphql:"endDate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Football) NewClubGamesQuery(
|
||||||
|
clubSlug string,
|
||||||
|
) *graphql.PaginatedQuery[Game, ClubGamesParams] {
|
||||||
|
return graphql.NewPaginatedQuery[Game, ClubGamesParams](
|
||||||
|
f.c,
|
||||||
|
"games",
|
||||||
|
[]string{"football", "club(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", clubSlug)
|
||||||
|
}
|
8
football/national_team.go
Normal file
8
football/national_team.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
type NationalTeam struct {
|
||||||
|
Team
|
||||||
|
InternationalCompetition struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"internationalCompetition"`
|
||||||
|
}
|
116
football/player.go
Normal file
116
football/player.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gql "github.com/llehouerou/go-graphql-client"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Injury struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Active bool `graphql:"active"`
|
||||||
|
StartDate time.Time `graphql:"startDate"`
|
||||||
|
ExpectedEndDate time.Time `graphql:"expectedEndDate"`
|
||||||
|
Kind string `graphql:"kind"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Suspension struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Active bool `graphql:"active"`
|
||||||
|
StartDate time.Time `graphql:"startDate"`
|
||||||
|
EndDate time.Time `graphql:"endDate"`
|
||||||
|
Kind string `graphql:"kind"`
|
||||||
|
Matches int `graphql:"matches"`
|
||||||
|
Reason string `graphql:"reason"`
|
||||||
|
Competition struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"competition"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
ActiveClub struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
ActiveCompetitions []struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
Format string `graphql:"format"`
|
||||||
|
} `graphql:"activeCompetitions"`
|
||||||
|
} `graphql:"activeClub"`
|
||||||
|
ActiveNationalTeam struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"activeNationalTeam"`
|
||||||
|
ActiveInjuries []Injury `graphql:"activeInjuries"`
|
||||||
|
ActiveSuspensions []Suspension `graphql:"activeSuspensions"`
|
||||||
|
Age int `graphql:"age"`
|
||||||
|
Appearances int `graphql:"appearances"`
|
||||||
|
BestFoot string `graphql:"bestFoot"`
|
||||||
|
BirthDate time.Time `graphql:"birthDate"`
|
||||||
|
CardPositions []types.Position `graphql:"cardPositions"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
DisplayName string `graphql:"displayName"`
|
||||||
|
FirstName string `graphql:"firstName"`
|
||||||
|
Height int `graphql:"height"`
|
||||||
|
Injuries []Injury `graphql:"injuries"`
|
||||||
|
LastClub struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"lastClub"`
|
||||||
|
LastName string `graphql:"lastName"`
|
||||||
|
MatchName string `graphql:"matchName"`
|
||||||
|
PictureUrl string `graphql:"pictureUrl"`
|
||||||
|
PlayingStatus types.PlayerPlayingStatus `graphql:"playingStatus"`
|
||||||
|
Country struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
Code string `graphql:"code"`
|
||||||
|
}
|
||||||
|
AvatarUrl string `graphql:"avatar:pictureUrl(derivative:\"avatar\")"`
|
||||||
|
|
||||||
|
Position types.Position `graphql:"position"`
|
||||||
|
ShirtNumber int `graphql:"shirtNumber"`
|
||||||
|
Suspensions []Suspension `graphql:"suspensions"`
|
||||||
|
User struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"user"`
|
||||||
|
Weight int `graphql:"weight"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerScore struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
So5Score So5Score `graphql:"so5Score(gameId:$gameId)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Football) PlayerCards(
|
||||||
|
playerSlug string,
|
||||||
|
) *graphql.PaginatedQuery[Card, CardsParams] {
|
||||||
|
return graphql.NewPaginatedQuery[Card, CardsParams](
|
||||||
|
f.c,
|
||||||
|
"cards",
|
||||||
|
[]string{"football", "player(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", playerSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Football) PlayersGameScores(
|
||||||
|
gameId gql.ID,
|
||||||
|
) *graphql.Query[[]PlayerScore, graphql.SlugsParams] {
|
||||||
|
return graphql.NewQuery[[]PlayerScore, graphql.SlugsParams](
|
||||||
|
f.c,
|
||||||
|
"players",
|
||||||
|
[]string{"football"},
|
||||||
|
).WithQueryParam("gameId", gameId)
|
||||||
|
}
|
||||||
|
|
||||||
|
type So5ScoresParams struct {
|
||||||
|
Position *types.Position `graphql:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Football) PlayerAllSo5Scores(
|
||||||
|
playerSlug string,
|
||||||
|
) *graphql.PaginatedQuery[So5Score, So5ScoresParams] {
|
||||||
|
return graphql.NewPaginatedQuery[So5Score, So5ScoresParams](
|
||||||
|
f.c,
|
||||||
|
"allSo5Scores",
|
||||||
|
[]string{"football", "player(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", playerSlug)
|
||||||
|
}
|
13
football/season.go
Normal file
13
football/season.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import "git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
|
||||||
|
type Season struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Name string `graphql:"name"`
|
||||||
|
StartYear int `graphql:"startYear"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SeasonParams struct {
|
||||||
|
StartYear int `graphql:"startYear"`
|
||||||
|
}
|
49
football/so5.go
Normal file
49
football/so5.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type So5 struct {
|
||||||
|
c *graphql.Client
|
||||||
|
|
||||||
|
So5Fixture *graphql.Query[So5Fixture, So5FixtureParams]
|
||||||
|
So5Fixtures *graphql.PaginatedQuery[So5Fixture, So5FixturesParams]
|
||||||
|
So5Score *graphql.Query[So5Score, graphql.IdParams]
|
||||||
|
So5Leaderboard *graphql.Query[So5Leaderboard, graphql.SlugParams]
|
||||||
|
|
||||||
|
UpcomingLeaderboards *graphql.Query[[]So5Leaderboard, UpcomingLeaderboardsParams]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSo5(c *graphql.Client) *So5 {
|
||||||
|
return &So5{
|
||||||
|
c: c,
|
||||||
|
|
||||||
|
So5Fixture: graphql.NewQuery[So5Fixture, So5FixtureParams](
|
||||||
|
c,
|
||||||
|
"so5Fixture",
|
||||||
|
[]string{"football", "so5"},
|
||||||
|
),
|
||||||
|
So5Fixtures: graphql.NewPaginatedQuery[So5Fixture, So5FixturesParams](
|
||||||
|
c,
|
||||||
|
"so5Fixtures",
|
||||||
|
[]string{"football", "so5"},
|
||||||
|
),
|
||||||
|
So5Score: graphql.NewQuery[So5Score, graphql.IdParams](
|
||||||
|
c,
|
||||||
|
"so5Score",
|
||||||
|
[]string{"football", "so5"},
|
||||||
|
),
|
||||||
|
So5Leaderboard: graphql.NewQuery[So5Leaderboard, graphql.SlugParams](
|
||||||
|
c,
|
||||||
|
"so5Leaderboard",
|
||||||
|
[]string{"football", "so5"},
|
||||||
|
),
|
||||||
|
|
||||||
|
UpcomingLeaderboards: graphql.NewQuery[[]So5Leaderboard, UpcomingLeaderboardsParams](
|
||||||
|
c,
|
||||||
|
"upcomingLeaderboards",
|
||||||
|
[]string{"football", "so5"},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
48
football/so5fixture.go
Normal file
48
football/so5fixture.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gql "github.com/llehouerou/go-graphql-client"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type So5Fixture struct {
|
||||||
|
AasmState string `graphql:"aasmState"`
|
||||||
|
CanCompose bool `graphql:"canCompose"`
|
||||||
|
CutOffDate time.Time `graphql:"cutOffDate"`
|
||||||
|
DisplayName string `graphql:"displayName"`
|
||||||
|
EndDate time.Time `graphql:"endDate"`
|
||||||
|
EventType string `graphql:"eventType"`
|
||||||
|
GameWeek int `graphql:"gameWeek"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
ReplayedFixture struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"replayedFixture"`
|
||||||
|
RewardsDeliveryDate time.Time `graphql:"rewardsDeliveryDate"`
|
||||||
|
ShortDisplayName string `graphql:"shortDisplayName"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
StartDate time.Time `graphql:"startDate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type So5FixtureParams struct {
|
||||||
|
Id *gql.ID `graphql:"id"`
|
||||||
|
Slug *string `graphql:"slug"`
|
||||||
|
Type *types.So5State `graphql:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type So5FixturesParams struct {
|
||||||
|
AasmStates []string `graphql:"aasmStates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *So5) FixtureGames(
|
||||||
|
so5FixtureSlug string,
|
||||||
|
) *graphql.Query[[]Game, graphql.EmptyParams] {
|
||||||
|
return graphql.NewQuery[[]Game, graphql.EmptyParams](
|
||||||
|
s.c,
|
||||||
|
"games",
|
||||||
|
[]string{"football", "so5", "so5Fixture(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", so5FixtureSlug)
|
||||||
|
}
|
53
football/so5leaderboard.go
Normal file
53
football/so5leaderboard.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type So5Tournament struct {
|
||||||
|
Id string `graphql:"id"`
|
||||||
|
DisplayName string `graphql:"displayName"`
|
||||||
|
ShortDisplayName string `graphql:"shortDisplayName:displayName(short:true)"`
|
||||||
|
So5LeaderboardType types.So5LeaderboardType `graphql:"so5LeaderboardType"`
|
||||||
|
SvgLogoUrl string `graphql:"svgLogoUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type So5Leaderboard struct {
|
||||||
|
CutOffDate time.Time `graphql:"cutOffDate"`
|
||||||
|
DefaultAverageScore types.AveragePlayerScore `graphql:"defaultAverageScore"`
|
||||||
|
Description string `graphql:"description"`
|
||||||
|
DisplayName string `graphql:"displayName"`
|
||||||
|
ShortDisplayName string `graphql:"shortDisplayName:displayName(short:true)"`
|
||||||
|
Division int `graphql:"division"`
|
||||||
|
EndDate time.Time `graphql:"endDate"`
|
||||||
|
GameWeek int `graphql:"gameWeek"`
|
||||||
|
IconUrl string `graphql:"iconUrl"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
MainRarityType types.Rarity `graphql:"mainRarityType"`
|
||||||
|
PowerCap float64 `graphql:"powerCap"`
|
||||||
|
RarityType string `graphql:"rarityType"`
|
||||||
|
RewardedLineupsCount int `graphql:"rewardedLineupsCount"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
So5Fixture struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"so5Fixture"`
|
||||||
|
So5LeaderBoardType types.So5LeaderboardType `graphql:"so5LeaderboardType"`
|
||||||
|
So5League struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"so5League"`
|
||||||
|
So5LineupsCount int `graphql:"so5LineupsCount"`
|
||||||
|
So5TournamentType So5Tournament `graphql:"so5TournamentType"`
|
||||||
|
StartDate time.Time `graphql:"startDate"`
|
||||||
|
SvgLogoUrl string `graphql:"svgLogoUrl"`
|
||||||
|
TeamsCap int `graphql:"teamsCap"`
|
||||||
|
Title string `graphql:"title"`
|
||||||
|
TournamentType string `graphql:"tournamentType"`
|
||||||
|
TrainingCenter bool `graphql:"trainingCenter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpcomingLeaderboardsParams struct {
|
||||||
|
Rarities []types.So5LeaderboardRarity `graphql:"rarities"`
|
||||||
|
}
|
94
football/so5score.go
Normal file
94
football/so5score.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatScore struct {
|
||||||
|
Category types.StatCategory `graphql:"category"`
|
||||||
|
Points decimal.Decimal `graphql:"points"`
|
||||||
|
Stat string `graphql:"stat"`
|
||||||
|
StatValue decimal.Decimal `graphql:"statValue"`
|
||||||
|
TotalScore decimal.Decimal `graphql:"totalScore"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerGameStats struct {
|
||||||
|
AccuratePass int `graphql:"accuratePass"`
|
||||||
|
CleanSheet int `graphql:"cleanSheet"`
|
||||||
|
CrossAccuracy int `graphql:"crossAccuracy"`
|
||||||
|
DuelWon int `graphql:"duelWon"`
|
||||||
|
EffectiveClearance int `graphql:"effectiveClearance"`
|
||||||
|
FieldStatus types.PlayerFieldStatus `graphql:"fieldStatus"`
|
||||||
|
FormationPlace int `graphql:"formationPlace"`
|
||||||
|
Fouls int `graphql:"fouls"`
|
||||||
|
Game struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
} `graphql:"game"`
|
||||||
|
GameStarted int `graphql:"gameStarted"`
|
||||||
|
GoalAssist int `graphql:"goalAssist"`
|
||||||
|
GoalKicks int `graphql:"goalKicks"`
|
||||||
|
Goals int `graphql:"goals"`
|
||||||
|
GoalsConceded int `graphql:"goalsConceded"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Live bool `graphql:"live"`
|
||||||
|
LostCorners int `graphql:"lostCorners"`
|
||||||
|
MinsPlayed int `graphql:"minsPlayed"`
|
||||||
|
OnGameSheet bool `graphql:"onGameSheet"`
|
||||||
|
OntargetScoringAtt int `graphql:"ontargetScoringAtt"`
|
||||||
|
OwnGoals int `graphql:"ownGoals"`
|
||||||
|
Parries int `graphql:"parries"`
|
||||||
|
PassAccuracy int `graphql:"passAccuracy"`
|
||||||
|
PenaltiesSaved int `graphql:"penaltiesSaved"`
|
||||||
|
PenaltyKickMissed int `graphql:"penaltyKickMissed"`
|
||||||
|
PenaltySave int `graphql:"penaltySave"`
|
||||||
|
Player struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"player"`
|
||||||
|
RedCard int `graphql:"redCard"`
|
||||||
|
Reviewed bool `graphql:"reviewed"`
|
||||||
|
Saves int `graphql:"saves"`
|
||||||
|
ShotAccuracy int `graphql:"shotAccuracy"`
|
||||||
|
ShotEfficiency int `graphql:"shotEfficiency"`
|
||||||
|
SingleGoalGame int `graphql:"singleGoalGame"`
|
||||||
|
Team struct {
|
||||||
|
Team struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"... on TeamInterface"`
|
||||||
|
} `graphql:"team"`
|
||||||
|
TotalClearance int `graphql:"totalClearance"`
|
||||||
|
TotalPass int `graphql:"totalPass"`
|
||||||
|
TotalScoringAtt int `graphql:"totalScoringAtt"`
|
||||||
|
TotalTackle int `graphql:"totalTackle"`
|
||||||
|
WasFouled int `graphql:"wasFouled"`
|
||||||
|
WonContest int `graphql:"wonContest"`
|
||||||
|
YellowCard int `graphql:"yellowCard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type So5Score struct {
|
||||||
|
AllAroundStats []StatScore `graphql:"allAroundStats"`
|
||||||
|
DecisiveScore StatScore `graphql:"decisiveScore"`
|
||||||
|
DetailedScore []StatScore `graphql:"detailedScore"`
|
||||||
|
Game struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Date time.Time `graphql:"date"`
|
||||||
|
So5Fixture struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"so5Fixture"`
|
||||||
|
} `graphql:"game"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
NegativeDecisiveStats []StatScore `graphql:"negativeDecisiveStats"`
|
||||||
|
Player struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"player"`
|
||||||
|
PlayerGameStats PlayerGameStats `graphql:"playerGameStats"`
|
||||||
|
Position string `graphql:"position"`
|
||||||
|
PositionTyped types.Position `graphql:"positionTyped"`
|
||||||
|
PositiveDecisiveStats []StatScore `graphql:"positiveDecisiveStats"`
|
||||||
|
Score decimal.Decimal `graphql:"score"`
|
||||||
|
ScoringVersion int `graphql:"scoringVersion"`
|
||||||
|
}
|
18
football/team.go
Normal file
18
football/team.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package football
|
||||||
|
|
||||||
|
type Team struct {
|
||||||
|
ActiveCompetitions []struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"activeCompetitions"`
|
||||||
|
Code string `graphql:"code"`
|
||||||
|
Country struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
}
|
||||||
|
Founded string `graphql:"founded"`
|
||||||
|
Name string `graphql:"name"`
|
||||||
|
PictureSecondaryUrl string `graphql:"pictureSecondaryUrl"`
|
||||||
|
PictureUrl string `graphql:"pictureUrl"`
|
||||||
|
Ready bool `graphql:"ready"`
|
||||||
|
ShortName string `graphql:"shortName"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
}
|
27
go.mod
Normal file
27
go.mod
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
module git.lehouerou.net/laurent/sorare
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/gorilla/websocket v1.5.1
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.5
|
||||||
|
github.com/llehouerou/go-graphql-client v0.9.6
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/rs/zerolog v1.32.0
|
||||||
|
github.com/shopspring/decimal v1.3.1
|
||||||
|
github.com/xlzd/gotp v0.1.0
|
||||||
|
golang.org/x/crypto v0.21.0
|
||||||
|
golang.org/x/time v0.5.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
golang.org/x/net v0.21.0 // indirect
|
||||||
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
|
nhooyr.io/websocket v1.8.7 // indirect
|
||||||
|
)
|
103
go.sum
Normal file
103
go.sum
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
|
github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc=
|
||||||
|
github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
|
||||||
|
github.com/graph-gophers/graphql-transport-ws v0.0.2 h1:DbmSkbIGzj8SvHei6n8Mh9eLQin8PtA8xY9eCzjRpvo=
|
||||||
|
github.com/graph-gophers/graphql-transport-ws v0.0.2/go.mod h1:5BVKvFzOd2BalVIBFfnfmHjpJi/MZ5rOj8G55mXvZ8g=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
|
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||||
|
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||||
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
|
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||||
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/llehouerou/go-graphql-client v0.9.6 h1:tZdCHrBz8p044KoxXwmEur9L4VNZ3luYeJmQZpLodPY=
|
||||||
|
github.com/llehouerou/go-graphql-client v0.9.6/go.mod h1:hyvnfe3diGLxMelSKF+cFPyAfxZLa1CGGBogmix4SyM=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||||
|
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
|
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||||
|
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||||
|
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||||
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||||
|
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
172
graphql/authentication.go
Normal file
172
graphql/authentication.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/xlzd/gotp"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const bCryptCost = 11
|
||||||
|
|
||||||
|
func (c *Client) getSaltFromEmail(ctx context.Context, email string) (string, error) {
|
||||||
|
var response struct {
|
||||||
|
Salt string `json:"salt"`
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"https://api.sorare.com/api/v1/users/"+email,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "creating request")
|
||||||
|
}
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "executing request")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", errors.Errorf("executing request failed with status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "decoding response")
|
||||||
|
}
|
||||||
|
return response.Salt[7:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashPasswordWithSalt(password, salt string) (string, error) {
|
||||||
|
hashed, err := bcrypt.GenerateFromPasswordAndSalt([]byte(password), []byte(salt), bCryptCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hashed), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type JwtToken struct {
|
||||||
|
Token string
|
||||||
|
ExpiredAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignIn struct {
|
||||||
|
CurrentUser struct {
|
||||||
|
Slug string
|
||||||
|
JwtToken JwtToken `graphql:"jwtToken(aud: $aud)"`
|
||||||
|
} `graphql:"currentUser"`
|
||||||
|
OtpSessionChallenge string
|
||||||
|
Errors []struct {
|
||||||
|
Message string
|
||||||
|
} `graphql:"errors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type signInInput struct {
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
OtpSessionChallenge string `json:"otpSessionChallenge,omitempty"`
|
||||||
|
OtpAttempt string `json:"otpAttempt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignInParams struct {
|
||||||
|
Input signInInput `graphql:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignInMutation struct {
|
||||||
|
*Mutation[SignIn, SignInParams]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSignInMutation(c *Client, audience string) *SignInMutation {
|
||||||
|
return &SignInMutation{
|
||||||
|
NewMutation[SignIn, SignInParams](c, "signIn").WithQueryParam("aud", audience),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getNewToken(
|
||||||
|
ctx context.Context,
|
||||||
|
email string,
|
||||||
|
password string,
|
||||||
|
audience string,
|
||||||
|
otp string,
|
||||||
|
) (JwtToken, error) {
|
||||||
|
|
||||||
|
salt, err := c.getSaltFromEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return JwtToken{}, errors.Wrap(err, "getting salt from email")
|
||||||
|
}
|
||||||
|
hashed, err := hashPasswordWithSalt(password, salt)
|
||||||
|
if err != nil {
|
||||||
|
return JwtToken{}, errors.Wrap(err, "hashing password with salt")
|
||||||
|
}
|
||||||
|
|
||||||
|
signInMutation := NewSignInMutation(c, audience)
|
||||||
|
res, err := signInMutation.Execute(ctx, SignInParams{
|
||||||
|
Input: signInInput{
|
||||||
|
Email: email,
|
||||||
|
Password: hashed,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return JwtToken{}, errors.Wrap(err, "executing signin mutation with email and password")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.Errors) == 0 {
|
||||||
|
if res.CurrentUser.JwtToken.Token == "" {
|
||||||
|
return JwtToken{}, errors.New(
|
||||||
|
"no errors & no jwt token returned on email/password mutation",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return res.CurrentUser.JwtToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Errors[0].Message == "invalid" {
|
||||||
|
return JwtToken{}, errors.New("invalid email or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Errors[0].Message == "2fa_missing" {
|
||||||
|
if otp == "" {
|
||||||
|
return JwtToken{}, errors.New("2fa missing & no otp provided")
|
||||||
|
}
|
||||||
|
token := gotp.NewDefaultTOTP(otp).Now()
|
||||||
|
|
||||||
|
signInMutation := NewSignInMutation(c, audience)
|
||||||
|
resOtp, err := signInMutation.Execute(ctx, SignInParams{
|
||||||
|
Input: signInInput{
|
||||||
|
OtpSessionChallenge: res.OtpSessionChallenge,
|
||||||
|
OtpAttempt: token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return JwtToken{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resOtp.Errors) == 0 {
|
||||||
|
if resOtp.CurrentUser.JwtToken.Token == "" {
|
||||||
|
return JwtToken{}, errors.New("no errors & no jwt token returned on otp mutation")
|
||||||
|
}
|
||||||
|
return resOtp.CurrentUser.JwtToken, nil
|
||||||
|
}
|
||||||
|
return JwtToken{}, errors.Errorf("validating otp: %s", resOtp.Errors[0].Message)
|
||||||
|
}
|
||||||
|
return JwtToken{}, errors.Errorf("unmanaged error: %s", res.Errors[0].Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Authenticate(
|
||||||
|
ctx context.Context,
|
||||||
|
email string,
|
||||||
|
password string,
|
||||||
|
audience string,
|
||||||
|
otp string,
|
||||||
|
) error {
|
||||||
|
token, err := c.getNewToken(ctx, email, password, audience, otp)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "getting new jwt token")
|
||||||
|
}
|
||||||
|
c.SetJWTToken(token, audience)
|
||||||
|
return nil
|
||||||
|
}
|
96
graphql/client.go
Normal file
96
graphql/client.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/llehouerou/go-graphql-client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rateLimitPeriod = time.Second
|
||||||
|
rateLimitBurst = 60
|
||||||
|
MaxAuthenticatedQueryComplexity = 30000
|
||||||
|
MaxAnonymousQueryComplexity = 500
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
gql *graphql.Client
|
||||||
|
rl *rate.Limiter
|
||||||
|
lock *sync.Mutex
|
||||||
|
authenticated bool
|
||||||
|
token JwtToken
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(httpclient *http.Client, baseUrl string) *Client {
|
||||||
|
return &Client{
|
||||||
|
httpClient: httpclient,
|
||||||
|
gql: graphql.NewClient(baseUrl, httpclient),
|
||||||
|
rl: rate.NewLimiter(rate.Every(rateLimitPeriod), rateLimitBurst),
|
||||||
|
lock: &sync.Mutex{},
|
||||||
|
authenticated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetCurrentToken() JwtToken {
|
||||||
|
return c.token
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetJWTToken(token JwtToken, audience string) {
|
||||||
|
c.gql = c.gql.WithRequestModifier(func(request *http.Request) {
|
||||||
|
request.Header.Set("Authorization", "Bearer "+token.Token)
|
||||||
|
request.Header.Set("JWT-AUD", audience)
|
||||||
|
})
|
||||||
|
c.token = token
|
||||||
|
c.authenticated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) MaxComplexity() int {
|
||||||
|
if c.authenticated {
|
||||||
|
return MaxAuthenticatedQueryComplexity
|
||||||
|
} else {
|
||||||
|
return MaxAnonymousQueryComplexity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ConstructRawQuery(
|
||||||
|
q interface{},
|
||||||
|
variables map[string]interface{},
|
||||||
|
) (string, error) {
|
||||||
|
return graphql.ConstructQuery(q, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Query(
|
||||||
|
ctx context.Context,
|
||||||
|
q interface{},
|
||||||
|
variables interface{},
|
||||||
|
options ...graphql.Option,
|
||||||
|
) error {
|
||||||
|
err := c.rl.Wait(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "waiting for rate limit")
|
||||||
|
}
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
return c.gql.Query(ctx, q, variables, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Mutate(
|
||||||
|
ctx context.Context,
|
||||||
|
q interface{},
|
||||||
|
variables interface{},
|
||||||
|
options ...graphql.Option,
|
||||||
|
) error {
|
||||||
|
err := c.rl.Wait(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "waiting for rate limit")
|
||||||
|
}
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
return c.gql.Mutate(ctx, q, variables, options...)
|
||||||
|
}
|
35
graphql/id.go
Normal file
35
graphql/id.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Id struct {
|
||||||
|
Type string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i Id) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(`"` + i.Type + `:` + i.Value + `"`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Id) UnmarshalJSON(bytes []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(bytes, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
splits := strings.Split(s, ":")
|
||||||
|
if len(splits) < 2 {
|
||||||
|
return errors.New("invalid id")
|
||||||
|
}
|
||||||
|
i.Value = splits[len(splits)-1]
|
||||||
|
i.Type = strings.Join(splits[:len(splits)-1], ":")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i Id) String() string {
|
||||||
|
return i.Type + ":" + i.Value
|
||||||
|
}
|
66
graphql/mutation.go
Normal file
66
graphql/mutation.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mutation[Payload any, Params any] struct {
|
||||||
|
c *Client
|
||||||
|
gqlType string
|
||||||
|
|
||||||
|
additionalPayloadParams map[string]interface{}
|
||||||
|
additionalQueryParams map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMutation[Payload any, Params any](c *Client, gqlType string) *Mutation[Payload, Params] {
|
||||||
|
return &Mutation[Payload, Params]{
|
||||||
|
c: c,
|
||||||
|
gqlType: gqlType,
|
||||||
|
additionalPayloadParams: make(map[string]interface{}),
|
||||||
|
additionalQueryParams: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutation[Payload, Params]) WithPayloadParam(
|
||||||
|
name string,
|
||||||
|
value interface{},
|
||||||
|
) *Mutation[Payload, Params] {
|
||||||
|
m.additionalPayloadParams[name] = value
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutation[Payload, Params]) WithQueryParam(
|
||||||
|
name string,
|
||||||
|
value interface{},
|
||||||
|
) *Mutation[Payload, Params] {
|
||||||
|
m.additionalQueryParams[name] = value
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutation[Payload, Params]) Execute(ctx context.Context, params Params) (Payload, error) {
|
||||||
|
|
||||||
|
paramsMap := convertParamsToMap[Params](params)
|
||||||
|
for k, v := range m.additionalPayloadParams {
|
||||||
|
paramsMap[k] = v
|
||||||
|
}
|
||||||
|
gqltype := m.gqlType
|
||||||
|
if len(paramsMap) > 0 {
|
||||||
|
keys := make([]string, 0, len(paramsMap))
|
||||||
|
for k := range paramsMap {
|
||||||
|
keys = append(keys, fmt.Sprintf("%s:$%s", k, k))
|
||||||
|
}
|
||||||
|
gqltype = fmt.Sprintf("%s(%s)", gqltype, strings.Join(keys, ","))
|
||||||
|
}
|
||||||
|
q := NewPayload[Payload, Params](gqltype)
|
||||||
|
for k, v := range m.additionalQueryParams {
|
||||||
|
paramsMap[k] = v
|
||||||
|
}
|
||||||
|
err := m.c.Mutate(ctx, &q, paramsMap)
|
||||||
|
if err != nil {
|
||||||
|
var res Payload
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
return q.GetValue(), nil
|
||||||
|
}
|
159
graphql/paginated_query.go
Normal file
159
graphql/paginated_query.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaginatedQuery[ResultType any, Params any] struct {
|
||||||
|
c *Client
|
||||||
|
gqltype string
|
||||||
|
containerLayers []string
|
||||||
|
|
||||||
|
additionalPayloadParams map[string]interface{}
|
||||||
|
additionalQueryParams map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPaginatedQuery[ResultType any, Params any](
|
||||||
|
c *Client,
|
||||||
|
gqlType string,
|
||||||
|
containerLayers []string,
|
||||||
|
) *PaginatedQuery[ResultType, Params] {
|
||||||
|
return &PaginatedQuery[ResultType, Params]{
|
||||||
|
c: c,
|
||||||
|
gqltype: gqlType,
|
||||||
|
containerLayers: containerLayers,
|
||||||
|
additionalPayloadParams: make(map[string]interface{}),
|
||||||
|
additionalQueryParams: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PaginatedQuery[ResultType, Params]) WithPayloadParam(
|
||||||
|
name string,
|
||||||
|
value interface{},
|
||||||
|
) *PaginatedQuery[ResultType, Params] {
|
||||||
|
pq.additionalPayloadParams[name] = value
|
||||||
|
return pq
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PaginatedQuery[ResultType, Params]) WithQueryParam(
|
||||||
|
name string,
|
||||||
|
value interface{},
|
||||||
|
) *PaginatedQuery[ResultType, Params] {
|
||||||
|
pq.additionalQueryParams[name] = value
|
||||||
|
return pq
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginatedQueryGetOptions struct {
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPaginatedQueryLimit(limit int) func(options *PaginatedQueryGetOptions) {
|
||||||
|
return func(options *PaginatedQueryGetOptions) {
|
||||||
|
options.Limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PaginatedQuery[ResultType, Params]) Get(
|
||||||
|
ctx context.Context,
|
||||||
|
params Params,
|
||||||
|
options ...func(options *PaginatedQueryGetOptions),
|
||||||
|
) ([]ResultType, error) {
|
||||||
|
opts := &PaginatedQueryGetOptions{
|
||||||
|
Limit: 0,
|
||||||
|
}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(opts)
|
||||||
|
}
|
||||||
|
var res []ResultType
|
||||||
|
after := ""
|
||||||
|
var noop ResultType
|
||||||
|
pageSize := (pq.c.MaxComplexity() - 9) / (GetComplexity(reflect.TypeOf(noop)) + 1)
|
||||||
|
log.Debug().Msgf("using page size %d", pageSize)
|
||||||
|
for {
|
||||||
|
page, pi, err := pq.getPage(ctx, params, after, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, item := range page {
|
||||||
|
res = append(res, item)
|
||||||
|
if opts.Limit > 0 && len(res) >= opts.Limit {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pi.HasNextPage {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
after = pi.EndCursor
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageInfo struct {
|
||||||
|
EndCursor string `graphql:"endCursor"`
|
||||||
|
HasNextPage bool `graphql:"hasNextPage"`
|
||||||
|
HasPreviousPage bool `graphql:"hasPreviousPage"`
|
||||||
|
StartCursor string `graphql:"startCursor"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PaginatedQuery[ResultType, Params]) getPage(
|
||||||
|
ctx context.Context,
|
||||||
|
params Params,
|
||||||
|
after string,
|
||||||
|
first int,
|
||||||
|
) ([]ResultType, PageInfo, error) {
|
||||||
|
type PageResult struct {
|
||||||
|
Nodes []ResultType `graphql:"nodes"`
|
||||||
|
PageInfo PageInfo `graphql:"pageInfo"`
|
||||||
|
}
|
||||||
|
q := NewQuery[PageResult, Params](pq.c, pq.gqltype, pq.containerLayers).
|
||||||
|
WithPayloadParam("first", first)
|
||||||
|
if after != "" {
|
||||||
|
q = q.WithPayloadParam("after", after)
|
||||||
|
}
|
||||||
|
for k, v := range pq.additionalPayloadParams {
|
||||||
|
q.WithPayloadParam(k, v)
|
||||||
|
}
|
||||||
|
for k, v := range pq.additionalQueryParams {
|
||||||
|
q.WithQueryParam(k, v)
|
||||||
|
}
|
||||||
|
res, err := q.Get(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, PageInfo{}, err
|
||||||
|
}
|
||||||
|
return res.Nodes, res.PageInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetComplexity(t reflect.Type) int {
|
||||||
|
var checkStruct func(t reflect.Type, complexity *int)
|
||||||
|
checkStruct = func(t reflect.Type, complexity *int) {
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
if field.Type.Kind() == reflect.Struct {
|
||||||
|
*complexity++
|
||||||
|
tag := field.Tag.Get("graphql")
|
||||||
|
if len(tag) > 6 && tag[:6] == "... on" {
|
||||||
|
*complexity--
|
||||||
|
}
|
||||||
|
checkStruct(field.Type, complexity)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if field.Type.Kind() == reflect.Slice {
|
||||||
|
*complexity++
|
||||||
|
tmpcomplexity := 0
|
||||||
|
checkStruct(field.Type.Elem(), &tmpcomplexity)
|
||||||
|
*complexity += tmpcomplexity * 10
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
*complexity++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
complexity := 0
|
||||||
|
checkStruct(t, &complexity)
|
||||||
|
return complexity
|
||||||
|
}
|
67
graphql/params.go
Normal file
67
graphql/params.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
gql "github.com/llehouerou/go-graphql-client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmptyParams struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type SlugParams struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SlugsParams struct {
|
||||||
|
Slugs []string `graphql:"slugs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdStringParams struct {
|
||||||
|
Id string `graphql:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdStringsParams struct {
|
||||||
|
Ids []string `graphql:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdParams struct {
|
||||||
|
Id gql.ID `graphql:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdsParams struct {
|
||||||
|
Ids []gql.ID `graphql:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssetIdParams struct {
|
||||||
|
AssetId string `graphql:"assetId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssetIdsParams struct {
|
||||||
|
AssetIds []string `graphql:"assetIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockchainIdParams struct {
|
||||||
|
BlockchainId string `graphql:"blockchainId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertParamsToMap[Params any](params Params) map[string]interface{} {
|
||||||
|
v := reflect.ValueOf(params)
|
||||||
|
paramsMap := make(map[string]interface{})
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
field := v.Type().Field(i)
|
||||||
|
value := v.Field(i)
|
||||||
|
key := field.Tag.Get("graphql")
|
||||||
|
if key == "" {
|
||||||
|
key = field.Name
|
||||||
|
}
|
||||||
|
if value.Kind() == reflect.Slice {
|
||||||
|
if value.Len() > 0 {
|
||||||
|
paramsMap[key] = value.Interface()
|
||||||
|
}
|
||||||
|
} else if !value.IsZero() {
|
||||||
|
paramsMap[key] = value.Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paramsMap
|
||||||
|
}
|
89
graphql/payload.go
Normal file
89
graphql/payload.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
type Payload[T any, P any] struct {
|
||||||
|
OutermostLayer ContainerLayer[T, P]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Payload[T, P]) GetValue() T {
|
||||||
|
if q.OutermostLayer == nil {
|
||||||
|
var res T
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
layer := q.OutermostLayer
|
||||||
|
for layer.GetInnerLayer() != nil {
|
||||||
|
layer = layer.GetInnerLayer()
|
||||||
|
}
|
||||||
|
return layer.GetValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPayload[T any, P any](gqlType string, containerLayers ...string) *Payload[T, P] {
|
||||||
|
if len(containerLayers) == 0 {
|
||||||
|
return &Payload[T, P]{
|
||||||
|
OutermostLayer: NewWrapper[T, P](gqlType),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildLayer func(index int) ContainerLayer[T, P]
|
||||||
|
buildLayer = func(index int) ContainerLayer[T, P] {
|
||||||
|
if index == len(containerLayers) {
|
||||||
|
return NewWrapper[T, P](gqlType)
|
||||||
|
}
|
||||||
|
return &NestedLayer[T, P]{
|
||||||
|
gqlType: containerLayers[index],
|
||||||
|
InnerLayer: buildLayer(index + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Payload[T, P]{OutermostLayer: buildLayer(0)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerLayer[T any, P any] interface {
|
||||||
|
GetInnerLayer() ContainerLayer[T, P]
|
||||||
|
GetValue() T
|
||||||
|
GetGraphQLType() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NestedLayer[T any, P any] struct {
|
||||||
|
gqlType string `graphql:"-"`
|
||||||
|
InnerLayer ContainerLayer[T, P]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nl *NestedLayer[T, P]) GetInnerLayer() ContainerLayer[T, P] {
|
||||||
|
return nl.InnerLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nl *NestedLayer[T, P]) GetValue() T {
|
||||||
|
var res T
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nl *NestedLayer[T, P]) GetGraphQLType() string {
|
||||||
|
return nl.gqlType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Wrapper[T any, P any] struct {
|
||||||
|
gqlType string `graphql:"-"`
|
||||||
|
Value T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w Wrapper[T, P]) GetInnerLayer() ContainerLayer[T, P] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w Wrapper[T, P]) GetValue() T {
|
||||||
|
return w.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w Wrapper[T, P]) GetGraphQLWrapped() T {
|
||||||
|
return w.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w Wrapper[T, P]) GetGraphQLType() string {
|
||||||
|
return w.gqlType
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWrapper[T any, P any](gqlType string) *Wrapper[T, P] {
|
||||||
|
return &Wrapper[T, P]{
|
||||||
|
gqlType: gqlType,
|
||||||
|
}
|
||||||
|
}
|
75
graphql/query.go
Normal file
75
graphql/query.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Query[ReturnType any, Params any] struct {
|
||||||
|
c *Client
|
||||||
|
gqltype string
|
||||||
|
containerLayers []string
|
||||||
|
|
||||||
|
additionalPayloadParams map[string]interface{}
|
||||||
|
additionalQueryParams map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQuery[ReturnType any, Params any](
|
||||||
|
c *Client,
|
||||||
|
gqlType string,
|
||||||
|
containerLayers []string,
|
||||||
|
) *Query[ReturnType, Params] {
|
||||||
|
return &Query[ReturnType, Params]{
|
||||||
|
c: c,
|
||||||
|
gqltype: gqlType,
|
||||||
|
containerLayers: containerLayers,
|
||||||
|
|
||||||
|
additionalPayloadParams: make(map[string]interface{}),
|
||||||
|
additionalQueryParams: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Query[ReturnType, Params]) WithPayloadParam(
|
||||||
|
name string,
|
||||||
|
value interface{},
|
||||||
|
) *Query[ReturnType, Params] {
|
||||||
|
r.additionalPayloadParams[name] = value
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Query[ReturnType, Params]) WithQueryParam(
|
||||||
|
name string,
|
||||||
|
value interface{},
|
||||||
|
) *Query[ReturnType, Params] {
|
||||||
|
r.additionalQueryParams[name] = value
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Query[ReturnType, Params]) Get(ctx context.Context, params Params) (ReturnType, error) {
|
||||||
|
|
||||||
|
paramsMap := convertParamsToMap[Params](params)
|
||||||
|
for k, v := range r.additionalPayloadParams {
|
||||||
|
paramsMap[k] = v
|
||||||
|
}
|
||||||
|
gqltype := r.gqltype
|
||||||
|
if len(paramsMap) > 0 {
|
||||||
|
keys := make([]string, 0, len(paramsMap))
|
||||||
|
for k := range paramsMap {
|
||||||
|
keys = append(keys, fmt.Sprintf("%s:$%s", k, k))
|
||||||
|
}
|
||||||
|
gqltype = fmt.Sprintf("%s(%s)", gqltype, strings.Join(keys, ","))
|
||||||
|
}
|
||||||
|
for k, v := range r.additionalQueryParams {
|
||||||
|
paramsMap[k] = v
|
||||||
|
}
|
||||||
|
q := NewPayload[ReturnType, Params](gqltype, r.containerLayers...)
|
||||||
|
err := r.c.Query(ctx, q, paramsMap)
|
||||||
|
if err != nil {
|
||||||
|
var res ReturnType
|
||||||
|
return res, errors.Wrap(err, "querying records")
|
||||||
|
}
|
||||||
|
return q.GetValue(), nil
|
||||||
|
}
|
128
mutations/accept_offer.go
Normal file
128
mutations/accept_offer.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type prepareAcceptOfferInput struct {
|
||||||
|
ClientMutationId string `json:"clientMutationId"`
|
||||||
|
OfferId string `json:"offerId"`
|
||||||
|
SettlementInfo SettlementInfo `json:"settlementInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrepareAcceptOfferParams struct {
|
||||||
|
Input prepareAcceptOfferInput `graphql:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) newPrepareAcceptOfferMutation() *graphql.Mutation[AuthorizationPayload, PrepareAcceptOfferParams] {
|
||||||
|
return graphql.NewMutation[AuthorizationPayload, PrepareAcceptOfferParams](
|
||||||
|
m.c,
|
||||||
|
"prepareAcceptOffer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type acceptOfferInput struct {
|
||||||
|
Approvals []AuthorizationApprovalInput `json:"approvals"`
|
||||||
|
ClientMutationId string `json:"clientMutationId"`
|
||||||
|
OfferId string `json:"offerId"`
|
||||||
|
SettlementInfo SettlementInfo `json:"settlementInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AcceptOfferParams struct {
|
||||||
|
Input acceptOfferInput `graphql:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) newAcceptOfferMutation() *graphql.Mutation[BaseMutationPayload, AcceptOfferParams] {
|
||||||
|
return graphql.NewMutation[BaseMutationPayload, AcceptOfferParams](
|
||||||
|
m.c,
|
||||||
|
"acceptOffer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AcceptOfferOptions struct {
|
||||||
|
PaymentMethod types.PaymentMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAcceptOfferPaymentMethod(paymentMethod types.PaymentMethod) func(*AcceptOfferOptions) {
|
||||||
|
return func(o *AcceptOfferOptions) {
|
||||||
|
o.PaymentMethod = paymentMethod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) AcceptOffer(
|
||||||
|
ctx context.Context,
|
||||||
|
offerId string,
|
||||||
|
currency types.SupportedCurrency,
|
||||||
|
privateKey string,
|
||||||
|
options ...func(*AcceptOfferOptions),
|
||||||
|
) error {
|
||||||
|
opts := AcceptOfferOptions{
|
||||||
|
PaymentMethod: types.PaymentMethodWallet,
|
||||||
|
}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(&opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
exchangeRate, err := m.ExchangeRate.Get(ctx, graphql.EmptyParams{})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "getting exchange rate")
|
||||||
|
}
|
||||||
|
si := SettlementInfo{
|
||||||
|
Currency: currency,
|
||||||
|
PaymentMethod: opts.PaymentMethod,
|
||||||
|
ExchangeRateId: exchangeRate.Id.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareAcceptOfferRes, err := m.newPrepareAcceptOfferMutation().Execute(
|
||||||
|
ctx,
|
||||||
|
PrepareAcceptOfferParams{
|
||||||
|
Input: prepareAcceptOfferInput{
|
||||||
|
ClientMutationId: GetRandomId(),
|
||||||
|
OfferId: offerId,
|
||||||
|
SettlementInfo: si,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "preparing accept offer")
|
||||||
|
}
|
||||||
|
if len(prepareAcceptOfferRes.Errors) > 0 {
|
||||||
|
return errors.Wrap(
|
||||||
|
errors.New(prepareAcceptOfferRes.Errors[0].Message),
|
||||||
|
"preparing accept offer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
approvals, err := signAuthorizationRequests(
|
||||||
|
privateKey,
|
||||||
|
prepareAcceptOfferRes.Authorizations,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "signing authorization requests")
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptOfferMutationRes, err := m.newAcceptOfferMutation().Execute(
|
||||||
|
ctx,
|
||||||
|
AcceptOfferParams{
|
||||||
|
Input: acceptOfferInput{
|
||||||
|
Approvals: approvals,
|
||||||
|
ClientMutationId: GetRandomId(),
|
||||||
|
OfferId: offerId,
|
||||||
|
SettlementInfo: si,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "accepting offer")
|
||||||
|
}
|
||||||
|
if len(acceptOfferMutationRes.Errors) > 0 {
|
||||||
|
return errors.Wrap(errors.New(acceptOfferMutationRes.Errors[0].Message), "accepting offer")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
8
mutations/amount_input.go
Normal file
8
mutations/amount_input.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import "git.lehouerou.net/laurent/sorare/types"
|
||||||
|
|
||||||
|
type AmountInput struct {
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
Currency types.SupportedCurrency `json:"currency"`
|
||||||
|
}
|
36
mutations/approval.go
Normal file
36
mutations/approval.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import "git.lehouerou.net/laurent/sorare/starkware"
|
||||||
|
|
||||||
|
type StarkExApprovalInput struct {
|
||||||
|
ExpirationTimestamp int `json:"expirationTimestamp"`
|
||||||
|
Nonce int `json:"nonce"`
|
||||||
|
Signature starkware.Signature `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MangopayWalletTransferApprovalInput struct {
|
||||||
|
Nonce int `json:"nonce"`
|
||||||
|
Signature starkware.Signature `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizationApprovalInput struct {
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
StarkExLimitOrderApproval *StarkExApprovalInput `json:"starkexLimitOrderApproval,omitempty"`
|
||||||
|
StarkExTransferApproval *StarkExApprovalInput `json:"starkexTransferApproval,omitempty"`
|
||||||
|
MangopayWalletTransferApproval *MangopayWalletTransferApprovalInput `json:"mangopayWalletTransferApproval,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func signAuthorizationRequests(
|
||||||
|
privatekey string,
|
||||||
|
requests []AuthorizationRequest,
|
||||||
|
) ([]AuthorizationApprovalInput, error) {
|
||||||
|
var approvals []AuthorizationApprovalInput
|
||||||
|
for _, request := range requests {
|
||||||
|
approval, err := request.sign(privatekey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
approvals = append(approvals, approval)
|
||||||
|
}
|
||||||
|
return approvals, nil
|
||||||
|
}
|
70
mutations/authorization_request.go
Normal file
70
mutations/authorization_request.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/starkware"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Typename string `graphql:"__typename"`
|
||||||
|
LimitOrder LimitOrder `graphql:"... on StarkexLimitOrderAuthorizationRequest"`
|
||||||
|
Transfer Transfer `graphql:"... on StarkexTransferAuthorizationRequest"`
|
||||||
|
MangoPayWalletTransfer MangopayWalletTransfer `graphql:"... on MangopayWalletTransferAuthorizationRequest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizationRequest struct {
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
Request Request `json:"request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r AuthorizationRequest) sign(privatekey string) (AuthorizationApprovalInput, error) {
|
||||||
|
switch r.Request.Typename {
|
||||||
|
case "StarkexLimitOrderAuthorizationRequest":
|
||||||
|
hash, err := r.Request.LimitOrder.hash()
|
||||||
|
if err != nil {
|
||||||
|
return AuthorizationApprovalInput{}, errors.Wrap(err, "hashing limit order")
|
||||||
|
}
|
||||||
|
signature := starkware.Sign(hash, privatekey)
|
||||||
|
return AuthorizationApprovalInput{
|
||||||
|
Fingerprint: r.Fingerprint,
|
||||||
|
StarkExLimitOrderApproval: &StarkExApprovalInput{
|
||||||
|
ExpirationTimestamp: r.Request.LimitOrder.ExpirationTimestamp,
|
||||||
|
Nonce: r.Request.LimitOrder.Nonce,
|
||||||
|
Signature: signature,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
case "StarkexTransferAuthorizationRequest":
|
||||||
|
hash, err := r.Request.Transfer.hash()
|
||||||
|
if err != nil {
|
||||||
|
return AuthorizationApprovalInput{}, errors.Wrap(err, "hashing transfer")
|
||||||
|
}
|
||||||
|
signature := starkware.Sign(hash, privatekey)
|
||||||
|
return AuthorizationApprovalInput{
|
||||||
|
Fingerprint: r.Fingerprint,
|
||||||
|
StarkExTransferApproval: &StarkExApprovalInput{
|
||||||
|
ExpirationTimestamp: r.Request.Transfer.ExpirationTimestamp,
|
||||||
|
Nonce: r.Request.Transfer.Nonce,
|
||||||
|
Signature: signature,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
case "MangopayWalletTransferAuthorizationRequest":
|
||||||
|
hash, err := r.Request.MangoPayWalletTransfer.hash()
|
||||||
|
if err != nil {
|
||||||
|
return AuthorizationApprovalInput{}, errors.Wrap(err, "hashing transfer")
|
||||||
|
}
|
||||||
|
signature := starkware.Sign(hash, privatekey)
|
||||||
|
return AuthorizationApprovalInput{
|
||||||
|
Fingerprint: r.Fingerprint,
|
||||||
|
MangopayWalletTransferApproval: &MangopayWalletTransferApprovalInput{
|
||||||
|
Nonce: r.Request.MangoPayWalletTransfer.Nonce,
|
||||||
|
Signature: signature,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return AuthorizationApprovalInput{}, errors.Errorf(
|
||||||
|
"unsupported request type %s",
|
||||||
|
r.Request.Typename,
|
||||||
|
)
|
||||||
|
}
|
113
mutations/bid.go
Normal file
113
mutations/bid.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
gql "github.com/llehouerou/go-graphql-client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bidInput struct {
|
||||||
|
Approvals []AuthorizationApprovalInput `json:"approvals"`
|
||||||
|
AuctionId string `json:"auctionId"`
|
||||||
|
Amount decimal.Decimal `json:"amount"`
|
||||||
|
SettlementInfo SettlementInfo `json:"settlementInfo"`
|
||||||
|
ClientMutationId string `json:"clientMutationId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BidPayload struct {
|
||||||
|
Bid struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
} `graphql:"bid"`
|
||||||
|
BaseMutationPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
type BidParams struct {
|
||||||
|
Input bidInput `graphql:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) newBidMutation() *graphql.Mutation[BidPayload, BidParams] {
|
||||||
|
return graphql.NewMutation[BidPayload, BidParams](m.c, "bid")
|
||||||
|
}
|
||||||
|
|
||||||
|
type BidOptions struct {
|
||||||
|
PaymentMethod types.PaymentMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBidPaymentMethod(paymentMethod types.PaymentMethod) func(*BidOptions) {
|
||||||
|
return func(o *BidOptions) {
|
||||||
|
o.PaymentMethod = paymentMethod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) BidOnAuction(
|
||||||
|
ctx context.Context,
|
||||||
|
auctionId string,
|
||||||
|
amount decimal.Decimal,
|
||||||
|
currency types.SupportedCurrency,
|
||||||
|
privateKey string,
|
||||||
|
options ...func(*BidOptions),
|
||||||
|
) (string, error) {
|
||||||
|
|
||||||
|
opts := BidOptions{
|
||||||
|
PaymentMethod: types.PaymentMethodWallet,
|
||||||
|
}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(&opts)
|
||||||
|
}
|
||||||
|
exchangeRate, err := m.ExchangeRate.Get(ctx, graphql.EmptyParams{})
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "getting exchange rate")
|
||||||
|
}
|
||||||
|
si := SettlementInfo{
|
||||||
|
Currency: currency,
|
||||||
|
PaymentMethod: opts.PaymentMethod,
|
||||||
|
ExchangeRateId: exchangeRate.Id.String(),
|
||||||
|
}
|
||||||
|
currencyShift := 2
|
||||||
|
if currency == types.SupportedCurrencyWEI {
|
||||||
|
currencyShift = 18
|
||||||
|
}
|
||||||
|
amount = amount.Shift(int32(currencyShift))
|
||||||
|
|
||||||
|
prepareBidRes, err := m.newPrepareBidMutation().Execute(ctx, PrepareBidParams{
|
||||||
|
Input: prepareBidInput{
|
||||||
|
AuctionId: gql.ID(auctionId),
|
||||||
|
Amount: amount,
|
||||||
|
SettlementInfo: si,
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "preparing bid")
|
||||||
|
}
|
||||||
|
if len(prepareBidRes.Errors) > 0 {
|
||||||
|
return "", errors.Wrap(errors.New(prepareBidRes.Errors[0].Message), "preparing bid")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvals, err := signAuthorizationRequests(
|
||||||
|
privateKey,
|
||||||
|
prepareBidRes.Authorizations,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "signing authorization requests")
|
||||||
|
}
|
||||||
|
|
||||||
|
bidRes, err := m.newBidMutation().Execute(ctx, BidParams{
|
||||||
|
Input: bidInput{
|
||||||
|
Approvals: approvals,
|
||||||
|
AuctionId: auctionId,
|
||||||
|
Amount: amount,
|
||||||
|
SettlementInfo: si,
|
||||||
|
ClientMutationId: GetRandomId(),
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "placing bid")
|
||||||
|
}
|
||||||
|
if len(bidRes.Errors) > 0 {
|
||||||
|
return "", errors.Wrap(errors.New(bidRes.Errors[0].Message), "placing bid")
|
||||||
|
}
|
||||||
|
return bidRes.Bid.Id.String(), nil
|
||||||
|
}
|
78
mutations/buy_deliverable_shop_item.go
Normal file
78
mutations/buy_deliverable_shop_item.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
gql "github.com/llehouerou/go-graphql-client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostalAddressInput struct {
|
||||||
|
AdditionalAddress string `graphql:"additionalAddress"`
|
||||||
|
Company string `graphql:"company"`
|
||||||
|
FirstName string `graphql:"firstName"`
|
||||||
|
LastName string `graphql:"lastName"`
|
||||||
|
StreetAddress string `graphql:"streetAddress"`
|
||||||
|
ZipCode string `graphql:"zipcode"`
|
||||||
|
City string `graphql:"city"`
|
||||||
|
CountryCode string `graphql:"countryCode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShirtSize string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ShirtSizeS ShirtSize = "S"
|
||||||
|
ShirtSizeM ShirtSize = "M"
|
||||||
|
ShirtSizeL ShirtSize = "L"
|
||||||
|
ShirtSizeXL ShirtSize = "XL"
|
||||||
|
ShirtSizeXS ShirtSize = "XS"
|
||||||
|
ShirtSizeXXL ShirtSize = "XXL"
|
||||||
|
)
|
||||||
|
|
||||||
|
type buyDeliverableShopItemInput struct {
|
||||||
|
ClientMutationId string `json:"clientMutationId"`
|
||||||
|
ShopItemId gql.ID `json:"shopItemId"`
|
||||||
|
ShirtSize ShirtSize `json:"shirtSize"`
|
||||||
|
PostalAddress PostalAddressInput `json:"postalAddress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuyDeliverableShoptItemParams struct {
|
||||||
|
Input buyDeliverableShopItemInput `graphql:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) newBuyDeliverableShopItemMutation() *graphql.Mutation[BaseMutationPayload, BuyDeliverableShoptItemParams] {
|
||||||
|
return graphql.NewMutation[BaseMutationPayload, BuyDeliverableShoptItemParams](
|
||||||
|
m.c,
|
||||||
|
"buyDeliverableShopItem",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) BuyDeliverableShopItem(
|
||||||
|
ctx context.Context,
|
||||||
|
shopItemId string,
|
||||||
|
size string,
|
||||||
|
address PostalAddressInput,
|
||||||
|
) error {
|
||||||
|
res, err := m.newBuyDeliverableShopItemMutation().Execute(ctx, BuyDeliverableShoptItemParams{
|
||||||
|
Input: buyDeliverableShopItemInput{
|
||||||
|
ClientMutationId: GetRandomId(),
|
||||||
|
ShopItemId: gql.ID(shopItemId),
|
||||||
|
ShirtSize: ShirtSize(size),
|
||||||
|
PostalAddress: address,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "executing buyDeliverableShopItem mutation")
|
||||||
|
}
|
||||||
|
if res.Errors != nil {
|
||||||
|
return errors.Wrap(
|
||||||
|
errors.New(res.Errors[0].Message),
|
||||||
|
"executing buyDeliverableShopItem mutation",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
38
mutations/cancel_offer.go
Normal file
38
mutations/cancel_offer.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cancelOfferInput struct {
|
||||||
|
BlockchainId string `json:"blockchainId"`
|
||||||
|
ClientMutationId string `json:"clientMutationId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CancelOfferParams struct {
|
||||||
|
Input cancelOfferInput `graphql:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) newCancelOfferMutation() *graphql.Mutation[BaseMutationPayload, CancelOfferParams] {
|
||||||
|
return graphql.NewMutation[BaseMutationPayload, CancelOfferParams](m.c, "cancelOffer")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) CancelOffer(ctx context.Context, blockchainId string) error {
|
||||||
|
res, err := m.newCancelOfferMutation().Execute(ctx, CancelOfferParams{
|
||||||
|
Input: cancelOfferInput{
|
||||||
|
BlockchainId: blockchainId,
|
||||||
|
ClientMutationId: GetRandomId(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cancelling offer")
|
||||||
|
}
|
||||||
|
if len(res.Errors) > 0 {
|
||||||
|
return errors.New(res.Errors[0].Message)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
151
mutations/create_direct_offer.go
Normal file
151
mutations/create_direct_offer.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createDirectOfferInput struct {
|
||||||
|
Approvals []AuthorizationApprovalInput `json:"approvals"`
|
||||||
|
ClientMutationId string `json:"clientMutationId"`
|
||||||
|
CounteredOfferId string `json:"counteredOfferId"`
|
||||||
|
DealId string `json:"dealId"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
ReceiveAmount AmountInput `json:"receiveAmount"`
|
||||||
|
ReceiveAssetIds []string `json:"receiveAssetIds"`
|
||||||
|
ReceiverSlug string `json:"receiverSlug"`
|
||||||
|
SendAmount AmountInput `json:"sendAmount"`
|
||||||
|
SendAssetIds []string `json:"sendAssetIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDirectOfferParams struct {
|
||||||
|
Input createDirectOfferInput `json:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) newCreateDirectOfferMutation() *graphql.Mutation[BaseMutationPayload, CreateDirectOfferParams] {
|
||||||
|
return graphql.NewMutation[BaseMutationPayload, CreateDirectOfferParams](
|
||||||
|
m.c,
|
||||||
|
"createDirectOffer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDirectOfferOptions struct {
|
||||||
|
Duration time.Duration
|
||||||
|
CounteredOfferId string
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCreateDirectOfferDuration(duration time.Duration) func(*CreateDirectOfferOptions) {
|
||||||
|
return func(o *CreateDirectOfferOptions) {
|
||||||
|
o.Duration = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCreateDirectOfferCounteredOfferId(
|
||||||
|
counteredOfferId string,
|
||||||
|
) func(*CreateDirectOfferOptions) {
|
||||||
|
return func(o *CreateDirectOfferOptions) {
|
||||||
|
o.CounteredOfferId = counteredOfferId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) CreateDirectOffer(
|
||||||
|
ctx context.Context,
|
||||||
|
sentAmount decimal.Decimal,
|
||||||
|
sentCurrency types.SupportedCurrency,
|
||||||
|
sentAssetIds []string,
|
||||||
|
receivedAmount decimal.Decimal,
|
||||||
|
receivedCurrency types.SupportedCurrency,
|
||||||
|
receivedAssetIds []string,
|
||||||
|
privateKey string,
|
||||||
|
receiverSlug string,
|
||||||
|
options ...func(*CreateDirectOfferOptions),
|
||||||
|
) error {
|
||||||
|
opts := CreateDirectOfferOptions{
|
||||||
|
Duration: 2 * 24 * time.Hour,
|
||||||
|
CounteredOfferId: "",
|
||||||
|
}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(&opts)
|
||||||
|
}
|
||||||
|
sentCurrencyShift := 2
|
||||||
|
if sentCurrency == types.SupportedCurrencyWEI {
|
||||||
|
sentCurrencyShift = 18
|
||||||
|
}
|
||||||
|
sentAmount = sentAmount.Shift(int32(sentCurrencyShift))
|
||||||
|
receivedCurrencyShift := 2
|
||||||
|
if receivedCurrency == types.SupportedCurrencyWEI {
|
||||||
|
receivedCurrencyShift = 18
|
||||||
|
}
|
||||||
|
receivedAmount = receivedAmount.Shift(int32(receivedCurrencyShift))
|
||||||
|
|
||||||
|
prepareOfferRes, err := m.newPrepareOfferMutation().Execute(ctx, PrepareOfferParams{
|
||||||
|
Input: prepareOfferInput{
|
||||||
|
ClientMutationId: GetRandomId(),
|
||||||
|
ReceiveAmount: &AmountInput{
|
||||||
|
Amount: receivedAmount.String(),
|
||||||
|
Currency: receivedCurrency,
|
||||||
|
},
|
||||||
|
ReceiveAssetIds: receivedAssetIds,
|
||||||
|
ReceiverSlug: receiverSlug,
|
||||||
|
SendAmount: &AmountInput{
|
||||||
|
Amount: sentAmount.String(),
|
||||||
|
Currency: sentCurrency,
|
||||||
|
},
|
||||||
|
SendAssetIds: sentAssetIds,
|
||||||
|
Type: types.OfferTypeDirectOffer,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "preparing offer")
|
||||||
|
}
|
||||||
|
if len(prepareOfferRes.Errors) > 0 {
|
||||||
|
return errors.Wrap(errors.New(prepareOfferRes.Errors[0].Message), "preparing offer")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvals, err := signAuthorizationRequests(
|
||||||
|
privateKey,
|
||||||
|
prepareOfferRes.Authorizations,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "signing authorization requests")
|
||||||
|
}
|
||||||
|
|
||||||
|
createDirectOfferRes, err := m.newCreateDirectOfferMutation().
|
||||||
|
Execute(ctx, CreateDirectOfferParams{
|
||||||
|
Input: createDirectOfferInput{
|
||||||
|
Approvals: approvals,
|
||||||
|
ClientMutationId: GetRandomId(),
|
||||||
|
CounteredOfferId: opts.CounteredOfferId,
|
||||||
|
DealId: GetRandomId(),
|
||||||
|
Duration: int(opts.Duration.Seconds()),
|
||||||
|
ReceiveAmount: AmountInput{
|
||||||
|
Amount: receivedAmount.String(),
|
||||||
|
Currency: receivedCurrency,
|
||||||
|
},
|
||||||
|
ReceiveAssetIds: receivedAssetIds,
|
||||||
|
ReceiverSlug: receiverSlug,
|
||||||
|
SendAmount: AmountInput{
|
||||||
|
Amount: sentAmount.String(),
|
||||||
|
Currency: sentCurrency,
|
||||||
|
},
|
||||||
|
SendAssetIds: sentAssetIds,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating direct offer")
|
||||||
|
}
|
||||||
|
if len(createDirectOfferRes.Errors) > 0 {
|
||||||
|
return errors.Wrap(
|
||||||
|
errors.New(createDirectOfferRes.Errors[0].Message),
|
||||||
|
"creating direct offer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
148
mutations/create_single_sale_offer.go
Normal file
148
mutations/create_single_sale_offer.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createSingleSaleOfferInput struct {
|
||||||
|
Approvals []AuthorizationApprovalInput `json:"approvals"`
|
||||||
|
AssetId string `json:"assetId"`
|
||||||
|
ClientMutationId string `json:"clientMutationId"`
|
||||||
|
DealId string `json:"dealId"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
ReceiveAmount AmountInput `json:"receiveAmount"`
|
||||||
|
SettlementCurrencies []types.SupportedCurrency `json:"settlementCurrencies"`
|
||||||
|
StartDate string `json:"startDate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateSingleSaleOfferParams struct {
|
||||||
|
Input createSingleSaleOfferInput `graphql:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) newCreateSingleSaleOfferMutation() *graphql.Mutation[BaseMutationPayload, CreateSingleSaleOfferParams] {
|
||||||
|
return graphql.NewMutation[BaseMutationPayload, CreateSingleSaleOfferParams](
|
||||||
|
m.c,
|
||||||
|
"createSingleSaleOffer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateSingleSaleOfferOptions struct {
|
||||||
|
Duration time.Duration
|
||||||
|
DelayBeforeStart time.Duration
|
||||||
|
SettlementCurrencies []types.SupportedCurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDuration(duration time.Duration) func(*CreateSingleSaleOfferOptions) {
|
||||||
|
return func(o *CreateSingleSaleOfferOptions) {
|
||||||
|
o.Duration = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDelayBeforeStart(delay time.Duration) func(*CreateSingleSaleOfferOptions) {
|
||||||
|
return func(o *CreateSingleSaleOfferOptions) {
|
||||||
|
o.DelayBeforeStart = delay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSettlementCurrencies(
|
||||||
|
currencies []types.SupportedCurrency,
|
||||||
|
) func(*CreateSingleSaleOfferOptions) {
|
||||||
|
return func(o *CreateSingleSaleOfferOptions) {
|
||||||
|
o.SettlementCurrencies = currencies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) CreateSingleSaleOffer(
|
||||||
|
ctx context.Context,
|
||||||
|
assetId string,
|
||||||
|
amount decimal.Decimal,
|
||||||
|
currency types.SupportedCurrency,
|
||||||
|
privateKey string,
|
||||||
|
receiverSlug string,
|
||||||
|
opts ...func(*CreateSingleSaleOfferOptions),
|
||||||
|
) error {
|
||||||
|
options := CreateSingleSaleOfferOptions{
|
||||||
|
Duration: 2 * 24 * time.Hour,
|
||||||
|
DelayBeforeStart: 5 * time.Second,
|
||||||
|
SettlementCurrencies: []types.SupportedCurrency{
|
||||||
|
types.SupportedCurrencyWEI,
|
||||||
|
types.SupportedCurrencyEUR,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if len(m.defaultSettlementCurrencies) > 0 {
|
||||||
|
options.SettlementCurrencies = m.defaultSettlementCurrencies
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
currencyShift := 2
|
||||||
|
if currency == types.SupportedCurrencyWEI {
|
||||||
|
currencyShift = 18
|
||||||
|
}
|
||||||
|
amount = amount.Shift(int32(currencyShift))
|
||||||
|
prepareOfferRes, err := m.newPrepareOfferMutation().Execute(ctx, PrepareOfferParams{
|
||||||
|
Input: prepareOfferInput{
|
||||||
|
Type: types.OfferTypeSingleSaleOffer,
|
||||||
|
SendAssetIds: []string{assetId},
|
||||||
|
ReceiveAssetIds: make([]string, 0),
|
||||||
|
ReceiveAmount: &AmountInput{
|
||||||
|
Amount: amount.String(),
|
||||||
|
Currency: currency,
|
||||||
|
},
|
||||||
|
ReceiverSlug: receiverSlug,
|
||||||
|
ClientMutationId: GetRandomId(),
|
||||||
|
SettlementCurrencies: options.SettlementCurrencies,
|
||||||
|
}})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "preparing offer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(prepareOfferRes.Errors) > 0 {
|
||||||
|
return errors.Wrap(errors.New(prepareOfferRes.Errors[0].Message), "preparing offer")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvals, err := signAuthorizationRequests(
|
||||||
|
privateKey,
|
||||||
|
prepareOfferRes.Authorizations,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "signing authorization requests")
|
||||||
|
}
|
||||||
|
|
||||||
|
createSingleSaleOfferRes, err := m.newCreateSingleSaleOfferMutation().
|
||||||
|
Execute(ctx, CreateSingleSaleOfferParams{
|
||||||
|
Input: createSingleSaleOfferInput{
|
||||||
|
Approvals: approvals,
|
||||||
|
AssetId: assetId,
|
||||||
|
ClientMutationId: GetRandomId(),
|
||||||
|
DealId: GetRandomId(),
|
||||||
|
Duration: int(options.Duration.Seconds()),
|
||||||
|
ReceiveAmount: AmountInput{
|
||||||
|
Amount: amount.String(),
|
||||||
|
Currency: currency,
|
||||||
|
},
|
||||||
|
SettlementCurrencies: options.SettlementCurrencies,
|
||||||
|
StartDate: time.Now().
|
||||||
|
Add(options.DelayBeforeStart).
|
||||||
|
UTC().
|
||||||
|
Format(time.RFC3339),
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating single sale offer")
|
||||||
|
}
|
||||||
|
if len(createSingleSaleOfferRes.Errors) > 0 {
|
||||||
|
return errors.Wrap(
|
||||||
|
errors.New(createSingleSaleOfferRes.Errors[0].Message),
|
||||||
|
"creating single sale offer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
191
mutations/limit_order.go
Normal file
191
mutations/limit_order.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/starkware"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LimitOrder struct {
|
||||||
|
AmountBuy string `json:"amountBuy"`
|
||||||
|
AmountSell string `json:"amountSell"`
|
||||||
|
ExpirationTimestamp int `json:"expirationTimestamp"`
|
||||||
|
FeeInfo *struct {
|
||||||
|
FeeLimit string `json:"feeLimit"`
|
||||||
|
SourceVaultId int `json:"sourceVaultId"`
|
||||||
|
TokenId string `json:"tokenId"`
|
||||||
|
} `json:"feeInfo"`
|
||||||
|
Nonce int `json:"nonce"`
|
||||||
|
TokenBuy string `json:"tokenBuy"`
|
||||||
|
TokenSell string `json:"tokenSell"`
|
||||||
|
VaultIdBuy int `json:"vaultIdBuy"`
|
||||||
|
VaultIdSell int `json:"vaultIdSell"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o LimitOrder) hash() (string, error) {
|
||||||
|
vaultSellBn := big.NewInt(int64(o.VaultIdSell))
|
||||||
|
vaultBuyBn := big.NewInt(int64(o.VaultIdBuy))
|
||||||
|
amountSellInt, err := strconv.ParseInt(o.AmountSell, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "parsing amountSell")
|
||||||
|
}
|
||||||
|
amountSellBn := big.NewInt(amountSellInt)
|
||||||
|
amountBuyInt, err := strconv.ParseInt(o.AmountBuy, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "parsing amountBuy")
|
||||||
|
}
|
||||||
|
amountBuyBn := big.NewInt(amountBuyInt)
|
||||||
|
nonceBn := big.NewInt(int64(o.Nonce))
|
||||||
|
expirationTimestampBn := big.NewInt(int64(o.ExpirationTimestamp))
|
||||||
|
|
||||||
|
tokenSell := o.TokenSell
|
||||||
|
if strings.HasPrefix(tokenSell, "0x") {
|
||||||
|
tokenSell = tokenSell[2:]
|
||||||
|
}
|
||||||
|
tokenBuy := o.TokenBuy
|
||||||
|
if strings.HasPrefix(tokenBuy, "0x") {
|
||||||
|
tokenBuy = tokenBuy[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.FeeInfo != nil {
|
||||||
|
feeVaultIdBn := big.NewInt(int64(o.FeeInfo.SourceVaultId))
|
||||||
|
feeLimitInt, err := strconv.ParseInt(o.FeeInfo.FeeLimit, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "parsing feeLimit")
|
||||||
|
}
|
||||||
|
feeLimitBn := big.NewInt(feeLimitInt)
|
||||||
|
feeToken := o.FeeInfo.TokenId
|
||||||
|
if strings.HasPrefix(feeToken, "0x") {
|
||||||
|
feeToken = feeToken[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashLimitOrderWithFee(
|
||||||
|
big.NewInt(3),
|
||||||
|
vaultSellBn,
|
||||||
|
vaultBuyBn,
|
||||||
|
amountSellBn,
|
||||||
|
amountBuyBn,
|
||||||
|
nonceBn,
|
||||||
|
expirationTimestampBn,
|
||||||
|
tokenSell,
|
||||||
|
tokenBuy,
|
||||||
|
feeToken,
|
||||||
|
feeVaultIdBn,
|
||||||
|
feeLimitBn,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return hashLimitOrder(
|
||||||
|
big.NewInt(0),
|
||||||
|
vaultSellBn,
|
||||||
|
vaultBuyBn,
|
||||||
|
amountSellBn,
|
||||||
|
amountBuyBn,
|
||||||
|
nonceBn,
|
||||||
|
expirationTimestampBn,
|
||||||
|
tokenSell,
|
||||||
|
tokenBuy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashLimitOrderWithFee(
|
||||||
|
instructionType *big.Int,
|
||||||
|
vaultSell *big.Int,
|
||||||
|
vaultBuy *big.Int,
|
||||||
|
amountSell *big.Int,
|
||||||
|
amountBuy *big.Int,
|
||||||
|
nonce *big.Int,
|
||||||
|
expirationTimestamp *big.Int,
|
||||||
|
tokenSell string,
|
||||||
|
tokenBuy string,
|
||||||
|
feeToken string,
|
||||||
|
feeVaultId *big.Int,
|
||||||
|
feeLimit *big.Int) (string, error) {
|
||||||
|
|
||||||
|
packedMessage1 := big.NewInt(0).Set(amountSell)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 64)
|
||||||
|
packedMessage1.Add(packedMessage1, amountBuy)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 64)
|
||||||
|
packedMessage1.Add(packedMessage1, feeLimit)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 32)
|
||||||
|
packedMessage1.Add(packedMessage1, nonce)
|
||||||
|
|
||||||
|
packedMessage2 := big.NewInt(0).Set(instructionType)
|
||||||
|
packedMessage2.Lsh(packedMessage2, 64)
|
||||||
|
packedMessage2.Add(packedMessage2, feeVaultId)
|
||||||
|
packedMessage2.Lsh(packedMessage2, 64)
|
||||||
|
packedMessage2.Add(packedMessage2, vaultSell)
|
||||||
|
packedMessage2.Lsh(packedMessage2, 64)
|
||||||
|
packedMessage2.Add(packedMessage2, vaultBuy)
|
||||||
|
packedMessage2.Lsh(packedMessage2, 32)
|
||||||
|
packedMessage2.Add(packedMessage2, expirationTimestamp)
|
||||||
|
packedMessage2.Lsh(packedMessage2, 17)
|
||||||
|
packedMessage2.Add(packedMessage2, big.NewInt(0))
|
||||||
|
|
||||||
|
tokenSellbn, ok := big.NewInt(0).SetString(tokenSell, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("token0 is not a hex string: %s", tokenSell)
|
||||||
|
}
|
||||||
|
tokenBuybn, ok := big.NewInt(0).SetString(tokenBuy, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("token1 is not a hex string: %s", tokenBuy)
|
||||||
|
}
|
||||||
|
feeTokenbn, ok := big.NewInt(0).SetString(feeToken, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("feeToken is not a hex string: %s", feeToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpHash := starkware.PedersenHash(
|
||||||
|
starkware.PedersenHash(tokenSellbn.String(), tokenBuybn.String()),
|
||||||
|
feeTokenbn.String(),
|
||||||
|
)
|
||||||
|
return starkware.PedersenHash(
|
||||||
|
starkware.PedersenHash(tmpHash, packedMessage1.String()),
|
||||||
|
packedMessage2.String(),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashLimitOrder(
|
||||||
|
instructionType *big.Int,
|
||||||
|
vaultSell *big.Int,
|
||||||
|
vaultBuy *big.Int,
|
||||||
|
amountSell *big.Int,
|
||||||
|
amountBuy *big.Int,
|
||||||
|
nonce *big.Int,
|
||||||
|
expirationTimestamp *big.Int,
|
||||||
|
tokenSell string,
|
||||||
|
tokenBuy string,
|
||||||
|
) (string, error) {
|
||||||
|
packedMessage := big.NewInt(0).Set(instructionType)
|
||||||
|
packedMessage.Lsh(packedMessage, 31)
|
||||||
|
packedMessage.Add(packedMessage, vaultSell)
|
||||||
|
packedMessage.Lsh(packedMessage, 31)
|
||||||
|
packedMessage.Add(packedMessage, vaultBuy)
|
||||||
|
packedMessage.Lsh(packedMessage, 63)
|
||||||
|
packedMessage.Add(packedMessage, amountSell)
|
||||||
|
packedMessage.Lsh(packedMessage, 63)
|
||||||
|
packedMessage.Add(packedMessage, amountBuy)
|
||||||
|
packedMessage.Lsh(packedMessage, 31)
|
||||||
|
packedMessage.Add(packedMessage, nonce)
|
||||||
|
packedMessage.Lsh(packedMessage, 22)
|
||||||
|
packedMessage.Add(packedMessage, expirationTimestamp)
|
||||||
|
|
||||||
|
tokenSellbn, ok := big.NewInt(0).SetString(tokenSell, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("tokenSell is not a hex string: %s", tokenSell)
|
||||||
|
}
|
||||||
|
tokenBuybn, ok := big.NewInt(0).SetString(tokenBuy, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("tokenBuy is not a hex string: %s", tokenBuy)
|
||||||
|
}
|
||||||
|
|
||||||
|
return starkware.PedersenHash(
|
||||||
|
starkware.PedersenHash(tokenSellbn.String(), tokenBuybn.String()),
|
||||||
|
packedMessage.String(),
|
||||||
|
), nil
|
||||||
|
}
|
56
mutations/mangopaywallet_transfer.go
Normal file
56
mutations/mangopaywallet_transfer.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/starkware"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MangopayWalletTransfer struct {
|
||||||
|
Amount int `graphql:"amount"`
|
||||||
|
Currency string `graphql:"currency"`
|
||||||
|
MangopayWalletId string `graphql:"mangopayWalletId"`
|
||||||
|
Nonce int `graphql:"nonce"`
|
||||||
|
OperationHash string `graphql:"operationHash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t MangopayWalletTransfer) hash() (string, error) {
|
||||||
|
message := fmt.Sprintf(
|
||||||
|
"%s:%s:%s:%d:%d",
|
||||||
|
t.MangopayWalletId,
|
||||||
|
t.OperationHash,
|
||||||
|
t.Currency,
|
||||||
|
t.Amount,
|
||||||
|
t.Nonce,
|
||||||
|
)
|
||||||
|
return hashMessage(message)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashMessage(
|
||||||
|
message string) (string, error) {
|
||||||
|
if message == "" {
|
||||||
|
return "", errors.New("message cannot be empty")
|
||||||
|
}
|
||||||
|
h := sha256.Sum256([]byte(message))
|
||||||
|
hexH := hex.EncodeToString(h[:])
|
||||||
|
|
||||||
|
if len(hexH) < 64 {
|
||||||
|
return "", errors.New("invalid hash length")
|
||||||
|
}
|
||||||
|
hexH1Bn, ok := big.NewInt(0).SetString(hexH[:32], 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("invalid hexH1")
|
||||||
|
}
|
||||||
|
hexH2Bn, ok := big.NewInt(0).SetString(hexH[32:], 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("invalid hexH2")
|
||||||
|
}
|
||||||
|
|
||||||
|
return starkware.PedersenHash(hexH1Bn.String(), hexH2Bn.String()), nil
|
||||||
|
}
|
49
mutations/mutations.go
Normal file
49
mutations/mutations.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExchangeRate struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutations struct {
|
||||||
|
c *graphql.Client
|
||||||
|
|
||||||
|
ExchangeRate *graphql.Query[ExchangeRate, graphql.EmptyParams]
|
||||||
|
|
||||||
|
defaultSettlementCurrencies []types.SupportedCurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMutations(c *graphql.Client) *Mutations {
|
||||||
|
return &Mutations{
|
||||||
|
c: c,
|
||||||
|
ExchangeRate: graphql.NewQuery[ExchangeRate, graphql.EmptyParams](
|
||||||
|
c,
|
||||||
|
"exchangeRate",
|
||||||
|
[]string{"config"},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) SetDefaultSettlementCurrencies(currencies []types.SupportedCurrency) {
|
||||||
|
m.defaultSettlementCurrencies = currencies
|
||||||
|
}
|
||||||
|
|
||||||
|
type MutationError struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
Path []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseMutationPayload struct {
|
||||||
|
Errors []MutationError `graphql:"errors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizationPayload struct {
|
||||||
|
BaseMutationPayload
|
||||||
|
Authorizations []AuthorizationRequest `graphql:"authorizations"`
|
||||||
|
ClientMutationId string `graphql:"clientMutationId"`
|
||||||
|
}
|
22
mutations/prepare_bid.go
Normal file
22
mutations/prepare_bid.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
gql "github.com/llehouerou/go-graphql-client"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type prepareBidInput struct {
|
||||||
|
AuctionId gql.ID `json:"auctionId"`
|
||||||
|
Amount decimal.Decimal `json:"amount"`
|
||||||
|
SettlementInfo SettlementInfo `json:"settlementInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrepareBidParams struct {
|
||||||
|
Input prepareBidInput `graphql:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) newPrepareBidMutation() *graphql.Mutation[AuthorizationPayload, PrepareBidParams] {
|
||||||
|
return graphql.NewMutation[AuthorizationPayload, PrepareBidParams](m.c, "prepareBid")
|
||||||
|
}
|
25
mutations/prepare_offer.go
Normal file
25
mutations/prepare_offer.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type prepareOfferInput struct {
|
||||||
|
ClientMutationId string `json:"clientMutationId"`
|
||||||
|
ReceiveAmount *AmountInput `json:"receiveAmount"`
|
||||||
|
ReceiveAssetIds []string `json:"receiveAssetIds"`
|
||||||
|
ReceiverSlug string `json:"receiverSlug"`
|
||||||
|
SendAmount *AmountInput `json:"sendAmount"`
|
||||||
|
SendAssetIds []string `json:"sendAssetIds"`
|
||||||
|
SettlementCurrencies []types.SupportedCurrency `json:"settlementCurrencies"`
|
||||||
|
Type types.OfferType `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrepareOfferParams struct {
|
||||||
|
Input prepareOfferInput `graphql:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) newPrepareOfferMutation() *graphql.Mutation[AuthorizationPayload, PrepareOfferParams] {
|
||||||
|
return graphql.NewMutation[AuthorizationPayload, PrepareOfferParams](m.c, "prepareOffer")
|
||||||
|
}
|
12
mutations/random.go
Normal file
12
mutations/random.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetRandomId() string {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
_, _ = rand.Read(b)
|
||||||
|
return fmt.Sprintf("%x", b)
|
||||||
|
}
|
40
mutations/reject_offer.go
Normal file
40
mutations/reject_offer.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rejectOfferInput struct {
|
||||||
|
BlockchainId string `json:"blockchainId"`
|
||||||
|
ClientMutationId string `json:"clientMutationId"`
|
||||||
|
Block bool `json:"block"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RejectOfferParams struct {
|
||||||
|
Input rejectOfferInput `graphql:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) newRejectOfferMutation() *graphql.Mutation[BaseMutationPayload, RejectOfferParams] {
|
||||||
|
return graphql.NewMutation[BaseMutationPayload, RejectOfferParams](m.c, "rejectOffer")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutations) RejectOffer(ctx context.Context, blockchainId string, block bool) error {
|
||||||
|
res, err := m.newRejectOfferMutation().Execute(ctx, RejectOfferParams{
|
||||||
|
Input: rejectOfferInput{
|
||||||
|
BlockchainId: blockchainId,
|
||||||
|
ClientMutationId: GetRandomId(),
|
||||||
|
Block: block,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "rejecting offer")
|
||||||
|
}
|
||||||
|
if len(res.Errors) > 0 {
|
||||||
|
return errors.New(res.Errors[0].Message)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
9
mutations/settlement_info.go
Normal file
9
mutations/settlement_info.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import "git.lehouerou.net/laurent/sorare/types"
|
||||||
|
|
||||||
|
type SettlementInfo struct {
|
||||||
|
Currency types.SupportedCurrency `json:"currency"`
|
||||||
|
PaymentMethod types.PaymentMethod `json:"paymentMethod"`
|
||||||
|
ExchangeRateId string `json:"exchangeRateId"`
|
||||||
|
}
|
222
mutations/transfer.go
Normal file
222
mutations/transfer.go
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
package mutations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/starkware"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Transfer struct {
|
||||||
|
Amount string `graphql:"amountAsString:amount"`
|
||||||
|
Nonce int `graphql:"nonce"`
|
||||||
|
SenderVaultId int `graphql:"senderVaultId"`
|
||||||
|
Token string `graphql:"token"`
|
||||||
|
ReceiverVaultId int `graphql:"receiverVaultId"`
|
||||||
|
ReceiverPublicKey string `graphql:"receiverPublicKey"`
|
||||||
|
ExpirationTimestamp int `graphql:"expirationTimestamp"`
|
||||||
|
Condition string `graphql:"condition"`
|
||||||
|
FeeInfoUser *struct {
|
||||||
|
FeeLimit string `graphql:"feeLimit"`
|
||||||
|
TokenId string `graphql:"tokenId"`
|
||||||
|
SourceVaultId int `graphql:"sourceVaultId"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Transfer) hash() (string, error) {
|
||||||
|
token := t.Token
|
||||||
|
if strings.HasPrefix(token, "0x") {
|
||||||
|
token = token[2:]
|
||||||
|
}
|
||||||
|
receiverPublicKey := t.ReceiverPublicKey
|
||||||
|
if strings.HasPrefix(receiverPublicKey, "0x") {
|
||||||
|
receiverPublicKey = receiverPublicKey[2:]
|
||||||
|
}
|
||||||
|
condition := t.Condition
|
||||||
|
if strings.HasPrefix(condition, "0x") {
|
||||||
|
condition = condition[2:]
|
||||||
|
}
|
||||||
|
amountInt, err := strconv.ParseInt(t.Amount, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "parsing amount")
|
||||||
|
}
|
||||||
|
amountBn := big.NewInt(amountInt)
|
||||||
|
nonceBn := big.NewInt(int64(t.Nonce))
|
||||||
|
senderVaultIdBn := big.NewInt(int64(t.SenderVaultId))
|
||||||
|
receiverVaultIdBn := big.NewInt(int64(t.ReceiverVaultId))
|
||||||
|
expirationTimestampBn := big.NewInt(int64(t.ExpirationTimestamp))
|
||||||
|
if t.FeeInfoUser != nil {
|
||||||
|
feeVaultIdBn := big.NewInt(int64(t.FeeInfoUser.SourceVaultId))
|
||||||
|
feeLimitInt, err := strconv.ParseInt(t.FeeInfoUser.FeeLimit, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "parsing feeLimit")
|
||||||
|
}
|
||||||
|
feeLimitBn := big.NewInt(feeLimitInt)
|
||||||
|
feeToken := t.FeeInfoUser.TokenId
|
||||||
|
if strings.HasPrefix(feeToken, "0x") {
|
||||||
|
feeToken = feeToken[2:]
|
||||||
|
}
|
||||||
|
instructionType := big.NewInt(4)
|
||||||
|
if t.Condition != "" {
|
||||||
|
instructionType = big.NewInt(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashTransferWithFee(
|
||||||
|
instructionType,
|
||||||
|
senderVaultIdBn,
|
||||||
|
receiverVaultIdBn,
|
||||||
|
amountBn,
|
||||||
|
nonceBn,
|
||||||
|
expirationTimestampBn,
|
||||||
|
token,
|
||||||
|
receiverPublicKey,
|
||||||
|
feeToken,
|
||||||
|
feeVaultIdBn,
|
||||||
|
feeLimitBn,
|
||||||
|
condition,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
instructionType := big.NewInt(1)
|
||||||
|
if t.Condition != "" {
|
||||||
|
instructionType = big.NewInt(2)
|
||||||
|
}
|
||||||
|
return hashMsg(
|
||||||
|
instructionType,
|
||||||
|
senderVaultIdBn,
|
||||||
|
receiverVaultIdBn,
|
||||||
|
amountBn,
|
||||||
|
big.NewInt(0),
|
||||||
|
nonceBn,
|
||||||
|
expirationTimestampBn,
|
||||||
|
token,
|
||||||
|
receiverPublicKey,
|
||||||
|
condition,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashMsg(
|
||||||
|
instructionType *big.Int,
|
||||||
|
senderVaultId *big.Int,
|
||||||
|
receiverVaultId *big.Int,
|
||||||
|
amount0 *big.Int,
|
||||||
|
amount1 *big.Int,
|
||||||
|
nonce *big.Int,
|
||||||
|
expirationTimestamp *big.Int,
|
||||||
|
transferToken string,
|
||||||
|
reveiverPublicKey string,
|
||||||
|
condition string) (string, error) {
|
||||||
|
|
||||||
|
packedMessage1 := big.NewInt(0).Set(instructionType)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 31)
|
||||||
|
packedMessage1.Add(packedMessage1, senderVaultId)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 31)
|
||||||
|
packedMessage1.Add(packedMessage1, receiverVaultId)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 63)
|
||||||
|
packedMessage1.Add(packedMessage1, amount0)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 63)
|
||||||
|
packedMessage1.Add(packedMessage1, amount1)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 31)
|
||||||
|
packedMessage1.Add(packedMessage1, nonce)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 22)
|
||||||
|
packedMessage1.Add(packedMessage1, expirationTimestamp)
|
||||||
|
|
||||||
|
transferTokenBn, ok := big.NewInt(0).SetString(transferToken, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("transferToken is not a hex string: %s", transferToken)
|
||||||
|
}
|
||||||
|
reveiverPublicKeyBn, ok := big.NewInt(0).SetString(reveiverPublicKey, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("reveiverPublicKey is not a hex string: %s", reveiverPublicKey)
|
||||||
|
}
|
||||||
|
if condition == "" {
|
||||||
|
return starkware.PedersenHash(
|
||||||
|
starkware.PedersenHash(transferTokenBn.String(), reveiverPublicKeyBn.String()),
|
||||||
|
packedMessage1.String(),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
conditionBn, ok := big.NewInt(0).SetString(condition, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("condition is not a hex string: %s", condition)
|
||||||
|
}
|
||||||
|
return starkware.PedersenHash(
|
||||||
|
starkware.PedersenHash(
|
||||||
|
starkware.PedersenHash(
|
||||||
|
transferTokenBn.String(),
|
||||||
|
reveiverPublicKeyBn.String(),
|
||||||
|
),
|
||||||
|
conditionBn.String(),
|
||||||
|
),
|
||||||
|
packedMessage1.String(),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashTransferWithFee(
|
||||||
|
instructionType *big.Int,
|
||||||
|
senderVaultId *big.Int,
|
||||||
|
receiverVaultId *big.Int,
|
||||||
|
amount *big.Int,
|
||||||
|
nonce *big.Int,
|
||||||
|
expirationTimestamp *big.Int,
|
||||||
|
transferToken string,
|
||||||
|
reveiverPublicKey string,
|
||||||
|
feeToken string,
|
||||||
|
feeVaultId *big.Int,
|
||||||
|
feeLimit *big.Int,
|
||||||
|
condition string) (string, error) {
|
||||||
|
|
||||||
|
packedMessage1 := big.NewInt(0).Set(senderVaultId)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 64)
|
||||||
|
packedMessage1.Add(packedMessage1, receiverVaultId)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 64)
|
||||||
|
packedMessage1.Add(packedMessage1, feeVaultId)
|
||||||
|
packedMessage1.Lsh(packedMessage1, 32)
|
||||||
|
packedMessage1.Add(packedMessage1, nonce)
|
||||||
|
|
||||||
|
packedMessage2 := big.NewInt(0).Set(instructionType)
|
||||||
|
packedMessage2.Lsh(packedMessage2, 64)
|
||||||
|
packedMessage2.Add(packedMessage2, amount)
|
||||||
|
packedMessage2.Lsh(packedMessage2, 64)
|
||||||
|
packedMessage2.Add(packedMessage2, feeLimit)
|
||||||
|
packedMessage2.Lsh(packedMessage2, 32)
|
||||||
|
packedMessage2.Add(packedMessage2, expirationTimestamp)
|
||||||
|
packedMessage2.Lsh(packedMessage2, 81)
|
||||||
|
packedMessage2.Add(packedMessage2, big.NewInt(0))
|
||||||
|
|
||||||
|
transferTokenBn, ok := big.NewInt(0).SetString(transferToken, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("transferToken is not a hex string: %s", transferToken)
|
||||||
|
}
|
||||||
|
reveiverPublicKeyBn, ok := big.NewInt(0).SetString(reveiverPublicKey, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("reveiverPublicKey is not a hex string: %s", reveiverPublicKey)
|
||||||
|
}
|
||||||
|
feeTokenbn, ok := big.NewInt(0).SetString(feeToken, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("feeToken is not a hex string: %s", feeToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpHash := starkware.PedersenHash(
|
||||||
|
starkware.PedersenHash(transferTokenBn.String(), feeTokenbn.String()),
|
||||||
|
reveiverPublicKeyBn.String(),
|
||||||
|
)
|
||||||
|
if condition == "" {
|
||||||
|
return starkware.PedersenHash(
|
||||||
|
starkware.PedersenHash(tmpHash, packedMessage1.String()),
|
||||||
|
packedMessage2.String(),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
conditionBn, ok := big.NewInt(0).SetString(condition, 16)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("condition is not a hex string: %s", condition)
|
||||||
|
}
|
||||||
|
return starkware.PedersenHash(
|
||||||
|
starkware.PedersenHash(
|
||||||
|
starkware.PedersenHash(tmpHash, conditionBn.String()), packedMessage1.String()),
|
||||||
|
packedMessage2.String(),
|
||||||
|
), nil
|
||||||
|
}
|
17
nbaplayer.go
Normal file
17
nbaplayer.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package sorare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NbaPlayer struct {
|
||||||
|
Id string `graphql:"id"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
DisplayName string `graphql:"displayName"`
|
||||||
|
BirthDate time.Time `graphql:"birthDate"`
|
||||||
|
BirthPlaceCountry string `graphql:"birthPlaceCountry"`
|
||||||
|
AvatarImageUrl string `graphql:"avatarImageUrl"`
|
||||||
|
Team struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
}
|
||||||
|
}
|
34
shop_item.go
Normal file
34
shop_item.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package sorare
|
||||||
|
|
||||||
|
type ShopItem struct {
|
||||||
|
ShopItemInterface struct {
|
||||||
|
Id string `graphql:"id"`
|
||||||
|
Name string `graphql:"name"`
|
||||||
|
Price int `graphql:"price"`
|
||||||
|
} `graphql:"... on ShopItemInterface"`
|
||||||
|
JerseyShopItem struct {
|
||||||
|
CurrentStockCount int `graphql:"currentStockCount"`
|
||||||
|
InitialStockCount int `graphql:"initialStockCount"`
|
||||||
|
LimitPerUser int `graphql:"limitPerUser"`
|
||||||
|
} `graphql:"... on JerseyShopItem"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShopItemType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ShopItemTypeJersey ShopItemType = "JERSEY"
|
||||||
|
ShopItemTypeBanner ShopItemType = "BANNER"
|
||||||
|
ShopItemTypeLogo ShopItemType = "LOGO"
|
||||||
|
ShopItemTypeShield ShopItemType = "SHIELD"
|
||||||
|
ShopItemTypeLevelUp ShopItemType = "LEVEL_UP"
|
||||||
|
ShopItemTypeExtraSwap ShopItemType = "EXTRA_SWAP"
|
||||||
|
ShopItemTypeExtraTeamsCap ShopItemType = "EXTRA_TEAMS_CAP"
|
||||||
|
ShopItemTypeRecoveryKit ShopItemType = "RECOVERY_KIT"
|
||||||
|
ShopItemTypeXpRestore ShopItemType = "XP_RESTORE"
|
||||||
|
ShopItemTypeDeliverableWithNoVariant ShopItemType = "DELIVERABLE_WITH_NO_VARIANT"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShopItemsParams struct {
|
||||||
|
Types []ShopItemType `graphql:"types"`
|
||||||
|
UnlockedOnly bool `graphql:"unlockedOnly"`
|
||||||
|
}
|
2041
starkware/consts.go
Normal file
2041
starkware/consts.go
Normal file
File diff suppressed because it is too large
Load Diff
52
starkware/math_pedersen.go
Normal file
52
starkware/math_pedersen.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package starkware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
# Starkware crypto functions implemented in Golang.
|
||||||
|
#
|
||||||
|
# python source:
|
||||||
|
# https://github.com/starkware-libs/starkex-resources/blob/0f08e6c55ad88c93499f71f2af4a2e7ae0185cdf/crypto/starkware/crypto/signature/signature.py
|
||||||
|
*/
|
||||||
|
|
||||||
|
type PedersenCfg struct {
|
||||||
|
Comment string `json:"_comment"`
|
||||||
|
FieldPrime *big.Int `json:"FIELD_PRIME"`
|
||||||
|
FieldGen int `json:"FIELD_GEN"`
|
||||||
|
EcOrder *big.Int `json:"EC_ORDER"`
|
||||||
|
ALPHA int `json:"ALPHA"`
|
||||||
|
BETA *big.Int `json:"BETA"`
|
||||||
|
ConstantPoints [][2]*big.Int `json:"CONSTANT_POINTS"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var pedersenCfg PedersenCfg
|
||||||
|
|
||||||
|
var EcOrder = new(big.Int)
|
||||||
|
var FieldPrime = new(big.Int)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
_ = json.Unmarshal([]byte(pedersenParams), &pedersenCfg)
|
||||||
|
EcOrder = pedersenCfg.EcOrder
|
||||||
|
FieldPrime = pedersenCfg.FieldPrime
|
||||||
|
}
|
||||||
|
|
||||||
|
func PedersenHash(str ...string) string {
|
||||||
|
NElementBitsHash := FieldPrime.BitLen()
|
||||||
|
point := pedersenCfg.ConstantPoints[0]
|
||||||
|
for i, s := range str {
|
||||||
|
x, _ := big.NewInt(0).SetString(s, 10)
|
||||||
|
pointList := pedersenCfg.ConstantPoints[2+i*NElementBitsHash : 2+(i+1)*NElementBitsHash]
|
||||||
|
n := big.NewInt(0)
|
||||||
|
for _, pt := range pointList {
|
||||||
|
n.And(x, big.NewInt(1))
|
||||||
|
if n.Cmp(big.NewInt(0)) > 0 {
|
||||||
|
point = eccAdd(point, pt, FieldPrime)
|
||||||
|
}
|
||||||
|
x = x.Rsh(x, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return point[0].String()
|
||||||
|
}
|
126
starkware/math_rfc6979.go
Normal file
126
starkware/math_rfc6979.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package starkware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"hash"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rfc6979 implemented in Golang.
|
||||||
|
// copy from https://raw.githubusercontent.com/codahale/rfc6979/master/rfc6979.go
|
||||||
|
/*
|
||||||
|
Package rfc6979 is an implementation of RFC 6979's deterministic DSA.
|
||||||
|
Such signatures are compatible with standard Digital Signature Algorithm
|
||||||
|
(DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA) digital
|
||||||
|
signatures and can be processed with unmodified verifiers, which need not be
|
||||||
|
aware of the procedure described therein. Deterministic signatures retain
|
||||||
|
the cryptographic security features associated with digital signatures but
|
||||||
|
can be more easily implemented in various environments, since they do not
|
||||||
|
need access to a source of high-quality randomness.
|
||||||
|
(https://tools.ietf.org/html/rfc6979)
|
||||||
|
Provides functions similar to crypto/dsa and crypto/ecdsa.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// mac returns an HMAC of the given key and message.
|
||||||
|
func mac(alg func() hash.Hash, k, m, buf []byte) []byte {
|
||||||
|
h := hmac.New(alg, k)
|
||||||
|
h.Write(m)
|
||||||
|
return h.Sum(buf[:0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc6979#section-2.3.2
|
||||||
|
func bits2int(in []byte, qlen int) *big.Int {
|
||||||
|
vlen := len(in) * 8
|
||||||
|
v := new(big.Int).SetBytes(in)
|
||||||
|
if vlen > qlen {
|
||||||
|
v = new(big.Int).Rsh(v, uint(vlen-qlen))
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc6979#section-2.3.3
|
||||||
|
func int2octets(v *big.Int, rolen int) []byte {
|
||||||
|
out := v.Bytes()
|
||||||
|
|
||||||
|
// pad with zeros if it's too short
|
||||||
|
if len(out) < rolen {
|
||||||
|
out2 := make([]byte, rolen)
|
||||||
|
copy(out2[rolen-len(out):], out)
|
||||||
|
return out2
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop most significant bytes if it's too long
|
||||||
|
if len(out) > rolen {
|
||||||
|
out2 := make([]byte, rolen)
|
||||||
|
copy(out2, out[len(out)-rolen:])
|
||||||
|
return out2
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc6979#section-2.3.4
|
||||||
|
func bits2octets(in []byte, q *big.Int, qlen, rolen int) []byte {
|
||||||
|
z1 := bits2int(in, qlen)
|
||||||
|
z2 := new(big.Int).Sub(z1, q)
|
||||||
|
if z2.Sign() < 0 {
|
||||||
|
return int2octets(z1, rolen)
|
||||||
|
}
|
||||||
|
return int2octets(z2, rolen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc6979#section-3.2
|
||||||
|
func generateSecret(
|
||||||
|
q, x *big.Int,
|
||||||
|
alg func() hash.Hash,
|
||||||
|
hash []byte,
|
||||||
|
extraEntropy []byte,
|
||||||
|
) *big.Int {
|
||||||
|
qlen := q.BitLen()
|
||||||
|
holen := alg().Size()
|
||||||
|
rolen := (qlen + 7) >> 3
|
||||||
|
bx := append(int2octets(x, rolen), bits2octets(hash, q, qlen, rolen)...)
|
||||||
|
// extra_entropy - extra added data in binary form as per section-3.6 of rfc6979
|
||||||
|
if len(extraEntropy) > 0 {
|
||||||
|
bx = append(bx, extraEntropy...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step B
|
||||||
|
v := bytes.Repeat([]byte{0x01}, holen)
|
||||||
|
|
||||||
|
// Step C
|
||||||
|
k := bytes.Repeat([]byte{0x00}, holen)
|
||||||
|
|
||||||
|
// Step D
|
||||||
|
k = mac(alg, k, append(append(v, 0x00), bx...), k)
|
||||||
|
|
||||||
|
// Step E
|
||||||
|
v = mac(alg, k, v, v)
|
||||||
|
|
||||||
|
// Step F
|
||||||
|
k = mac(alg, k, append(append(v, 0x01), bx...), k)
|
||||||
|
|
||||||
|
// Step G
|
||||||
|
v = mac(alg, k, v, v)
|
||||||
|
|
||||||
|
// Step H
|
||||||
|
for {
|
||||||
|
// Step H1
|
||||||
|
var t []byte
|
||||||
|
|
||||||
|
// Step H2
|
||||||
|
for len(t) < qlen/8 {
|
||||||
|
v = mac(alg, k, v, v)
|
||||||
|
t = append(t, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step H3
|
||||||
|
secret := bits2int(t, qlen)
|
||||||
|
if secret.Cmp(one) >= 0 && secret.Cmp(q) < 0 {
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
k = mac(alg, k, append(v, 0x00), k)
|
||||||
|
v = mac(alg, k, v, v)
|
||||||
|
}
|
||||||
|
}
|
121
starkware/math_utils.go
Normal file
121
starkware/math_utils.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package starkware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
var zero = big.NewInt(0)
|
||||||
|
var one = big.NewInt(1)
|
||||||
|
var two = big.NewInt(2)
|
||||||
|
|
||||||
|
// ecMult Multiplies by m a point on the elliptic curve with equation y^2 = x^3 + alpha*x + beta mod p.
|
||||||
|
// Assumes the point is given in affine form (x, y) and that 0 < m < order(point).
|
||||||
|
func ecMult(m *big.Int, point [2]*big.Int, alpha int, p *big.Int) [2]*big.Int {
|
||||||
|
if m.Cmp(one) == 0 {
|
||||||
|
return point
|
||||||
|
}
|
||||||
|
//return point
|
||||||
|
if big.NewInt(0).Mod(m, two).Cmp(zero) == 0 {
|
||||||
|
return ecMult(big.NewInt(0).Quo(m, two), ecDouble(point, alpha, p), alpha, p)
|
||||||
|
}
|
||||||
|
return eccAdd(ecMult(big.NewInt(0).Sub(m, one), point, alpha, p), point, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ecDouble Doubles a point on an elliptic curve with the equation y^2 = x^3 + alpha*x + beta mod p.
|
||||||
|
func ecDouble(point [2]*big.Int, alpha int, p *big.Int) [2]*big.Int {
|
||||||
|
// m = div_mod(3 * point[0] * point[0] + alpha, 2 * point[1], p)
|
||||||
|
p1 := big.NewInt(3)
|
||||||
|
p1.Mul(p1, big.NewInt(0).Mul(point[0], point[0]))
|
||||||
|
p1.Add(p1, big.NewInt(int64(alpha)))
|
||||||
|
p2 := big.NewInt(0)
|
||||||
|
p2.Mul(two, point[1])
|
||||||
|
m := divMod(p1, p2, p)
|
||||||
|
// x = (m * m - 2 * point[0]) % p
|
||||||
|
x := big.NewInt(0)
|
||||||
|
x.Sub(big.NewInt(0).Mul(m, m), big.NewInt(0).Mul(two, point[0]))
|
||||||
|
x.Mod(x, p)
|
||||||
|
// y = (m * (point[0] - x) - point[1]) % p
|
||||||
|
y := big.NewInt(0)
|
||||||
|
y.Sub(big.NewInt(0).Mul(m, big.NewInt(0).Sub(point[0], x)), point[1])
|
||||||
|
y.Mod(y, p)
|
||||||
|
return [2]*big.Int{x, y}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes the point is given in affine form (x, y) and has y != 0.
|
||||||
|
|
||||||
|
// eccAdd Gets two points on an elliptic curve mod p and returns their sum.
|
||||||
|
// Assumes the points are given in affine form (x, y) and have different x coordinates.
|
||||||
|
func eccAdd(point1 [2]*big.Int, point2 [2]*big.Int, p *big.Int) [2]*big.Int {
|
||||||
|
// m = div_mod(point1[1] - point2[1], point1[0] - point2[0], p)
|
||||||
|
d1 := big.NewInt(0).Sub(point1[1], point2[1])
|
||||||
|
d2 := big.NewInt(0).Sub(point1[0], point2[0])
|
||||||
|
m := divMod(d1, d2, p)
|
||||||
|
|
||||||
|
// x = (m * m - point1[0] - point2[0]) % p
|
||||||
|
x := big.NewInt(0)
|
||||||
|
x.Sub(big.NewInt(0).Mul(m, m), point1[0])
|
||||||
|
x.Sub(x, point2[0])
|
||||||
|
x.Mod(x, p)
|
||||||
|
|
||||||
|
// y := (m*(point1[0]-x) - point1[1]) % p
|
||||||
|
y := big.NewInt(0)
|
||||||
|
y.Mul(m, big.NewInt(0).Sub(point1[0], x))
|
||||||
|
y.Sub(y, point1[1])
|
||||||
|
y.Mod(y, p)
|
||||||
|
|
||||||
|
return [2]*big.Int{x, y}
|
||||||
|
}
|
||||||
|
|
||||||
|
// divMod Finds a nonnegative integer 0 <= x < p such that (m * x) % p == n
|
||||||
|
func divMod(n, m, p *big.Int) *big.Int {
|
||||||
|
a, _, _ := igcdex(m, p)
|
||||||
|
// (n * a) % p
|
||||||
|
tmp := big.NewInt(0).Mul(n, a)
|
||||||
|
return tmp.Mod(tmp, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
igcdex
|
||||||
|
|
||||||
|
Returns x, y, g such that g = x*a + y*b = gcd(a, b).
|
||||||
|
|
||||||
|
>>> from sympy.core.numbers import igcdex
|
||||||
|
>>> igcdex(2, 3)
|
||||||
|
(-1, 1, 1)
|
||||||
|
>>> igcdex(10, 12)
|
||||||
|
(-1, 1, 2)
|
||||||
|
>>> x, y, g = igcdex(100, 2004)
|
||||||
|
>>> x, y, g
|
||||||
|
(-20, 1, 4)
|
||||||
|
>>> x*100 + y*2004
|
||||||
|
4
|
||||||
|
*/
|
||||||
|
func igcdex(a, b *big.Int) (*big.Int, *big.Int, *big.Int) {
|
||||||
|
if a.Cmp(zero) == 0 && a.Cmp(zero) == 0 {
|
||||||
|
return big.NewInt(0), big.NewInt(1), big.NewInt(0)
|
||||||
|
}
|
||||||
|
if a.Cmp(zero) == 0 {
|
||||||
|
return big.NewInt(0), big.NewInt(0).Quo(b, big.NewInt(0).Abs(b)), big.NewInt(0).Abs(b)
|
||||||
|
}
|
||||||
|
if b.Cmp(zero) == 0 {
|
||||||
|
return big.NewInt(0).Quo(a, big.NewInt(0).Abs(a)), big.NewInt(0), big.NewInt(0).Abs(a)
|
||||||
|
}
|
||||||
|
xSign := big.NewInt(1)
|
||||||
|
ySign := big.NewInt(1)
|
||||||
|
if a.Cmp(zero) == -1 {
|
||||||
|
a, xSign = a.Neg(a), big.NewInt(-1)
|
||||||
|
}
|
||||||
|
if b.Cmp(zero) == -1 {
|
||||||
|
b, ySign = b.Neg(b), big.NewInt(-1)
|
||||||
|
}
|
||||||
|
x, y, r, s := big.NewInt(1), big.NewInt(0), big.NewInt(0), big.NewInt(1)
|
||||||
|
for b.Cmp(zero) > 0 {
|
||||||
|
c, q := big.NewInt(0).Mod(a, b), big.NewInt(0).Quo(a, b)
|
||||||
|
a, b, r, s, x, y = b, c, big.NewInt(0).
|
||||||
|
Sub(x, big.NewInt(0).Mul(q, r)),
|
||||||
|
big.NewInt(0).
|
||||||
|
Sub(y, big.NewInt(0).Mul(big.NewInt(0).Neg(q), s)),
|
||||||
|
r, s
|
||||||
|
}
|
||||||
|
return x.Mul(x, xSign), y.Mul(y, ySign), a
|
||||||
|
}
|
84
starkware/sign.go
Normal file
84
starkware/sign.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package starkware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateKRfc6979(msgHash, priKey *big.Int, seed int) *big.Int {
|
||||||
|
msgHash = big.NewInt(0).Set(msgHash) // copy
|
||||||
|
bitMod := msgHash.BitLen() % 8
|
||||||
|
if bitMod <= 4 && bitMod >= 1 && msgHash.BitLen() > 248 {
|
||||||
|
msgHash.Mul(msgHash, big.NewInt(16))
|
||||||
|
}
|
||||||
|
var extra []byte
|
||||||
|
if seed > 0 {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
var data interface{}
|
||||||
|
if seed < 256 {
|
||||||
|
data = uint8(seed)
|
||||||
|
} else if seed < 65536 {
|
||||||
|
data = uint16(seed)
|
||||||
|
} else if seed < 4294967296 {
|
||||||
|
data = uint32(seed)
|
||||||
|
} else {
|
||||||
|
data = uint64(seed)
|
||||||
|
}
|
||||||
|
_ = binary.Write(buf, binary.BigEndian, data)
|
||||||
|
extra = buf.Bytes()
|
||||||
|
}
|
||||||
|
return generateSecret(EcOrder, priKey, sha256.New, msgHash.Bytes(), extra)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Signature struct {
|
||||||
|
R string `json:"r"`
|
||||||
|
S string `json:"s"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sign(messageHash string, privateKey string) Signature {
|
||||||
|
if strings.HasPrefix(privateKey, "0x") {
|
||||||
|
privateKey = privateKey[2:]
|
||||||
|
}
|
||||||
|
privKey, _ := new(big.Int).SetString(privateKey, 16)
|
||||||
|
msgHash, _ := new(big.Int).SetString(messageHash, 10)
|
||||||
|
seed := 0
|
||||||
|
EcGen := pedersenCfg.ConstantPoints[1]
|
||||||
|
alpha := pedersenCfg.ALPHA
|
||||||
|
nBit := big.NewInt(0).Exp(big.NewInt(2), NElementBitsEcdsa, nil)
|
||||||
|
for {
|
||||||
|
k := GenerateKRfc6979(msgHash, privKey, seed)
|
||||||
|
// Update seed for next iteration in case the value of k is bad.
|
||||||
|
if seed == 0 {
|
||||||
|
seed = 1
|
||||||
|
} else {
|
||||||
|
seed += 1
|
||||||
|
}
|
||||||
|
// Cannot fail because 0 < k < EC_ORDER and EC_ORDER is prime.
|
||||||
|
x := ecMult(k, EcGen, alpha, FieldPrime)[0]
|
||||||
|
// !(1 <= x < 2 ** N_ELEMENT_BITS_ECDSA)
|
||||||
|
if !(x.Cmp(one) > 0 && x.Cmp(nBit) < 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// msg_hash + r * priv_key
|
||||||
|
x1 := big.NewInt(0).Add(msgHash, big.NewInt(0).Mul(x, privKey))
|
||||||
|
// (msg_hash + r * priv_key) % EC_ORDER == 0
|
||||||
|
if big.NewInt(0).Mod(x1, EcOrder).Cmp(zero) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// w = div_mod(k, msg_hash + r * priv_key, EC_ORDER)
|
||||||
|
w := divMod(k, x1, EcOrder)
|
||||||
|
// not (1 <= w < 2 ** N_ELEMENT_BITS_ECDSA)
|
||||||
|
if !(w.Cmp(one) > 0 && w.Cmp(nBit) < 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s1 := divMod(one, w, EcOrder)
|
||||||
|
return Signature{
|
||||||
|
R: "0x" + fmt.Sprintf("%x", x),
|
||||||
|
S: "0x" + fmt.Sprintf("%x", s1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
subscriptions/subscriptions.go
Normal file
61
subscriptions/subscriptions.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package subscriptions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/football"
|
||||||
|
"git.lehouerou.net/laurent/sorare/tokens"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Subscriptions struct {
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubscriptions() *Subscriptions {
|
||||||
|
return &Subscriptions{
|
||||||
|
debug: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscriptions) SetDebug(debug bool) {
|
||||||
|
s.debug = debug
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscriptions) NewGameWasUpdatedSubscription(
|
||||||
|
ctx context.Context,
|
||||||
|
) (<-chan football.Game, error) {
|
||||||
|
return scheduleWs[football.Game](ctx, "gameWasUpdated", "", s.debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscriptions) NewTokenOfferWasUpdatedSubscription(
|
||||||
|
ctx context.Context,
|
||||||
|
sports ...types.Sport,
|
||||||
|
) (<-chan tokens.TokenOffer, error) {
|
||||||
|
params := ""
|
||||||
|
var sportsStr []string
|
||||||
|
for _, sport := range sports {
|
||||||
|
sportsStr = append(sportsStr, string(sport))
|
||||||
|
}
|
||||||
|
if len(sports) > 0 {
|
||||||
|
params = fmt.Sprintf("(sports:[%s])", strings.Join(sportsStr, ","))
|
||||||
|
}
|
||||||
|
return scheduleWs[tokens.TokenOffer](ctx, "tokenOfferWasUpdated", params, s.debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscriptions) NewTokenAuctionWasUpdatedSubscription(
|
||||||
|
ctx context.Context,
|
||||||
|
sports ...types.Sport,
|
||||||
|
) (<-chan tokens.TokenAuction, error) {
|
||||||
|
params := ""
|
||||||
|
var sportsStr []string
|
||||||
|
for _, sport := range sports {
|
||||||
|
sportsStr = append(sportsStr, string(sport))
|
||||||
|
}
|
||||||
|
if len(sports) > 0 {
|
||||||
|
params = fmt.Sprintf("(sports:[%s])", strings.Join(sportsStr, ","))
|
||||||
|
}
|
||||||
|
return scheduleWs[tokens.TokenAuction](ctx, "tokenAuctionWasUpdated", params, s.debug)
|
||||||
|
}
|
350
subscriptions/wsclient.go
Normal file
350
subscriptions/wsclient.go
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
package subscriptions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/llehouerou/go-graphql-client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const WSURL = "wss://ws.sorare.com/cable"
|
||||||
|
|
||||||
|
type wsClient[T any] struct {
|
||||||
|
cancel context.CancelFunc
|
||||||
|
c *websocket.Conn
|
||||||
|
|
||||||
|
Done chan struct{}
|
||||||
|
|
||||||
|
rawMessageChan chan []byte
|
||||||
|
writeChan chan []byte
|
||||||
|
|
||||||
|
lastPing time.Time
|
||||||
|
pingMutex *sync.Mutex
|
||||||
|
|
||||||
|
Data chan T
|
||||||
|
subscriptionName string
|
||||||
|
subscriptionParams string
|
||||||
|
|
||||||
|
channelId string
|
||||||
|
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWsClient[T any](
|
||||||
|
ctx context.Context,
|
||||||
|
subscriptionName string,
|
||||||
|
subscriptionParams string,
|
||||||
|
debug bool,
|
||||||
|
) (*wsClient[T], error) {
|
||||||
|
localctx, cancel := context.WithCancel(ctx)
|
||||||
|
channelId := uuid.New().String()[0:7]
|
||||||
|
w := &wsClient[T]{
|
||||||
|
cancel: cancel,
|
||||||
|
rawMessageChan: make(chan []byte),
|
||||||
|
writeChan: make(chan []byte),
|
||||||
|
lastPing: time.Now(),
|
||||||
|
pingMutex: &sync.Mutex{},
|
||||||
|
Data: make(chan T),
|
||||||
|
subscriptionName: subscriptionName,
|
||||||
|
subscriptionParams: subscriptionParams,
|
||||||
|
Done: make(chan struct{}),
|
||||||
|
debug: debug,
|
||||||
|
|
||||||
|
channelId: channelId,
|
||||||
|
}
|
||||||
|
err := w.connect()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "connecting websocket")
|
||||||
|
}
|
||||||
|
go w.processMessages()
|
||||||
|
go w.readMessages(localctx)
|
||||||
|
go w.writeMessages(localctx)
|
||||||
|
|
||||||
|
err = w.subscribe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "subscribing")
|
||||||
|
}
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsClient[T]) connect() error {
|
||||||
|
dialer := &websocket.Dialer{
|
||||||
|
HandshakeTimeout: 30 * time.Second,
|
||||||
|
EnableCompression: true,
|
||||||
|
}
|
||||||
|
connection, _, err := dialer.Dial(WSURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "dialing websocket")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.c = connection
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type WsMessage struct {
|
||||||
|
Type string
|
||||||
|
Message json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
type WsError struct {
|
||||||
|
Message string
|
||||||
|
Locations []struct {
|
||||||
|
Line int
|
||||||
|
Column int
|
||||||
|
}
|
||||||
|
Path []string
|
||||||
|
Extensions struct {
|
||||||
|
Code string
|
||||||
|
TypeName string `json:"typeName"`
|
||||||
|
ArgumentName string `json:"argumentName"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsClient[T]) processMessage(message json.RawMessage) error {
|
||||||
|
var m struct {
|
||||||
|
More bool
|
||||||
|
Result struct {
|
||||||
|
Data map[string]json.RawMessage
|
||||||
|
Errors []WsError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(message, &m)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "unmarshalling message")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.Result.Errors) > 0 {
|
||||||
|
return errors.New("graphql errors")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.Result.Data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, ok := m.Result.Data[w.subscriptionName]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if string(data) == "null" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmarshalledData T
|
||||||
|
err = graphql.UnmarshalGraphQL(data, &unmarshalledData)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "unmarshalling graphql data")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Data <- unmarshalledData
|
||||||
|
|
||||||
|
if !m.More {
|
||||||
|
w.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsClient[T]) Stop() {
|
||||||
|
w.cancel()
|
||||||
|
_ = w.c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsClient[T]) processMessages() {
|
||||||
|
defer close(w.Data)
|
||||||
|
for message := range w.rawMessageChan {
|
||||||
|
if w.debug {
|
||||||
|
fmt.Println("<-- " + string(message))
|
||||||
|
}
|
||||||
|
var m WsMessage
|
||||||
|
err := json.Unmarshal(message, &m)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("unmarshalling message")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m.Type {
|
||||||
|
case "welcome":
|
||||||
|
case "ping":
|
||||||
|
err = w.processPing(m.Message)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("unmarshalling message")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case "":
|
||||||
|
err = w.processMessage(m.Message)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("unmarshalling message")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PingMessage time.Time
|
||||||
|
|
||||||
|
func (p *PingMessage) UnmarshalJSON(bytes []byte) error {
|
||||||
|
var raw int64
|
||||||
|
err := json.Unmarshal(bytes, &raw)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "unmarshalling ping message")
|
||||||
|
}
|
||||||
|
*p = PingMessage(time.Unix(raw, 0))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PingMessage) Time() time.Time {
|
||||||
|
return time.Time(*p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsClient[T]) processPing(message json.RawMessage) error {
|
||||||
|
var m PingMessage
|
||||||
|
err := json.Unmarshal(message, &m)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "unmarshalling ping message")
|
||||||
|
}
|
||||||
|
w.pingMutex.Lock()
|
||||||
|
w.lastPing = m.Time()
|
||||||
|
w.pingMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsClient[T]) writeMessages(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case d := <-w.writeChan:
|
||||||
|
if w.debug {
|
||||||
|
fmt.Println("--> " + string(d))
|
||||||
|
}
|
||||||
|
err := w.c.WriteMessage(websocket.TextMessage, d)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsClient[T]) readMessages(ctx context.Context) {
|
||||||
|
defer close(w.rawMessageChan)
|
||||||
|
defer close(w.Done)
|
||||||
|
w.c.SetCloseHandler(func(code int, text string) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
w.c.SetPongHandler(func(pong string) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
w.c.SetPingHandler(func(ping string) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
t, msg, err := w.c.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case websocket.TextMessage:
|
||||||
|
w.rawMessageChan <- msg
|
||||||
|
case websocket.BinaryMessage:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type identifier struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
ChannelId string `json:"channelId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type message struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
Data string `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type queryData struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
Variables []string `json:"variables,omitempty"`
|
||||||
|
OperationName string `json:"operationName,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsClient[T]) sendMessage(message any) error {
|
||||||
|
data, err := json.Marshal(message)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "marshalling message")
|
||||||
|
}
|
||||||
|
w.writeChan <- data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsClient[T]) subscribe() error {
|
||||||
|
go func() {
|
||||||
|
identifier, err := json.Marshal(identifier{
|
||||||
|
Channel: "GraphqlChannel",
|
||||||
|
ChannelId: w.channelId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = w.sendMessage(message{
|
||||||
|
Command: "subscribe",
|
||||||
|
Identifier: string(identifier),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
var query struct {
|
||||||
|
SubscriptionName T
|
||||||
|
}
|
||||||
|
queryMarshalled, err := graphql.ConstructSubscription(query, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queryMarshalled = strings.Replace(
|
||||||
|
queryMarshalled,
|
||||||
|
"subscriptionName",
|
||||||
|
w.subscriptionName+w.subscriptionParams,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
querystr, err := json.Marshal(queryData{
|
||||||
|
Action: "execute",
|
||||||
|
Query: queryMarshalled,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.sendMessage(message{
|
||||||
|
Command: "message",
|
||||||
|
Identifier: string(identifier),
|
||||||
|
Data: string(querystr),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
116
subscriptions/wsscheduler.go
Normal file
116
subscriptions/wsscheduler.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package subscriptions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minDurationSeconds = 3600
|
||||||
|
maxDurationSeconds = 5400
|
||||||
|
sleepDuration = 10 * time.Second
|
||||||
|
maxRetries = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func scheduleWs[T any](
|
||||||
|
ctx context.Context,
|
||||||
|
subscriptionName string,
|
||||||
|
subscriptionParams string,
|
||||||
|
debug bool,
|
||||||
|
) (chan T, error) {
|
||||||
|
res := make(chan T)
|
||||||
|
go func() {
|
||||||
|
defer close(res)
|
||||||
|
duration := newRandomDuration(minDurationSeconds, maxDurationSeconds)
|
||||||
|
t := time.NewTicker(duration)
|
||||||
|
var ws *wsClient[T]
|
||||||
|
ws, err := getNewWsClientWithRetry[T](
|
||||||
|
ctx,
|
||||||
|
subscriptionName,
|
||||||
|
subscriptionParams,
|
||||||
|
res,
|
||||||
|
maxRetries,
|
||||||
|
debug,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
ws.Stop()
|
||||||
|
<-ws.Done
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-t.C:
|
||||||
|
|
||||||
|
newws, err := getNewWsClientWithRetry[T](
|
||||||
|
ctx,
|
||||||
|
subscriptionName,
|
||||||
|
subscriptionParams,
|
||||||
|
res,
|
||||||
|
maxRetries,
|
||||||
|
debug,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
ws.Stop()
|
||||||
|
ws = newws
|
||||||
|
case <-ws.Done:
|
||||||
|
|
||||||
|
newws, err := getNewWsClientWithRetry[T](
|
||||||
|
ctx,
|
||||||
|
subscriptionName,
|
||||||
|
subscriptionParams,
|
||||||
|
res,
|
||||||
|
maxRetries,
|
||||||
|
debug,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
ws.Stop()
|
||||||
|
ws = newws
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRandomDuration(minseconds int, maxseconds int) time.Duration {
|
||||||
|
return time.Duration(rand.Intn(maxseconds-minseconds)+minseconds) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNewWsClientWithRetry[T any](
|
||||||
|
ctx context.Context,
|
||||||
|
subscriptionName string,
|
||||||
|
subscriptionParams string,
|
||||||
|
resultChan chan T,
|
||||||
|
maxRetries int,
|
||||||
|
debug bool,
|
||||||
|
) (*wsClient[T], error) {
|
||||||
|
var ws *wsClient[T]
|
||||||
|
var err error
|
||||||
|
for i := 0; i < maxRetries; i++ {
|
||||||
|
ws, err = newWsClient[T](ctx, subscriptionName, subscriptionParams, debug)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(i+1) * sleepDuration)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "creating new websocket graphQlClient")
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for data := range ws.Data {
|
||||||
|
resultChan <- data
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return ws, nil
|
||||||
|
}
|
43
tokens/monetary_amount.go
Normal file
43
tokens/monetary_amount.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MonetaryAmount struct {
|
||||||
|
ReferenceCurrency types.SupportedCurrency `graphql:"referenceCurrency"`
|
||||||
|
Eur *decimal.Decimal `graphql:"eur"`
|
||||||
|
Usd *decimal.Decimal `graphql:"usd"`
|
||||||
|
Gbp *decimal.Decimal `graphql:"gbp"`
|
||||||
|
Wei *decimal.Decimal `graphql:"wei"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MonetaryAmount) AmountInReferenceCurrency() (decimal.Decimal, error) {
|
||||||
|
switch m.ReferenceCurrency {
|
||||||
|
case types.SupportedCurrencyEUR:
|
||||||
|
if m.Eur == nil {
|
||||||
|
return decimal.Zero, errors.Errorf("missing EUR amount")
|
||||||
|
}
|
||||||
|
return *m.Eur, nil
|
||||||
|
case types.SupportedCurrencyUSD:
|
||||||
|
if m.Usd == nil {
|
||||||
|
return decimal.Zero, errors.Errorf("missing USD amount")
|
||||||
|
}
|
||||||
|
return *m.Usd, nil
|
||||||
|
case types.SupportedCurrencyGBP:
|
||||||
|
if m.Gbp == nil {
|
||||||
|
return decimal.Zero, errors.Errorf("missing GBP amount")
|
||||||
|
}
|
||||||
|
return *m.Gbp, nil
|
||||||
|
case types.SupportedCurrencyWEI:
|
||||||
|
if m.Wei == nil {
|
||||||
|
return decimal.Zero, errors.Errorf("missing WEI amount")
|
||||||
|
}
|
||||||
|
return *m.Wei, nil
|
||||||
|
default:
|
||||||
|
return decimal.Zero, errors.Errorf("unsupported currency %s", m.ReferenceCurrency)
|
||||||
|
}
|
||||||
|
}
|
48
tokens/token.go
Normal file
48
tokens/token.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenCardMetadata struct {
|
||||||
|
PlayerDisplayName string `graphql:"playerDisplayName"`
|
||||||
|
PlayerSlug string `graphql:"playerSlug"`
|
||||||
|
Rarity string `graphql:"rarity"`
|
||||||
|
SerialNumber int `graphql:"serialNumber"`
|
||||||
|
SeasonStartYear int `graphql:"seasonStartYear"`
|
||||||
|
SingleCivilYear bool `graphql:"singleCivilYear"`
|
||||||
|
Supply int `graphql:"supply"`
|
||||||
|
TeamSlug string `graphql:"teamSlug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
AssetId string `graphql:"assetId"`
|
||||||
|
Collection string `graphql:"collection"`
|
||||||
|
ContractAddress string `graphql:"contractAddress"`
|
||||||
|
EthereumId string `graphql:"ethereumId"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
LatestEnglishAuction *struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
} `graphql:"latestEnglishAuction"`
|
||||||
|
LiveSingleSaleOffer *struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
} `graphql:"liveSingleSaleOffer"`
|
||||||
|
Metadata struct {
|
||||||
|
Football TokenCardMetadata `graphql:"... on TokenCardMetadataInterface"`
|
||||||
|
}
|
||||||
|
Name string `graphql:"name"`
|
||||||
|
PictureUrl string `graphql:"pictureUrl(derivative: \"tinified\")"`
|
||||||
|
PrivateMinPrice decimal.Decimal `graphql:"privateMinPrice"`
|
||||||
|
PublicMinPrice decimal.Decimal `graphql:"publicMinPrice"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
Sport string `graphql:"sport"`
|
||||||
|
|
||||||
|
OwnershipHistory []TokenOwner `graphql:"ownershipHistory"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllNftsParams struct {
|
||||||
|
Sport []types.Sport `graphql:"sport"`
|
||||||
|
}
|
59
tokens/token_auction.go
Normal file
59
tokens/token_auction.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenBid struct {
|
||||||
|
Amount decimal.Decimal `graphql:"amount"`
|
||||||
|
AmountInFiat struct {
|
||||||
|
Eur decimal.Decimal `graphql:"eur"`
|
||||||
|
Usd decimal.Decimal `graphql:"usd"`
|
||||||
|
Gbp decimal.Decimal `graphql:"gbp"`
|
||||||
|
} `graphql:"amountInFiat"`
|
||||||
|
Amounts MonetaryAmount `graphql:"amounts"`
|
||||||
|
Auction struct {
|
||||||
|
Id string `graphql:"id"`
|
||||||
|
} `graphql:"auction"`
|
||||||
|
Bidder struct {
|
||||||
|
User struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
Nickname string `graphql:"nickname"`
|
||||||
|
} `graphql:"... on User"`
|
||||||
|
} `graphql:"bidder"`
|
||||||
|
CreatedAt time.Time `graphql:"createdAt"`
|
||||||
|
FiatPayment bool `graphql:"fiatPayment"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
MaximumAmount decimal.Decimal `graphql:"maximumAmount"`
|
||||||
|
MaximumAmountInFiat struct {
|
||||||
|
Eur decimal.Decimal `graphql:"eur"`
|
||||||
|
Usd decimal.Decimal `graphql:"usd"`
|
||||||
|
Gbp decimal.Decimal `graphql:"gbp"`
|
||||||
|
} `graphql:"maximumAmountInFiat"`
|
||||||
|
MaximumAmounts MonetaryAmount `graphql:"maximumAmounts"`
|
||||||
|
}
|
||||||
|
type TokenAuction struct {
|
||||||
|
AutoBid bool `graphql:"autoBid"`
|
||||||
|
BestBid TokenBid `graphql:"bestBid"`
|
||||||
|
BidsCount int `graphql:"bidsCount"`
|
||||||
|
BlockchainId string `graphql:"blockchainId"`
|
||||||
|
Cancelled bool `graphql:"cancelled"`
|
||||||
|
CreditCardFee decimal.Decimal `graphql:"creditCardFee"`
|
||||||
|
Currency types.SupportedCurrency `graphql:"currency"`
|
||||||
|
CurrentPrice decimal.Decimal `graphql:"currentPrice"`
|
||||||
|
EndDate time.Time `graphql:"endDate"`
|
||||||
|
HasTrade bool `graphql:"hasTrade"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
MinNextBid decimal.Decimal `graphql:"minNextBid"`
|
||||||
|
Nfts []TokenOfferNft `graphql:"nfts"`
|
||||||
|
Open bool `graphql:"open"`
|
||||||
|
StartDate time.Time `graphql:"startDate"`
|
||||||
|
Team string `graphql:"team"`
|
||||||
|
UpdatedAt time.Time `graphql:"updatedAt"`
|
||||||
|
}
|
49
tokens/token_offer.go
Normal file
49
tokens/token_offer.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenOfferNft struct {
|
||||||
|
AssetId string `graphql:"assetId"`
|
||||||
|
Collection string `graphql:"collection"`
|
||||||
|
Metadata struct {
|
||||||
|
TokenCardMetadataInterface TokenCardMetadata `graphql:"... on TokenCardMetadataInterface"`
|
||||||
|
} `graphql:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenOfferUser struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Nickname string `graphql:"nickname"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
EthVaultId int `graphql:"ethVaultId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenOfferSide struct {
|
||||||
|
Amounts MonetaryAmount `graphql:"amounts"`
|
||||||
|
Nfts []TokenOfferNft `graphql:"nfts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenOffer struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
BlockchainId string `graphql:"blockchainId"`
|
||||||
|
AcceptedAt time.Time `graphql:"acceptedAt"`
|
||||||
|
CreatedAt time.Time `graphql:"createdAt"`
|
||||||
|
CancelledAt time.Time `graphql:"cancelledAt"`
|
||||||
|
EndDate time.Time `graphql:"endDate"`
|
||||||
|
StartDate time.Time `graphql:"startDate"`
|
||||||
|
Status string
|
||||||
|
MarketFeeAmounts MonetaryAmount `graphql:"marketFeeAmounts"`
|
||||||
|
Receiver struct {
|
||||||
|
User TokenOfferUser `graphql:"... on User"`
|
||||||
|
} `graphql:"actualReceiver"`
|
||||||
|
ReceiverSide TokenOfferSide `graphql:"receiverSide"`
|
||||||
|
Sender struct {
|
||||||
|
User TokenOfferUser `graphql:"... on User"`
|
||||||
|
}
|
||||||
|
SenderSide TokenOfferSide `graphql:"senderSide"`
|
||||||
|
SettlementCurrencies []types.SupportedCurrency `graphql:"settlementCurrencies"`
|
||||||
|
}
|
28
tokens/token_owner.go
Normal file
28
tokens/token_owner.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenOwner struct {
|
||||||
|
Deal struct {
|
||||||
|
Typename string `graphql:"__typename"`
|
||||||
|
Node struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
} `graphql:"... on Node"`
|
||||||
|
} `graphql:"deal"`
|
||||||
|
From time.Time `graphql:"from"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Price MonetaryAmount `graphql:"price"`
|
||||||
|
SettleAt time.Time `graphql:"settleAt"`
|
||||||
|
Token struct {
|
||||||
|
AssetId string `graphql:"assetId"`
|
||||||
|
} `graphql:"token"`
|
||||||
|
TransferType types.OwnerTransfer `graphql:"transferType"`
|
||||||
|
User struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"user"`
|
||||||
|
}
|
40
tokens/token_price.go
Normal file
40
tokens/token_price.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenPrice struct {
|
||||||
|
Amount decimal.Decimal `graphql:"amount"`
|
||||||
|
AmountInFiat struct {
|
||||||
|
Eur decimal.Decimal `graphql:"eur"`
|
||||||
|
Usd decimal.Decimal `graphql:"usd"`
|
||||||
|
Gbp decimal.Decimal `graphql:"gbp"`
|
||||||
|
} `graphql:"amountInFiat"`
|
||||||
|
Amounts MonetaryAmount `graphql:"amounts"`
|
||||||
|
Date time.Time `graphql:"date"`
|
||||||
|
Deal struct {
|
||||||
|
Typename string `graphql:"__typename"`
|
||||||
|
TokenAuction struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
} `graphql:"... on TokenAuction"`
|
||||||
|
TokenPrimaryOffer struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
} `graphql:"... on TokenPrimaryOffer"`
|
||||||
|
TokenOffer struct {
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
} `graphql:"... on TokenOffer"`
|
||||||
|
}
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenPricesParams struct {
|
||||||
|
Collection types.Collection `graphql:"collection"`
|
||||||
|
PlayerSlug string `graphql:"playerSlug"`
|
||||||
|
Rarity types.Rarity `graphql:"rarity"`
|
||||||
|
}
|
25
tokens/token_primary_offer.go
Normal file
25
tokens/token_primary_offer.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenPrimaryOffer struct {
|
||||||
|
AppleProductId string `graphql:"appleProductId"`
|
||||||
|
Buyer struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
} `graphql:"buyer"`
|
||||||
|
CancelledAt time.Time `graphql:"cancelledAt"`
|
||||||
|
EndDate time.Time `graphql:"endDate"`
|
||||||
|
HasTrade bool `graphql:"hasTrade"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Nfts []TokenOfferNft `graphql:"nfts"`
|
||||||
|
Price MonetaryAmount `graphql:"price"`
|
||||||
|
SettledAt time.Time `graphql:"settledAt"`
|
||||||
|
SignedAmount string `graphql:"signedAmount"`
|
||||||
|
StartDate time.Time `graphql:"startDate"`
|
||||||
|
Status string `graphql:"status"`
|
||||||
|
UpdatedAt time.Time `graphql:"updatedAt"`
|
||||||
|
}
|
103
tokens/tokens.go
Normal file
103
tokens/tokens.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LiveParams struct {
|
||||||
|
UpdatedAfter *types.ISO8601DateTime `graphql:"updatedAfter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LiveSingleSaleOffersParams struct {
|
||||||
|
LiveParams
|
||||||
|
Sport *types.Sport `graphql:"sport"`
|
||||||
|
PlayerSlug *string `graphql:"playerSlug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LiveAuctionsParams struct {
|
||||||
|
LiveParams
|
||||||
|
Sport *types.Sport `graphql:"sport"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tokens struct {
|
||||||
|
c *graphql.Client
|
||||||
|
|
||||||
|
AllNfts *graphql.PaginatedQuery[Token, AllNftsParams]
|
||||||
|
Auction *graphql.Query[TokenAuction, graphql.IdStringParams]
|
||||||
|
Auctions *graphql.Query[[]TokenAuction, graphql.IdStringsParams]
|
||||||
|
LiveAuctions *graphql.PaginatedQuery[TokenAuction, LiveAuctionsParams]
|
||||||
|
LivePrimaryOffers *graphql.PaginatedQuery[TokenPrimaryOffer, LiveParams]
|
||||||
|
LiveSingleSaleOffers *graphql.PaginatedQuery[TokenOffer, LiveSingleSaleOffersParams]
|
||||||
|
Nft *graphql.Query[Token, graphql.AssetIdParams]
|
||||||
|
Nfts *graphql.Query[[]Token, graphql.AssetIdsParams]
|
||||||
|
Offer *graphql.Query[TokenOffer, graphql.IdStringParams]
|
||||||
|
Offers *graphql.Query[[]TokenOffer, graphql.IdStringsParams]
|
||||||
|
PrimaryOffer *graphql.Query[TokenPrimaryOffer, graphql.IdStringParams]
|
||||||
|
PrimaryOffers *graphql.Query[[]TokenPrimaryOffer, graphql.IdStringsParams]
|
||||||
|
TokenPrices *graphql.Query[[]TokenPrice, TokenPricesParams]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTokens(c *graphql.Client) *Tokens {
|
||||||
|
return &Tokens{
|
||||||
|
c: c,
|
||||||
|
|
||||||
|
AllNfts: graphql.NewPaginatedQuery[Token, AllNftsParams](
|
||||||
|
c,
|
||||||
|
"allNfts",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
Nft: graphql.NewQuery[Token, graphql.AssetIdParams](c, "nft", []string{"tokens"}),
|
||||||
|
Nfts: graphql.NewQuery[[]Token, graphql.AssetIdsParams](c, "nfts", []string{"tokens"}),
|
||||||
|
Offer: graphql.NewQuery[TokenOffer, graphql.IdStringParams](
|
||||||
|
c,
|
||||||
|
"offer",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
Offers: graphql.NewQuery[[]TokenOffer, graphql.IdStringsParams](
|
||||||
|
c,
|
||||||
|
"offers",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
PrimaryOffer: graphql.NewQuery[TokenPrimaryOffer, graphql.IdStringParams](
|
||||||
|
c,
|
||||||
|
"primaryOffer",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
PrimaryOffers: graphql.NewQuery[[]TokenPrimaryOffer, graphql.IdStringsParams](
|
||||||
|
c,
|
||||||
|
"primaryOffers",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
Auction: graphql.NewQuery[TokenAuction, graphql.IdStringParams](
|
||||||
|
c,
|
||||||
|
"auction",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
Auctions: graphql.NewQuery[[]TokenAuction, graphql.IdStringsParams](
|
||||||
|
c,
|
||||||
|
"auctions",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
LiveAuctions: graphql.NewPaginatedQuery[TokenAuction, LiveAuctionsParams](
|
||||||
|
c,
|
||||||
|
"liveAuctions",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
LivePrimaryOffers: graphql.NewPaginatedQuery[TokenPrimaryOffer, LiveParams](
|
||||||
|
c,
|
||||||
|
"livePrimaryOffers",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
LiveSingleSaleOffers: graphql.NewPaginatedQuery[TokenOffer, LiveSingleSaleOffersParams](
|
||||||
|
c,
|
||||||
|
"liveSingleSaleOffers",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
TokenPrices: graphql.NewQuery[[]TokenPrice, TokenPricesParams](
|
||||||
|
c,
|
||||||
|
"tokenPrices",
|
||||||
|
[]string{"tokens"},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
8
types/average_player_score.go
Normal file
8
types/average_player_score.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type AveragePlayerScore string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AveragePlayerScoreLastFifteenSo5AverageScore AveragePlayerScore = "LAST_FIFTEEN_SO5_AVERAGE_SCORE"
|
||||||
|
AveragePlayerScoreLastFiveSo5AverageScore AveragePlayerScore = "LAST_FIVE_SO5_AVERAGE_SCORE"
|
||||||
|
)
|
10
types/collection.go
Normal file
10
types/collection.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type Collection string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CollectionBaseball Collection = "BASEBALL"
|
||||||
|
CollectionFootball Collection = "FOOTBALL"
|
||||||
|
CollectionFootballNationalSeries Collection = "FOOTBALL_NATIONAL_SERIES"
|
||||||
|
CollectionNba Collection = "NBA"
|
||||||
|
)
|
23
types/iso8601datetime.go
Normal file
23
types/iso8601datetime.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ISO8601DateTime time.Time
|
||||||
|
|
||||||
|
func NewISO8601DateTime(t time.Time) *ISO8601DateTime {
|
||||||
|
i := ISO8601DateTime(t)
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ISO8601DateTime) UnmarshalJSON(b []byte) error {
|
||||||
|
t, err := time.Parse(`"2006-01-02T15:04:05Z"`, string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*i = ISO8601DateTime(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ISO8601DateTime) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(time.Time(i).Format(`"2006-01-02T15:04:05Z"`)), nil
|
||||||
|
}
|
9
types/offer_type.go
Normal file
9
types/offer_type.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type OfferType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OfferTypeDirectOffer OfferType = "DIRECT_OFFER"
|
||||||
|
OfferTypeSingleBuyOffer OfferType = "SINGLE_BUY_OFFER"
|
||||||
|
OfferTypeSingleSaleOffer OfferType = "SINGLE_SALE_OFFER"
|
||||||
|
)
|
18
types/owner_transfer.go
Normal file
18
types/owner_transfer.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type OwnerTransfer string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OwnerTransferBundledEnglishAuction OwnerTransfer = "BUNDLED_ENGLISH_AUCTION"
|
||||||
|
OwnerTransferDeposit OwnerTransfer = "DEPOSIT"
|
||||||
|
OwnerTransferDirectOffer OwnerTransfer = "DIRECT_OFFER"
|
||||||
|
OwnerTransferEnglishAuction OwnerTransfer = "ENGLISH_AUCTION"
|
||||||
|
OwnerTransferMint OwnerTransfer = "MINT"
|
||||||
|
OwnerTransferPack OwnerTransfer = "PACK"
|
||||||
|
OwnerTransferReferral OwnerTransfer = "REFERRAL"
|
||||||
|
OwnerTransferReward OwnerTransfer = "REWARD"
|
||||||
|
OwnerTransferSingleBuyOffer OwnerTransfer = "SINGLE_BUY_OFFER"
|
||||||
|
OwnerTransferSingleSaleOffer OwnerTransfer = "SINGLE_SALE_OFFER"
|
||||||
|
OwnerTransferTransfer OwnerTransfer = "TRANSFER"
|
||||||
|
OwnerTransferWithdrawal OwnerTransfer = "WITHDRAWAL"
|
||||||
|
)
|
9
types/payment_method.go
Normal file
9
types/payment_method.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type PaymentMethod string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PaymentMethodWallet PaymentMethod = "WALLET"
|
||||||
|
PaymentMethodCreditCard PaymentMethod = "CREDIT_CARD"
|
||||||
|
PaymentMethodAppleIap PaymentMethod = "APPLE_IAP"
|
||||||
|
)
|
11
types/playerfieldstatus.go
Normal file
11
types/playerfieldstatus.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type PlayerFieldStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlayerFieldStatusNotOnGameSheet PlayerFieldStatus = "NOT_ON_GAME_SHEET"
|
||||||
|
PlayerFieldStatusOnBench PlayerFieldStatus = "ON_BENCH"
|
||||||
|
PlayerFieldStatusOnField PlayerFieldStatus = "ON_FIELD"
|
||||||
|
PlayerFieldStatusSubstituted PlayerFieldStatus = "SUBSTITUTED"
|
||||||
|
PlayerFieldStatusUnknown PlayerFieldStatus = "UNKNOWN"
|
||||||
|
)
|
12
types/playerplayingstatus.go
Normal file
12
types/playerplayingstatus.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type PlayerPlayingStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlayerPlayingStatusNotPlaying PlayerPlayingStatus = "NOT_PLAYING"
|
||||||
|
PlayerPlayingStatusRegular PlayerPlayingStatus = "REGULAR"
|
||||||
|
PlayerPlayingStatusRetired PlayerPlayingStatus = "RETIRED"
|
||||||
|
PlayerPlayingStatusStarter PlayerPlayingStatus = "STARTER"
|
||||||
|
PlayerPlayingStatusSubstitute PlayerPlayingStatus = "SUBSTITUTE"
|
||||||
|
PlayerPlayingStatusSuperSubstitute PlayerPlayingStatus = "SUPER_SUBSTITUTE"
|
||||||
|
)
|
12
types/position.go
Normal file
12
types/position.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type Position string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PositionGoalkeeper Position = "Goalkeeper"
|
||||||
|
PositionDefender Position = "Defender"
|
||||||
|
PositionMidfielder Position = "Midfielder"
|
||||||
|
PositionForward Position = "Forward"
|
||||||
|
PositionCoach Position = "Coach"
|
||||||
|
PositionUnknown Position = "Unknown"
|
||||||
|
)
|
12
types/rarity.go
Normal file
12
types/rarity.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type Rarity string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RarityCommon Rarity = "common"
|
||||||
|
RarityLimited Rarity = "limited"
|
||||||
|
RarityRare Rarity = "rare"
|
||||||
|
RaritySuperRare Rarity = "super_rare"
|
||||||
|
RarityUnique Rarity = "unique"
|
||||||
|
RarityCustomSeries Rarity = "custom_series"
|
||||||
|
)
|
13
types/so5_leaderboard_rarity.go
Normal file
13
types/so5_leaderboard_rarity.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type So5LeaderboardRarity string
|
||||||
|
|
||||||
|
const (
|
||||||
|
So5LeaderboardRarityCommon So5LeaderboardRarity = "COMMON"
|
||||||
|
So5LeaderboardRarityLimited So5LeaderboardRarity = "LIMITED"
|
||||||
|
So5LeaderboardRarityMix So5LeaderboardRarity = "MIX"
|
||||||
|
So5LeaderboardRarityRare So5LeaderboardRarity = "RARE"
|
||||||
|
So5LeaderboardRarityRarePro So5LeaderboardRarity = "RARE_PRO"
|
||||||
|
So5LeaderboardRaritySuperRare So5LeaderboardRarity = "SUPER_RARE"
|
||||||
|
So5LeaderboardRarityUnique So5LeaderboardRarity = "UNIQUE"
|
||||||
|
)
|
99
types/so5_leaderboard_type.go
Normal file
99
types/so5_leaderboard_type.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type So5LeaderboardType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
So5LeaderboardTypeChallengerEuropeLimited So5LeaderboardType = "CHALLENGER_EUROPE_LIMITED"
|
||||||
|
So5LeaderboardTypeChallengerEuropeRare So5LeaderboardType = "CHALLENGER_EUROPE_RARE"
|
||||||
|
So5LeaderboardTypeChallengerEuropeRarePro So5LeaderboardType = "CHALLENGER_EUROPE_RARE_PRO"
|
||||||
|
So5LeaderboardTypeChallengerEuropeSuperRare So5LeaderboardType = "CHALLENGER_EUROPE_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeChallengerEuropeUnique So5LeaderboardType = "CHALLENGER_EUROPE_UNIQUE"
|
||||||
|
So5LeaderboardTypeChampionAmericaLimited So5LeaderboardType = "CHAMPION_AMERICA_LIMITED"
|
||||||
|
So5LeaderboardTypeChampionAmericaRare So5LeaderboardType = "CHAMPION_AMERICA_RARE"
|
||||||
|
So5LeaderboardTypeChampionAmericaRarePro So5LeaderboardType = "CHAMPION_AMERICA_RARE_PRO"
|
||||||
|
So5LeaderboardTypeChampionAmericaSuperRare So5LeaderboardType = "CHAMPION_AMERICA_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeChampionAmericaUnique So5LeaderboardType = "CHAMPION_AMERICA_UNIQUE"
|
||||||
|
So5LeaderboardTypeChampionAsiaLimited So5LeaderboardType = "CHAMPION_ASIA_LIMITED"
|
||||||
|
So5LeaderboardTypeChampionAsiaRare So5LeaderboardType = "CHAMPION_ASIA_RARE"
|
||||||
|
So5LeaderboardTypeChampionAsiaRarePro So5LeaderboardType = "CHAMPION_ASIA_RARE_PRO"
|
||||||
|
So5LeaderboardTypeChampionAsiaSuperRare So5LeaderboardType = "CHAMPION_ASIA_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeChampionAsiaUnique So5LeaderboardType = "CHAMPION_ASIA_UNIQUE"
|
||||||
|
So5LeaderboardTypeChampionEuropeLimited So5LeaderboardType = "CHAMPION_EUROPE_LIMITED"
|
||||||
|
So5LeaderboardTypeChampionEuropeRare So5LeaderboardType = "CHAMPION_EUROPE_RARE"
|
||||||
|
So5LeaderboardTypeChampionEuropeRarePro So5LeaderboardType = "CHAMPION_EUROPE_RARE_PRO"
|
||||||
|
So5LeaderboardTypeChampionEuropeSuperRare So5LeaderboardType = "CHAMPION_EUROPE_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeChampionEuropeUnique So5LeaderboardType = "CHAMPION_EUROPE_UNIQUE"
|
||||||
|
So5LeaderboardTypeChampionJupilerRare So5LeaderboardType = "CHAMPION_JUPILER_RARE"
|
||||||
|
So5LeaderboardTypeChampionJupilerRarePro So5LeaderboardType = "CHAMPION_JUPILER_RARE_PRO"
|
||||||
|
So5LeaderboardTypeChampionJupilerSuperRare So5LeaderboardType = "CHAMPION_JUPILER_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeChampionJupilerUnique So5LeaderboardType = "CHAMPION_JUPILER_UNIQUE"
|
||||||
|
So5LeaderboardTypeFirstDivisionEnglandAmateur So5LeaderboardType = "FIRST_DIVISION_ENGLAND_AMATEUR"
|
||||||
|
So5LeaderboardTypeFirstDivisionEnglandPro So5LeaderboardType = "FIRST_DIVISION_ENGLAND_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionEnglandSemiPro So5LeaderboardType = "FIRST_DIVISION_ENGLAND_SEMI_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionFranceAmateur So5LeaderboardType = "FIRST_DIVISION_FRANCE_AMATEUR"
|
||||||
|
So5LeaderboardTypeFirstDivisionFrancePro So5LeaderboardType = "FIRST_DIVISION_FRANCE_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionFranceSemiPro So5LeaderboardType = "FIRST_DIVISION_FRANCE_SEMI_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionGermanyAmateur So5LeaderboardType = "FIRST_DIVISION_GERMANY_AMATEUR"
|
||||||
|
So5LeaderboardTypeFirstDivisionGermanyPro So5LeaderboardType = "FIRST_DIVISION_GERMANY_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionGermanySemiPro So5LeaderboardType = "FIRST_DIVISION_GERMANY_SEMI_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionItalyAmateur So5LeaderboardType = "FIRST_DIVISION_ITALY_AMATEUR"
|
||||||
|
So5LeaderboardTypeFirstDivisionItalyPro So5LeaderboardType = "FIRST_DIVISION_ITALY_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionItalySemiPro So5LeaderboardType = "FIRST_DIVISION_ITALY_SEMI_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionSpainAmateur So5LeaderboardType = "FIRST_DIVISION_SPAIN_AMATEUR"
|
||||||
|
So5LeaderboardTypeFirstDivisionSpainPro So5LeaderboardType = "FIRST_DIVISION_SPAIN_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionSpainSemiPro So5LeaderboardType = "FIRST_DIVISION_SPAIN_SEMI_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionUsAmateur So5LeaderboardType = "FIRST_DIVISION_US_AMATEUR"
|
||||||
|
So5LeaderboardTypeFirstDivisionUsPro So5LeaderboardType = "FIRST_DIVISION_US_PRO"
|
||||||
|
So5LeaderboardTypeFirstDivisionUsSemiPro So5LeaderboardType = "FIRST_DIVISION_US_SEMI_PRO"
|
||||||
|
So5LeaderboardTypeGlobalAllStarLimited So5LeaderboardType = "GLOBAL_ALL_STAR_LIMITED"
|
||||||
|
So5LeaderboardTypeGlobalAllStarRare So5LeaderboardType = "GLOBAL_ALL_STAR_RARE"
|
||||||
|
So5LeaderboardTypeGlobalAllStarRarePro So5LeaderboardType = "GLOBAL_ALL_STAR_RARE_PRO"
|
||||||
|
So5LeaderboardTypeGlobalAllStarSuperRare So5LeaderboardType = "GLOBAL_ALL_STAR_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeGlobalAllStarUnique So5LeaderboardType = "GLOBAL_ALL_STAR_UNIQUE"
|
||||||
|
So5LeaderboardTypeGlobalCap220Limited So5LeaderboardType = "GLOBAL_CAP_220_LIMITED"
|
||||||
|
So5LeaderboardTypeGlobalCap220Rare So5LeaderboardType = "GLOBAL_CAP_220_RARE"
|
||||||
|
So5LeaderboardTypeGlobalCap220SuperRare So5LeaderboardType = "GLOBAL_CAP_220_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeGlobalCap220Unique So5LeaderboardType = "GLOBAL_CAP_220_UNIQUE"
|
||||||
|
So5LeaderboardTypeGlobalCap240Limited So5LeaderboardType = "GLOBAL_CAP_240_LIMITED"
|
||||||
|
So5LeaderboardTypeGlobalCap240Rare So5LeaderboardType = "GLOBAL_CAP_240_RARE"
|
||||||
|
So5LeaderboardTypeGlobalCap240SuperRare So5LeaderboardType = "GLOBAL_CAP_240_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeGlobalCap240Unique So5LeaderboardType = "GLOBAL_CAP_240_UNIQUE"
|
||||||
|
So5LeaderboardTypeGlobalCap270Limited So5LeaderboardType = "GLOBAL_CAP_270_LIMITED"
|
||||||
|
So5LeaderboardTypeGlobalCap270Rare So5LeaderboardType = "GLOBAL_CAP_270_RARE"
|
||||||
|
So5LeaderboardTypeGlobalCap270SuperRare So5LeaderboardType = "GLOBAL_CAP_270_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeGlobalCap270Unique So5LeaderboardType = "GLOBAL_CAP_270_UNIQUE"
|
||||||
|
So5LeaderboardTypeGlobalKickoffLimited So5LeaderboardType = "GLOBAL_KICKOFF_LIMITED"
|
||||||
|
So5LeaderboardTypeGlobalKickoffRare So5LeaderboardType = "GLOBAL_KICKOFF_RARE"
|
||||||
|
So5LeaderboardTypeGlobalKickoffSuperRare So5LeaderboardType = "GLOBAL_KICKOFF_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeGlobalKickoffUnique So5LeaderboardType = "GLOBAL_KICKOFF_UNIQUE"
|
||||||
|
So5LeaderboardTypeGlobalSpecialistLimited So5LeaderboardType = "GLOBAL_SPECIALIST_LIMITED"
|
||||||
|
So5LeaderboardTypeGlobalSpecialistRare So5LeaderboardType = "GLOBAL_SPECIALIST_RARE"
|
||||||
|
So5LeaderboardTypeGlobalSpecialistSuperRare So5LeaderboardType = "GLOBAL_SPECIALIST_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeGlobalSpecialistSuperRareUnique So5LeaderboardType = "GLOBAL_SPECIALIST_SUPER_RARE_UNIQUE"
|
||||||
|
So5LeaderboardTypeGlobalSpecialistUnique So5LeaderboardType = "GLOBAL_SPECIALIST_UNIQUE"
|
||||||
|
So5LeaderboardTypeGlobalUnderdogLimited So5LeaderboardType = "GLOBAL_UNDERDOG_LIMITED"
|
||||||
|
So5LeaderboardTypeGlobalUnderdogRare So5LeaderboardType = "GLOBAL_UNDERDOG_RARE"
|
||||||
|
So5LeaderboardTypeGlobalUnderdogSuperRare So5LeaderboardType = "GLOBAL_UNDERDOG_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeGlobalUnderdogUnique So5LeaderboardType = "GLOBAL_UNDERDOG_UNIQUE"
|
||||||
|
So5LeaderboardTypeGlobalUnderTwentyOneLimited So5LeaderboardType = "GLOBAL_UNDER_TWENTY_ONE_LIMITED"
|
||||||
|
So5LeaderboardTypeGlobalUnderTwentyOneRare So5LeaderboardType = "GLOBAL_UNDER_TWENTY_ONE_RARE"
|
||||||
|
So5LeaderboardTypeGlobalUnderTwentyOneRarePro So5LeaderboardType = "GLOBAL_UNDER_TWENTY_ONE_RARE_PRO"
|
||||||
|
So5LeaderboardTypeGlobalUnderTwentyOneSuperRare So5LeaderboardType = "GLOBAL_UNDER_TWENTY_ONE_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeGlobalUnderTwentyOneUnique So5LeaderboardType = "GLOBAL_UNDER_TWENTY_ONE_UNIQUE"
|
||||||
|
So5LeaderboardTypeGlobalUniqueOnly So5LeaderboardType = "GLOBAL_UNIQUE_ONLY"
|
||||||
|
So5LeaderboardTypeLegend So5LeaderboardType = "LEGEND"
|
||||||
|
So5LeaderboardTypeLegendPro So5LeaderboardType = "LEGEND_PRO"
|
||||||
|
So5LeaderboardTypeSecondDivisionEuropeLimited So5LeaderboardType = "SECOND_DIVISION_EUROPE_LIMITED"
|
||||||
|
So5LeaderboardTypeSecondDivisionEuropeRare So5LeaderboardType = "SECOND_DIVISION_EUROPE_RARE"
|
||||||
|
So5LeaderboardTypeSecondDivisionEuropeRarePro So5LeaderboardType = "SECOND_DIVISION_EUROPE_RARE_PRO"
|
||||||
|
So5LeaderboardTypeSecondDivisionEuropeSuperRare So5LeaderboardType = "SECOND_DIVISION_EUROPE_SUPER_RARE"
|
||||||
|
So5LeaderboardTypeSecondDivisionEuropeUnique So5LeaderboardType = "SECOND_DIVISION_EUROPE_UNIQUE"
|
||||||
|
So5LeaderboardTypeSpecialGlobalCup So5LeaderboardType = "SPECIAL_GLOBAL_CUP"
|
||||||
|
So5LeaderboardTypeSpecialTrainingCenter So5LeaderboardType = "SPECIAL_TRAINING_CENTER"
|
||||||
|
So5LeaderboardTypeSpecialWeekly So5LeaderboardType = "SPECIAL_WEEKLY"
|
||||||
|
So5LeaderboardTypeStarterAcademyAdvanced So5LeaderboardType = "STARTER_ACADEMY_ADVANCED"
|
||||||
|
So5LeaderboardTypeStarterAcademyIntermediate So5LeaderboardType = "STARTER_ACADEMY_INTERMEDIATE"
|
||||||
|
So5LeaderboardTypeStarterAcademyNovice So5LeaderboardType = "STARTER_ACADEMY_NOVICE"
|
||||||
|
So5LeaderboardTypeStarterAcademyProficient So5LeaderboardType = "STARTER_ACADEMY_PROFICIENT"
|
||||||
|
So5LeaderboardTypeStarterRookie So5LeaderboardType = "STARTER_ROOKIE"
|
||||||
|
)
|
9
types/so5state.go
Normal file
9
types/so5state.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type So5State string
|
||||||
|
|
||||||
|
const (
|
||||||
|
So5StateLive So5State = "LIVE"
|
||||||
|
So5StateUpcoming So5State = "UPCOMING"
|
||||||
|
So5StatePast So5State = "PAST"
|
||||||
|
)
|
8
types/sorting_option.go
Normal file
8
types/sorting_option.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type SortingOption string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SortingOptionAsc SortingOption = "ASC"
|
||||||
|
SortingOptionDesc SortingOption = "DESC"
|
||||||
|
)
|
9
types/sport.go
Normal file
9
types/sport.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type Sport string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SportFootball Sport = "FOOTBALL"
|
||||||
|
SportNba Sport = "NBA"
|
||||||
|
SportBaseBall Sport = "BASEBALL"
|
||||||
|
)
|
15
types/statcategory.go
Normal file
15
types/statcategory.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type StatCategory string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatCategoryAttacking StatCategory = "ATTACKING"
|
||||||
|
StatCategoryDefending StatCategory = "DEFENDING"
|
||||||
|
StatCategoryGeneral StatCategory = "GENERAL"
|
||||||
|
StatCategoryGoalkeeping StatCategory = "GOALKEEPING"
|
||||||
|
StatCategoryNegativeDecisiveStat StatCategory = "NEGATIVE_DECISIVE_STAT"
|
||||||
|
StatCategoryPositiveDecisiveStat StatCategory = "POSITIVE_DECISIVE_STAT"
|
||||||
|
StatCategoryPassing StatCategory = "PASSING"
|
||||||
|
StatCategoryPossession StatCategory = "POSSESSION"
|
||||||
|
StatCategoryUnknown StatCategory = "UNKNOWN"
|
||||||
|
)
|
39
types/supported_currency.go
Normal file
39
types/supported_currency.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SupportedCurrency string
|
||||||
|
|
||||||
|
func (s *SupportedCurrency) UnmarshalJSON(bytes []byte) error {
|
||||||
|
var str string
|
||||||
|
if err := json.Unmarshal(bytes, &str); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch str {
|
||||||
|
case "WEI":
|
||||||
|
*s = SupportedCurrencyWEI
|
||||||
|
case "EUR":
|
||||||
|
*s = SupportedCurrencyEUR
|
||||||
|
case "USD":
|
||||||
|
*s = SupportedCurrencyUSD
|
||||||
|
case "GBP":
|
||||||
|
*s = SupportedCurrencyGBP
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported currency: %s", str)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SupportedCurrency) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(string(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
SupportedCurrencyWEI SupportedCurrency = "WEI"
|
||||||
|
SupportedCurrencyEUR SupportedCurrency = "EUR"
|
||||||
|
SupportedCurrencyUSD SupportedCurrency = "USD"
|
||||||
|
SupportedCurrencyGBP SupportedCurrency = "GBP"
|
||||||
|
)
|
225
user.go
Normal file
225
user.go
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
package sorare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"git.lehouerou.net/laurent/sorare/football"
|
||||||
|
"git.lehouerou.net/laurent/sorare/graphql"
|
||||||
|
"git.lehouerou.net/laurent/sorare/tokens"
|
||||||
|
"git.lehouerou.net/laurent/sorare/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type baseUser struct {
|
||||||
|
Active bool `graphql:"active"`
|
||||||
|
CardCounts struct {
|
||||||
|
Common int `graphql:"common"`
|
||||||
|
CustomSeries int `graphql:"customSeries"`
|
||||||
|
Limited int `graphql:"limited"`
|
||||||
|
Rare int `graphql:"rare"`
|
||||||
|
SuperRare int `graphql:"superRare"`
|
||||||
|
Unique int `graphql:"unique"`
|
||||||
|
Total int `graphql:"total"`
|
||||||
|
} `graphql:"cardCounts"`
|
||||||
|
CreatedAt time.Time `graphql:"createdAt"`
|
||||||
|
EthVaultId int `graphql:"ethVaultId"`
|
||||||
|
EthereumAddress string `graphql:"ethereumAddress"`
|
||||||
|
FollowersCount int `graphql:"followersCount"`
|
||||||
|
FollowingCount int `graphql:"followingCount"`
|
||||||
|
Id graphql.Id `graphql:"id"`
|
||||||
|
Nickname string `graphql:"nickname"`
|
||||||
|
Player struct {
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
}
|
||||||
|
Profile struct {
|
||||||
|
ClubName string `graphql:"clubName"`
|
||||||
|
DiscordUsername string `graphql:"discordUsername"`
|
||||||
|
TwitterUsername string `graphql:"twitterUsername"`
|
||||||
|
} `graphql:"profile"`
|
||||||
|
Slug string `graphql:"slug"`
|
||||||
|
SorareAddress string `graphql:"sorareAddress"`
|
||||||
|
StarkKey string `graphql:"starkKey"`
|
||||||
|
Suspended bool `graphql:"suspended"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
baseUser
|
||||||
|
AvatarUrl string `graphql:"avatarUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CurrentUser struct {
|
||||||
|
baseUser
|
||||||
|
MyAccounts []struct {
|
||||||
|
Accountable struct {
|
||||||
|
TypeName string `graphql:"__typename"`
|
||||||
|
FiatWallet struct {
|
||||||
|
AvailableBalance decimal.Decimal `graphql:"availableBalance"`
|
||||||
|
PublicInfo struct {
|
||||||
|
Currency types.SupportedCurrency `graphql:"currency"`
|
||||||
|
} `graphql:"publicInfo"`
|
||||||
|
} `graphql:"... on PrivateFiatWalletAccount"`
|
||||||
|
} `graphql:"accountable"`
|
||||||
|
} `graphql:"myAccounts"`
|
||||||
|
ApiKey *string `graphql:"apiKey"`
|
||||||
|
AvailableBalance decimal.Decimal `graphql:"availableBalance"`
|
||||||
|
AvailableBalanceForWithdrawal decimal.Decimal `graphql:"availableBalanceForWithdrawal"`
|
||||||
|
BankBalance decimal.Decimal `graphql:"bankBalance"`
|
||||||
|
BankMappedEthereumAddress string `graphql:"bankMappedEthereumAddress"`
|
||||||
|
BankMappedEthereumAddressApprovedBank bool `graphql:"bankMappedEthereumAddressApprovedBank"`
|
||||||
|
BetaTester bool `graphql:"betaTester"`
|
||||||
|
BlockchainCardsInLineups []string `graphql:"blockchainCardsInLineups"`
|
||||||
|
BlockedUntil time.Time `graphql:"blockedUntil"`
|
||||||
|
CoinBalance int `graphql:"coinBalance"`
|
||||||
|
Confirmed bool `graphql:"confirmed"`
|
||||||
|
ConfirmedDevice bool `graphql:"confirmedDevice"`
|
||||||
|
DepositedEth bool `graphql:"depositedEth"`
|
||||||
|
Email string `graphql:"email"`
|
||||||
|
FiatEnabled bool `graphql:"fiatEnabled"`
|
||||||
|
FootballLast30DaysLineupsCount int `graphql:"footballLast30DaysLineupsCount"`
|
||||||
|
FromPath string `graphql:"fromPath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Users struct {
|
||||||
|
c *graphql.Client
|
||||||
|
|
||||||
|
User *graphql.Query[User, graphql.SlugParams]
|
||||||
|
UserById *graphql.Query[User, graphql.IdParams]
|
||||||
|
Users *graphql.Query[[]User, graphql.SlugsParams]
|
||||||
|
CurrentUser *graphql.Query[CurrentUser, graphql.EmptyParams]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUsers(c *graphql.Client) *Users {
|
||||||
|
return &Users{c: c,
|
||||||
|
User: graphql.NewQuery[User, graphql.SlugParams](c, "user", nil),
|
||||||
|
UserById: graphql.NewQuery[User, graphql.IdParams](c, "userById", nil),
|
||||||
|
Users: graphql.NewQuery[[]User, graphql.SlugsParams](c, "users", nil),
|
||||||
|
CurrentUser: graphql.NewQuery[CurrentUser, graphql.EmptyParams](c, "currentUser", nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserTokensParams struct {
|
||||||
|
SortByEndDate *types.SortingOption `graphql:"sortByEndDate"`
|
||||||
|
Sport []types.Sport `graphql:"sport"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) BoughtSingleSaleTokenOffers(
|
||||||
|
userSlug string,
|
||||||
|
) *graphql.PaginatedQuery[tokens.TokenOffer, UserTokensParams] {
|
||||||
|
return graphql.NewPaginatedQuery[tokens.TokenOffer, UserTokensParams](
|
||||||
|
u.c,
|
||||||
|
"boughtSingleSaleTokenOffers",
|
||||||
|
[]string{"user(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", userSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserBuyingTokenAuctionsParams struct {
|
||||||
|
NewlyListed bool `graphql:"newlyListed"`
|
||||||
|
Sport []types.Sport `graphql:"sport"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) BuyingTokenAuctions(
|
||||||
|
userSlug string,
|
||||||
|
) *graphql.Query[[]tokens.TokenAuction, UserBuyingTokenAuctionsParams] {
|
||||||
|
return graphql.NewQuery[[]tokens.TokenAuction, UserBuyingTokenAuctionsParams](
|
||||||
|
u.c,
|
||||||
|
"buyingTokenAuctions",
|
||||||
|
[]string{"user(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", userSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) EndedWithNoBuyerSingleSaleTokenOffers(
|
||||||
|
userSlug string,
|
||||||
|
) *graphql.PaginatedQuery[tokens.TokenOffer, UserTokensParams] {
|
||||||
|
return graphql.NewPaginatedQuery[tokens.TokenOffer, UserTokensParams](
|
||||||
|
u.c,
|
||||||
|
"endedWithNoBuyerSingleSaleTokenOffers",
|
||||||
|
[]string{"user(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", userSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) FootballCards(
|
||||||
|
userSlug string,
|
||||||
|
) *graphql.PaginatedQuery[football.Card, football.CardsParams] {
|
||||||
|
return graphql.NewPaginatedQuery[football.Card, football.CardsParams](
|
||||||
|
u.c,
|
||||||
|
"footballCards",
|
||||||
|
[]string{"user(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", userSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) LiveSingleSaleTokenOffers(
|
||||||
|
userSlug string,
|
||||||
|
) *graphql.PaginatedQuery[tokens.TokenOffer, UserTokensParams] {
|
||||||
|
return graphql.NewPaginatedQuery[tokens.TokenOffer, UserTokensParams](
|
||||||
|
u.c,
|
||||||
|
"liveSingleSaleTokenOffers",
|
||||||
|
[]string{"user(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", userSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) LostTokenAuctions(
|
||||||
|
userSlug string,
|
||||||
|
) *graphql.PaginatedQuery[tokens.TokenAuction, UserTokensParams] {
|
||||||
|
return graphql.NewPaginatedQuery[tokens.TokenAuction, UserTokensParams](
|
||||||
|
u.c,
|
||||||
|
"lostTokenAuctions",
|
||||||
|
[]string{"user(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", userSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) SingleSaleTokenOffers(
|
||||||
|
userSlug string,
|
||||||
|
) *graphql.PaginatedQuery[tokens.TokenOffer, UserTokensParams] {
|
||||||
|
return graphql.NewPaginatedQuery[tokens.TokenOffer, UserTokensParams](
|
||||||
|
u.c,
|
||||||
|
"singleSaleTokenOffers",
|
||||||
|
[]string{"user(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", userSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) SoldSingleSaleTokenOffers(
|
||||||
|
userSlug string,
|
||||||
|
) *graphql.PaginatedQuery[tokens.TokenOffer, UserTokensParams] {
|
||||||
|
return graphql.NewPaginatedQuery[tokens.TokenOffer, UserTokensParams](
|
||||||
|
u.c,
|
||||||
|
"soldSingleSaleTokenOffers",
|
||||||
|
[]string{"user(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", userSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) TokenAuctions(
|
||||||
|
userSlug string,
|
||||||
|
) *graphql.PaginatedQuery[tokens.TokenAuction, UserTokensParams] {
|
||||||
|
return graphql.NewPaginatedQuery[tokens.TokenAuction, UserTokensParams](
|
||||||
|
u.c,
|
||||||
|
"tokenAuctions",
|
||||||
|
[]string{"user(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", userSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) WonTokenAuctions(
|
||||||
|
userSlug string,
|
||||||
|
) *graphql.PaginatedQuery[tokens.TokenAuction, UserTokensParams] {
|
||||||
|
return graphql.NewPaginatedQuery[tokens.TokenAuction, UserTokensParams](
|
||||||
|
u.c,
|
||||||
|
"wonTokenAuctions",
|
||||||
|
[]string{"user(slug:$slug)"},
|
||||||
|
).WithQueryParam("slug", userSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) CurrentUserPendingTokenOffersReceived() *graphql.PaginatedQuery[tokens.TokenOffer, UserTokensParams] {
|
||||||
|
return graphql.NewPaginatedQuery[tokens.TokenOffer, UserTokensParams](
|
||||||
|
u.c,
|
||||||
|
"pendingTokenOffersReceived",
|
||||||
|
[]string{"currentUser"},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) CurrentUserPendingTokenOffersSent() *graphql.PaginatedQuery[tokens.TokenOffer, UserTokensParams] {
|
||||||
|
return graphql.NewPaginatedQuery[tokens.TokenOffer, UserTokensParams](
|
||||||
|
u.c,
|
||||||
|
"pendingTokenOffersSent",
|
||||||
|
[]string{"currentUser"},
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user