From 16afe021803362ee74fde5f4273694fcc11a1267 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Thu, 23 May 2024 21:22:02 +0300 Subject: [PATCH] Refactor withdrawals code --- docs/spec/components/schemas/Withdraw.yaml | 5 +++ internal/service/handlers/withdraw.go | 44 ++++++++++++++++++---- internal/service/requests/withdraw.go | 10 +++-- resources/model_withdraw_attributes.go | 4 ++ 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/docs/spec/components/schemas/Withdraw.yaml b/docs/spec/components/schemas/Withdraw.yaml index e720b7a..709fd15 100644 --- a/docs/spec/components/schemas/Withdraw.yaml +++ b/docs/spec/components/schemas/Withdraw.yaml @@ -10,6 +10,7 @@ allOf: required: - amount - address + - proof properties: amount: type: integer @@ -20,3 +21,7 @@ allOf: type: string description: Rarimo address to withdraw to. Can be any valid address. example: rarimo15hcd6tv7pe8hk2re7hu0zg0aphqdm2dtjrs0ds + proof: + type: object + format: json.RawMessage + description: JSON encoded ZK passport verification proof. diff --git a/internal/service/handlers/withdraw.go b/internal/service/handlers/withdraw.go index a15c85b..f99de56 100644 --- a/internal/service/handlers/withdraw.go +++ b/internal/service/handlers/withdraw.go @@ -1,20 +1,28 @@ package handlers import ( + "encoding/json" "fmt" + "math/big" "net/http" cosmos "github.com/cosmos/cosmos-sdk/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/ethereum/go-ethereum/common/hexutil" validation "github.com/go-ozzo/ozzo-validation/v4" + zkptypes "github.com/iden3/go-rapidsnark/types" "github.com/rarimo/decentralized-auth-svc/pkg/auth" "github.com/rarimo/rarime-points-svc/internal/data" "github.com/rarimo/rarime-points-svc/internal/service/requests" "github.com/rarimo/rarime-points-svc/resources" + zk "github.com/rarimo/zkverifier-kit" "gitlab.com/distributed_lab/ape" "gitlab.com/distributed_lab/ape/problems" + "gitlab.com/distributed_lab/logan/v3/errors" ) +const usaAuthorithy = "8571562" + func Withdraw(w http.ResponseWriter, r *http.Request) { req, err := requests.NewWithdraw(r) if err != nil { @@ -33,12 +41,14 @@ func Withdraw(w http.ResponseWriter, r *http.Request) { return } - if !auth.Authenticates(UserClaims(r), auth.UserGrant(req.Data.ID)) { + nullifier := req.Data.ID + + if !auth.Authenticates(UserClaims(r), auth.UserGrant(nullifier)) { ape.RenderErr(w, problems.Unauthorized()) return } - balance, err := BalancesQ(r).FilterByNullifier(req.Data.ID).Get() + balance, err := BalancesQ(r).FilterByNullifier(nullifier).Get() if err != nil { log.WithError(err).Error("Failed to get balance by nullifier") ape.RenderErr(w, problems.InternalError()) @@ -50,20 +60,38 @@ func Withdraw(w http.ResponseWriter, r *http.Request) { return } - if err = isEligibleToWithdraw(balance, req.Data.Attributes.Amount); err != nil { + var proof zkptypes.ZKProof + if err := json.Unmarshal(req.Data.Attributes.Proof, &proof); err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + // MustDecode will never panic, because of the previous logic + proof.PubSignals[zk.Nullifier] = new(big.Int).SetBytes(hexutil.MustDecode(nullifier)).String() + if err := Verifier(r).VerifyProof(proof, zk.WithProofSelectorValue("23073")); err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + if proof.PubSignals[zk.Citizenship] == usaAuthorithy { + ape.RenderErr(w, problems.BadRequest(validation.Errors{"authority": errors.New("Incorrect authority")})...) + return + } + + if err = isEligibleToWithdraw(r, balance, req.Data.Attributes.Amount); err != nil { ape.RenderErr(w, problems.BadRequest(err)...) return } var withdrawal *data.Withdrawal err = EventsQ(r).Transaction(func() error { - err = BalancesQ(r).FilterByNullifier(req.Data.ID).UpdateAmountBy(-req.Data.Attributes.Amount) + err = BalancesQ(r).FilterByNullifier(nullifier).UpdateAmountBy(-req.Data.Attributes.Amount) if err != nil { return fmt.Errorf("decrease points amount: %w", err) } withdrawal, err = WithdrawalsQ(r).Insert(data.Withdrawal{ - Nullifier: req.Data.ID, + Nullifier: nullifier, Amount: req.Data.Attributes.Amount, Address: req.Data.Attributes.Address, }) @@ -84,7 +112,7 @@ func Withdraw(w http.ResponseWriter, r *http.Request) { } // balance should exist cause of previous logic - balance, err = BalancesQ(r).GetWithRank(req.Data.ID) + balance, err = BalancesQ(r).GetWithRank(nullifier) if err != nil { log.WithError(err).Error("Failed to get balance by nullifier with rank") ape.RenderErr(w, problems.InternalError()) @@ -112,7 +140,7 @@ func newWithdrawResponse(w data.Withdrawal, balance data.Balance) *resources.Wit return &resp } -func isEligibleToWithdraw(balance *data.Balance, amount int64) error { +func isEligibleToWithdraw(r *http.Request, balance *data.Balance, amount int64) error { mapValidationErr := func(field, format string, a ...any) validation.Errors { return validation.Errors{ field: fmt.Errorf(format, a...), @@ -124,6 +152,8 @@ func isEligibleToWithdraw(balance *data.Balance, amount int64) error { return mapValidationErr("is_disabled", "user must be referred to withdraw") case balance.Amount < amount: return mapValidationErr("data/attributes/amount", "insufficient balance: %d", balance.Amount) + case !Levels(r)[balance.Level].WithdrawalAllowed: + return mapValidationErr("withdrawal not allowed", "user must up level to have withdraw ability") } return nil diff --git a/internal/service/requests/withdraw.go b/internal/service/requests/withdraw.go index c0a57c4..181d1dc 100644 --- a/internal/service/requests/withdraw.go +++ b/internal/service/requests/withdraw.go @@ -3,6 +3,7 @@ package requests import ( "encoding/json" "net/http" + "strings" "github.com/go-chi/chi" validation "github.com/go-ozzo/ozzo-validation/v4" @@ -10,15 +11,18 @@ import ( ) func NewWithdraw(r *http.Request) (req resources.WithdrawRequest, err error) { - nullifier := chi.URLParam(r, "nullifier") - if err = json.NewDecoder(r.Body).Decode(&req); err != nil { err = newDecodeError("body", err) return } + req.Data.ID = strings.ToLower(req.Data.ID) + return req, validation.Errors{ - "data/id": validation.Validate(req.Data.ID, validation.Required, validation.In(nullifier)), + "data/id": validation.Validate(req.Data.ID, + validation.Required, + validation.In(strings.ToLower(chi.URLParam(r, "nullifier"))), + validation.Match(nullifierRegexp)), "data/type": validation.Validate(req.Data.Type, validation.Required, validation.In(resources.WITHDRAW)), "data/attributes/amount": validation.Validate(req.Data.Attributes.Amount, validation.Required, validation.Min(1)), "data/attributes/address": validation.Validate(req.Data.Attributes.Address, validation.Required), diff --git a/resources/model_withdraw_attributes.go b/resources/model_withdraw_attributes.go index 2f6bdb6..65a0cdb 100644 --- a/resources/model_withdraw_attributes.go +++ b/resources/model_withdraw_attributes.go @@ -4,9 +4,13 @@ package resources +import "encoding/json" + type WithdrawAttributes struct { // Rarimo address to withdraw to. Can be any valid address. Address string `json:"address"` // Amount of points to withdraw Amount int64 `json:"amount"` + // JSON encoded ZK passport verification proof. + Proof json.RawMessage `json:"proof"` }