Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: join rewards program #32

Merged
merged 4 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 2 additions & 20 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ levels:
withdrawal_allowed: true

countries:
verification_key: "37bc75afc97f8bdcd21cda85ae7b2885b5f1205ae3d79942e56457230f1636a037cc7ebfe42998d66a3dd3446b9d29366271b4f2bd8e0d307db1d320b38fc02f"
countries:
- code: "UKR"
reserve_limit: 100000
Expand Down Expand Up @@ -97,23 +98,4 @@ root_verifier:
request_timeout: 10s

withdrawal:
point_price_urmo: 100

sbt_check:
networks:
- name: polygon
rpc: https://your-rpc
contract: 0x...
request_timeout: 5s
start_from_block: 48984542
block_window: 3
max_blocks_per_request: 5000
- name: ethereum
rpc: https://your-rpc
contract: 0x...
request_timeout: 5s
- name: disabled_sample
disabled: true
rpc: https://your-rpc
contract: 0x...
request_timeout: 5s
point_price_urmo: 1000000
15 changes: 15 additions & 0 deletions docs/spec/components/schemas/JoinProgram.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
allOf:
- $ref: '#/components/schemas/JoinProgramKey'
- type: object
x-go-is-request: true
required:
- attributes
properties:
attributes:
type: object
required:
- country
properties:
country:
type: string
example: "5589842"
13 changes: 13 additions & 0 deletions docs/spec/components/schemas/JoinProgramKey.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type: object
required:
- id
- type
properties:
id:
type: string
description: Nullifier of the points owner
example: "0x123...abc"
pattern: '^0x[0-9a-fA-F]{64}$'
type:
type: string
enum: [ join_program ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
post:
tags:
- Points balance
summary: Join rewards program
description: Join rewards program
operationId: joinRewardsProgram
parameters:
- $ref: '#/components/parameters/pathNullifier'
requestBody:
required: true
content:
application/vnd.api+json:
schema:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/JoinProgram'
responses:
200:
description: Success
content:
application/vnd.api+json:
schema:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/PassportEventState'
400:
$ref: '#/components/responses/invalidParameter'
401:
$ref: '#/components/responses/invalidAuth'
404:
description: Balance not exists.
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/Errors'
429:
description: Passport already verified or event absent for user.
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/Errors'
500:
$ref: '#/components/responses/internalError'
5 changes: 5 additions & 0 deletions internal/assets/migrations/003_passport_proven.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE balances ADD COLUMN is_passport_proven boolean NOT NULL DEFAULT FALSE;

-- +migrate Down
ALTER TABLE balances DROP COLUMN is_passport_proven;
19 changes: 10 additions & 9 deletions internal/data/balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@ import (

const (
ColAmount = "amount"
ColReferredBy = "referred_by"
ColLevel = "level"
ColCountry = "country"
ColIsPassport = "is_passport_proven"
)

type Balance struct {
Nullifier string `db:"nullifier"`
Amount int64 `db:"amount"`
CreatedAt int32 `db:"created_at"`
UpdatedAt int32 `db:"updated_at"`
ReferredBy sql.NullString `db:"referred_by"`
Rank *int `db:"rank"`
Level int `db:"level"`
Country *string `db:"country"`
Nullifier string `db:"nullifier"`
Amount int64 `db:"amount"`
CreatedAt int32 `db:"created_at"`
UpdatedAt int32 `db:"updated_at"`
ReferredBy sql.NullString `db:"referred_by"`
Rank *int `db:"rank"`
Level int `db:"level"`
Country *string `db:"country"`
IsPassportProven bool `db:"is_passport_proven"`
}

type BalancesQ interface {
Expand Down
12 changes: 12 additions & 0 deletions internal/service/handlers/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/rarimo/rarime-points-svc/internal/config"
"github.com/rarimo/rarime-points-svc/internal/data"
"github.com/rarimo/rarime-points-svc/internal/data/evtypes"
"github.com/rarimo/rarime-points-svc/internal/service/workers/countrier"
"github.com/rarimo/saver-grpc-lib/broadcaster"
zk "github.com/rarimo/zkverifier-kit"
"gitlab.com/distributed_lab/logan/v3"
Expand All @@ -28,6 +29,7 @@ const (
pointPriceCtxKey
verifierCtxKey
levelsCtxKey
countriesConfigCtxKey
)

func CtxLog(entry *logan.Entry) func(context.Context) context.Context {
Expand Down Expand Up @@ -149,3 +151,13 @@ func CtxLevels(levels config.Levels) func(context.Context) context.Context {
func Levels(r *http.Request) config.Levels {
return r.Context().Value(levelsCtxKey).(config.Levels)
}

func CtxCountriesConfig(config countrier.Config) func(context.Context) context.Context {
return func(ctx context.Context) context.Context {
return context.WithValue(ctx, countriesConfigCtxKey, config)
}
}

func CountriesConfig(r *http.Request) countrier.Config {
return r.Context().Value(countriesConfigCtxKey).(countrier.Config)
}
77 changes: 77 additions & 0 deletions internal/service/handlers/join_program.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package handlers

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"

"github.com/rarimo/rarime-points-svc/internal/data"
"github.com/rarimo/rarime-points-svc/internal/data/evtypes"
"github.com/rarimo/rarime-points-svc/internal/service/requests"
"gitlab.com/distributed_lab/ape"
"gitlab.com/distributed_lab/ape/problems"
)

func JoinProgram(w http.ResponseWriter, r *http.Request) {
req, err := requests.NewJoinProgram(r)
if err != nil {
Log(r).WithError(err).Debug("Bad request")
ape.RenderErr(w, problems.BadRequest(err)...)
return
}

gotSig := r.Header.Get("Signature")
wantSig := calculateCountrySignature(CountriesConfig(r).VerificationKey, req.Data.ID, req.Data.Attributes.Country)
if gotSig != wantSig {
Log(r).Warnf("Unauthorized access: HMAC signature mismatch: got %s, want %s", gotSig, wantSig)
ape.RenderErr(w, problems.Forbidden())
return
}

balance, errs := getAndVerifyBalanceEligibility(r, req.Data.ID, nil)
if len(errs) > 0 {
ape.RenderErr(w, errs...)
return
}

if balance.Country != nil {
Log(r).Debugf("Balance %s already joined rewards program", balance.Nullifier)
ape.RenderErr(w, problems.TooManyRequests())
return
}

err = EventsQ(r).Transaction(func() error {
return doPassportScanUpdates(r, *balance, req.Data.Attributes.Country, false)
})
if err != nil {
Log(r).WithError(err).Error("Failed to execute transaction")
ape.RenderErr(w, problems.InternalError())
return
}

event, err := EventsQ(r).FilterByNullifier(balance.Nullifier).
FilterByType(evtypes.TypePassportScan).
FilterByStatus(data.EventClaimed).Get()
if err != nil {
Log(r).WithError(err).Error("Failed to get claimed event")
ape.RenderErr(w, problems.InternalError())
return
}

ape.Render(w, newPassportEventStateResponse(req.Data.ID, event))
}

func calculateCountrySignature(key []byte, nullifier, country string) string {
bNull, err := hex.DecodeString(nullifier[2:])
if err != nil {
panic(fmt.Errorf("nullifier was not properly validated as hex: %w", err))
}

h := hmac.New(sha256.New, key)
msg := append(bNull, []byte(country)...)
h.Write(msg)

return hex.EncodeToString(h.Sum(nil))
}
71 changes: 50 additions & 21 deletions internal/service/handlers/verify_passport.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,42 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) {
return
}

countryCode, err := extractCountry(req.Data.Attributes.Proof)
if err != nil {
Log(r).WithError(err).Error("Critical: invalid country code provided, while the proof was valid")
ape.RenderErr(w, problems.InternalError())
return
}

if balance.Country != nil {
Log(r).Debugf("Balance %s already verified", balance.Nullifier)
ape.RenderErr(w, problems.TooManyRequests())
if balance.IsPassportProven {
Log(r).Debugf("Balance %s already verified", balance.Nullifier)
ape.RenderErr(w, problems.TooManyRequests())
return
}

if *balance.Country != countryCode {
ape.RenderErr(w, problems.BadRequest(validation.Errors{
"country": fmt.Errorf("country mismatch: got %s, joined program with %s", countryCode, *balance.Country),
})...)
return
}

err = BalancesQ(r).FilterByNullifier(balance.Nullifier).Update(map[string]any{
data.ColIsPassport: true,
})
if err != nil {
Log(r).WithError(err).Error("Failed to update balance")
ape.RenderErr(w, problems.InternalError())
return
}

ape.Render(w, newPassportEventStateResponse(req.Data.ID, nil))
return
}

err = EventsQ(r).Transaction(func() error {
return doPassportScanUpdates(r, *balance, req.Data.Attributes.Proof)
return doPassportScanUpdates(r, *balance, countryCode, true)
})
if err != nil {
Log(r).WithError(err).Error("Failed to execute transaction")
Expand All @@ -60,12 +88,15 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) {
return
}

ape.Render(w, newPassportEventStateResponse(req.Data.ID, event))
}

func newPassportEventStateResponse(id string, event *data.Event) resources.PassportEventStateResponse {
var res resources.PassportEventStateResponse
res.Data.ID = req.Data.ID
res.Data.ID = id
res.Data.Type = resources.PASSPORT_EVENT_STATE
res.Data.Attributes.Claimed = (event != nil)

ape.Render(w, res)
res.Data.Attributes.Claimed = event != nil
return res
}

// getAndVerifyBalanceEligibility provides shared logic to verify that the user
Expand All @@ -90,7 +121,7 @@ func getAndVerifyBalanceEligibility(
if errs = checkVerificationEligibility(r, balance); len(errs) > 0 {
return nil, errs
}
// for withdrawal
// for withdrawal and joining program
if proof == nil {
return balance, nil
}
Expand Down Expand Up @@ -123,8 +154,8 @@ func checkVerificationEligibility(r *http.Request, balance *data.Balance) (errs
// doPassportScanUpdates performs all the necessary updates when the passport
// scan proof is provided. This logic is shared between verification and
// withdrawal handlers.
func doPassportScanUpdates(r *http.Request, balance data.Balance, proof zkptypes.ZKProof) error {
country, err := updateBalanceCountry(r, balance, proof)
func doPassportScanUpdates(r *http.Request, balance data.Balance, countryCode string, proven bool) error {
country, err := updateBalanceCountry(r, balance, countryCode, proven)
if err != nil {
return fmt.Errorf("update balance country: %w", err)
}
Expand Down Expand Up @@ -175,8 +206,8 @@ func doPassportScanUpdates(r *http.Request, balance data.Balance, proof zkptypes
return nil
}

func updateBalanceCountry(r *http.Request, balance data.Balance, proof zkptypes.ZKProof) (*data.Country, error) {
country, err := getOrCreateCountry(CountriesQ(r), proof)
func updateBalanceCountry(r *http.Request, balance data.Balance, code string, proven bool) (*data.Country, error) {
country, err := getOrCreateCountry(CountriesQ(r), code)
if err != nil {
return nil, fmt.Errorf("get or create country: %w", err)
}
Expand All @@ -189,9 +220,12 @@ func updateBalanceCountry(r *http.Request, balance data.Balance, proof zkptypes.
return nil, errors.New("countries mismatch")
}

err = BalancesQ(r).FilterByNullifier(balance.Nullifier).Update(map[string]any{
data.ColCountry: country.Code,
})
toUpd := map[string]any{data.ColCountry: country.Code}
if proven {
toUpd[data.ColIsPassport] = true
}

err = BalancesQ(r).FilterByNullifier(balance.Nullifier).Update(toUpd)
if err != nil {
return nil, fmt.Errorf("update balance country: %w", err)
}
Expand Down Expand Up @@ -414,12 +448,7 @@ func addEventForReferrer(r *http.Request, evTypeRef *evtypes.EventConfig, balanc
return nil
}

func getOrCreateCountry(q data.CountriesQ, proof zkptypes.ZKProof) (*data.Country, error) {
code, err := extractCountry(proof)
if err != nil {
return nil, fmt.Errorf("extract country: %w", err)
}

func getOrCreateCountry(q data.CountriesQ, code string) (*data.Country, error) {
c, err := q.FilterByCodes(code).Get()
if err != nil {
return nil, fmt.Errorf("get country by code: %w", err)
Expand Down
Loading
Loading