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/user levels #16

Merged
merged 6 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
78 changes: 15 additions & 63 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,89 +18,41 @@ event_types:
frequency: one-time
action_url: https://...
logo: https://...
- name: get_poh
title: Get PoH credential
reward: 50
description: Prove that you are human
short_description: Short description
frequency: one-time
expires_at: 2020-01-01T00:00:00Z
- name: free_weekly
title: Free weekly points
reward: 100
frequency: weekly
description: Get free points every week by visiting the platform and claiming your reward
short_description: Short description
- name: daily_login
title: Daily login
reward: 5
frequency: daily
description: Login every day
short_description: Short description
disabled: true
- name: be_referred
title: Referral welcome bonus
reward: 5
frequency: one-time
description: Be referred by a friend and get a welcome bonus
short_description: Short description
no_auto_open: true
- name: referral_common
title: Refer new users
reward: 25
frequency: one-time
description: Refer friends and get a reward for each friend who verifies the passport
short_description: Short description
- name: referral_specific
title: Refer user {:did}
reward: 25
frequency: unlimited
description: The user {:did} has verified the passport. Claim the reward!
short_description: Short description
no_auto_open: true
- name: planned
title: Planned event
reward: 25
frequency: unlimited
description: Event that start at specified time
short_description: Short description
starts_at: 2020-01-01T00:00:00Z
- name: generate_proof_age
title: Generate proof age
reward: 25
frequency: one-time
description: Event that become fulfilled when user create proof that prove age
short_description: Short description
- name: generate_proof_nationality
title: Generate proof nationality
reward: 50
frequency: one-time
description: Event that become fulfilled when user create proof that prove nationality
short_description: Short description
- name: verify_proof_age
title: Verify proof age
reward: 25
frequency: one-time
description: Event that become fulfilled when user verify someone else's proof age
short_description: Short description
- name: verify_proof_nationality
title: Verify proof nationality
reward: 50
frequency: one-time
description: Event that become fulfilled when user verify someone else's proof nationality
short_description: Short description
- name: verified_proof_age
title: Have proof age verified
reward: 25
frequency: one-time
description: Event that become fulfilled when another user verify you proof age (user that verify must have verified passport)
short_description: Short description
- name: verified_proof_nationality
title: Have proof nationality verified
reward: 50
frequency: one-time
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 user permissions and features
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
);

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

import (
"fmt"
"slices"

"gitlab.com/distributed_lab/figure/v3"
"gitlab.com/distributed_lab/kit/kv"
)

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) Levels() Levels {
return c.levels.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(fmt.Errorf("failed to figure out levels config: %w", err))
}

if len(cfg.Lvls) == 0 {
panic(fmt.Errorf("no levels provided in config: %w", err))
}

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

return res
}).(Levels)
}

// Calculate new lvl. New lvl always greater then current level
func (l Levels) LvlUp(currentLevel int, totalAmount int64) (refCoundToAdd int, newLevel int) {
lvls := make([]int, 0, len(l))
for k, v := range l {
if k <= currentLevel {
continue
}
if int64(v.Threshold) > totalAmount {
break
}

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

if len(lvls) == 0 {
return 0, currentLevel
}

newLevel = slices.Max(lvls)
return
}

// slices.Min will not panic because of previous logic
func (l Levels) MinLvl() int {
lvls := make([]int, 0, len(l))
for k := range l {
lvls = append(lvls, k)
}

return slices.Min(lvls)
}
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

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

levels 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
12 changes: 12 additions & 0 deletions internal/data/pg/balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func (q *balances) Insert(bal data.Balance) error {
"nullifier": bal.Nullifier,
"amount": bal.Amount,
"referred_by": bal.ReferredBy,
"level": bal.Level,
})

if err := q.db.Exec(stmt); err != nil {
Expand Down Expand Up @@ -65,6 +66,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
31 changes: 19 additions & 12 deletions internal/service/handlers/claim_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

"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"
Expand Down Expand Up @@ -48,15 +47,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 +60,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 +80,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 := Levels(r).LvlUp(balance.Level, reward+balance.Amount)
if level != balance.Level {
count, err := ReferralsQ(r).FilterByNullifier(event.Nullifier).Count()
if err != nil {
return fmt.Errorf("failed to get referral count: %w", err)
}

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

if err = BalancesQ(r).FilterByNullifier(event.Nullifier).SetLevel(level); err != nil {
return fmt.Errorf("failed to update level: %w", err)
}
}

updated, err := EventsQ(r).FilterByID(event.ID).Update(data.EventClaimed, nil, &reward)
if err != nil {
return fmt.Errorf("update event status: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions internal/service/handlers/create_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ func createBalanceWithEvents(nullifier, refBy string, events []data.Event, r *ht
err := BalancesQ(r).Insert(data.Balance{
Nullifier: nullifier,
ReferredBy: sql.NullString{String: refBy, Valid: refBy != ""},
Level: Levels(r).MinLvl(),
})

if err != nil {
Expand All @@ -121,6 +122,7 @@ func createBalanceWithEventsAndReferrals(nullifier, refBy string, events []data.
err := BalancesQ(r).Insert(data.Balance{
Nullifier: nullifier,
ReferredBy: sql.NullString{String: refBy, Valid: refBy != ""},
Level: Levels(r).MinLvl(),
})

if err != nil {
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)
}
Loading
Loading