Skip to content

Commit

Permalink
adjusted kyc handling logic to fetch latest state from eskimo for exi…
Browse files Browse the repository at this point in the history
…sting users that don't have kyc state in db yet
  • Loading branch information
ice-ares committed Nov 14, 2023
1 parent 771fe0c commit cb248c1
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 18 deletions.
1 change: 1 addition & 0 deletions application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ cmd/freezer-refrigerant:
jwtSecret: bogus
tokenomics: &tokenomics
kyc:
get-eskimo-user-state-url: https://something.example.com
config-json-url: https://ice-staging.b-cdn.net/something/somebogus.json
liveness-delay: 5m
bookkeeper/storage: &bookkeeperStorage
Expand Down
8 changes: 5 additions & 3 deletions cmd/freezer-refrigerant/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ type (
StartNewMiningSessionRequestBody struct {
// Specify this if you want to resurrect the user.
// `true` recovers all the lost balance, `false` deletes it forever, `null/undefined` does nothing. Default is `null/undefined`.
Resurrect *bool `json:"resurrect" example:"true"`
UserID string `uri:"userId" swaggerignore:"true" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"`
XClientType string `form:"x_client_type" swaggerignore:"true" required:"false" example:"web"`
Resurrect *bool `json:"resurrect" example:"true"`
UserID string `uri:"userId" swaggerignore:"true" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"`
XClientType string `form:"x_client_type" swaggerignore:"true" required:"false" example:"web"`
Authorization string `header:"Authorization" swaggerignore:"true" required:"true" example:"some token"`
XAccountMetadata string `header:"X-Account-Metadata" swaggerignore:"true" required:"false" example:"some token"`
// Specify this if you want to skip one or more specific KYC steps before starting a new mining session or extending an existing one.
// Some KYC steps are not skippable.
SkipKYCSteps []users.KYCStep `json:"skipKYCSteps" example:"0,1"`
Expand Down
7 changes: 5 additions & 2 deletions cmd/freezer-refrigerant/tokenomics.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ func (s *service) StartNewMiningSession( //nolint:gocritic // False negative.
req *server.Request[StartNewMiningSessionRequestBody, tokenomics.MiningSummary],
) (*server.Response[tokenomics.MiningSummary], *server.Response[server.ErrorResponse]) {
ms := &tokenomics.MiningSummary{MiningSession: &tokenomics.MiningSession{UserID: &req.Data.UserID}}
enhancedCtx := tokenomics.ContextWithClientType(contextWithHashCode(ctx, req), req.Data.XClientType)
if err := s.tokenomicsProcessor.StartNewMiningSession(enhancedCtx, ms, req.Data.Resurrect, req.Data.SkipKYCSteps); err != nil {
ctx = contextWithHashCode(ctx, req)
ctx = tokenomics.ContextWithClientType(ctx, req.Data.XClientType)
ctx = tokenomics.ContextWithAuthorization(ctx, req.Data.Authorization)
ctx = tokenomics.ContextWithXAccountMetadata(ctx, req.Data.XAccountMetadata)
if err := s.tokenomicsProcessor.StartNewMiningSession(ctx, ms, req.Data.Resurrect, req.Data.SkipKYCSteps); err != nil {
err = errors.Wrapf(err, "failed to start a new mining session for userID:%v, data:%#v", req.Data.UserID, req.Data)
switch {
case errors.Is(err, tokenomics.ErrNegativeMiningProgressDecisionRequired):
Expand Down
36 changes: 30 additions & 6 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package model

import (
"bytes"
"context"
"fmt"
"strconv"
stdlibtime "time"
Expand Down Expand Up @@ -252,16 +253,16 @@ type (
HideRanking bool `redis:"hide_ranking"`
}
KYCStepsCreatedAtField struct {
KYCStepsCreatedAt *TimeSlice `redis:"kyc_steps_created_at"`
KYCStepsCreatedAt *TimeSlice `json:"kycStepsCreatedAt" redis:"kyc_steps_created_at"`
}
KYCStepsLastUpdatedAtField struct {
KYCStepsLastUpdatedAt *TimeSlice `redis:"kyc_steps_last_updated_at"`
KYCStepsLastUpdatedAt *TimeSlice `json:"kycStepsLastUpdatedAt" redis:"kyc_steps_last_updated_at"`
}
KYCStepPassedField struct {
KYCStepPassed users.KYCStep `redis:"kyc_step_passed"`
KYCStepPassed users.KYCStep `json:"kycStepPassed" redis:"kyc_step_passed"`
}
KYCStepBlockedField struct {
KYCStepBlocked users.KYCStep `redis:"kyc_step_blocked"`
KYCStepBlocked users.KYCStep `json:"kycStepBlocked" redis:"kyc_step_blocked"`
}
DeserializedBackupUsersKey struct {
ID int64 `redis:"-"`
Expand Down Expand Up @@ -369,14 +370,37 @@ func (t *TimeSlice) UnmarshalBinary(text []byte) error {
return t.UnmarshalText(text)
}

func (t *TimeSlice) UnmarshalJSON(text []byte) error {
if len(text) == 0 || (len(text) == 2 && string(text) == "[]") {
return nil
}
sep := make([]byte, 1)
sep[0] = ','
elems := bytes.Split(text[1:len(text)-1], sep)
timeSlice := make(TimeSlice, 0, len(elems))
for _, val := range elems {
unmarshalledTime := new(time.Time)
if err := unmarshalledTime.UnmarshalJSON(context.Background(), val); err != nil {
return errors.Wrapf(err, "failed to UnmarshalJSON %#v:%v", unmarshalledTime, string(val))
}
if !unmarshalledTime.IsNil() {
timeSlice = append(timeSlice, unmarshalledTime)
}
}
*t = timeSlice

return nil
}

func (t *TimeSlice) UnmarshalText(text []byte) error {
if len(text) == 0 || (len(text) == 1 && string(text) == "") {
return nil
}
timeSlice := *t
sep := make([]byte, 1)
sep[0] = ','
for _, val := range bytes.Split(text, sep) {
elems := bytes.Split(text, sep)
timeSlice := make(TimeSlice, 0, len(elems))
for _, val := range elems {
unmarshalledTime := new(time.Time)
if err := unmarshalledTime.UnmarshalText(val); err != nil {
return errors.Wrapf(err, "failed to unmarshall %#v:%v", unmarshalledTime, string(val))
Expand Down
35 changes: 35 additions & 0 deletions model/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"testing"
stdlibtime "time"

"github.com/goccy/go-json"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ice-blockchain/eskimo/users"
"github.com/ice-blockchain/wintr/connectors/storage/v3"
Expand Down Expand Up @@ -103,3 +105,36 @@ func TestTimeSliceEquals(t *testing.T) {
assert.False(t, (&TimeSlice{now, now}).Equals(&TimeSlice{now}))
assert.False(t, (&TimeSlice{nil, now}).Equals(&TimeSlice{now, nil}))
}

func TestEskimoToFreezerKYCStateDeserialization(t *testing.T) {
t.Parallel()
stepA := users.LivenessDetectionKYCStep
stepB := users.Social2KYCStep
usr := &users.User{
KYCStepsCreatedAt: &[]*time.Time{time.Now()},
KYCStepsLastUpdatedAt: &[]*time.Time{time.Now(), time.Now()},
KYCStepPassed: &stepB,
KYCStepBlocked: &stepA,
}
serializedUser, err := json.Marshal(usr)
require.NoError(t, err)

type (
KYCState struct {
KYCStepsCreatedAtField
KYCStepsLastUpdatedAtField
KYCStepPassedField
KYCStepBlockedField
}
)
var deserializedUser KYCState
err = json.Unmarshal(serializedUser, &deserializedUser)
require.NoError(t, err)
assert.EqualValues(t, stepA, deserializedUser.KYCStepBlocked)
assert.EqualValues(t, stepB, deserializedUser.KYCStepPassed)
assert.EqualValues(t, 1, len(*usr.KYCStepsCreatedAt))
assert.EqualValues(t, (*usr.KYCStepsCreatedAt)[0], (*deserializedUser.KYCStepsCreatedAt)[0])
assert.EqualValues(t, 2, len(*usr.KYCStepsLastUpdatedAt))
assert.EqualValues(t, (*usr.KYCStepsLastUpdatedAt)[0], (*deserializedUser.KYCStepsLastUpdatedAt)[0])
assert.EqualValues(t, (*usr.KYCStepsLastUpdatedAt)[1], (*deserializedUser.KYCStepsLastUpdatedAt)[1])
}
7 changes: 5 additions & 2 deletions tokenomics/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ const (
requestingUserIDCtxValueKey = "requestingUserIDCtxValueKey"
clientTypeCtxValueKey = "clientTypeCtxValueKey"
userHashCodeCtxValueKey = "userHashCodeCtxValueKey"
authorizationCtxValueKey = "authorizationCtxValueKey"
xAccountMetadataCtxValueKey = "xAccountMetadataCtxValueKey"
requestDeadline = 25 * stdlibtime.Second

floatToStringFormatter = "%.2f"
Expand Down Expand Up @@ -268,8 +270,9 @@ type (
disableAdvancedTeam *atomic.Pointer[[]string]
kycConfigJSON *atomic.Pointer[kycConfigJSON]
KYC struct {
ConfigJSONURL string `yaml:"config-json-url" mapstructure:"config-json-url"`
LivenessDelay stdlibtime.Duration `yaml:"liveness-delay" mapstructure:"liveness-delay"`
GetEskimoUserStateURL string `yaml:"get-eskimo-user-state-url" mapstructure:"get-eskimo-user-state-url"`
ConfigJSONURL string `yaml:"config-json-url" mapstructure:"config-json-url"`
LivenessDelay stdlibtime.Duration `yaml:"liveness-delay" mapstructure:"liveness-delay"`
} `yaml:"kyc" mapstructure:"kyc"`
AdoptionMilestoneSwitch struct {
ActiveUserMilestones []struct {
Expand Down
38 changes: 33 additions & 5 deletions tokenomics/kyc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package tokenomics

import (
"context"
"fmt"
"math/rand"
"net/http"
"strings"
Expand Down Expand Up @@ -108,7 +109,7 @@ func (r *repository) validateKYC(ctx context.Context, state *getCurrentMiningSes
case users.NoneKYCStep:
if !state.MiningSessionSoloLastStartedAt.IsNil() && r.isKYCEnabled(ctx, users.FacialRecognitionKYCStep) {
if doFallbackRecursion {
if err := r.overrideKYCStateWithEskimoKYCState(ctx, &state.KYCState); err != nil {
if err := r.overrideKYCStateWithEskimoKYCState(ctx, state.UserID, &state.KYCState); err != nil {
return errors.Wrapf(err, "failed to overrideKYCStateWithEskimoKYCState for %#v", state)
}

Expand Down Expand Up @@ -159,10 +160,37 @@ func (r *repository) isKYCEnabled(ctx context.Context, _ users.KYCStep) bool {
Because existing users have empty KYCState in dragonfly cuz usersTableSource might not have updated it yet.
So we need to query Eskimo for the valid kyc state of the user to be sure.
*/
func (r *repository) overrideKYCStateWithEskimoKYCState(ctx context.Context, state *KYCState) error {
// TODO impl this or remove it if not needed.

return nil
func (r *repository) overrideKYCStateWithEskimoKYCState(ctx context.Context, userID string, state *KYCState) error {
if resp, err := req.
SetContext(ctx).
SetRetryCount(25).
SetRetryBackoffInterval(10*stdlibtime.Millisecond, 1*stdlibtime.Second).
SetRetryHook(func(resp *req.Response, err error) {
if err != nil {
log.Error(errors.Wrap(err, "failed to fetch eskimo user's state, retrying..."))
} else {
body, bErr := resp.ToString()
log.Error(errors.Wrapf(bErr, "failed to parse negative response body for eskimo user's state"))
log.Error(errors.Errorf("failed to fetch eskimo user's state with status code:%v, body:%v, retrying...", resp.GetStatusCode(), body))
}
}).
SetRetryCondition(func(resp *req.Response, err error) bool {
return err != nil || (resp.GetStatusCode() != http.StatusOK && resp.GetStatusCode() != http.StatusUnauthorized)
}).
AddQueryParam("caller", "freezer-refrigerant").
SetHeader("Authorization", authorization(ctx)).
SetHeader("X-Account-Metadata", xAccountMetadata(ctx)).
SetHeader("Accept", "application/json").
SetHeader("Cache-Control", "no-cache, no-store, must-revalidate").
SetHeader("Pragma", "no-cache").
SetHeader("Expires", "0").
Get(fmt.Sprintf("%v/users/%v", r.cfg.KYC.GetEskimoUserStateURL, userID)); err != nil {
return errors.Wrapf(err, "failed to fetch eskimo user state for userID:%v", userID)
} else if data, err2 := resp.ToBytes(); err2 != nil {
return errors.Wrapf(err2, "failed to read body of eskimo user state request for userID:%v", userID)
} else {
return errors.Wrapf(json.Unmarshal(data, state), "failed to unmarshal into %#v, data: %v", state, string(data))
}
}

func mustGetLivenessLoadDistributionStartDate(ctx context.Context, db storage.DB) (livenessLoadDistributionStartDate *time.Time) {
Expand Down
28 changes: 28 additions & 0 deletions tokenomics/tokenomics.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,34 @@ func requestingUserID(ctx context.Context) (requestingUserID string) {
return
}

func ContextWithAuthorization(ctx context.Context, authorization string) context.Context {
if authorization == "" {
return ctx
}

return context.WithValue(ctx, authorizationCtxValueKey, authorization) //nolint:revive,staticcheck // Not an issue.
}

func ContextWithXAccountMetadata(ctx context.Context, xAccountMetadata string) context.Context {
if xAccountMetadata == "" {
return ctx
}

return context.WithValue(ctx, xAccountMetadataCtxValueKey, xAccountMetadata) //nolint:revive,staticcheck // Not an issue.
}

func authorization(ctx context.Context) (authorization string) {
authorization, _ = ctx.Value(authorizationCtxValueKey).(string) //nolint:errcheck // Not needed.

return
}

func xAccountMetadata(ctx context.Context) (xAccountMetadata string) {
xAccountMetadata, _ = ctx.Value(xAccountMetadataCtxValueKey).(string) //nolint:errcheck // Not needed.

return
}

func ContextWithClientType(ctx context.Context, clientType string) context.Context {
if clientType == "" {
return ctx
Expand Down

0 comments on commit cb248c1

Please sign in to comment.