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

Feat/withdraw #17

Merged
merged 5 commits into from
May 24, 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
15 changes: 15 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ event_types:
description: Event that become fulfilled when another user verify you proof nationality (user that verify must have verified passport)
short_description: Short description

levels:
levels:
- lvl: 1
threshold: 0
referrals: 5
withdrawal_allowed: false
- lvl: 2
threshold: 20
referrals: 5
withdrawal_allowed: true
- lvl: 3
threshold: 40
referrals: 5
withdrawal_allowed: true

auth:
addr: http://rarime-auth

Expand Down
15 changes: 6 additions & 9 deletions docs/spec/components/schemas/Balance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,16 @@ allOf:
type: object
required:
- amount
- is_verified
- is_disabled
- created_at
- updated_at
- level
properties:
amount:
type: integer
format: int64
description: Amount of points
example: 580
is_verified:
type: boolean
description: Whether the user has scanned passport
example: true
is_disabled:
type: boolean
description: |
Expand Down Expand Up @@ -56,7 +52,8 @@ allOf:
example: ["73k3bdYaFWM", "9csIL7dW65m"]
items:
type: string
is_withdrawal_allowed:
type: boolean
description: Whether the user can withdraw tokens. Returned only for the single user.
example: true
level:
type: integer
format: int
description: The level indicates how many possibilities the user has
example: 2
5 changes: 5 additions & 0 deletions docs/spec/components/schemas/Withdraw.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ allOf:
required:
- amount
- address
- proof
properties:
amount:
type: integer
Expand All @@ -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.
1 change: 1 addition & 0 deletions internal/assets/migrations/001_initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS balances
created_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()),
updated_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()),
referred_by text UNIQUE,
level INT NOT NULL DEFAULT 1
);

CREATE INDEX IF NOT EXISTS balances_page_index ON balances (amount, updated_at) WHERE referred_by IS NOT NULL;
Expand Down
38 changes: 38 additions & 0 deletions internal/config/levels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package config

import (
"gitlab.com/distributed_lab/figure/v3"
"gitlab.com/distributed_lab/kit/kv"
"gitlab.com/distributed_lab/logan/v3/errors"
)

type Level struct {
Level int `fig:"lvl,required"`
Threshold int `fig:"threshold,required"`
Referrals int `fig:"referrals,required"`
WithdrawalAllowed bool `fig:"withdrawal_allowed"`
}

type Levels map[int]Level

func (c *config) Leveler() Levels {
return c.leveler.Do(func() interface{} {
var cfg struct {
Lvls []Level `fig:"levels,required"`
}

err := figure.Out(&cfg).
From(kv.MustGetStringMap(c.getter, "levels")).
Please()
if err != nil {
panic(errors.Wrap(err, "failed to figure out levels config"))
}

res := make(Levels)
for _, v := range cfg.Lvls {
res[v.Level] = v
}

return res
}).(Levels)
}
2 changes: 2 additions & 0 deletions internal/config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Config interface {
evtypes.EventTypeser
sbtcheck.SbtChecker

Leveler() Levels
Verifier() *zk.Verifier
PointPrice() PointsPrice
}
Expand All @@ -33,6 +34,7 @@ type config struct {
evtypes.EventTypeser
sbtcheck.SbtChecker

leveler comfig.Once
verifier comfig.Once
pointPrice comfig.Once
getter kv.Getter
Expand Down
2 changes: 2 additions & 0 deletions internal/data/balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ type Balance struct {
UpdatedAt int32 `db:"updated_at"`
ReferredBy sql.NullString `db:"referred_by"`
Rank *int `db:"rank"`
Level int `db:"level"`
}

type BalancesQ interface {
New() BalancesQ
Insert(Balance) error
UpdateAmountBy(points int64) error
SetReferredBy(referralCode string) error
SetLevel(level int) error

Page(*pgdb.OffsetPageParams) BalancesQ
Select() ([]Balance, error)
Expand Down
11 changes: 11 additions & 0 deletions internal/data/pg/balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ func (q *balances) SetReferredBy(referralCode string) error {
return nil
}

func (q *balances) SetLevel(level int) error {
stmt := q.updater.
Set("level", level)

if err := q.db.Exec(stmt); err != nil {
return fmt.Errorf("set level: %w", err)
}

return nil
}

func (q *balances) Page(page *pgdb.OffsetPageParams) data.BalancesQ {
q.selector = page.ApplyTo(q.selector, "amount", "updated_at")
return q
Expand Down
52 changes: 40 additions & 12 deletions internal/service/handlers/claim_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package handlers
import (
"fmt"
"net/http"
"slices"

"github.com/rarimo/decentralized-auth-svc/pkg/auth"
"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"
"github.com/rarimo/rarime-points-svc/resources"
"gitlab.com/distributed_lab/ape"
"gitlab.com/distributed_lab/ape/problems"
"gitlab.com/distributed_lab/logan/v3/errors"
)

func ClaimEvent(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -48,15 +49,6 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) {
ape.RenderErr(w, problems.Forbidden())
return
}
if event.Type == evtypes.TypePassportScan {
if event.PointsAmount == nil {
Log(r).WithError(err).Errorf("PointsAmount can't be nil for event %s",
event.Type)
ape.RenderErr(w, problems.InternalError())
return
}
evType.Reward = *event.PointsAmount
}

balance, err := BalancesQ(r).FilterByNullifier(event.Nullifier).FilterDisabled().Get()
if err != nil {
Expand All @@ -70,7 +62,7 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) {
return
}

event, err = claimEventWithPoints(*event, evType.Reward, r)
event, err = claimEventWithPoints(r, *event, evType.Reward, balance)
if err != nil {
Log(r).WithError(err).Errorf("Failed to claim event %s and accrue %d points to the balance %s",
event.ID, evType.Reward, event.Nullifier)
Expand All @@ -90,8 +82,25 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) {
}

// requires: event exist
func claimEventWithPoints(event data.Event, reward int64, r *http.Request) (claimed *data.Event, err error) {
func claimEventWithPoints(r *http.Request, event data.Event, reward int64, balance *data.Balance) (claimed *data.Event, err error) {
err = EventsQ(r).Transaction(func() error {
refsCount, level := levelsToUp(r, balance.Level, reward+balance.Amount)
if level != 0 {
count, err := ReferralsQ(r).FilterByNullifier(event.Nullifier).Count()
if err != nil {
return errors.Wrap(err, "failed to get referral count")
}

refToAdd := prepareReferralsToAdd(event.Nullifier, uint64(refsCount), count)
if err = ReferralsQ(r).Insert(refToAdd...); err != nil {
return errors.Wrap(err, "failed to insert referrals")
}

if err = BalancesQ(r).FilterByNullifier(event.Nullifier).SetLevel(level); err != nil {
return errors.Wrap(err, "failed to update level")
}
}

updated, err := EventsQ(r).FilterByID(event.ID).Update(data.EventClaimed, nil, &reward)
if err != nil {
return fmt.Errorf("update event status: %w", err)
Expand All @@ -108,6 +117,25 @@ func claimEventWithPoints(event data.Event, reward int64, r *http.Request) (clai
return
}

// Take summary points after claim and calculate new lvls
func levelsToUp(r *http.Request, level int, points int64) (referralsToAdd int, newLevel int) {
lvls := make([]int, 0, len(Levels(r)))
for k, v := range Levels(r) {
if k <= level {
continue
}
if int64(v.Threshold) > points {
break
}

referralsToAdd += v.Referrals
lvls = append(lvls, k)
}

newLevel = slices.Max(lvls)
return
}

func newClaimEventResponse(
event data.Event,
meta resources.EventStaticMeta,
Expand Down
11 changes: 11 additions & 0 deletions internal/service/handlers/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
broadcasterCtxKey
pointPriceCtxKey
verifierCtxKey
levelsCtxKey
)

func CtxLog(entry *logan.Entry) func(context.Context) context.Context {
Expand Down Expand Up @@ -127,3 +128,13 @@ func CtxVerifier(verifier *zk.Verifier) func(context.Context) context.Context {
func Verifier(r *http.Request) *zk.Verifier {
return r.Context().Value(verifierCtxKey).(*zk.Verifier)
}

func CtxLevels(levels config.Levels) func(context.Context) context.Context {
return func(ctx context.Context) context.Context {
return context.WithValue(ctx, levelsCtxKey, levels)
}
}

func Levels(r *http.Request) config.Levels {
return r.Context().Value(levelsCtxKey).(config.Levels)
}
1 change: 1 addition & 0 deletions internal/service/handlers/get_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func newBalanceModel(balance data.Balance) resources.Balance {
CreatedAt: balance.CreatedAt,
UpdatedAt: balance.UpdatedAt,
Rank: balance.Rank,
Level: balance.Level,
},
}
}
Expand Down
42 changes: 38 additions & 4 deletions internal/service/handlers/verify_passport.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handlers

import (
"encoding/json"
"fmt"
"math/big"
"net/http"

Expand Down Expand Up @@ -87,11 +88,44 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) {
return
}

_, err = EventsQ(r).
FilterByID(event.ID).
Update(data.EventFulfilled, nil, nil)
var referralEvent = true
evType = EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive)
if evType == nil {
Log(r).Debug("Referral event type is disabled or expired, not accruing points to referrer")
referralEvent = false
}

err = EventsQ(r).Transaction(func() (err error) {
if referralEvent {
// ReferredBy always valid because of the previous logic
referral, err := ReferralsQ(r).Get(balance.ReferredBy.String)
if err != nil {
return errors.Wrap(err, "failed to get referral by ID")
}

err = EventsQ(r).Insert(data.Event{
Nullifier: referral.Nullifier,
Type: evType.Name,
Status: data.EventFulfilled,
Meta: data.Jsonb(fmt.Sprintf(`{"nullifier": "%s"}`, nullifier)),
})
if err != nil {
return errors.Wrap(err, "add event for referrer")
}
}

_, err = EventsQ(r).
FilterByID(event.ID).
Update(data.EventFulfilled, nil, nil)
if err != nil {
return errors.Wrap(err, "failed to update passport scan event")
}

return nil
})

if err != nil {
Log(r).WithError(err).Error("Failed to update passport scan event")
Log(r).WithError(err).Error("Failed to add referral event and update verify passport event")
ape.RenderErr(w, problems.InternalError())
return
}
Expand Down
Loading
Loading