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