From b99d855805261f06db2a86f15839ed60f6c846a2 Mon Sep 17 00:00:00 2001 From: Laurent Le Houerou Date: Fri, 8 Mar 2024 14:48:21 +0400 Subject: [PATCH] initial commit --- .gitignore | 1 + api.go | 132 ++ bcrypt/base64.go | 35 + bcrypt/bcrypt.go | 325 ++++ bcrypt/bcrypt_test.go | 265 +++ bcrypt/patch.go | 36 + cmd/console/main.go | 40 + config/config.go | 71 + country.go | 15 + football/card.go | 42 + football/club.go | 42 + football/competition.go | 45 + football/football.go | 113 ++ football/game.go | 86 + football/national_team.go | 8 + football/player.go | 116 ++ football/season.go | 13 + football/so5.go | 49 + football/so5fixture.go | 48 + football/so5leaderboard.go | 53 + football/so5score.go | 94 ++ football/team.go | 18 + go.mod | 27 + go.sum | 103 ++ graphql/authentication.go | 172 ++ graphql/client.go | 96 ++ graphql/id.go | 35 + graphql/mutation.go | 66 + graphql/paginated_query.go | 159 ++ graphql/params.go | 67 + graphql/payload.go | 89 ++ graphql/query.go | 75 + mutations/accept_offer.go | 128 ++ mutations/amount_input.go | 8 + mutations/approval.go | 36 + mutations/authorization_request.go | 70 + mutations/bid.go | 113 ++ mutations/buy_deliverable_shop_item.go | 78 + mutations/cancel_offer.go | 38 + mutations/create_direct_offer.go | 151 ++ mutations/create_single_sale_offer.go | 148 ++ mutations/limit_order.go | 191 +++ mutations/mangopaywallet_transfer.go | 56 + mutations/mutations.go | 49 + mutations/prepare_bid.go | 22 + mutations/prepare_offer.go | 25 + mutations/random.go | 12 + mutations/reject_offer.go | 40 + mutations/settlement_info.go | 9 + mutations/transfer.go | 222 +++ nbaplayer.go | 17 + shop_item.go | 34 + starkware/consts.go | 2041 ++++++++++++++++++++++++ starkware/math_pedersen.go | 52 + starkware/math_rfc6979.go | 126 ++ starkware/math_utils.go | 121 ++ starkware/sign.go | 84 + subscriptions/subscriptions.go | 61 + subscriptions/wsclient.go | 350 ++++ subscriptions/wsscheduler.go | 116 ++ tokens/monetary_amount.go | 43 + tokens/token.go | 48 + tokens/token_auction.go | 59 + tokens/token_offer.go | 49 + tokens/token_owner.go | 28 + tokens/token_price.go | 40 + tokens/token_primary_offer.go | 25 + tokens/tokens.go | 103 ++ types/average_player_score.go | 8 + types/collection.go | 10 + types/iso8601datetime.go | 23 + types/offer_type.go | 9 + types/owner_transfer.go | 18 + types/payment_method.go | 9 + types/playerfieldstatus.go | 11 + types/playerplayingstatus.go | 12 + types/position.go | 12 + types/rarity.go | 12 + types/so5_leaderboard_rarity.go | 13 + types/so5_leaderboard_type.go | 99 ++ types/so5state.go | 9 + types/sorting_option.go | 8 + types/sport.go | 9 + types/statcategory.go | 15 + types/supported_currency.go | 39 + user.go | 225 +++ 86 files changed, 7870 insertions(+) create mode 100644 .gitignore create mode 100644 api.go create mode 100644 bcrypt/base64.go create mode 100644 bcrypt/bcrypt.go create mode 100644 bcrypt/bcrypt_test.go create mode 100644 bcrypt/patch.go create mode 100644 cmd/console/main.go create mode 100644 config/config.go create mode 100644 country.go create mode 100644 football/card.go create mode 100644 football/club.go create mode 100644 football/competition.go create mode 100644 football/football.go create mode 100644 football/game.go create mode 100644 football/national_team.go create mode 100644 football/player.go create mode 100644 football/season.go create mode 100644 football/so5.go create mode 100644 football/so5fixture.go create mode 100644 football/so5leaderboard.go create mode 100644 football/so5score.go create mode 100644 football/team.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 graphql/authentication.go create mode 100644 graphql/client.go create mode 100644 graphql/id.go create mode 100644 graphql/mutation.go create mode 100644 graphql/paginated_query.go create mode 100644 graphql/params.go create mode 100644 graphql/payload.go create mode 100644 graphql/query.go create mode 100644 mutations/accept_offer.go create mode 100644 mutations/amount_input.go create mode 100644 mutations/approval.go create mode 100644 mutations/authorization_request.go create mode 100644 mutations/bid.go create mode 100644 mutations/buy_deliverable_shop_item.go create mode 100644 mutations/cancel_offer.go create mode 100644 mutations/create_direct_offer.go create mode 100644 mutations/create_single_sale_offer.go create mode 100644 mutations/limit_order.go create mode 100644 mutations/mangopaywallet_transfer.go create mode 100644 mutations/mutations.go create mode 100644 mutations/prepare_bid.go create mode 100644 mutations/prepare_offer.go create mode 100644 mutations/random.go create mode 100644 mutations/reject_offer.go create mode 100644 mutations/settlement_info.go create mode 100644 mutations/transfer.go create mode 100644 nbaplayer.go create mode 100644 shop_item.go create mode 100644 starkware/consts.go create mode 100644 starkware/math_pedersen.go create mode 100644 starkware/math_rfc6979.go create mode 100644 starkware/math_utils.go create mode 100644 starkware/sign.go create mode 100644 subscriptions/subscriptions.go create mode 100644 subscriptions/wsclient.go create mode 100644 subscriptions/wsscheduler.go create mode 100644 tokens/monetary_amount.go create mode 100644 tokens/token.go create mode 100644 tokens/token_auction.go create mode 100644 tokens/token_offer.go create mode 100644 tokens/token_owner.go create mode 100644 tokens/token_price.go create mode 100644 tokens/token_primary_offer.go create mode 100644 tokens/tokens.go create mode 100644 types/average_player_score.go create mode 100644 types/collection.go create mode 100644 types/iso8601datetime.go create mode 100644 types/offer_type.go create mode 100644 types/owner_transfer.go create mode 100644 types/payment_method.go create mode 100644 types/playerfieldstatus.go create mode 100644 types/playerplayingstatus.go create mode 100644 types/position.go create mode 100644 types/rarity.go create mode 100644 types/so5_leaderboard_rarity.go create mode 100644 types/so5_leaderboard_type.go create mode 100644 types/so5state.go create mode 100644 types/sorting_option.go create mode 100644 types/sport.go create mode 100644 types/statcategory.go create mode 100644 types/supported_currency.go create mode 100644 user.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d74e21 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/ diff --git a/api.go b/api.go new file mode 100644 index 0000000..55920dc --- /dev/null +++ b/api.go @@ -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) +} diff --git a/bcrypt/base64.go b/bcrypt/base64.go new file mode 100644 index 0000000..fc31160 --- /dev/null +++ b/bcrypt/base64.go @@ -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 +} diff --git a/bcrypt/bcrypt.go b/bcrypt/bcrypt.go new file mode 100644 index 0000000..6e56abd --- /dev/null +++ b/bcrypt/bcrypt.go @@ -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 +} diff --git a/bcrypt/bcrypt_test.go b/bcrypt/bcrypt_test.go new file mode 100644 index 0000000..8096e04 --- /dev/null +++ b/bcrypt/bcrypt_test.go @@ -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) + } +} diff --git a/bcrypt/patch.go b/bcrypt/patch.go new file mode 100644 index 0000000..ec36a9c --- /dev/null +++ b/bcrypt/patch.go @@ -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 +} diff --git a/cmd/console/main.go b/cmd/console/main.go new file mode 100644 index 0000000..9603143 --- /dev/null +++ b/cmd/console/main.go @@ -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) +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..cc9c2db --- /dev/null +++ b/config/config.go @@ -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 +} diff --git a/country.go b/country.go new file mode 100644 index 0000000..41ee611 --- /dev/null +++ b/country.go @@ -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)"` +} diff --git a/football/card.go b/football/card.go new file mode 100644 index 0000000..e54c9e7 --- /dev/null +++ b/football/card.go @@ -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) +} diff --git a/football/club.go b/football/club.go new file mode 100644 index 0000000..1d4283c --- /dev/null +++ b/football/club.go @@ -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) +} diff --git a/football/competition.go b/football/competition.go new file mode 100644 index 0000000..bad73fa --- /dev/null +++ b/football/competition.go @@ -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) +} diff --git a/football/football.go b/football/football.go new file mode 100644 index 0000000..fc392ac --- /dev/null +++ b/football/football.go @@ -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), + } +} diff --git a/football/game.go b/football/game.go new file mode 100644 index 0000000..4968d54 --- /dev/null +++ b/football/game.go @@ -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) +} diff --git a/football/national_team.go b/football/national_team.go new file mode 100644 index 0000000..9d142be --- /dev/null +++ b/football/national_team.go @@ -0,0 +1,8 @@ +package football + +type NationalTeam struct { + Team + InternationalCompetition struct { + Slug string `graphql:"slug"` + } `graphql:"internationalCompetition"` +} diff --git a/football/player.go b/football/player.go new file mode 100644 index 0000000..a6e29e2 --- /dev/null +++ b/football/player.go @@ -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) +} diff --git a/football/season.go b/football/season.go new file mode 100644 index 0000000..e4c1d84 --- /dev/null +++ b/football/season.go @@ -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"` +} diff --git a/football/so5.go b/football/so5.go new file mode 100644 index 0000000..8e89393 --- /dev/null +++ b/football/so5.go @@ -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"}, + ), + } +} diff --git a/football/so5fixture.go b/football/so5fixture.go new file mode 100644 index 0000000..f3b8589 --- /dev/null +++ b/football/so5fixture.go @@ -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) +} diff --git a/football/so5leaderboard.go b/football/so5leaderboard.go new file mode 100644 index 0000000..56b703a --- /dev/null +++ b/football/so5leaderboard.go @@ -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"` +} diff --git a/football/so5score.go b/football/so5score.go new file mode 100644 index 0000000..a8f3ba3 --- /dev/null +++ b/football/so5score.go @@ -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"` +} diff --git a/football/team.go b/football/team.go new file mode 100644 index 0000000..62fb92c --- /dev/null +++ b/football/team.go @@ -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"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fd7b1e4 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..95bf104 --- /dev/null +++ b/go.sum @@ -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= diff --git a/graphql/authentication.go b/graphql/authentication.go new file mode 100644 index 0000000..12679b1 --- /dev/null +++ b/graphql/authentication.go @@ -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 +} diff --git a/graphql/client.go b/graphql/client.go new file mode 100644 index 0000000..dbb827e --- /dev/null +++ b/graphql/client.go @@ -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...) +} diff --git a/graphql/id.go b/graphql/id.go new file mode 100644 index 0000000..ca7def7 --- /dev/null +++ b/graphql/id.go @@ -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 +} diff --git a/graphql/mutation.go b/graphql/mutation.go new file mode 100644 index 0000000..deccfe5 --- /dev/null +++ b/graphql/mutation.go @@ -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 +} diff --git a/graphql/paginated_query.go b/graphql/paginated_query.go new file mode 100644 index 0000000..eb6e4db --- /dev/null +++ b/graphql/paginated_query.go @@ -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 +} diff --git a/graphql/params.go b/graphql/params.go new file mode 100644 index 0000000..adc1820 --- /dev/null +++ b/graphql/params.go @@ -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 +} diff --git a/graphql/payload.go b/graphql/payload.go new file mode 100644 index 0000000..ca3a718 --- /dev/null +++ b/graphql/payload.go @@ -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, + } +} diff --git a/graphql/query.go b/graphql/query.go new file mode 100644 index 0000000..3fb45fc --- /dev/null +++ b/graphql/query.go @@ -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 +} diff --git a/mutations/accept_offer.go b/mutations/accept_offer.go new file mode 100644 index 0000000..9ef32a2 --- /dev/null +++ b/mutations/accept_offer.go @@ -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 + +} diff --git a/mutations/amount_input.go b/mutations/amount_input.go new file mode 100644 index 0000000..6185c5e --- /dev/null +++ b/mutations/amount_input.go @@ -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"` +} diff --git a/mutations/approval.go b/mutations/approval.go new file mode 100644 index 0000000..1f927e7 --- /dev/null +++ b/mutations/approval.go @@ -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 +} diff --git a/mutations/authorization_request.go b/mutations/authorization_request.go new file mode 100644 index 0000000..81392b7 --- /dev/null +++ b/mutations/authorization_request.go @@ -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, + ) +} diff --git a/mutations/bid.go b/mutations/bid.go new file mode 100644 index 0000000..38981f9 --- /dev/null +++ b/mutations/bid.go @@ -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 +} diff --git a/mutations/buy_deliverable_shop_item.go b/mutations/buy_deliverable_shop_item.go new file mode 100644 index 0000000..ddee284 --- /dev/null +++ b/mutations/buy_deliverable_shop_item.go @@ -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 + +} diff --git a/mutations/cancel_offer.go b/mutations/cancel_offer.go new file mode 100644 index 0000000..ebd7e30 --- /dev/null +++ b/mutations/cancel_offer.go @@ -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 +} diff --git a/mutations/create_direct_offer.go b/mutations/create_direct_offer.go new file mode 100644 index 0000000..3ee7099 --- /dev/null +++ b/mutations/create_direct_offer.go @@ -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 +} diff --git a/mutations/create_single_sale_offer.go b/mutations/create_single_sale_offer.go new file mode 100644 index 0000000..e244792 --- /dev/null +++ b/mutations/create_single_sale_offer.go @@ -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 +} diff --git a/mutations/limit_order.go b/mutations/limit_order.go new file mode 100644 index 0000000..6f0c6d6 --- /dev/null +++ b/mutations/limit_order.go @@ -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 +} diff --git a/mutations/mangopaywallet_transfer.go b/mutations/mangopaywallet_transfer.go new file mode 100644 index 0000000..35b3170 --- /dev/null +++ b/mutations/mangopaywallet_transfer.go @@ -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 +} diff --git a/mutations/mutations.go b/mutations/mutations.go new file mode 100644 index 0000000..15ae20e --- /dev/null +++ b/mutations/mutations.go @@ -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"` +} diff --git a/mutations/prepare_bid.go b/mutations/prepare_bid.go new file mode 100644 index 0000000..a252441 --- /dev/null +++ b/mutations/prepare_bid.go @@ -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") +} diff --git a/mutations/prepare_offer.go b/mutations/prepare_offer.go new file mode 100644 index 0000000..1513798 --- /dev/null +++ b/mutations/prepare_offer.go @@ -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") +} diff --git a/mutations/random.go b/mutations/random.go new file mode 100644 index 0000000..f30481b --- /dev/null +++ b/mutations/random.go @@ -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) +} diff --git a/mutations/reject_offer.go b/mutations/reject_offer.go new file mode 100644 index 0000000..92eec08 --- /dev/null +++ b/mutations/reject_offer.go @@ -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 +} diff --git a/mutations/settlement_info.go b/mutations/settlement_info.go new file mode 100644 index 0000000..fe717f1 --- /dev/null +++ b/mutations/settlement_info.go @@ -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"` +} diff --git a/mutations/transfer.go b/mutations/transfer.go new file mode 100644 index 0000000..471f426 --- /dev/null +++ b/mutations/transfer.go @@ -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 +} diff --git a/nbaplayer.go b/nbaplayer.go new file mode 100644 index 0000000..2288cf0 --- /dev/null +++ b/nbaplayer.go @@ -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"` + } +} diff --git a/shop_item.go b/shop_item.go new file mode 100644 index 0000000..901c739 --- /dev/null +++ b/shop_item.go @@ -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"` +} diff --git a/starkware/consts.go b/starkware/consts.go new file mode 100644 index 0000000..548cded --- /dev/null +++ b/starkware/consts.go @@ -0,0 +1,2041 @@ +package starkware + +import "math/big" + +var NElementBitsEcdsa = big.NewInt(251) + +const pedersenParams = `{ + "_comment": "The following data was auto-generated. PLEASE DO NOT EDIT.", + "FIELD_PRIME": 3618502788666131213697322783095070105623107215331596699973092056135872020481, + "FIELD_GEN": 3, + "EC_ORDER": 3618502788666131213697322783095070105526743751716087489154079457884512865583, + "ALPHA": 1, + "BETA": 3141592653589793238462643383279502884197169399375105820974944592307816406665, + "CONSTANT_POINTS": [ + [ + 2089986280348253421170679821480865132823066470938446095505822317253594081284, + 1713931329540660377023406109199410414810705867260802078187082345529207694986 + ], + [ + 874739451078007766457464989774322083649278607533249481151382481072868806602, + 152666792071518830868575557812948353041420400780739481342941381225525861407 + ], + [ + 996781205833008774514500082376783249102396023663454813447423147977397232763, + 1668503676786377725805489344771023921079126552019160156920634619255970485781 + ], + [ + 100775230685312048816501234355008830851785728808228209380195522984287974518, + 3198314560325546891798262260233968848553481119985289977998522774043088964633 + ], + [ + 1837189741429329983886833789246131275985545035599091291966623919967841244204, + 469920083884440505232139273974987899994000885911056071194573294589259802432 + ], + [ + 1337726844298689299569036965005062374791732295462158862097564380968412485659, + 3094702644796621069343809899235459280874613277076424986270525032931210979878 + ], + [ + 2997390320399291502365701712271136720873363591256030629621859546399086933620, + 2725742381037070528763700586156979930560374472266472382691451570287013862562 + ], + [ + 3608386523905995279224196894194758246854376991737956048428718275550441491554, + 299638830690759628369563708877422667364443387620215168054000198378323554222 + ], + [ + 1733017745745290190841058775834438078769759612359153596488000160651631909868, + 1973340172374381850851160588687352250788736199336041450103281811142396650489 + ], + [ + 855657745844414012325398643860801166203065495756352613799675558543302817038, + 1379036914678019505188657918379814767819231204146554192918997656166330268474 + ], + [ + 2860710426779608457334569506319606721823380279653117262373857444958848532006, + 1390846552016301495855136360351297463700036202880431397235275981413499580322 + ], + [ + 2395624363109833935111082867579092089638282063493755655374369403894420657396, + 351237427147755677344067136337758392262982966921757802462075586166198965221 + ], + [ + 1518817631841006315871038165514435660668372956425694825094659891110110998470, + 2435234811597428668734076452595083530950234466401981561306652881621340269965 + ], + [ + 2173245854114081430013864960244839145346281378834121479101410821419573677603, + 2546798213917003006819050845641858786968858658397560158974242382076827691040 + ], + [ + 2842565516483040219247288049386440051275065340592157409909273207335045943247, + 3243970369543480657564144388570283526584293743815525434693286186817417955980 + ], + [ + 334001339911595275369567085510917903426590364565508070786916614629507192987, + 3111246400312591389547128607242178414610449977696648758380570718520342084022 + ], + [ + 1524160182224703084171959692156493185929433834577885561910744542328224256855, + 1537801596806048756579645687819844574476915843680990392821821911338917834516 + ], + [ + 1534228784878613855372285213913393282004680247144707529194564051083323737667, + 3521706376781514787959257460794337508069645724875214092054188903006114926236 + ], + [ + 2578937995141029655393232141271255572790413762563128577126763729975116228193, + 17390356333795810120168422581001175036590566546824644641783194730252048211 + ], + [ + 947940612979492942947148169286573131514814097313999984923945564630579515590, + 2308193393705297792974084886503909156992936885451139308263357445074155842124 + ], + [ + 732404465937527082089939128149870791505934917542321234949662968808570781433, + 143709480454569956048931032102611838633822436488408778496842771196869318906 + ], + [ + 241248627215637165874725355816367843299343644290443713521922700286140902436, + 3252553440660691138666231381716834106176440363202963142721270080741642531818 + ], + [ + 3333115552336678637619322993761507811794447605372046548664704236825849321847, + 2074314011440265695926966409849756773065015399410882685131987099183343980472 + ], + [ + 2828708362623152676836369441327395494506045083356924287447843608221054063061, + 107382801318187992328203770492115828936772008265759480771447426051158848300 + ], + [ + 3093728769381682918281543022553646237541394965209383769732554106568421526166, + 3204173745255459543321323207111205642245664180117592291733272407863239345733 + ], + [ + 2408410160653222627937499570601090771762354825212795227033567284727088044150, + 2304538566806563442614047090440785060491938762209719835685218901694719627776 + ], + [ + 2758360715188072223623539313334284404194065029791792170224299872004682172868, + 1002646182229402950578888347706450598176482335256655665515308125378628073404 + ], + [ + 2379910339855741683480364155463331175570260120162494489033888506779165916952, + 2649708833736663077287705299849077665696945338155198794587505679066706972556 + ], + [ + 1176714920396664309204390318351093831295503091238549055894748705033779114462, + 715774545317274898026140714630411642171682270543528864055131586173491427672 + ], + [ + 2785974441098456234843127330799770200846625265290625972155616950088804499059, + 307863489533861377687037248795744305150392367370243564208692826588510059533 + ], + [ + 3127903794657845782054923624413460963746108626701051648823597412593664219443, + 2832400994360149010034695923237223654142501296305205824531678157494587069403 + ], + [ + 1131830029003838132931634271160654858275272609594100638978880153740390535738, + 3607754722674459909791405256520586221653709952825470711876211792388292839610 + ], + [ + 2759794674261780431984200708995704387783325908768350345798229435903528807938, + 1260417916396710926345525006943606967340884567049582926597956766543273788168 + ], + [ + 2830057895043497782751868691208958763779500933641414034760294364554584648598, + 3148801330152002136119343944143478481505330324328753740340717562089558415415 + ], + [ + 2506640265270419609137616465635205683276867684162736282412466285973014171890, + 517183264945713035190384665697926865674306942691511144684578407765174829225 + ], + [ + 297135274309227547571122074141892368978567606416603548099453251059259457396, + 738308515934554491948011858951484620721581230541917718419817808748771885016 + ], + [ + 3287710003144516108009450594509223314440628263909148329742349774812346409307, + 225177091586755328705836619671963709040245082813366263262829376695813391167 + ], + [ + 1902667075164809149654789463437998238417922554649696913795190312096632954124, + 3609476325943007214468624874971854834826753291984136726633316509571578121273 + ], + [ + 3452217073856686233854377494033704667278961088549888774623466171791636016755, + 2444544408047898094236889539040891081629098003972134793849574953018755818545 + ], + [ + 2069167537346986671273731269107346759773110422380837126332493778223975152855, + 357269144817598369811221449775153952909196646906055962631273486250245080334 + ], + [ + 1047243972090526803876529530618926456830431728514494419504365869386003201726, + 493385597033162791196594107722557650629404615534399757351633988473756315396 + ], + [ + 965109286411904242713728581817485738428793211202426744037474223240067211186, + 2413690664561921424572393647761853376475215279589433231249258838213909974115 + ], + [ + 1034828486658124322341241159997750207973283714015564270100039839265723642437, + 3320659525509256706388336697822491014651786050898707765488292630053240833630 + ], + [ + 2924967592602073254569141793533465663326310402870978597207810641466195156731, + 2828397747600941786969312189630745459745605646639868710157419955197341205208 + ], + [ + 1208329771806436797417016120088886878299172415075658275611114767604464163273, + 2810710693404583496584233908768327740199404051365142000873009254465681789118 + ], + [ + 2187000042175732773516370724251479381096587891539947053664138571731476871402, + 746498189666538551392041580103908969319041062419800073057943031798594621183 + ], + [ + 2000452964676707688903182602940959322690781577603915068119089450310903786954, + 866954387631286490641992457358203082174659959202834537990114265745821626191 + ], + [ + 1296834309098498653963459206815713058813876510889656567958698555814791917906, + 900321515532234476515871433334993494171305935536543684417444011164731278279 + ], + [ + 1595376832537627540806114085753076669172519984983967639366317789074759898235, + 1219266627855965397760533984052253611682860681989985974389624113620845749733 + ], + [ + 1823240537897691300512000714094702014772232075125035605123065504505635249040, + 1906261986240090609038909222466712928329872469704972427112770233118685440655 + ], + [ + 778303663772980866291056760213466667611301230393329301216572062540133184143, + 2984789228888160339109292850517099811453943454548552440328876677370962441196 + ], + [ + 3543009119282959041814671650391719969699313481882590413207543576117841443934, + 1490213523215199378557197585333711645365263188010733339965078460230935145833 + ], + [ + 1176236937487751405961855617527764992282992896230256112304717984169523263763, + 327501310716241925530534584357204203311238806558120970999031300336125027957 + ], + [ + 2406374227501733859839714271591391036982988438954690468147627905682319529429, + 2498960794066678523664440543302013058525262611284856216226688049821146904878 + ], + [ + 3452133497919418476271423809649290075304287340106989073706651714039300732642, + 1721794031770397703814538822528137647140579794352083932468384744314312603894 + ], + [ + 1149850245936233973982137051189893468427998957468612707869248329754912343300, + 3458926667343838493950348154788263034977717528749131548957463567618227215963 + ], + [ + 889697158819326131781010711389595245311511671705340221964679483759691059211, + 2019807322878676390755723975464224869137141739292295209065143377221547630036 + ], + [ + 1115329342882491971826579323754611049286425400118842701871616630850493708107, + 279298782170669703031554266329450929495785208527313704344065678320374720785 + ], + [ + 3365519876326833923487050935023315949049714901059255282163633136201965868269, + 1868260280532817409831058300719431634773697469228981331390874154636702517757 + ], + [ + 2669519052792032403625707785046224942570603898100356640293648365762798392505, + 1740654081939116207510779753054238062700744649382792523869583125748991653229 + ], + [ + 3121331648294614359396440970780137002689430823241383158647529355695088932901, + 873685066624425351999444458200994077868639460913455806504495956245857350007 + ], + [ + 1816660828193076969492079690868149793376961857938776531133929200951688539889, + 1617163330180274180907112309026652344859788476549850480902966972316617122251 + ], + [ + 928998762106806361096934313135623561544110643422813945289044484271228303836, + 464078854784700975668790446344005332658112186605529977536203111266829995975 + ], + [ + 496484433760448456019075524089809339174015679144397089226817311073355526314, + 656343647912825200827812764729017634457244310809059156122299990679423100787 + ], + [ + 1973676598953671410920434538501081964242302598763799105681937944844095305545, + 307489436917501023536717840176704363109773690206006513056594461780393179493 + ], + [ + 518536787018692743767191710241721928752533989539898170372615978836875877432, + 2521626354257053998255710814293449754273786720271901759585107157901808273967 + ], + [ + 690595468184470683559850269431813912957862648277508592008590933748732173747, + 2414429566032394919031053748274838227119236756361308391916397575422431579532 + ], + [ + 2501095101472669025652293419986706422252484721018908950115277955062729551801, + 2787294359824056854441860940419185812333861607391901444904784313692077324784 + ], + [ + 565118044580500186326118761527011487144705745596478022906701885062524158603, + 1799231527053210762358771329838631545632111862410291843983616507220396846052 + ], + [ + 2037340835455495949556975891561839169602876578929015568690589435716642289270, + 2204387525434065888311157590483645040393870259860783204107223503007512510657 + ], + [ + 2114937389277866993631127029230629118622631637448671765297876516930885448024, + 2772043872341063054220063825377798998299638741683281348998984204361894484463 + ], + [ + 2097763236780897995115236415009286780722416534493059040644518024665003224715, + 2246556465712181592290422124919768779861225704408240007042932086214236197576 + ], + [ + 3612119898822167069923931382556535386023574823466693257258182923730749602297, + 1269484610898538742657592460359658026176515519847024771198089933933954376158 + ], + [ + 2881403268965266082547256964340568575654154178897275699709021163679539118655, + 1692819601432103623771042884269589390667189502599274435132350636867812310182 + ], + [ + 750328371578183028452365126719312491229205795053566378454173411358094968605, + 953119186030327873981560224892058145045695472203493861081607133113834466378 + ], + [ + 1656613311827787565035393466226110392419398195273172946657205368665450125099, + 2599745208280264384426758693760932474903320975443399225749144566172064401743 + ], + [ + 2103692960985198293421966097241235578982164940176248129325331384077838061492, + 2570917734334329081039835855157625956461983506342767455507599344910133039387 + ], + [ + 2956028884218562660581192985661469944972780357808337397013249554146206921192, + 408046758724714924414144046633476879802492779243354079741111917699605101737 + ], + [ + 3295092543885183250245242131201657515391353210344574951962581116320755471573, + 1642807297265426717090166824916187234878363764999511552230372565887780637331 + ], + [ + 3254747955211938731592559910914052760563649051803026767692046312174361324755, + 2597825541939769956115552787003343561035903289305000309435219987085455364052 + ], + [ + 3061661576103456171574781131851984681727274357576590194413976242212020548895, + 1440946704104779823819051619396174008908467766221900978049401983321224347705 + ], + [ + 838962129372725157269649943170983294581294061955271030939039912615696392969, + 2450855656904900531871678154573620082208900269187857841648756800522172293828 + ], + [ + 623738578803750870859101112646088321855359403665383400994605481921028955581, + 1896987617779463068695767102228909909503881877404532152457554106592081120284 + ], + [ + 3005336317328034100451063789097066979058288244531776915568829475740707033442, + 589659290793751995469380634546964377934517395448274738900772252627065284531 + ], + [ + 12756189276497641079437863756137709474617047416617345367587373008165663758, + 1456142055978131072821045695636502275738045623685201320945241755292775664199 + ], + [ + 1074674018821399880635425421583189259497571069746227839865670725436462244312, + 258858007368753107323242423300483485125794748975129950895267988132082308316 + ], + [ + 681655034855073789847960596245322342285304909895796912572444021565696961022, + 2638585878119722820634387321875125243576892120356441191562743859796708349183 + ], + [ + 484275861089119330366419813397313284916585101030604880621077715936286046362, + 2124340832207188633301820498272014794490866326891190193431359618065899307375 + ], + [ + 405073150800337564971044639647327667948449004749665061152270060911349262451, + 1489771173602690638325396124433593050299556632624187628464627170731054511183 + ], + [ + 2210274362697653963013276763896406028937726941229800687802476566182252928069, + 107281585620564596853732679859399446219714819235699352412476509704530753455 + ], + [ + 1574843453768695165378209623495036727403155857878058624836922271817659867629, + 1555661183633642518402202300513733784743599394430116656383692711334768895734 + ], + [ + 1123478630620218654285047429952065588412730975530361457071991777237712271756, + 2829567770920061439901039508116248775591132699709271161323340183772358690173 + ], + [ + 1297650053639848975729187455845252204377426797153455899043246280172874099679, + 1368877225005672215217218872501867910626338344551878492763876596080463242259 + ], + [ + 1370690034945541589206390781179695998551445194688862467131056389583582009970, + 1470539777067742221259042319802797839837448440044303811384116671245122402709 + ], + [ + 3447187626984579154758815357460309989835728867823525757738362796350869385148, + 1816603979655684111669599224323721980301208636016613170580056711560881185458 + ], + [ + 1813653163931885994109909806517422543930671118466754170167858925831571853970, + 3512341620174737184060311796746868634824078847254277831432645720920236439319 + ], + [ + 2837307695083915103816975903131537789416753154015497312646069123780420491714, + 616619065833574133353826959462642204697394233876988991826103321230494864299 + ], + [ + 594079125176886234216350989657852431152571525526195525372802362637040396383, + 2494644159641327263522333201651180948727022160625648361761185979144016156268 + ], + [ + 2908396604150117877413275805593843594741970566352971841958566678954509739722, + 2270651079544400929734484036612745055500450526594372099549710701035274374884 + ], + [ + 2678766929120334020289731872716504689707050034449508844312788171212235149009, + 335161156418784138664067328871308180390826302690183885244799298812600140579 + ], + [ + 292866251242750359983852405638782648443335655395113648998608114038199112084, + 1929874131411396072089047462553509072816290408149385625319723159940078151305 + ], + [ + 882230982554387611436849430391722624915254936568405968528170702860016504332, + 212032938028459898975134792582247420939126124794435382098720034240512634047 + ], + [ + 128726401362994700814735301317860445928097536803417431852613367962589785439, + 3394124118935926464118252583967031473364357246179309645829616988941527947171 + ], + [ + 677401161940040406778133733501716798305847724216576059351439033320244262845, + 2925999450717968641674678177839015248195288227966544952091044204990727381539 + ], + [ + 2609307202957921769201454597723184920663265694086171832237331231605229071753, + 801185762689670517839772705471851009753066843170718046248101910206876036095 + ], + [ + 3164184719276807654208802895852587431370308788690119771658913844464285523544, + 1905779810840212631464048113896329901522020622508861085333684930860131307278 + ], + [ + 3611134164349757211212839193742321198628214417127680099335976437205997336534, + 2043780132560451089266915016427409559115509465273768103961201197432849551860 + ], + [ + 674219880578998785256251799630500124321523321851449394348048261225376968813, + 3482911053866603432328977995361936638192865211341332440041477929097847986703 + ], + [ + 3592898179794583311559546383565181628347625484079136119992096748458485223135, + 1465170880570843353446345695255388397672185811127330686304727767842553737080 + ], + [ + 3479945591612641094508473900775458017345694655844890750906112888442669167437, + 3101936119331166017640859304880796239643688879875971179134677606262220157899 + ], + [ + 1109415449398025759620102870831886622665877707210305893342279346428768457000, + 2860907454930708330476655483302818728978205359193672814876717458325223426655 + ], + [ + 1357688207483205144399854674655145367528198873070486160807161516476844171794, + 2883341787687489538845947867713953817206085374003322509387259371280461173550 + ], + [ + 2673153466150226365693822405439428986034577351717746967078667507408831331822, + 1711608722888314089357865502727953201489434753448475578858737527005192231458 + ], + [ + 3399600989769963316575978481506459307955666332477901574428783938231920315833, + 3215591135790328619011904119580811201910745729655431204459208397816583809934 + ], + [ + 1836471948074401070182493369353230908796463402812051721703561360825727986274, + 2080945706738209357213401773997996954868066166149050730738271792162977075010 + ], + [ + 3078673344420931157936045314816499193943345153816528362806332351627801597029, + 1525425884468796152026606689861526369646610285280224319617687192867257228599 + ], + [ + 2722661126018430265331502523339121563282558062510457654064467493725828237046, + 2961950709147303512642570304801930511214999580227551995337732709133755475031 + ], + [ + 3274644392893639165667150492068461730297126947445956777112794513023991125188, + 2045331969496016255571307687320013551853048491733214352646441341466617604859 + ], + [ + 112765822376634836986557327486952401905342511371567932656784212360129920193, + 2925784973032974919740053039088226660025084255241123579355659877189527261408 + ], + [ + 1615139200617101600744409619536119151548135245881789111743749077087127202607, + 928952548463900597431429074097494026341638502402888819595959975139532597586 + ], + [ + 89110835703618626006281975484745142479630710003689233432269611283359724860, + 215577133560624823939271084084157878843706221683942956847919314607432159969 + ], + [ + 2365204845648678418296386461366871427710672197784591168077970521383279482418, + 1305956795685348559654727794862561316677393382417655218949059817923691390961 + ], + [ + 3079404798552502156907780297645909071656015399012681371402409463283987518254, + 21056707131743755971547625076568417377953234492785298586731786122687871587 + ], + [ + 1774615721844036510130640283396354163953067525827829514432813052928446640946, + 2335149358230730291686085189834918003837685389648150105141235796252419309479 + ], + [ + 1735808336641971303276438949279976616427622212845904655791080520595464040270, + 1731419497051910986191800745158520013128509429166504524452212426369487243072 + ], + [ + 225628732048332665517140883504098234968194122687532130198116739739190659914, + 2967548981364742052624681942465301778302189310173531702492567780724033172480 + ], + [ + 799750724987433962024178782345988625264638811940430837962580781131993819643, + 2706844911451851414869588131946375233343117270234616506423955430971490523817 + ], + [ + 1369724618979165159567545620227678034464433354639639473366228046556595024708, + 2336720505293481339154253277307615910867992206313138581663759107186258146704 + ], + [ + 1930319710428527822612172469423758994346239585108798378474140896733181974186, + 3075715260783582884087648822116793974103741019719484227525273088977328650560 + ], + [ + 3338564282673470966018775402725903223528182579868287010100527892104759349598, + 3420593956586273983490728381161913171223107153319939485895365605089451302873 + ], + [ + 3555800392979470758767025620764223025989211483193054579480906006330657301837, + 2149899957029896266930433713982567969321434161670385808908337955713550175074 + ], + [ + 3608545457332454869249792272060591549043786202385519868035962077469064609868, + 2809440208316315053241747840271233428261159310708153434099156262944856645508 + ], + [ + 3322687396347671973732888070910250198991075315814763746941655084877070735058, + 832722278416792540687431048542953362405174329235971097088928697230453072453 + ], + [ + 3256622670111196871124104758094097761744096984947383503745643093253708235118, + 978374695622574912744133217293321822318821008760942454268417456141195625584 + ], + [ + 3410173130373258134073085888536026114751965010082957684030563699696697480724, + 1029778749451237919869173468746897632364581346265567722171293156697958069877 + ], + [ + 3112433780585229726581053697334217985356658602483885980524816220344484038215, + 2532342168888613388980558686859546650509221859779222781594186523997684356788 + ], + [ + 2105373184300453833566715269278756869308617438842998862447898598873739219726, + 877369785681960743494801702556244962845285506808792672979162666273085193389 + ], + [ + 1393232346062500558436496118495653473949473378263920202909352539047530421805, + 2756127133762923853208501814856325859087297216739373809908612188013181865996 + ], + [ + 2602735208672053198262611186569615211175565049926056251480233572878547833479, + 629329025417775185892796948300727047077553416972749774530835029538350502253 + ], + [ + 1793674060552460299974233062088263069538015472516735538751555695772239138820, + 3267910293235826685471049883722469455862230494501330393412138559038075359794 + ], + [ + 86863311610097694600007716574788775452468848027297474155593683107676444091, + 2437707467955141280580805831659061124536906723244797886865002980932492722630 + ], + [ + 647853547298757687427688341299451732092383134491816678870980494283372695378, + 3617636854438826076469516140770697015113683007543372219324291693845766252558 + ], + [ + 3334781490544017130581236435947958883590529475501049503331616858692678672100, + 1524104441388820711776621309003922630015731846383839927647934980171536936944 + ], + [ + 1706920492017425179811478489658749041569247255320747028220786202351882167016, + 3136062931779844207701097197470225751864670973511824570767144629934893764829 + ], + [ + 149961882640494348338043440935347934018050992098250101342463562074704074105, + 3512185327682078074777552971488882847736861539286017805546782388407073599152 + ], + [ + 370860151698809780775659878430714854391039795566897134502189484720213964480, + 1238632390866429469777130064298502868565198081327737370254478336416980048965 + ], + [ + 1282856472050108295483684218758047396436543346709509434400652555424465185837, + 3192157841056717224043634359360155098877589892941349993757962372268962797115 + ], + [ + 3312829654231446830533616425871987858809512713259532177921343043407846981843, + 1806976906510042164961209222220236480094335196359899499152673663644589005295 + ], + [ + 2826214476236947463134060188026701900955812519597178470627224598830931756638, + 1580208692732193723402036455390144043645806993218896764366725410799716087870 + ], + [ + 379282466405240907198946408283110293887947304133389197401888303636683398267, + 2678326696284237155462064013317839501356719176588712513572353629324338176024 + ], + [ + 523694191683203858486629087773330056719704186407500237062037916699142935895, + 2926557820959194747499923406515961933437184756076777735860240549085691053587 + ], + [ + 124833202191020085496726895079225667003902078587119506095688146305802394652, + 3363531045637590226091127058344713074476460386293564329053575414302767346399 + ], + [ + 2143651374532047414518845091756289685497664604612574318922876963772765730070, + 2673827034448835877302891179434422590334329367577351180190722501245625474345 + ], + [ + 3221138751067425577747344145841964056907436939401545122534796814786108536883, + 235336175612617006468583569743134666841147774340073069013500151038053147462 + ], + [ + 2555268812438549803835155012590418157639496654385486677760162898546595196513, + 1689750620936872465271905664884810894356966245587199342495604460756806777168 + ], + [ + 625234954128220194452646182586699463591457452260757480259168774898052708673, + 213161676086201716193087398992571428821719377385651036018131821081133441051 + ], + [ + 791204612818253360042670315855828265280744396446868065071639490833059389857, + 2903319472504514375570923883911935291656049846866663158923319947614657595965 + ], + [ + 2454646497469354145786043440836113873410753626627793200029667058037294177092, + 2032912288570051024788875629005185779516597292453270591316074924761292028478 + ], + [ + 2123337906329005118437052559658178011321855724007242277348144379266185921681, + 1894503378078529242131889602767919022671882244815906977803914704575270961364 + ], + [ + 3602877859331983834496856242375420741994478431198393391854598373337944528390, + 1390248307806555980791681315101366407220742869690031661994109003230157894293 + ], + [ + 639878381660983183381027433798087557736798886846049125326586828176785833470, + 3512419367912974372084649665826686824136068377404083433804922599142564888100 + ], + [ + 1419134474578125103135604337269063811149389272963545586390539000069337598859, + 2235806112698740444007125277529836633381483234992038997663439878202987160165 + ], + [ + 2778935883893248105666429190432349575416381537333107318387561120528861164061, + 1077278765722002922040368598422352116268480578772186030121792709039469085744 + ], + [ + 428120812125473710465911539814876147732404851270862105382572167865288414733, + 311433631492674653227552598028197969484982737235311408621797869297871619494 + ], + [ + 2952060790673513881810338872881675262252364545105524564508325071871367391468, + 3582199809541312286609332154241682748678754599041344486017973240874423950806 + ], + [ + 1594461313084220822848406979098581830091616974334911374972620995310916825614, + 2807125397722833979284621487977999302186252215199452982876117209095859541679 + ], + [ + 2043845413859332839989544551581949027414436310630598302726839258465898602603, + 1493460761394315567854883351837738680253927509880345067100858649883130457048 + ], + [ + 910520306205610055407037560562219297599712042011958124271729124108713600122, + 1227332044591912072232756174484900317585931037443282067667157281800895006658 + ], + [ + 1356322905904034407802797063629117404975010366322135572201704229186917875805, + 3062811278605169224008088325523606193685397687377873754697372903028059871958 + ], + [ + 2651699680375521157731474794703697878196552894539932179312542308190248741615, + 1374999585439430684113300998459982464172009456194373596393318069525504982141 + ], + [ + 2478956707907870604028063666882128463565422187569850243079631251653021026113, + 3484420957530723200820826205205389208856700316444106810018542093549855044781 + ], + [ + 2745165789654622430617796031383732860380537410258722201878555322390065895409, + 1389786264162289064932319984048404041828181018706839586582409596900469763316 + ], + [ + 792291068607630837524967871082697288161825884871154205347723150298096233265, + 1595885520235983259847467303703354388143203603602941324268342974672349807911 + ], + [ + 1112766696158267198974840537128095637678660084973877593826070735478819877428, + 76854025577946938851193238773961903814332442336300641590764677722879845665 + ], + [ + 3075088147214758449280318679771364592694228314680731955170139149431172226453, + 2631464825354322427832325664266556982858783834009349057603625173618876560686 + ], + [ + 1946952991061874087036397224151139450490763292883750208451596293164980017180, + 1671486844817988227846350440864329091397847162425205354534975696550529649006 + ], + [ + 447922002514491195056340638904274303849743543434622916451489369322819570022, + 481989665314939062190810517257104327416911189393567287995442516666694890745 + ], + [ + 278768424991728679375985793004023299002069942702077846697446381167229872879, + 2117939832550233574127330090850877444733498561141023361942969078992294709841 + ], + [ + 3216273780587845885344648641198104357960391646838456530374473185745790254775, + 3606237386675957259713749411947060068172141797816584221362457562749070134457 + ], + [ + 888490252135075821787230358754699355629649708262317489940736076372880196421, + 2243899191640238208431440104126936619623401575061085948377697882800556601532 + ], + [ + 1748762228708691375443278014066210232528543730943854135660867476022163374357, + 299810809101280072373711346937892828549469075660577914238588790523849116964 + ], + [ + 21800463980443581727936921323539002523916110909487141095813330024439604509, + 3239991592162948599965263758537867210199504881270279333333530879723254864285 + ], + [ + 1945649262091085266248324326387300230209483523911852369537103537534602188669, + 937547850790372496342721899405171497997439341142225383636485893081045476840 + ], + [ + 95187198852190497553925369717104578448538072022660253488035756790745062479, + 3284691659350683572361562338651440251050266476442725062173141352883841793890 + ], + [ + 396966715395293979183022035167233963473677021753099328784772352776439798275, + 1613322234499535439350563907086596802838800251144492804438492852751836971863 + ], + [ + 1705843197146248589209974041619858441399461896194255757072890762703413482026, + 2709234803093296173390529287307769438552461179797518815269401286400797214943 + ], + [ + 965896257227883986820160011629505229958129089095279852981573428580834882124, + 3222878455111086494470662906288505024631219103956585024274050126506278071618 + ], + [ + 1711062768895650467512736187912681238407905324392544748841566987114959619940, + 2037166002293356114375846587992164902616900087753130705770148512236761109532 + ], + [ + 306929171824313514409556697011428394218011837518848517597157431560368406919, + 2733907887575749650664983471979960087709811489243842872308868788028064963544 + ], + [ + 1589132075979748817708848065945386431745143428233530475552120730272187989177, + 3479574566456289934918449243936076329515058364276769923171744108995406563734 + ], + [ + 535088375724656896741944948092293887713409890408387277644439471611811707698, + 3084839991780046496389632054225148955101489706660124944353740712683200220129 + ], + [ + 1108714469400502294909191174424037717245383586375174474928489701713698780203, + 344696096736515166863459054453204973598183407079213029580530012456358476055 + ], + [ + 1896541196944951993777265586048099398652846183460110390523151621587553084390, + 1356944636529832014079353150870395726873584547194854259003641654476043751831 + ], + [ + 2129740212445659709473295068445927295211143686808997136843204790705829600051, + 2784093412242244048858451658511978494505560368170364505963748965317959330652 + ], + [ + 1164682312446525913981569213875150029132760939832308651498251601400403129032, + 860011516352836343688401715896426496333552023448545234232950198907527747215 + ], + [ + 403381603306414312665420204496598046512017134030800187206579847445734886844, + 156710329648174386978898329138661964114318418114565494873287202679709752521 + ], + [ + 3612709524322508414252878964415432233546157461869758422797096779080105910197, + 3298597797713539922720249912653326762917459398002362406667649739024691730641 + ], + [ + 2788472028266807168790808585452088947945831098822807970786023663768554965883, + 780984533356516602828788864429847085423068302018929746266555371946539918101 + ], + [ + 765015221711443098273066494715116075624315673921356068577773696969663768888, + 121864134634742875401471744901077698759842590065938894637311780483786803739 + ], + [ + 3420458059929261250134488209708428351089317850483628650712157545902459535082, + 2753073224523413466263788479220930266258909876418753081552150110011967761473 + ], + [ + 2435788601301668445647253094872163056944118800775256416712610409880768679053, + 600038247016283378097972299409314284965737590535869850167688589980357128171 + ], + [ + 2791234127851217827076638673185921990434565258934782489657649399234080946948, + 3019532625505810270756207292319968517034464214472035802893765165728566407791 + ], + [ + 2175512767988966931157181387193147869682253002700484615343260272758293076239, + 16356925783563920024655061104076219675967060212474163352137306664035714196 + ], + [ + 2346289056582359849501003989438672266264448907419687867675159268542148069281, + 269429309748271385175467663457270641975914217681440684037010041835530573698 + ], + [ + 481679002404926691230990214906065876613510532429501667509718578731645569407, + 1609976879746739928846288345159176310170130379856291476257183396230103110555 + ], + [ + 2127782061956380596600761098774414382531840909564578638354721422890231607858, + 3522660228464874209807741153414518456209790498657511195587886434205303653276 + ], + [ + 2990749966959304578563050621982841125682975201749885210174805380941485460083, + 295197603441351883186074553477194964532333406546643184464952640061744781463 + ], + [ + 322299859114885008311771586431338392030172898629995798709098486233393241698, + 1023285551085384033485966778701460844235475326250602612915962425374866217237 + ], + [ + 2679886206461898569320731641611496876582297592825799115233896892980598546123, + 3099565537546363902378775008459983997262715254411954165424270988571981464521 + ], + [ + 853528159824941445672647399048964399831359191507493106790462832690982526176, + 1009640753029626140011297690792696405115462988587033910421277684043461588633 + ], + [ + 1303674320335969124745880365084447847319437106063642511200542633107159295205, + 862053741658449155711084470286087651524720201941852653377417774320602998882 + ], + [ + 371784976647836509603140476112794524387140033126839183825485397106022337443, + 1481126947243599849498136670060476522501192641446886664638431717153471858730 + ], + [ + 256524228763970643322523560381608580385109852183329255389135850583181280611, + 406062519770964392170499110443173965524033961539587233636744514155485982986 + ], + [ + 889909761640899390466461648537512393214660407373609451150870536404743826631, + 3579579432590016439918468051553122806010260839218529712227712227008716479453 + ], + [ + 2181687776535382366933204874050926043644394510983340241902960182125931413775, + 125591469249575554746765789850870005842434420699124766244689384267560349454 + ], + [ + 1698011648746714871368937594549801676987990963546029514109897028714017239145, + 2032238188811755521211420251330988300583305301151292725000783151183349336764 + ], + [ + 207686199415566734810376310722982179068617367123996792470313948717972567639, + 1457942055660330932304294699096717861622662715626699977446009835743129689987 + ], + [ + 732258411798183046441278957122759122697038828507788948928154704288224686840, + 2612299176220286719934749375400756907183274870137646747294920834312868644617 + ], + [ + 1131610047798062548492567521656272602549897300746434874306725974344582031445, + 592276015836506844762700749171355258254203250155822561654862572344939966418 + ], + [ + 1683203479448902722167828838268483614879733285176640956474769817134838920079, + 2990934852576563000604264577641206566403815930150479384314858100664762791309 + ], + [ + 3379046363093840121427219819819164110884408212445336400953102193400631404907, + 3581767823598790218171818955173138326266037759069958949236204540729167542495 + ], + [ + 267147147550523725868561003543093307186644052136871409944613118661005584614, + 1306549083362239169609006162611383775071361663461903546405449470601023271459 + ], + [ + 296975912955049993838036596802334313092629826214334467025699377929310754375, + 1563707217733470474074970856533625421700727823760061087588764083345926207730 + ], + [ + 409930698864338128894354352383270205190831536703476012076004429065058122460, + 810813209917474447569624498205572650260997215620628649252215243057189717098 + ], + [ + 1244080118151222628554430592856097691345343883216811606005263269357031069918, + 3112961342564635769384393489844295061472524432493301559668801711930497029521 + ], + [ + 3287905561524106163037657312076054800525947413789669708058413492038554328847, + 2950266460046239577741220775173597014955783375111361479081038783371923101261 + ], + [ + 1834667074444753359896291977244884324984125181165619220523638440982002878973, + 1028938841439025536907121844571940895060940730436245288628559554616030756654 + ], + [ + 2696579144808608214357559333220835033277874753649087677547926288655864862396, + 1205667596625326964488748126939548850792621845142605787353435428502428194722 + ], + [ + 2681502303973249854884858344399536104286034851010362500805472440066210957701, + 2090624297500029615299510863468795665118802449870439334493020091696427901315 + ], + [ + 1353314499186573664458206377062562808249080943135426913783009658343962961481, + 2765083962822990885404819840064727313062737767044627137615530427213920746301 + ], + [ + 418493206714369123577433462079076010029158131200185602134250755432510923649, + 707998386543299716720343491204719022813222859618895609544390001004782644320 + ], + [ + 994749436490026363483216765171719719813756635373513071896945407046466009916, + 3542023359278349542922434792313919575183783573969934645527389879728870323439 + ], + [ + 1890008117079085184019219906457544800392678580280084811260498224586609070286, + 530233764155506684122011374141421351751148778529665626269052967779624308325 + ], + [ + 1380484666773413147984807138259355829485624671596401959499503325097178446404, + 2703143567161792821055257608672826415980743659971254233408855036509811875962 + ], + [ + 2143881856351418759124748482049358565778910428733836748398777699149555891034, + 2186504045350479968631256614333785165240148508469904761831572298072325147795 + ], + [ + 2794147613012298673104841054632587797837325721650333247952170883643570500710, + 2613093490820907283519815366146280477230890436771830509953858178043843459008 + ], + [ + 2625979094712716243539648580331695634041650060241725094563615224537054056479, + 145143322058807873791926380354826174760141531075716146087195666113341406100 + ], + [ + 2912042742810958222559628370002186243459717464446177149380721928196350211880, + 1095311779741249761721276459609161253222688862762095992035499286908387724976 + ], + [ + 1779688139950750136680613010527778524901137919784933323807835083178662865397, + 2443673470483539351187560062972650745287069240410986822268053157528350814229 + ], + [ + 2131257578483960220347030216154712350782825143774667810440672905625819556803, + 868491622632592338574614600558827126185061061280839427086037749491272636942 + ], + [ + 2219005280109778946483442325473674751018277532855518690831693876689847358912, + 268228073082292114390894552479318098546823436290210690954657472261887508233 + ], + [ + 2467777871565605588821089206361725030593121231139954405293743820019316429337, + 2069991336503010889436721322920702620510647146193316568814112316083040831176 + ], + [ + 3577501695097063979000880929536422727582550178446643147830010270693854635348, + 2035294884291881522017348441414523234930446764971044277218808172917534058938 + ], + [ + 2916007638012715951040881085223392656327753091743451984926427831539302440694, + 101999113634064710169079402836107323839592545174054596535848670931124794072 + ], + [ + 2824482828379328312126109430912691348039395134484392469690037535472985287979, + 170123149569960330410734287368175015043768931496132108203420592659991682297 + ], + [ + 1722717317273017311710033441508078253294103515222910371014974460548425614659, + 2455794982320624552496521720819953146480298894982220570859668985011878438440 + ], + [ + 3323332844428319232630841298737492488606773387534937061908049192786445054249, + 2775452603532873679877923100539353685218185462328621498382613902225794012998 + ], + [ + 1608012153975591201510431671483571431120513181059080848126129285407754084748, + 575592304166370707508545110070168578909474113291472127326388774685581172878 + ], + [ + 916809345825028426832118427347317839178426151786124666474024597549724310957, + 3069555112506328035675763181932227894553434381085323151827207060709357462248 + ], + [ + 153560760075250381766471501226441998688409013753252062800770032940893580349, + 2329038724186047986576327612748573076822768420981449905843361105210433443270 + ], + [ + 2251563274489750535117886426533222435294046428347329203627021249169616184184, + 1798716007562728905295480679789526322175868328062420237419143593021674992973 + ], + [ + 1952032427782133959985228051054870623876234309599006856796795466237953231448, + 744413679445899225088843138289996934867918835619653321950444531036817070023 + ], + [ + 3162883296795762041497700357918312017339509549159370543230518526626437112653, + 1148816493574468887215135074549621724232310091624566542087195747872792868322 + ], + [ + 786950607934610388520508637842877731308927224203614303759337398330090790623, + 444134697710380413360573180457740543414390667171294106626775858829645857259 + ], + [ + 2138414695194151160943305727036575959195309218611738193261179310511854807447, + 113410276730064486255102093846540133784865286929052426931474106396135072156 + ], + [ + 955835104121335308947276399583596848959851821452355864667907221325383590383, + 1935683370537209806184243406809646867168041420482463925534831987536519170687 + ], + [ + 1499611007946205543199170798196098786940176368383871138867689500928607330374, + 2662008410012406974490205380527854666032675717377005780162761284021647432331 + ], + [ + 2335197826477933983066242217922654955176345813321200025173943441365679314150, + 86799141726517067858463816876244730494086420214168736349361077938664862865 + ], + [ + 2426305297748003258516448191982872419574201776532744568583614742711471397997, + 611146900843862661160935598912412637915904741887617221933114742269852367033 + ], + [ + 1819742747902080962279305391799448622733402573848283520339621821028260796959, + 1894241144684539746894260685967399165405614901992249187163143950195275701901 + ], + [ + 1566900754533300858179807012858131246327928750222566819288909586499451206334, + 2323811598271524355017219639060977623543930469383254150460465078663134678714 + ], + [ + 1338097767250711433700785616700918851742529208514529951106274243527927831096, + 3601290097662913510540208850467494388944371519879253105483940917906091319433 + ], + [ + 1683076217460316609061101548674917172577600596732354862732552514637488814573, + 2586296790559369551539502725860235780473467857462493373020086667442440193461 + ], + [ + 3470075416984304553645258515396988848402044843556043814143694549291509194436, + 1666299363887708992239894153878840011200424023864139235605837090808931067390 + ], + [ + 129884650079435408471813606095297277059523238954518094473826756223429238429, + 913031838387203882010246435451325905809656720680281880669305438372695953289 + ], + [ + 3445652480962486836667659474432628384578750077745289335336644514805586651762, + 1741653654983215120166402497338863916929176017304303902363120840611844416235 + ], + [ + 1173074699967858342716736787366933102436976281324119440358211746283473098092, + 3075964035581410146694612776964161590283242001470153540218734715257792166291 + ], + [ + 1948864707183825487572530934859230839519236819821721157111541446530257224135, + 798780108424505404488913172014962400148988458061505603829909122085722968572 + ], + [ + 3165333849798411161138142302194551326502959074734249886012073955960267320358, + 2854077232678939262337800847761960471531706329482426572081757005793264065021 + ], + [ + 584219684922011437746073096716853840728059302768805720402293458731947997988, + 2691605392627141392945611586058726186445966055416403501891746673585009447073 + ], + [ + 264865088823397344079537851791331047852344700250947641609789815815437187715, + 856557814156118883640651140266045634237123586226962432863642966399740605873 + ], + [ + 1212105529543873848411369707567547187480526619270372944387195134251016552437, + 2552579868998151139055870387891260255591250119487805856658511804923635744103 + ], + [ + 2759929251220121311757096521396321905172783069804137943187241377410890848589, + 1013388642776766405334075600441854256934688539631486641630323046104523794325 + ], + [ + 3435662243060467637677279408549422366191108299439077218684237876763341810879, + 1206100200764686560370450625743787352415337337047308087134628700539206972086 + ], + [ + 1977901254287550894956728572957134897985203087357872251941076511047285452123, + 1346842642356348816775497637530312996348462152604627197529080983616576235485 + ], + [ + 1581907638985290987975047412112008678916597048367684526184099730550194065415, + 2288246533671252346247687962179397967163408912494860510981460961024159618572 + ], + [ + 3215377452673680185835021004830688268079390648010676635220128902519168416923, + 1674957009593589739638616939189211577318542927235942161494800623173476894918 + ], + [ + 3396057617195027713159897957858092093258826926757827467732086865786377444102, + 2403568170323924513104032431512050355627094479417158474914772545814875796054 + ], + [ + 325310856310207888841271598900176645709547071516271373679357299878055669525, + 2861677118910432905996905108378731982262901938718390134034663016688312343427 + ], + [ + 1925427404573041560549638633971822727278056640694873177265388259055141273564, + 2149185931265263003264629477521934555446245963141224065368283592107464297049 + ], + [ + 1057655004272797062633705695597766087036831552982842977913880603549380151466, + 2130130885627581719544847915036196574607634081790527579681856058518350163936 + ], + [ + 1786242059890666469307365196518174814453782118901787482186686744245818012463, + 2172680482993759140271167348144713503721696709986572085951066664839388293484 + ], + [ + 2592525455921742135918400168106149306210970009440868747852006912674191605906, + 2550305630907505809534995587257079662509793820536464892848197484572098781014 + ], + [ + 111752568734708072011137681957171091740064310210630952635788677462490949489, + 1966538689680059826741582906363349945575114015419692615822155104740977191321 + ], + [ + 2324264192758669920944928844116594735168711655659168025211571634463212989215, + 2453209181874565651016960427707879723586882840596638134526741863234420038217 + ], + [ + 818792252748214652222203568101444114842946877275244081137505104552993989249, + 2471135473251293858404864644674805198948427457640609601515835463680395027066 + ], + [ + 776167224210253175836285301928858826524230272765386107941590743597167238720, + 3044593275856778999572277625341930135050598088363464944610781758478552167560 + ], + [ + 2767418361596469194389897577063980781767359707916121685125519618739970163355, + 1124183356341334924855702050560569148670124301541230516023323823985404910253 + ], + [ + 3513158900447819618700445228863547036694626890925843610953740962149603482734, + 2038286698090839235054970774714871346106378670221757061557885786099548421055 + ], + [ + 517681831514430710817158481088000571456636089484182444914961386383717794582, + 1459737506190708125961565377404508114648445797956698177580320945537417189767 + ], + [ + 2764450063359771480330846034975350106048674448417446553801535244886068866892, + 2500955915434901122019038304617790171097079017113487693837548584032628559083 + ], + [ + 3184601066099193053823632069416872853499121524829816523443698520118638972221, + 111399286855641690694757616558822271075634785548525857820633539322287438076 + ], + [ + 1861764219607632476451968430680705159045765477029736548085454909942815526303, + 3542081125583459227861063385092240523576730790188055012978461087647652048846 + ], + [ + 10925555246854055743289295413923076326390541878672074284471241830391529976, + 810379063403665716239985697021625834252465819699617633253170927366131373937 + ], + [ + 1925469472110966024666451107813057200428670133173573112269655451746350245386, + 1187189363484828103108277567024110279560050728332792104397849042655830458502 + ], + [ + 1257234032480175630185580604439948675879173824493927714083831311549585046766, + 482591378023150470327639933521764621888328430410377343971291074047537101431 + ], + [ + 2334319993908953455700110484178707117214967972232088939662977870360647156970, + 2818056499399790981578195943345246458116384474394556164088380917912187593212 + ], + [ + 1453145677148250112321822321443172335864030607730961070308740138762391252935, + 3617981792798631412887889951712686522258702128207981768326584469873932484754 + ], + [ + 264643236147417159992044802699391039945632102423375233223220671619422214434, + 610904689309149986363589451581676922381464434446910426922817060056083567097 + ], + [ + 2773555644105234303846021450759411618679442176305095821653566380427481880668, + 2878405667973852006171581845359598408350517329366546879570244783921286317021 + ], + [ + 2380074631138752101437386328647668302433161481157298955599519485146473998414, + 2940162033501039253058405622329360778854888527422589401316007987697096499987 + ], + [ + 59923324335814857005652966889729706976632643533439489239403033851211490829, + 3181196819190011787131540430610930324408745638588775047528706552833472475080 + ], + [ + 1571134397970145634572604253962346713191790104259693653437624133884723196915, + 511309416166810662427969443722895543878733742083314900009440861143766415361 + ], + [ + 21090530989878836908367190554625378531500191692603997969723554269923877778, + 2771500794785272828011587832172996564585508124740321638858260733779070073032 + ], + [ + 621303564182234060808181503818404204870407855544326969616228975404966772646, + 1381758119149546152274589114806432955337739448745667222869629159555498152415 + ], + [ + 1603367875109750844401808777605019545782079142993759227560886400939494569928, + 3404233473008527201696274593235213333767802823633271526617525914604458121747 + ], + [ + 3064432839007969661251145008088305693584410819966794512614368597856219962482, + 783256745930834086288610539747908078655069214006352981421868494072213571354 + ], + [ + 2573509260611834641929601495624663326040450806493971688424856342734838037808, + 489632329414366712473800098158402333281555035744201997032862455100685111530 + ], + [ + 1373013110025567380060459077575726370083805340382038749245013725221282327457, + 2271658983436031571764113689216829854166985975189230813662757962593273370705 + ], + [ + 243466876262712870310499646879826213360676861126171299065326838396697300684, + 891923608292171984769797750319491387015995834783589731015608767716778489152 + ], + [ + 2035465927594189567864787276274121052811830838707542644166137070067107322500, + 2293243214098049976425980695256140756537564052456935637846524530699635529072 + ], + [ + 1715507122267435124880907385699722499007841543359383748906967432836714601915, + 2065683622001523424317888964929493250974883654939917054448293842930286201120 + ], + [ + 2906653642813167415863836383642871880050990532175816005542735996509246708445, + 2101103582375172516655999605681405804159432128657161521495199065707274548843 + ], + [ + 1560478280364678701345899621740888632653158026800096320697331299727453323955, + 2502904681091262932533150924633878024793617322541538974642118205130709004124 + ], + [ + 1088825136235742041556075285669369753699521218552343596173215811771470309575, + 607071558939135607706029375168245965172230512282049740495005044883485254652 + ], + [ + 416480930348830698091647060484670913532953382502409400899099946640850762655, + 2303434329028515556374106529351629273202094072128674875449835104642893549223 + ], + [ + 1059226326329269227798760274851746346121838934144866840722833084379215695718, + 455998255867283640545200132173274853754233138086189946906020858549998434504 + ], + [ + 1024559718945017260657672873923464530437289768440141950557818820996778411842, + 525500033723589597309036453730301911712422803406229661029628208639589866511 + ], + [ + 2056028306188984024699016201885443563164872865972667060670091877117616276834, + 1419412493706512914027360568510428136300835377049697326470468114560769950308 + ], + [ + 2521038744059190227863027780309672155134248925803510182471085885871826328010, + 2055664921579551707407017847075753140537141245184993747243302270696485943484 + ], + [ + 1408309654797092197568389828883985051397496347829594535681640019995307652445, + 295921513421790144469734685069951228414338437813405505294106756210992032229 + ], + [ + 1987857595984681386153299845716030466354725651174836127615155461210837869564, + 2293923060372667346124288305276256569940238390418855305479173407955750665287 + ], + [ + 1074973828105956295697455820943040985586724095278707830076756602555601108444, + 3326765437245957223406988059616777659852354773867298474672160569379624598430 + ], + [ + 1401862061794147305806657568952279890802547970381675217693442582127912005310, + 910058274349373118447954573574441457656632333180366570053619401133150262813 + ], + [ + 2040966475187373851921668684777109791068677257197750053913982531497273133975, + 1167958332666304570814202030094477634345363022473150543742806601986529818461 + ], + [ + 2881780782119017791457583552000797082603415839437706575588805401515980505801, + 2436105237174666086889653702042028393653105027534224748338553775980791489395 + ], + [ + 1138781660141971412207137170011812332951155719283266572057616562955205995829, + 2657768812194147024915418195050409322486415583242017023943729410483676474626 + ], + [ + 88581112279530612334542943009705636136002388463681106693549082166685838725, + 3496134215297708121790719225608456452574651558469594407547800088961881026765 + ], + [ + 866718519953191029204822844477480603275933535155329865978768093814160529674, + 2022882774094084071548678749116103222489874860976240925473298660635212244802 + ], + [ + 1103918207045639319456460136379598726437052653753960546349780312287131158223, + 1656811459960430943611479537236804458740132078453755963357596576700576629867 + ], + [ + 2975764785119134105842544511285384981490365867343400021422469912399500877645, + 2687980107817591862475555961986751017099827078208385723645417765552791682099 + ], + [ + 2054447365318177209738241671190147649701574355450360479017715224292009927393, + 1227206940403531159772964107232373497220530020193348767638035040017379885984 + ], + [ + 402275007431361887464920597581616397711484740464854159502779178788056390885, + 1547405786479003871142635515616266099578579586990668594451288428801472362903 + ], + [ + 2669324117696876754084336282080783960314916540857113798726442554388023656184, + 3074286759765067223595900394644532377637168990646921851127828180794850850636 + ], + [ + 611932396022950536967338459506782836623876614969030243077175645873783092390, + 2986433254570022449958119232739190557250941610456904564848292348906028792067 + ], + [ + 1110995377995699045828083953623434494450657345505392977032896710538879544144, + 3071470793346350302357608763590610007478960895381257765814531991563057030742 + ], + [ + 2191168535074842056167853224940102773478321048031820308130136731344529117475, + 492617323352024208418412531798559543092874332708570994021422823607454559418 + ], + [ + 1818759115593575585308068448791508192789866090941451793170759417890828490762, + 1305353903124550364838683686338031038285043137449345518795766390174337151903 + ], + [ + 2631013586413722723014078756848572110540232554858227410413816548047325168051, + 2105417412111265728359541491742019675861543959418319743611117424131871073867 + ], + [ + 494722738667200879981866755759016615873180216451714847099458992922362500227, + 1539404447578224883291548288433633968332223396361092124453992010526489574088 + ], + [ + 1894316435470914382555165707375744683572341270069865836107052559173764639667, + 401287370121559774698743821124544382770349508247396498174321596110198582372 + ], + [ + 1339247525522496394127023115420380868431447741930632224270989291600289579235, + 2330930519705316142879600410372647788413471043363313385713997026124191817517 + ], + [ + 2320587088454453474461645476206103420492539243459981382841070259500342411471, + 707303545980793210971675132996162637283200017126206538680628878370110521153 + ], + [ + 2377659897024221375470644518744478788982138263672700674443630590978849683809, + 3419989952269036538781036113174387172326270380530270982346793780460729200034 + ], + [ + 2687585515362755421896341880578567868284030156693703993329134509207013359293, + 550674138339341111775701426337380778844351792663711912206724163064378058172 + ], + [ + 619785501193479957534515714310017569776352132000912157061487205381157886132, + 1961366910419378396302827972401933597848792446727660497590498798601343853897 + ], + [ + 2960597928226130798805429646261618976240340007412692237798738046399566677924, + 3508695180344311906202946672427154710274134142520774461776498819979015518079 + ], + [ + 2926849849449158589418504157622781316761588577631946629896933307400237548255, + 3579904677936943713540587614497052174139389714845927197988064028772422409729 + ], + [ + 2528266877173878010055911786705258188545546534139599446181068037003246641972, + 2090593124885859667054468890302718556366617309852711480847196828672489780384 + ], + [ + 2444375104485440204606531945078810145038148666393834678853843328800784491413, + 2715725794432857257355582195062001572910526811270194737205485553709223325149 + ], + [ + 22830464908174189828089916657865484119153417213263588787115884234394820551, + 2144656372197073668726998695697011727390449826738494763726208492934675687351 + ], + [ + 2160911736065509245304608365930348966695110267270046309451656634373627649798, + 1163682116033125825657519424156769471044453229066709940373641637423931934054 + ], + [ + 3337375727076925923347860860990507186997043901018925752183704995009780982617, + 789849303430659746633537234664825369690245855962498730835768457501816445262 + ], + [ + 2716386435720211080341482697862373973972358535390181213762877306777737012402, + 1694833211852186945995412154030088403536183890500755705683031155572812198349 + ], + [ + 2068303401887779893147375033717840031634024130559056428854775490478246923099, + 1302558735633136858997628501160702103360976241643583615145118788605800501823 + ], + [ + 2528669114399089536204487719847492540160633221112006566613245017377953815975, + 3355482321728480378876070603715444548564989647597270339584107783929482403385 + ], + [ + 1261599618177618962793920186597687295419265824130061936332619302494768478919, + 2783617516834130711477034905360493329011706287809097673216506817763076782638 + ], + [ + 2296010137652334615076906848600702206420819604705598933314103330038064507904, + 593137988508667983800537181778974626700843711375991890922024569808180330689 + ], + [ + 3550068663997426488921976653382588625011435366616319009448120503817895378028, + 333410037907206817306890788780367628702748092247180262748072669817226553712 + ], + [ + 1578244233650262393441190533726865755228496176869169339972883019206417143875, + 2916675240291132751272523072177820169167325640678555998767122677068069504203 + ], + [ + 2578708307946151727285356080366654771256622596667386016883098096024764936468, + 3547149991717605187248264974055140267141465574101933313814496584854066409070 + ], + [ + 1067242594064079501648168388172061910333397472745655832282853323030820063843, + 108409220808480307369448490727067533487994293376444700684037781053298403259 + ], + [ + 1438879968873284678993516485008080871992105419269026450500135928583384168688, + 333856453999414929392413408589358932084193824353874579187651515313020359769 + ], + [ + 2085247683945044480852971266898030909967072967111091473808832254928660354607, + 2485835628881711191910958556160457454778996828420985267633930125611726115768 + ], + [ + 477210647966136763919276861800763093178095337893692383194424592690231940061, + 249607430987277999234526517540164575126654109544108544480054797746736570800 + ], + [ + 3439303457066047146897968415062046665605853275226669283725228224973410585910, + 576124419653395914968187737099874519814103127209134795696921745172483058236 + ], + [ + 3028687819239678122848199946542573863395992135514552484651611678970005763129, + 2961643823025691121513520662201781259179479037567198808346912150041025760642 + ], + [ + 3217248534103588798939334232631337032410025788470581973981396764231465681047, + 2382487873308676654385098962584563887276844064402347832716704744043722784812 + ], + [ + 70223145378785170243702578121178644986245368450401326703486231450287863627, + 566197969536520322901287947074318556204558945230303949390810423446802406323 + ], + [ + 3028144136397350722039435806065376945342224221363467830775872476071042099596, + 167098690867154485139002287996988541908068352838119188548912894969546326088 + ], + [ + 1424858530411014835161893358951895950005600021438253129735780882682252689593, + 2305727793424806539572292293130051301594968780927626275319782923070998204084 + ], + [ + 2137513716516327603808109201538743935930903413061745351331773762548511760158, + 1642530299556920094344308735995008094003081457462443723760327041776469019076 + ], + [ + 2170444911584390774802338541806841054793712556211735475250435550223474552592, + 925789261675057834371037875523035088704965828465840628994713637520457333016 + ], + [ + 239299595833165228919005931278081722748571104921165506568868902567605074538, + 485001543376876278371961680415603784960370504830892419674368813289405424875 + ], + [ + 3080380062043844759018736411849858262358110468668995639984779021951968636078, + 2402265611218391177266008066876605247210502592712781467199097469773462678225 + ], + [ + 726523175548388386261502324171774291430220813407320118077006064537560392330, + 786330320941681238670628195289980168691286367143496676979339036480301999376 + ], + [ + 527181411328890981971052946489613295601720332326053123212209359114463976205, + 2153641849366054831187204654204168306183349514724183599886856681068048360647 + ], + [ + 548403912567710249138395852931702442214070304260935779684833978128636533986, + 1820119432165084967589110130985988930607430366654346658826186881340393135345 + ], + [ + 1055707400443116102787479640936967686027177253722371245083270825233659393085, + 1529404944427212407459549740446357844894249706911152782301314430602619804108 + ], + [ + 3604699563774018613013478662139866299897364136688436784910471089709564771121, + 1877420639173665439817870412707771026257151154196414247016132896387470215862 + ], + [ + 1334227623025169197188407571635794051283637310231760303667085410581915700021, + 1949877078010470778680860969498171171122335730952599024624803418631307495103 + ], + [ + 3308762084022080981421381628996649710517264452719086435874478919169368804715, + 2402101985509711398701847117755018203425777675996940000965112623957330145868 + ], + [ + 1665881871958289138802712383541734953129067055326286239727041865486531283570, + 3616247644667366899175075134655397038478794588048970408476914992988017251161 + ], + [ + 2112164335316897143532032199025725867588905166012009239598709182355280065515, + 2825402900866689367007140167090188180425426910788355395313762834688260248117 + ], + [ + 634951525865404702868066023611980901349684561467873427882470721637495099919, + 2178647893306834053326446190201725650048611097288227403773599186740980976285 + ], + [ + 1341178436815612054719172387245100541421209962514061177862693819245929984135, + 1326643103813011222249037873591741605099080649257821474786188634904101413693 + ], + [ + 1928710057077228449920040314271488497108943704097705502064531376997670270931, + 2266181469128837281287656381761672050739367882500404480638892977272852366881 + ], + [ + 969218608841843132346322324841729971529593954176078767277133720598489668357, + 606900183168454040777559974758602149603502324650244544303079399500690042393 + ], + [ + 2013350832011494097843192023013584600183073420263235866273101164869312611708, + 90712660236563155479754789398598402903732977477056735001011271430022436372 + ], + [ + 3396976507268771940572242446745957441443927281190120339106378150765794683243, + 2412741837818252081470894623640222254809923360460175760983381510548136273698 + ], + [ + 3606409001635563285156761927226165887070229114178147857036216526058311335249, + 1092396833411428890667907446500502751825053988217814009934347833612745642070 + ], + [ + 1544923824473254019116239003129558626125362616742425500116394033599505254529, + 1465349443371708061238467795142261954743713344644799596216285990321977000373 + ], + [ + 113491804090467857612125750322844575213787440679203395630526874638303077885, + 2028057101479207102461777655354995420997496563459354054546268475793811643845 + ], + [ + 1652495545845371778467883968680026022222422089600601509520876233591221298667, + 1257512680478734992352959448953843834930888651298716349936596227713642044718 + ], + [ + 1735132339493184243340725542423753987254833174769560101474361526571946583351, + 27330540439335548628321946448059137005887875330678118281933391074303528644 + ], + [ + 999589601024651596708840623565818878499651672244526787051482333844246243941, + 1556707544224102520772713942483754928174646286011090264336584778980967757013 + ], + [ + 1681312560856804229526683984753830811943700510863269888887897515341932766251, + 2065757756192665992865056781757244443742379869237378116242523960469865631998 + ], + [ + 2058951870735992982026605733702307208101813514180467857318656495202383185176, + 3268996299695136959341188422640393244547564085474313632107392993963324140014 + ], + [ + 2056960676848885737798371134589584163780497279931334671919866051948581811168, + 1741359147160041371302807510240561900995865771279421888187249838945794238723 + ], + [ + 1167002070266749840825005896490956016236165528302729567545471999112944906391, + 724716405138963198014997547695908810889048549156514080027596147160291370305 + ], + [ + 717433242268160049163784137690479364638414455354853503549047874912074602102, + 1548748206365927633434472413810887260565959748266730133832127499425371516599 + ], + [ + 1113770395880110291540688660582111108167550152891676896388447618267044770699, + 225614903467949057887693353344008462684737622787852941160491287904124662539 + ], + [ + 1485058593675291436770632813229171213081600725926085342541850536046844108938, + 1793621954142096162709529585852653649155672217087744074142012644930653464937 + ], + [ + 3476670695984120275482827092638881388770060638684248848714588194147006700081, + 1886524232365250879956678425943479251575532912879489640410209691903853633501 + ], + [ + 78942502761918568770028495237440346726648682399909104284710887733399086106, + 2352329060006211734866796061368085624251562468004096360671974444953215839619 + ], + [ + 813411306209308549231686755344288448950826770557611647490856063401538266082, + 388260357229176651922921451505223699462456291090208287248038895703757789197 + ], + [ + 2400377653395741560762585517571389386824872672242294297839787377212513042206, + 625963574857112775116550276725479329998557438550085741821320481021022314633 + ], + [ + 2328311306030212189043981540771838515808167713931548944984202663139515716155, + 2492267419179473373149177460543961766435995439950143787911026640253198630634 + ], + [ + 2387278462118275663019682655223568758817746496473668610784380666316276847549, + 2366392912056517351075178537570019722508034279024404671859228940369425653110 + ], + [ + 2114043901163989266960550512790503188352140805965143750858598214845925650606, + 1263535257267454609784459072919175411484521178579707297022413088083025242903 + ], + [ + 59742145227906406483850036502479681434689473008676866553687016399244912392, + 2542708853923609004067985135730679808561712875491472740521746862583772210812 + ], + [ + 636010637932045133320800908704730609314370804715286131693312911610589568614, + 961184011917051478754418596971670071602153673068923576248721877819369343802 + ], + [ + 1385321288061225556756217710115128107797163633073592537851457891527164069955, + 884133458066727093516473175829774361048238638693699012568518768170315343692 + ], + [ + 758367055389438625364170431009355773885665422613428814074753204465381568635, + 1297727568042288931569138891121976026502156814623013130863114577102812764955 + ], + [ + 347250288556045883174524792293804631653970133899259158293492482097831569584, + 1961316292966031207016509998947408765986081794211886403901120755246344327975 + ], + [ + 1618690143706255801807773941554549190817935595982010405820190876061657720006, + 869166062594354289846899813362309895350435099932527145815122092397979607037 + ], + [ + 1838634315648320098377933813041132543577219555069642930639820023291216325415, + 1529983605494771676066515349178506396294249260663468133203781588859819007226 + ], + [ + 1605761809110472849967286703249646269449262088256390507097425667304759998260, + 2778705695237273113564800057362189513234607857754067598956643514325456052945 + ], + [ + 3411908887615707635377666445857046055873541270252857286357865762696431003144, + 731871578063282896385513070338640294885089868946599519285295305156675019160 + ], + [ + 3087826179342245618253320870694339144383060104280990174148199503867526625383, + 3491370412646223145431588455284304413010853471815192082926914611232193577659 + ], + [ + 1436482751270429389743178806038120026461471807922309148229447715029903414786, + 1666868445318653269944338633661851246179662160526363040276638175667354338167 + ], + [ + 2083331818739824497741363579518842947887353398223338451239015909684569222581, + 803732308089347618300580814078570019393110433387113056533917045601819866173 + ], + [ + 1333165738767294811324643473740459024513360282013848003638568328453569807861, + 3324872774354633383424936425649606578793830970593411555017483797025902771350 + ], + [ + 1701353517399938600269579188937389421636718828509341624534004474699861544304, + 982045252370486712463601150815912589800926352240648829942255288306435594694 + ], + [ + 154403491180633257693040438747387470378033364863415878175486169393433469355, + 3204938488959824048546277939996603027140231747699901842459271581175521669368 + ], + [ + 723459534533890816489433777735842913898349668429456824296910758117461179034, + 242799587564906089500242560200906533162881507520621156475321851862194708981 + ], + [ + 2753553332546729968386491500304817162737325726065761497175967890302687642812, + 1607942131855693595003166153590488091960419143993462584570217492975562129656 + ], + [ + 1318380449900940858865061333131606747009833602974109493687016869645480407519, + 824831465625011385685225132275899244334146133266851046834317903702436443339 + ], + [ + 3406605858133553361333265720938744225870879664771897953784469453809670221140, + 951771439755520751413184037659273661906475899204291367776225253586492726939 + ], + [ + 322881185844437667428941222631269048437916337535492466690110884912942662893, + 2408973683372380251710642884281850709418801321112976394586138388159314573007 + ], + [ + 405651299272338889177305609117326577035390334272323319632791541534976007116, + 2403189350929802616202215631814070933678092854541380504903809068323291560762 + ], + [ + 224362689432001608162440684107608338550077679080690583663897253625637368804, + 1089475930411433192334001657654788489125032803526758217361824123802939165263 + ], + [ + 3427005033874860450481529297496326414099998098069008018777339291027217607772, + 1838523767315535107424920135038015356690704952046462913411177863052889918094 + ], + [ + 971265369167429416740822747897568727646706124584943344118217646401474769333, + 1841017974237665428880957941884047733042022771551566493876831131552515616862 + ], + [ + 593870383382776876660279010014844606756390978498220975404532129561707141345, + 1686799834245885653972636115594234495471835994524713453126028613189622517928 + ], + [ + 158526420068961916859457596090914564342752702816639997380569526445580395317, + 829117473118004491391038359292216575457137041347974965494549565557957856696 + ], + [ + 1145779849055685339419364615770769333393499278335670275163049680913140509683, + 2788919154400397863209918313477978879516215345298341199550275309881538159925 + ], + [ + 3116362794833038183976492329109547313095843900691440633169686540582464014059, + 3493161563205582068593545591027301392887053692389997071622408416554079460959 + ], + [ + 1484626752109997960148427439402147978447920625565926552692192513400801032840, + 106197164952655019784492783946283241349642545386756801239606388396528838557 + ], + [ + 1905886704700624859591161108842694006068401527337545253689518506514368311453, + 2930319791340393503878111037261975689788636509301758506631633748789105811524 + ], + [ + 2520975745491353699235764487116637116999082965127347369976279028452140192996, + 1121629571053704096076618751099836481664435087676492791791464786738401934277 + ], + [ + 243197297174950489047429059272755382647269925832357483684904121385442353161, + 2417007034799146494310932386003928951978739444041601103686030150489540099099 + ], + [ + 808704503940529633042533527196170354739879165235854099335954018048090166094, + 3250069164566476474875427019235306310423414676070245668865855537758860899226 + ], + [ + 154229390082782874040656704404635500870072484057583506571360633170672994819, + 2702580522127958238752043600053843204107536082922189692357192526470020183763 + ], + [ + 2296876062395150663198655775945704456626070946553449162794630784785525562017, + 2954047352783324167659477602480890942097095078541501369002104009282407983605 + ], + [ + 2369843936932443670822535910120393374869045918271657278773939763527759009977, + 1504523220772650882417075434754212591932774780940943124208852030753505106366 + ], + [ + 939818528450006271533633387575035651098636084551955010694000346778607054148, + 3288036962429189904257974342922419507417520404552917887258043999949243251337 + ], + [ + 3261482392020733556192001297773623607067547108643955544388526649674482826812, + 3591646574338256506691865028073929692592246225874807859597509821099836701353 + ], + [ + 1787025500314509707209621873912066561653490978360118110526034359358411964417, + 3421109838181987883898563834025345980326698037134918112926105203592921164968 + ], + [ + 2311067462891595099991833582046690970468764702035366824733716384542858538356, + 470591103372156288650571624843786296549423881481598013229606695835762897581 + ], + [ + 342559482224571965025189023541087083168706464251790471061097523992515769052, + 2368202754453331817392255123704696636258753331639084720511390832405023708522 + ], + [ + 2572444430230869831563822697183525106374829659556462575156759807443152314062, + 1629527166738200666038465328035159152469458766421654684492365855846947518815 + ], + [ + 1737389803319738463977618408470915902963778725702701713925657991380950954649, + 607082767712049126327196794168950822631607164722155751481706035333917604213 + ], + [ + 2871556558000552242893836092551691122481664492915647218631883507493547887264, + 500384672074954803514661117946887054859762315512424468765642494189718439748 + ], + [ + 3461109898461294404913712148331642005392308171104928758316008253448940997221, + 1749611977268851605988230236285965166361721258988901681470546570063157256891 + ], + [ + 78615564253467413638171585726207276986620474184921384429093571227531830518, + 1981471018460691817560093727855711117339515783816308036046814473313844294189 + ], + [ + 1474473102515930508594064469182212180992913337870765723235685741540431041359, + 1469840572053747949720853289796099707562318378077626046833147818685776895044 + ], + [ + 2774277348282328170085742674659971495571500392161601753293416467112865068097, + 3137849475565620656681313987560185334413793189698703604034985791122356336275 + ], + [ + 672166412479844650342747861081472852496472428762577522054587528815692740558, + 1663866879616164274903483340205017481052054060286752355521506181082184021495 + ], + [ + 3180945697595716737370693680466484191429900897946060952610697010699886591271, + 370261256417300103212360232751326303498788243730491970857436044670395786744 + ], + [ + 1201682462650513594149029305106730482154241597912555054934133306913124052927, + 2702519627582077982048055809065014719130205668704469328908061241215382284753 + ], + [ + 2573098232031718801548737595348908190100964683270928802802545310591952021697, + 421880381247280215923854338509978900130125935079274502459764130391391089799 + ], + [ + 3080354619128885268149548971617703089581818473398812557354479708280835955809, + 256390985131798172675848658065159234227830929883285189158702397702435740036 + ], + [ + 3305006433068254197629607860194494003641107593674801290918966761186348868243, + 3339026583930460592461023889277320373514761589020084302062859793412730118034 + ], + [ + 1923241940978623629561560707497295652725232345651095835780507541645042579369, + 1034987404476549078390013964157937064176009291357135198452850234267991254512 + ], + [ + 1242878056067701165715334664693705019638823755244701687487484225344981621708, + 2778020811088591142544537894197696376781458923989081525073574174237563505877 + ], + [ + 103159350300766901891674139940778235823347527941771255048984655353665811131, + 2998408586900586528158477448701326671048806329090729531148457644075161991659 + ], + [ + 71218614431436475107665191107448218056096758969742620338129750710339397239, + 2301542035924343190515686028267024414404475677263930511758245534549493426119 + ], + [ + 1192223060475270158246514565467403478627193592812213292121865811070708026418, + 3208674842884877565377729626392486507374130037365033772947426862394273679084 + ], + [ + 2701994874964587709938210915198043688526355413043491003913483399075336026535, + 2886333482965974313702782452985415254520267819631653595494065556318924996302 + ], + [ + 3306009714468475346677294651301864623371911173285781201209240934162798592783, + 605466198613255313875059832114128899690381126611676641778478133738267702577 + ], + [ + 1450098270166428194278914193816868101013739489893607935042066987079292018245, + 60254834310937851613452000117427371272748499709961477924319944539270878930 + ], + [ + 3472634693037182832142729498155218303319113785483118130849719300023909673520, + 1641508292869191604146734378916767617013327049910565729753281855691648138118 + ], + [ + 1894817968472977732589643163725633598408826498334595898528746877517402007256, + 193046947184625502016096786396456839948351998258967537298275688147378157385 + ], + [ + 798518742331453195093601874576575285888369661705098199469223675278431898599, + 107377575629504498627957848614396996685294283100119805971247351789316564355 + ], + [ + 72958015465538907944252844948497831886498059248823205873862483987609118945, + 382019162223291772579736282379325284805025953290359275955015292364404489133 + ], + [ + 3314147563388677609583640640385513143694481753397403848688825124581323140921, + 3133015892701145825892878874198767217439365812782859844688129544070997319541 + ], + [ + 2444095071507078967491725470181706633826193926858202958437137361403432629390, + 3611326330343873710939978620686319096878621369582121415988028359537867630792 + ], + [ + 2107740367200683219354058003519333570583941271011338026730935614636775950367, + 953104025671531038979165726896993106119997272237926795749220750824852831326 + ], + [ + 3234656093348684616411954956071945712517868853923240328793520762092030352104, + 593797219275153872348854593022027262920747808515266478952508298984248786194 + ], + [ + 1602095369325252473599793473477096021341779224145909730627015015569257565602, + 3272207934512491788245765497830881358513949839139669547034252308088069134971 + ], + [ + 3149982146511124605253586861983261220008132001764223357617600582721741955729, + 2095033709871635786785037791315376486221829475125386227641672208905379085173 + ], + [ + 3001928273494873166012582714776651165587585607934603548408913923445707516447, + 1090266449665582127890982346068170500195143341692394417827957481874850148101 + ], + [ + 1200188694803418163698740652082064619102559508865389996854547617715365179733, + 2701249247866363010207334776256734312390753341409567681725083482804664533572 + ], + [ + 2181535855195652401374211290838795366238299923929423873085735410894622521047, + 2376412857416063533601832417302250911062085318826783757605801470091451365057 + ], + [ + 482216227882593950837484293882738923170227555670484160841935409522897080275, + 3568625956076637648484022304136932386854238480511347282187445648692365982275 + ], + [ + 2597799669063340267995599892195854202917996255210920411104497198234262573206, + 3599688873496047961188179975715306136394737017927100659789000949688648352738 + ], + [ + 1074803073878259665128704812644508804681887514658526880458531694790041080733, + 2797007314237358398853761880558407828488654212157591536243300688627270327611 + ], + [ + 468516314257643341712703808920275922572701541039900608201362821198817231181, + 1374527914917866113032496762177078616084751647116110264806150108724834406388 + ], + [ + 1208295021405021769918747031548141820393186404250802478605705170489054454157, + 1808650369660257850053512198148641736021245568921649705336796845945032764079 + ], + [ + 1565766387634045687737221757815822397332528241094852872444435795933964809539, + 3012257951756551767601579252037773433674622118381097513581891561867035592007 + ], + [ + 296854483963576847362550837105567519963678928320906132062353506063381160038, + 2997621846549637009998526882695309289938245125374301351844169021609166074993 + ], + [ + 967192065183772403576792198824105189328662921952353748318585728895552782317, + 343259257621685032456419166732348373845783852374756662051672936363788935338 + ], + [ + 456199991075423266290193634515181612721738873753897142916892886319728411618, + 3009804764464949268795409945347301690744692042792259709767667423717478448229 + ], + [ + 242210936510534966601707881924518098930470912600217515366037129037143259602, + 2698396953115175496998177800235267214917400761768300598355994305287141321271 + ], + [ + 943070145114606420402378547694273832277173459266557776382587828687889744387, + 463274201648439487604179878678549669204059390508994779482933722127905403494 + ], + [ + 958740504301613688526614902955971923282735882762427544770668882582144756278, + 1648715733425449195104866742590789175535499260400395501324113794510596751843 + ], + [ + 2738467504442851843676882768027062309179245908086784030957627999641833602734, + 403058158250173068725971433188527745485878446592356816888847490041658726521 + ], + [ + 851584175891221844656684173543344216725376446573878021753176795750916050165, + 2989949954698892115645230865442980084822288702406021287339635187958555345139 + ], + [ + 2648599427528401936572004779373684054684563511138695864967500218937169746782, + 545540526832939914359571382575431287745621956136188859527645452849687661804 + ], + [ + 2781909713387830233242081629088160756893816968845366829353355409163126437112, + 3474066345655056798683792894555452083305620276965763551620554269294421970656 + ], + [ + 62007937137219837991330947293062511223089197148302827535260096716770644685, + 3537197938414984401398955238628360208832764753222288521326061893140886724271 + ], + [ + 2379962749567351885752724891227938183011949129833673362440656643086021394946, + 776496453633298175483985398648758586525933812536653089401905292063708816422 + ], + [ + 553697491755753712548822408932664734674730150084063981046477343718694621804, + 2797798649021537247229237999331435556632872779265479409612091247299955463913 + ], + [ + 2026114267613810970244390071397350467776533880677809710454617259260017487512, + 3330593270696197494966018967263043594632970418364498628573044882141635806155 + ], + [ + 1254733481274108825174693797237617285863727098996450904398879255272288617861, + 2644890941682394074696857415419096381561354281743803087373802494123523779468 + ] + ] +} +` diff --git a/starkware/math_pedersen.go b/starkware/math_pedersen.go new file mode 100644 index 0000000..8febe77 --- /dev/null +++ b/starkware/math_pedersen.go @@ -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() +} diff --git a/starkware/math_rfc6979.go b/starkware/math_rfc6979.go new file mode 100644 index 0000000..dd90504 --- /dev/null +++ b/starkware/math_rfc6979.go @@ -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) + } +} diff --git a/starkware/math_utils.go b/starkware/math_utils.go new file mode 100644 index 0000000..06c80d5 --- /dev/null +++ b/starkware/math_utils.go @@ -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 +} diff --git a/starkware/sign.go b/starkware/sign.go new file mode 100644 index 0000000..636db98 --- /dev/null +++ b/starkware/sign.go @@ -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), + } + } +} diff --git a/subscriptions/subscriptions.go b/subscriptions/subscriptions.go new file mode 100644 index 0000000..edef5c5 --- /dev/null +++ b/subscriptions/subscriptions.go @@ -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) +} diff --git a/subscriptions/wsclient.go b/subscriptions/wsclient.go new file mode 100644 index 0000000..39333be --- /dev/null +++ b/subscriptions/wsclient.go @@ -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 + +} diff --git a/subscriptions/wsscheduler.go b/subscriptions/wsscheduler.go new file mode 100644 index 0000000..d8eb59d --- /dev/null +++ b/subscriptions/wsscheduler.go @@ -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 +} diff --git a/tokens/monetary_amount.go b/tokens/monetary_amount.go new file mode 100644 index 0000000..ded2bd4 --- /dev/null +++ b/tokens/monetary_amount.go @@ -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) + } +} diff --git a/tokens/token.go b/tokens/token.go new file mode 100644 index 0000000..bdeba1e --- /dev/null +++ b/tokens/token.go @@ -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"` +} diff --git a/tokens/token_auction.go b/tokens/token_auction.go new file mode 100644 index 0000000..581c8fd --- /dev/null +++ b/tokens/token_auction.go @@ -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"` +} diff --git a/tokens/token_offer.go b/tokens/token_offer.go new file mode 100644 index 0000000..598dfe5 --- /dev/null +++ b/tokens/token_offer.go @@ -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"` +} diff --git a/tokens/token_owner.go b/tokens/token_owner.go new file mode 100644 index 0000000..64f5c83 --- /dev/null +++ b/tokens/token_owner.go @@ -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"` +} diff --git a/tokens/token_price.go b/tokens/token_price.go new file mode 100644 index 0000000..237297c --- /dev/null +++ b/tokens/token_price.go @@ -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"` +} diff --git a/tokens/token_primary_offer.go b/tokens/token_primary_offer.go new file mode 100644 index 0000000..f74b519 --- /dev/null +++ b/tokens/token_primary_offer.go @@ -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"` +} diff --git a/tokens/tokens.go b/tokens/tokens.go new file mode 100644 index 0000000..bf61e64 --- /dev/null +++ b/tokens/tokens.go @@ -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"}, + ), + } +} diff --git a/types/average_player_score.go b/types/average_player_score.go new file mode 100644 index 0000000..c2892b7 --- /dev/null +++ b/types/average_player_score.go @@ -0,0 +1,8 @@ +package types + +type AveragePlayerScore string + +const ( + AveragePlayerScoreLastFifteenSo5AverageScore AveragePlayerScore = "LAST_FIFTEEN_SO5_AVERAGE_SCORE" + AveragePlayerScoreLastFiveSo5AverageScore AveragePlayerScore = "LAST_FIVE_SO5_AVERAGE_SCORE" +) diff --git a/types/collection.go b/types/collection.go new file mode 100644 index 0000000..5ac0592 --- /dev/null +++ b/types/collection.go @@ -0,0 +1,10 @@ +package types + +type Collection string + +const ( + CollectionBaseball Collection = "BASEBALL" + CollectionFootball Collection = "FOOTBALL" + CollectionFootballNationalSeries Collection = "FOOTBALL_NATIONAL_SERIES" + CollectionNba Collection = "NBA" +) diff --git a/types/iso8601datetime.go b/types/iso8601datetime.go new file mode 100644 index 0000000..ae78ad8 --- /dev/null +++ b/types/iso8601datetime.go @@ -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 +} diff --git a/types/offer_type.go b/types/offer_type.go new file mode 100644 index 0000000..7fa3f6c --- /dev/null +++ b/types/offer_type.go @@ -0,0 +1,9 @@ +package types + +type OfferType string + +const ( + OfferTypeDirectOffer OfferType = "DIRECT_OFFER" + OfferTypeSingleBuyOffer OfferType = "SINGLE_BUY_OFFER" + OfferTypeSingleSaleOffer OfferType = "SINGLE_SALE_OFFER" +) diff --git a/types/owner_transfer.go b/types/owner_transfer.go new file mode 100644 index 0000000..2b155b5 --- /dev/null +++ b/types/owner_transfer.go @@ -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" +) diff --git a/types/payment_method.go b/types/payment_method.go new file mode 100644 index 0000000..da35353 --- /dev/null +++ b/types/payment_method.go @@ -0,0 +1,9 @@ +package types + +type PaymentMethod string + +const ( + PaymentMethodWallet PaymentMethod = "WALLET" + PaymentMethodCreditCard PaymentMethod = "CREDIT_CARD" + PaymentMethodAppleIap PaymentMethod = "APPLE_IAP" +) diff --git a/types/playerfieldstatus.go b/types/playerfieldstatus.go new file mode 100644 index 0000000..ac12426 --- /dev/null +++ b/types/playerfieldstatus.go @@ -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" +) diff --git a/types/playerplayingstatus.go b/types/playerplayingstatus.go new file mode 100644 index 0000000..98e56f1 --- /dev/null +++ b/types/playerplayingstatus.go @@ -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" +) diff --git a/types/position.go b/types/position.go new file mode 100644 index 0000000..7c0de6d --- /dev/null +++ b/types/position.go @@ -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" +) diff --git a/types/rarity.go b/types/rarity.go new file mode 100644 index 0000000..9bc35f1 --- /dev/null +++ b/types/rarity.go @@ -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" +) diff --git a/types/so5_leaderboard_rarity.go b/types/so5_leaderboard_rarity.go new file mode 100644 index 0000000..fdaacdd --- /dev/null +++ b/types/so5_leaderboard_rarity.go @@ -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" +) diff --git a/types/so5_leaderboard_type.go b/types/so5_leaderboard_type.go new file mode 100644 index 0000000..980ca11 --- /dev/null +++ b/types/so5_leaderboard_type.go @@ -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" +) diff --git a/types/so5state.go b/types/so5state.go new file mode 100644 index 0000000..54fc833 --- /dev/null +++ b/types/so5state.go @@ -0,0 +1,9 @@ +package types + +type So5State string + +const ( + So5StateLive So5State = "LIVE" + So5StateUpcoming So5State = "UPCOMING" + So5StatePast So5State = "PAST" +) diff --git a/types/sorting_option.go b/types/sorting_option.go new file mode 100644 index 0000000..6583a96 --- /dev/null +++ b/types/sorting_option.go @@ -0,0 +1,8 @@ +package types + +type SortingOption string + +const ( + SortingOptionAsc SortingOption = "ASC" + SortingOptionDesc SortingOption = "DESC" +) diff --git a/types/sport.go b/types/sport.go new file mode 100644 index 0000000..d39ddcd --- /dev/null +++ b/types/sport.go @@ -0,0 +1,9 @@ +package types + +type Sport string + +const ( + SportFootball Sport = "FOOTBALL" + SportNba Sport = "NBA" + SportBaseBall Sport = "BASEBALL" +) diff --git a/types/statcategory.go b/types/statcategory.go new file mode 100644 index 0000000..4cab0e5 --- /dev/null +++ b/types/statcategory.go @@ -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" +) diff --git a/types/supported_currency.go b/types/supported_currency.go new file mode 100644 index 0000000..ec30549 --- /dev/null +++ b/types/supported_currency.go @@ -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" +) diff --git a/user.go b/user.go new file mode 100644 index 0000000..76a0203 --- /dev/null +++ b/user.go @@ -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"}, + ) +}