initial commit

This commit is contained in:
Laurent Le Houerou 2024-03-08 14:48:21 +04:00
commit b99d855805
86 changed files with 7870 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.vscode/

132
api.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
}

View 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
View 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
}

View 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
View 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
}

View 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
}

View 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
View 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
}

View 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
View 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
View 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")
}

View 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
View 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
View 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
}

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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),
}
}
}

View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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"`
}

View 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
View 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"},
),
}
}

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,9 @@
package types
type PaymentMethod string
const (
PaymentMethodWallet PaymentMethod = "WALLET"
PaymentMethodCreditCard PaymentMethod = "CREDIT_CARD"
PaymentMethodAppleIap PaymentMethod = "APPLE_IAP"
)

View 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"
)

View 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
View 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
View 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"
)

View 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"
)

View 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
View 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
View File

@ -0,0 +1,8 @@
package types
type SortingOption string
const (
SortingOptionAsc SortingOption = "ASC"
SortingOptionDesc SortingOption = "DESC"
)

9
types/sport.go Normal file
View 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
View 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"
)

View 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
View 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"},
)
}