diff --git a/docs/spec/components/schemas/Balance.yaml b/docs/spec/components/schemas/Balance.yaml new file mode 100644 index 0000000..3915ef0 --- /dev/null +++ b/docs/spec/components/schemas/Balance.yaml @@ -0,0 +1,16 @@ +allOf: + - $ref: '#/components/schemas/TokenKey' + - type: object + required: + - attributes + properties: + attributes: + type: object + required: + - amount + properties: + amount: + type: integer + format: "*big.Int" + description: Amount of the tokens on the user address + example: "123" diff --git a/docs/spec/components/schemas/TokenKey.yaml b/docs/spec/components/schemas/TokenKey.yaml new file mode 100644 index 0000000..4b59a69 --- /dev/null +++ b/docs/spec/components/schemas/TokenKey.yaml @@ -0,0 +1,10 @@ +type: object +required: + - id + - type +properties: + id: + type: string + type: + type: string + enum: [ token ] diff --git a/docs/spec/paths/token@balance.yaml b/docs/spec/paths/token@balance.yaml new file mode 100644 index 0000000..48a43f3 --- /dev/null +++ b/docs/spec/paths/token@balance.yaml @@ -0,0 +1,28 @@ +get: + tags: + - Tokens + summary: Get Balance + description: Get token's balance by the given user address + operationId: GetBalance + parameters: + - in: query + name: address + required: true + schema: + type: string + format: common.Address + responses: + 200: + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Balance' + 400: + $ref: '#/components/responses/invalidParameter' + 500: + $ref: '#/components/responses/internalError' diff --git a/go.mod b/go.mod index 221c57a..850176d 100644 --- a/go.mod +++ b/go.mod @@ -123,6 +123,7 @@ require ( github.com/tklauser/numcpus v0.6.1 // indirect gitlab.com/distributed_lab/figure v2.1.2+incompatible // indirect gitlab.com/distributed_lab/lorem v0.2.1 // indirect + gitlab.com/distributed_lab/urlval v3.0.0+incompatible // indirect go.etcd.io/bbolt v1.3.7 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.22.0 // indirect diff --git a/go.sum b/go.sum index faaea24..86a4a7c 100644 --- a/go.sum +++ b/go.sum @@ -2298,6 +2298,8 @@ gitlab.com/distributed_lab/lorem v0.2.1 h1:A1QoiEDRN3vlPrwsXJmPlENanQwu3FxpDl5vE gitlab.com/distributed_lab/lorem v0.2.1/go.mod h1:wkzrGoB1L/yUBu56SfoJ/vNiPqiHZcg75AnBkWNcjhQ= gitlab.com/distributed_lab/running v1.6.0 h1:O2GQHgYkpNRDbobR11atOCH2rhS1okNKyadiAqi+kQI= gitlab.com/distributed_lab/running v1.6.0/go.mod h1:4TnADX84dQjQMRHKIMPCVL0L97rD/Jxv0xDbrN6aKzk= +gitlab.com/distributed_lab/urlval v3.0.0+incompatible h1:OWU3CcZU+z0BVooufOSQFO9biDfsjM3e7YYauC278HU= +gitlab.com/distributed_lab/urlval v3.0.0+incompatible/go.mod h1:cKnUlnZCHUuke/l95YLvW5JoGC2yn53HVgF9rt1WiIg= go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= diff --git a/internal/service/api/handlers/get_balance.go b/internal/service/api/handlers/get_balance.go new file mode 100644 index 0000000..2135015 --- /dev/null +++ b/internal/service/api/handlers/get_balance.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "net/http" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/rarimo/evm-airdrop-svc/internal/service/api" + "github.com/rarimo/evm-airdrop-svc/internal/service/api/models" + "github.com/rarimo/evm-airdrop-svc/internal/service/api/requests" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func GetBalance(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewGetBalanceRequest(r) + if err != nil { + api.Log(r).WithError(err).Error("failed to parse request") + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + balance, err := api.ERC20Permit(r).BalanceOf(&bind.CallOpts{}, req.Address) + if err != nil { + api.Log(r).WithError(err).Error("failed to get user balance") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, models.NewBalanceResponse(req.Address.String(), balance)) +} diff --git a/internal/service/api/models/token.go b/internal/service/api/models/token.go new file mode 100644 index 0000000..d40a632 --- /dev/null +++ b/internal/service/api/models/token.go @@ -0,0 +1,21 @@ +package models + +import ( + "math/big" + + "github.com/rarimo/evm-airdrop-svc/resources" +) + +func NewBalanceResponse(addr string, amount *big.Int) resources.BalanceResponse { + return resources.BalanceResponse{ + Data: resources.Balance{ + Key: resources.Key{ + ID: addr, + Type: resources.TOKEN, + }, + Attributes: resources.BalanceAttributes{ + Amount: amount, + }, + }, + } +} diff --git a/internal/service/api/requests/get_balance.go b/internal/service/api/requests/get_balance.go new file mode 100644 index 0000000..5035e77 --- /dev/null +++ b/internal/service/api/requests/get_balance.go @@ -0,0 +1,32 @@ +package requests + +import ( + "net/http" + + "github.com/ethereum/go-ethereum/common" + validation "github.com/go-ozzo/ozzo-validation/v4" + "gitlab.com/distributed_lab/logan/v3/errors" + "gitlab.com/distributed_lab/urlval" +) + +type GetUserBalanceRequest struct { + Address common.Address `url:"address"` +} + +func NewGetBalanceRequest(r *http.Request) (GetUserBalanceRequest, error) { + var req GetUserBalanceRequest + + if err := urlval.Decode(r.URL.Query(), &req); err != nil { + return req, validation.Errors{ + "query": errors.Wrap(err, "failed to decode url"), + }.Filter() + } + + return req, req.validate() +} + +func (r *GetUserBalanceRequest) validate() error { + return validation.Errors{ + "/address": validation.Validate(r.Address, validation.Required), + }.Filter() +} diff --git a/internal/service/router.go b/internal/service/router.go index 5aab711..eec9519 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -55,6 +55,10 @@ func Run(ctx context.Context, cfg *config.Config) { r.Post("/permit-hash", handlers.BuildPermitHash) }) + + r.Route("/token", func(r chi.Router) { + r.Get("/balance", handlers.GetBalance) + }) }) cfg.Log().Info("Service started") diff --git a/resources/model_balance.go b/resources/model_balance.go new file mode 100644 index 0000000..00524a6 --- /dev/null +++ b/resources/model_balance.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type Balance struct { + Key + Attributes BalanceAttributes `json:"attributes"` +} +type BalanceResponse struct { + Data Balance `json:"data"` + Included Included `json:"included"` +} + +type BalanceListResponse struct { + Data []Balance `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *BalanceListResponse) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *BalanceListResponse) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustBalance - returns Balance from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustBalance(key Key) *Balance { + var balance Balance + if c.tryFindEntry(key, &balance) { + return &balance + } + return nil +} diff --git a/resources/model_balance_attributes.go b/resources/model_balance_attributes.go new file mode 100644 index 0000000..dd7c2ad --- /dev/null +++ b/resources/model_balance_attributes.go @@ -0,0 +1,12 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "math/big" + +type BalanceAttributes struct { + // Amount of the tokens on the user address + Amount *big.Int `json:"amount"` +} diff --git a/resources/model_resource_type.go b/resources/model_resource_type.go index 3c98861..6cc037d 100644 --- a/resources/model_resource_type.go +++ b/resources/model_resource_type.go @@ -10,5 +10,6 @@ type ResourceType string const ( AIRDROP ResourceType = "airdrop" CREATE_AIRDROP ResourceType = "create_airdrop" + TOKEN ResourceType = "token" TRANSFER_ERC20 ResourceType = "transfer_erc20" )