From d89df2b5a601c95edd474f61b7837e9b90830321 Mon Sep 17 00:00:00 2001 From: Ares <75481906+ice-ares@users.noreply.github.com> Date: Sat, 16 Dec 2023 00:22:27 +0200 Subject: [PATCH] coin distribution preparation --- Makefile | 2 +- application.yaml | 7 +- cmd/freezer-refrigerant/api/docs.go | 11 ++- cmd/freezer-refrigerant/api/swagger.json | 11 ++- cmd/freezer-refrigerant/api/swagger.yaml | 8 +- cmd/freezer-refrigerant/contract.go | 4 +- coin-distribution/DDL.sql | 62 ++++++-------- coin-distribution/coin_distribution.go | 4 +- coin-distribution/contract.go | 9 +- coin-distribution/eligibility.go | 61 ++++++++++++++ ...stribution_review.go => pending_review.go} | 13 ++- coin-distribution/trigger.go | 72 ++++++++++++++++ extra-bonus-notifier/calculation.go | 11 +-- go.mod | 2 +- go.sum | 4 +- miner/days_off.go | 3 +- model/model.go | 83 ++++++++++++++++++- model/model_test.go | 19 +++-- tokenomics/contract.go | 18 ++-- tokenomics/kyc.go | 32 +++---- tokenomics/mining.go | 6 +- tokenomics/mining_sessions.go | 10 +-- tokenomics/users.go | 5 +- 23 files changed, 335 insertions(+), 122 deletions(-) create mode 100644 coin-distribution/eligibility.go rename coin-distribution/{distribution_review.go => pending_review.go} (67%) create mode 100644 coin-distribution/trigger.go diff --git a/Makefile b/Makefile index 67ca534..827fd41 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,7 @@ generate-swagger: generate-swaggers: go install github.com/swaggo/swag/cmd/swag@latest set -xe; \ - [ -d cmd ] && find ./cmd -mindepth 1 -maxdepth 1 -type d -print | grep -v 'fixture' | grep -v 'freezer-miner' | sed 's/\.\///g' | while read service; do \ + [ -d cmd ] && find ./cmd -mindepth 1 -maxdepth 1 -type d -print | grep -v 'fixture' | grep -v 'freezer-miner' | grep -v 'freezer-coin-distributer' | sed 's/\.\///g' | while read service; do \ env SERVICE=$${service} $(MAKE) generate-swagger; \ done; diff --git a/application.yaml b/application.yaml index 91cb07d..1108ec9 100644 --- a/application.yaml +++ b/application.yaml @@ -176,7 +176,12 @@ wintr/connectors/storage/v2: &db miner: bookkeeper/storage: *bookkeeperStorage development: true - workers: 1 + workers: 2 + batchSize: 100 + wintr/connectors/storage/v2: *db +coin-distribution: + development: true + workers: 2 batchSize: 100 wintr/connectors/storage/v2: *db extra-bonus-notifier: diff --git a/cmd/freezer-refrigerant/api/docs.go b/cmd/freezer-refrigerant/api/docs.go index fce6338..383182c 100644 --- a/cmd/freezer-refrigerant/api/docs.go +++ b/cmd/freezer-refrigerant/api/docs.go @@ -383,13 +383,17 @@ const docTemplate = `{ } }, "definitions": { - "coindistribution.CoinDistibutionForReview": { + "coindistribution.PendingReview": { "type": "object", "properties": { "ethAddress": { "type": "string", "example": "0x43...." }, + "ice": { + "type": "number", + "example": 1000 + }, "iceflakes": { "type": "string", "example": "100000000000000" @@ -416,12 +420,13 @@ const docTemplate = `{ "type": "object", "properties": { "cursor": { - "type": "integer" + "type": "integer", + "example": 5065 }, "distributions": { "type": "array", "items": { - "$ref": "#/definitions/coindistribution.CoinDistibutionForReview" + "$ref": "#/definitions/coindistribution.PendingReview" } } } diff --git a/cmd/freezer-refrigerant/api/swagger.json b/cmd/freezer-refrigerant/api/swagger.json index 97d06af..4481b52 100644 --- a/cmd/freezer-refrigerant/api/swagger.json +++ b/cmd/freezer-refrigerant/api/swagger.json @@ -377,13 +377,17 @@ } }, "definitions": { - "coindistribution.CoinDistibutionForReview": { + "coindistribution.PendingReview": { "type": "object", "properties": { "ethAddress": { "type": "string", "example": "0x43...." }, + "ice": { + "type": "number", + "example": 1000 + }, "iceflakes": { "type": "string", "example": "100000000000000" @@ -410,12 +414,13 @@ "type": "object", "properties": { "cursor": { - "type": "integer" + "type": "integer", + "example": 5065 }, "distributions": { "type": "array", "items": { - "$ref": "#/definitions/coindistribution.CoinDistibutionForReview" + "$ref": "#/definitions/coindistribution.PendingReview" } } } diff --git a/cmd/freezer-refrigerant/api/swagger.yaml b/cmd/freezer-refrigerant/api/swagger.yaml index eee35a7..5a0a258 100644 --- a/cmd/freezer-refrigerant/api/swagger.yaml +++ b/cmd/freezer-refrigerant/api/swagger.yaml @@ -2,11 +2,14 @@ basePath: /v1w definitions: - coindistribution.CoinDistibutionForReview: + coindistribution.PendingReview: properties: ethAddress: example: 0x43.... type: string + ice: + example: 1000 + type: number iceflakes: example: "100000000000000" type: string @@ -26,10 +29,11 @@ definitions: main.CoinDistributionsForReview: properties: cursor: + example: 5065 type: integer distributions: items: - $ref: '#/definitions/coindistribution.CoinDistibutionForReview' + $ref: '#/definitions/coindistribution.PendingReview' type: array type: object main.StartNewMiningSessionRequestBody: diff --git a/cmd/freezer-refrigerant/contract.go b/cmd/freezer-refrigerant/contract.go index 05322d2..de5f781 100644 --- a/cmd/freezer-refrigerant/contract.go +++ b/cmd/freezer-refrigerant/contract.go @@ -33,8 +33,8 @@ type ( } CoinDistributionsForReview struct { - Distributions []*coindistribution.CoinDistibutionForReview `json:"distributions"` - Cursor uint64 `json:"cursor" example: 5065` + Distributions []*coindistribution.PendingReview `json:"distributions"` + Cursor uint64 `json:"cursor" example:"5065"` } GetCoinDistributionForReviewParams struct { diff --git a/coin-distribution/DDL.sql b/coin-distribution/DDL.sql index 40dda6e..4e27df9 100644 --- a/coin-distribution/DDL.sql +++ b/coin-distribution/DDL.sql @@ -1,37 +1,27 @@ -- SPDX-License-Identifier: ice License 1.0 +DO $$ BEGIN + CREATE DOMAIN uint256 AS NUMERIC(78,0) NOT NULL DEFAULT 0 + CHECK (VALUE >= 0 AND VALUE <= 115792089237316195423570985008687907853269984665640564039457584007913129639935) + CHECK (SCALE(VALUE) = 0); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + CREATE TABLE IF NOT EXISTS pending_coin_distributions ( created_at timestamp NOT NULL, internal_id bigint NOT NULL, - iceflakes bigint NOT NULL, + iceflakes uint256, user_id text NOT NULL PRIMARY KEY, eth_address text NOT NULL); CREATE INDEX IF NOT EXISTS pending_coin_distributions_worker_number_ix ON pending_coin_distributions ((internal_id % 10), created_at ASC); -CREATE TABLE IF NOT EXISTS pending_coin_distribution_configurations ( +CREATE TABLE IF NOT EXISTS global ( key text NOT NULL primary key, value text NOT NULL ); -INSERT INTO pending_coin_distribution_configurations(key,value) VALUES ('enabled','true') ON CONFLICT(key) DO NOTHING; - ---- Flow: ---infinite loop: -- with 30 sec sleep between iterations if 0 rows returned ---do in transaction: ---1. SELECT * --- FROM pending_coin_distributions --- WHERE internal_id % 10 = $1 --- ORDER BY created_at ASC --- LIMIT $2 --- FOR UPDATE ---2. delete from pending_coin_distributions WHERE user_id = ANY($1) ---3. call ERC-20 smart contract method to airdrop coins - -DO $$ BEGIN - CREATE DOMAIN uint256 AS NUMERIC(78,0) NOT NULL DEFAULT 0 - CHECK (VALUE >= 0 AND VALUE <= 115792089237316195423570985008687907853269984665640564039457584007913129639935) - CHECK (SCALE(VALUE) = 0); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; +INSERT INTO global (key,value) + VALUES ('coin_distributer_enabled','true') + ON CONFLICT(key) DO NOTHING; CREATE TABLE IF NOT EXISTS coin_distributions_by_earner ( created_at timestamp NOT NULL, @@ -47,11 +37,14 @@ CREATE TABLE IF NOT EXISTS coin_distributions_by_earner ( CREATE TABLE IF NOT EXISTS coin_distributions_pending_review ( created_at timestamp NOT NULL, internal_id bigint NOT NULL, - iceflakes uint256, + ice bigint NOT NULL, + day date NOT NULL, + iceflakes uint256 , username text NOT NULL, referred_by_username text NOT NULL, - user_id text NOT NULL PRIMARY KEY, - eth_address text NOT NULL); + user_id text NOT NULL, + eth_address text NOT NULL, + PRIMARY KEY(day, user_id)); CREATE INDEX IF NOT EXISTS coin_distributions_pending_review_internal_id_ix ON coin_distributions_pending_review (internal_id); @@ -59,21 +52,14 @@ CREATE TABLE IF NOT EXISTS reviewed_coin_distributions ( reviewed_at timestamp NOT NULL, created_at timestamp NOT NULL, internal_id bigint NOT NULL, - iceflakes uint256, + ice bigint NOT NULL, + day date NOT NULL, + review_day date NOT NULL, + iceflakes uint256 , username text NOT NULL, referred_by_username text NOT NULL, user_id text NOT NULL , eth_address text NOT NULL, reviewer_user_id text NOT NULL, decision text NOT NULL, - PRIMARY KEY(user_id, reviewed_at)); - -CREATE TABLE IF NOT EXISTS pending_coin_distribution_statistics ( - created_at timestamp NOT NULL PRIMARY KEY, - total_iceflakes uint256, - marketing_25percent_iceflakes uint256, - marketing_45percent_iceflakes uint256, - marketing_30percent_iceflakes uint256, - marketing_25percent_eth_address text NOT NULL, - marketing_45percent_eth_address text NOT NULL, - marketing_30percent_eth_address text NOT NULL); \ No newline at end of file + PRIMARY KEY(user_id, day, review_day)); \ No newline at end of file diff --git a/coin-distribution/coin_distribution.go b/coin-distribution/coin_distribution.go index 8b906c6..ae7e7ed 100644 --- a/coin-distribution/coin_distribution.go +++ b/coin-distribution/coin_distribution.go @@ -100,7 +100,7 @@ func (cd *coinDistributer) isEnabled(rooCtx context.Context) bool { defer cancel() val, err := storage.Get[struct { Enabled bool - }](ctx, cd.db, `SELECT value::bool as enabled FROM pending_coin_distribution_configurations WHERE key = 'enabled'`) + }](ctx, cd.db, `SELECT value::bool as enabled FROM global WHERE key = 'coin_distributer_enabled'`) if err != nil { log.Error(errors.Wrap(err, "failed to check if coinDistributer is enabled")) @@ -113,7 +113,7 @@ func (cd *coinDistributer) isEnabled(rooCtx context.Context) bool { func (cd *coinDistributer) Disable(rooCtx context.Context) error { ctx, cancel := context.WithTimeout(rooCtx, requestDeadline) defer cancel() - rows, err := storage.Exec(ctx, cd.db, `UPDATE pending_coin_distribution_configurations SET value = 'false' WHERE key = 'enabled'`) + rows, err := storage.Exec(ctx, cd.db, `UPDATE global SET value = 'false' WHERE key = 'coin_distributer_enabled'`) if err != nil { return errors.Wrap(err, "failed to disable coinDistributer") } diff --git a/coin-distribution/contract.go b/coin-distribution/contract.go index 6acdc6d..153f054 100644 --- a/coin-distribution/contract.go +++ b/coin-distribution/contract.go @@ -23,17 +23,19 @@ type ( Repository interface { io.Closer - GetCoinDistributionsForReview(ctx context.Context, cursor, limit uint64) (updatedCursor uint64, distributions []*CoinDistibutionForReview, err error) + GetCoinDistributionsForReview(ctx context.Context, cursor, limit uint64) (updatedCursor uint64, distributions []*PendingReview, err error) CheckHealth(ctx context.Context) error } - CoinDistibutionForReview struct { + PendingReview struct { CreatedAt *time.Time `json:"time" swaggertype:"string" example:"2022-01-03T16:20:52.156534Z"` Iceflakes string `json:"iceflakes" swaggertype:"string" example:"100000000000000"` Username string `json:"username" swaggertype:"string" example:"myusername"` ReferredByUsername string `json:"referredByUsername" swaggertype:"string" example:"myrefusername"` UserID string `json:"userId" swaggertype:"string" example:"12746386-03de-44d7-91c7-856fa66b6ed6"` EthAddress string `json:"ethAddress" swaggertype:"string" example:"0x43...."` + Ice float64 `json:"ice" db:"-" example:"1000"` + IceInternal int64 `json:"-" db:"ice" swaggerignore:"true"` } ) @@ -69,7 +71,8 @@ type ( Development bool `yaml:"development"` } coinDistribution struct { - *CoinDistibutionForReview + *PendingReview + Day stdlibtime.Time InternalID uint64 } ) diff --git a/coin-distribution/eligibility.go b/coin-distribution/eligibility.go new file mode 100644 index 0000000..a303a49 --- /dev/null +++ b/coin-distribution/eligibility.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: ice License 1.0 + +package coindistribution + +import ( + stdlibtime "time" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/freezer/model" + "github.com/ice-blockchain/wintr/time" +) + +const ( + miningSessionSoloEndedAtNetworkDelayAdjustment = 20 * stdlibtime.Second +) + +func CalculateEthereumDistributionICEBalance( + standardBalance float64, + ethereumDistributionFrequencyMin, ethereumDistributionFrequencyMax stdlibtime.Duration, + now, ethereumDistributionEndDate *time.Time, +) float64 { + delta := ethereumDistributionEndDate.Truncate(ethereumDistributionFrequencyMin).Sub(now.Truncate(ethereumDistributionFrequencyMin)) + if delta <= ethereumDistributionFrequencyMax { + return standardBalance + } + + //TODO: should this be fractional or natural? + return standardBalance / (float64(delta.Nanoseconds()) / float64(ethereumDistributionFrequencyMax.Nanoseconds())) +} + +func IsEligibleForEthereumDistribution( + minMiningStreaksRequired uint64, + standardBalance, minEthereumDistributionICEBalanceRequired float64, + ethAddress, country string, + distributionDeniedCountries map[string]struct{}, + now, miningSessionSoloStartedAt, miningSessionSoloEndedAt, ethereumDistributionEndDate *time.Time, + kycState model.KYCState, + miningSessionDuration, ethereumDistributionFrequencyMin, ethereumDistributionFrequencyMax stdlibtime.Duration) bool { + var countryAllowed bool + if _, countryDenied := distributionDeniedCountries[country]; country != "" && !countryDenied { + countryAllowed = true + } + + return countryAllowed && + !miningSessionSoloEndedAt.IsNil() && miningSessionSoloEndedAt.After(now.Add(miningSessionSoloEndedAtNetworkDelayAdjustment)) && + isEthereumAddressValid(ethAddress) && + CalculateEthereumDistributionICEBalance(standardBalance, ethereumDistributionFrequencyMin, ethereumDistributionFrequencyMax, now, ethereumDistributionEndDate) >= minEthereumDistributionICEBalanceRequired && //nolint:lll // . + model.CalculateMiningStreak(now, miningSessionSoloStartedAt, miningSessionSoloEndedAt, miningSessionDuration) >= minMiningStreaksRequired && + kycState.KYCStepPassedCorrectly(users.QuizKYCStep) +} + +func isEthereumAddressValid(ethAddress string) bool { + if ethAddress == "" { + return false + } + if ethAddress == "skip" { + return true + } + + return true +} diff --git a/coin-distribution/distribution_review.go b/coin-distribution/pending_review.go similarity index 67% rename from coin-distribution/distribution_review.go rename to coin-distribution/pending_review.go index 61efc75..55e2546 100644 --- a/coin-distribution/distribution_review.go +++ b/coin-distribution/pending_review.go @@ -10,8 +10,12 @@ import ( "github.com/ice-blockchain/wintr/connectors/storage/v2" ) -func (r *repository) GetCoinDistributionsForReview(ctx context.Context, cursor, limit uint64) (updCursor uint64, distributions []*CoinDistibutionForReview, err error) { - sql := `SELECT * FROM coin_distributions_pending_review WHERE internal_id > $1 ORDER BY internal_id LIMIT $2` +func (r *repository) GetCoinDistributionsForReview(ctx context.Context, cursor, limit uint64) (updCursor uint64, distributions []*PendingReview, err error) { + sql := `SELECT * + FROM coin_distributions_pending_review + WHERE internal_id > $1 + ORDER BY internal_id + LIMIT $2` args := []any{cursor, limit} result, err := storage.Select[coinDistribution](ctx, r.db, sql, args...) if err != nil { @@ -21,9 +25,10 @@ func (r *repository) GetCoinDistributionsForReview(ctx context.Context, cursor, if uint64(len(result)) == limit { updCursor = result[len(result)-1].InternalID } - distributions = make([]*CoinDistibutionForReview, len(result)) //nolint:makezero // . + distributions = make([]*PendingReview, len(result)) //nolint:makezero // . for i, d := range result { - distributions[i] = d.CoinDistibutionForReview + d.PendingReview.Ice = float64(d.PendingReview.IceInternal) / 100 + distributions[i] = d.PendingReview } return diff --git a/coin-distribution/trigger.go b/coin-distribution/trigger.go new file mode 100644 index 0000000..95337f7 --- /dev/null +++ b/coin-distribution/trigger.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: ice License 1.0 + +package coindistribution + +import ( + "context" + "fmt" + "strings" + stdlibtime "time" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/wintr/connectors/storage/v2" + "github.com/ice-blockchain/wintr/time" +) + +type ( + TriggeringRecord struct { + CreatedAt *time.Time + Username string + ReferredByUsername string + UserID string + EarnerUserID string + EthAddress string + InternalID int64 + Balance float64 + } +) + +func TriggerCoinDistribution(ctx context.Context, db storage.Execer, records []*TriggeringRecord) error { + const columns = 8 + values := make([]string, 0, len(records)) + args := make([]any, 0, len(records)*columns) + for ix, record := range records { + values = append(values, generateValuesSQLParams(ix, columns)) + args = append(args, + record.CreatedAt.Time, + record.InternalID, + int64(record.Balance*100), + record.Username, + record.ReferredByUsername, + record.UserID, + record.EarnerUserID, + record.EthAddress) + } + sql := fmt.Sprintf(`INSERT INTO coin_distributions_by_earner(created_at,internal_id,balance,username,referred_by_username,user_id,earner_user_id,eth_address) + VALUES %v + ON CONFLICT (user_id, earner_user_id) DO UPDATE + SET balance = EXCLUDED.balance`, strings.Join(values, ",\n")) + _, err := storage.Exec(ctx, db, sql, args...) + + return errors.Wrapf(err, "failed to insert into coin_distributions_by_earner %#v", records) +} + +func generateValuesSQLParams(index, columns int) string { + params := make([]string, 0, columns) + for ii := 1; ii <= columns; ii++ { + params = append(params, fmt.Sprintf("$%v", index*columns+ii)) + } + + return fmt.Sprintf("(%v)", strings.Join(params, ",")) +} + +func X(ctx context.Context, db storage.Execer) error { + sql := `INSERT INTO global(key,value) VALUES ('latest_processing_date',$1) + ON CONFLICT (key) DO UPDATE + SET value = EXCLUDED.value + WHERE value != EXCLUDED.value` + _, err := storage.Exec(ctx, db, sql, time.Now().Format(stdlibtime.DateOnly)) + + return errors.Wrap(err, "failed to XXX ") +} diff --git a/extra-bonus-notifier/calculation.go b/extra-bonus-notifier/calculation.go index d3b0b93..5134d33 100644 --- a/extra-bonus-notifier/calculation.go +++ b/extra-bonus-notifier/calculation.go @@ -3,6 +3,7 @@ package extrabonusnotifier import ( + "github.com/ice-blockchain/freezer/model" "github.com/ice-blockchain/wintr/time" ) @@ -15,7 +16,7 @@ func CalculateExtraBonus( newsSeenBonusValues = cfg.ExtraBonuses.NewsSeenValues miningStreakValues = cfg.ExtraBonuses.MiningStreakValues bonusPercentageRemaining = float64(100 * (1 + extraBonusDaysClaimNotAvailable)) - miningStreak = calculateMiningStreak(now, miningSessionSoloStartedAt, miningSessionSoloEndedAt) + miningStreak = model.CalculateMiningStreak(now, miningSessionSoloStartedAt, miningSessionSoloEndedAt, cfg.MiningSessionDuration) flatBonusValue = cfg.ExtraBonuses.FlatValues[extraBonusIndex] firstDelayedClaimPenaltyWindow = int64(float64(cfg.ExtraBonuses.DelayedClaimPenaltyWindow.Nanoseconds()) * networkDelayDelta) ) @@ -34,11 +35,3 @@ func CalculateExtraBonus( return (float64(flatBonusValue+miningStreakValues[miningStreak]+newsSeenBonusValues[newsSeen]) * bonusPercentageRemaining) / 100 } - -func calculateMiningStreak(now, start, end *time.Time) uint64 { - if start.IsNil() || end.IsNil() || now.After(*end.Time) || now.Before(*start.Time) { - return 0 - } - - return uint64(now.Sub(*start.Time) / cfg.MiningSessionDuration) -} diff --git a/go.mod b/go.mod index 3299a47..79a69a8 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/ice-blockchain/eskimo v1.225.0 github.com/ice-blockchain/go-tarantool-client v0.0.0-20230327200757-4fc71fa3f7bb - github.com/ice-blockchain/wintr v1.126.0 + github.com/ice-blockchain/wintr v1.127.0 github.com/imroc/req/v3 v3.42.2 github.com/pkg/errors v0.9.1 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 diff --git a/go.sum b/go.sum index b9862ee..5e5ca62 100644 --- a/go.sum +++ b/go.sum @@ -222,8 +222,8 @@ github.com/ice-blockchain/eskimo v1.225.0 h1:aA+PL0vXbD52dRc9QZMZoNDkMyVd0r8qAwU github.com/ice-blockchain/eskimo v1.225.0/go.mod h1:Du7qkQbFadYFb9YddPfGrErk0tX3zEX7ckIzxBZXwQY= github.com/ice-blockchain/go-tarantool-client v0.0.0-20230327200757-4fc71fa3f7bb h1:8TnFP3mc7O+tc44kv2e0/TpZKnEVUaKH+UstwfBwRkk= github.com/ice-blockchain/go-tarantool-client v0.0.0-20230327200757-4fc71fa3f7bb/go.mod h1:ZsQU7i3mxhgBBu43Oev7WPFbIjP4TniN/b1UPNGbrq8= -github.com/ice-blockchain/wintr v1.126.0 h1:zPN0BnIOb3n7Fv8GLmts0kUKL2lzKqV8FGGizaIYSRI= -github.com/ice-blockchain/wintr v1.126.0/go.mod h1:Wq/uG3vDL/Na9lhpOo9NI7y3gLNZn2u/oa6+t85GT9c= +github.com/ice-blockchain/wintr v1.127.0 h1:YuGfLCGu91mLtsH0AcdNKnDERZPD6+3er93T/m7vF2Q= +github.com/ice-blockchain/wintr v1.127.0/go.mod h1:Wq/uG3vDL/Na9lhpOo9NI7y3gLNZn2u/oa6+t85GT9c= github.com/imroc/req/v3 v3.42.2 h1:/BwrKXGR7X1/ptccaQAiziDCeZ7T6ye55g3ZhiLy1fc= github.com/imroc/req/v3 v3.42.2/go.mod h1:W7dOrfQORA9nFoj+CafIZ6P5iyk+rWdbp2sffOAvABU= github.com/ip2location/ip2location-go/v9 v9.6.1 h1:4tYtSoRNpUwbbgf3NUmF7c5vV0QVCKEeRPSbMCXiQfE= diff --git a/miner/days_off.go b/miner/days_off.go index 686975b..51b252c 100644 --- a/miner/days_off.go +++ b/miner/days_off.go @@ -9,6 +9,7 @@ import ( "github.com/goccy/go-json" "github.com/pkg/errors" + "github.com/ice-blockchain/freezer/model" messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" "github.com/ice-blockchain/wintr/log" "github.com/ice-blockchain/wintr/time" @@ -36,7 +37,7 @@ func didANewDayOffJustStart(now *time.Time, usr *user) *DayOffStarted { UserID: usr.UserID, ID: fmt.Sprintf("%v~%v", usr.UserID, startedAt.UnixNano()/miningSessionDuration.Nanoseconds()), RemainingFreeMiningSessions: uint64(usr.MiningSessionSoloEndedAt.Sub(*now.Time) / miningSessionDuration), - MiningStreak: uint64(now.Sub(*usr.MiningSessionSoloStartedAt.Time) / miningSessionDuration), + MiningStreak: model.CalculateMiningStreak(now, usr.MiningSessionSoloStartedAt, usr.MiningSessionSoloEndedAt, miningSessionDuration), } } diff --git a/model/model.go b/model/model.go index 1d83f8a..1338806 100644 --- a/model/model.go +++ b/model/model.go @@ -29,6 +29,9 @@ type ( ResurrectTMinus1UsedAtField MiningSessionSoloDayOffLastAwardedAtField ExtraBonusLastClaimAvailableAtField + SoloLastEthereumCoinDistributionProcessedAtField + ForT0LastEthereumCoinDistributionProcessedAtField + ForTMinus1LastEthereumCoinDistributionProcessedAtField ProfilePictureNameField UsernameField MiningBlockchainAccountAddressField @@ -51,6 +54,12 @@ type ( BalanceT2Field BalanceForT0Field BalanceForTMinus1Field + BalanceSoloEthereumField + BalanceT0EthereumField + BalanceT1EthereumField + BalanceT2EthereumField + BalanceForT0EthereumField + BalanceForTMinus1EthereumField SlashingRateSoloField SlashingRateT0Field SlashingRateT1Field @@ -69,6 +78,21 @@ type ( ExtraBonusDaysClaimNotAvailableField HideRankingField } + KYCState struct { + KYCStepsCreatedAtField + KYCStepsLastUpdatedAtField + KYCStepPassedField + KYCStepBlockedField + } + SoloLastEthereumCoinDistributionProcessedAtField struct { + SoloLastEthereumCoinDistributionProcessedAt *time.Time `redis:"solo_last_ethereum_coin_distribution_processed_at,omitempty"` + } + ForT0LastEthereumCoinDistributionProcessedAtField struct { + ForT0LastEthereumCoinDistributionProcessedAt *time.Time `redis:"for_t0_last_ethereum_coin_distribution_processed_at,omitempty"` + } + ForTMinus1LastEthereumCoinDistributionProcessedAtField struct { + ForTMinus1LastEthereumCoinDistributionProcessedAt *time.Time `redis:"for_tminus1_last_ethereum_coin_distribution_processed_at,omitempty"` + } BalanceLastUpdatedAtField struct { BalanceLastUpdatedAt *time.Time `redis:"balance_last_updated_at,omitempty"` } @@ -115,14 +139,17 @@ type ( Username string `redis:"username,omitempty"` } MiningBlockchainAccountAddressField struct { - MiningBlockchainAccountAddress string `redis:"mining_blockchain_account_address,omitempty"` + MiningBlockchainAccountAddress string `redis:"mining_blockchain_account_address" json:"miningBlockchainAccountAddress"` } BlockchainAccountAddressField struct { - BlockchainAccountAddress string `redis:"blockchain_account_address,omitempty"` + BlockchainAccountAddress string `redis:"blockchain_account_address"` } LatestDeviceField struct { LatestDevice string `redis:"latest_device,omitempty"` } + CountryField struct { + Country string `redis:"country" json:"country"` + } BalanceTotalStandardField struct { BalanceTotalStandard float64 `redis:"balance_total_standard"` } @@ -156,21 +183,45 @@ type ( BalanceSoloField struct { BalanceSolo float64 `redis:"balance_solo"` } + BalanceSoloEthereumField struct { + BalanceSoloEthereum float64 `redis:"balance_solo_ethereum"` + } BalanceT0Field struct { BalanceT0 float64 `redis:"balance_t0"` } + BalanceT0EthereumField struct { + BalanceT0Ethereum float64 `redis:"balance_t0_ethereum"` + } BalanceT1Field struct { BalanceT1 float64 `redis:"balance_t1"` } + BalanceT1EthereumField struct { + BalanceT1Ethereum float64 `redis:"balance_t1_ethereum"` + } + BalanceT1EthereumPendingField struct { + BalanceT1EthereumPending float64 `redis:"balance_t1_ethereum_pending"` + } BalanceT2Field struct { BalanceT2 float64 `redis:"balance_t2"` } + BalanceT2EthereumField struct { + BalanceT2Ethereum float64 `redis:"balance_t2_ethereum"` + } + BalanceT2EthereumPendingField struct { + BalanceT2EthereumPending float64 `redis:"balance_t2_ethereum_pending"` + } BalanceForT0Field struct { BalanceForT0 float64 `redis:"balance_for_t0"` } + BalanceForT0EthereumField struct { + BalanceForT0Ethereum float64 `redis:"balance_for_t0_ethereum"` + } BalanceForTMinus1Field struct { BalanceForTMinus1 float64 `redis:"balance_for_tminus1"` } + BalanceForTMinus1EthereumField struct { + BalanceForTMinus1Ethereum float64 `redis:"balance_for_tminus1_ethereum"` + } SlashingRateSoloField struct { SlashingRateSolo float64 `redis:"slashing_rate_solo"` } @@ -199,7 +250,7 @@ type ( ExtraBonus float64 `redis:"extra_bonus,omitempty"` } DeserializedUsersKey struct { - ID int64 `redis:"-"` + ID int64 `redis:"-" json:"-"` } IDT0Field struct { IDT0 int64 `redis:"id_t0,omitempty"` @@ -287,6 +338,32 @@ func SerializedUsersKey(val any) string { } } +func CalculateMiningStreak(now, start, end *time.Time, miningSessionDuration stdlibtime.Duration) uint64 { + if start.IsNil() || end.IsNil() || now.After(*end.Time) || now.Before(*start.Time) { + return 0 + } + + return uint64(now.Sub(*start.Time) / miningSessionDuration) +} + +func (kyc *KYCState) KYCStepPassedCorrectly(kycStep users.KYCStep) bool { + return (kyc.KYCStepBlocked == users.NoneKYCStep || kyc.KYCStepBlocked > kycStep) && + kycStep == kyc.KYCStepPassed && + kyc.KYCStepAttempted(kycStep) +} + +func (kyc *KYCState) KYCStepNotAttempted(kycStep users.KYCStep) bool { + return !kyc.KYCStepAttempted(kycStep) +} + +func (kyc *KYCState) KYCStepAttempted(kycStep users.KYCStep) bool { + return kyc.KYCStepsLastUpdatedAt != nil && len(*kyc.KYCStepsLastUpdatedAt) >= int(kycStep) && !(*kyc.KYCStepsLastUpdatedAt)[kycStep-1].IsNil() +} + +func (kyc *KYCState) DelayPassedSinceLastKYCStepAttempt(kycStep users.KYCStep, duration stdlibtime.Duration) bool { + return kyc.KYCStepAttempted(kycStep) && time.Now().Sub(*(*kyc.KYCStepsLastUpdatedAt)[kycStep-1].Time) >= duration +} + type ( TimeSlice []*time.Time ) diff --git a/model/model_test.go b/model/model_test.go index b83d287..e8ae30e 100644 --- a/model/model_test.go +++ b/model/model_test.go @@ -117,20 +117,21 @@ func TestEskimoToFreezerKYCStateDeserialization(t *testing.T) { KYCStepPassed: &stepB, KYCStepBlocked: &stepA, } + usr.Country = "XX" + usr.MiningBlockchainAccountAddress = "YY" serializedUser, err := json.Marshal(usr) require.NoError(t, err) - type ( - KYCState struct { - KYCStepsCreatedAtField - KYCStepsLastUpdatedAtField - KYCStepPassedField - KYCStepBlockedField - } - ) - var deserializedUser KYCState + var deserializedUser struct { + CountryField + MiningBlockchainAccountAddressField + KYCState + DeserializedUsersKey + } err = json.Unmarshal(serializedUser, &deserializedUser) require.NoError(t, err) + assert.EqualValues(t, "XX", deserializedUser.Country) + assert.EqualValues(t, "YY", deserializedUser.MiningBlockchainAccountAddress) assert.EqualValues(t, stepA, deserializedUser.KYCStepBlocked) assert.EqualValues(t, stepB, deserializedUser.KYCStepPassed) assert.EqualValues(t, 1, len(*usr.KYCStepsCreatedAt)) diff --git a/tokenomics/contract.go b/tokenomics/contract.go index 46dccd0..1a92a8b 100644 --- a/tokenomics/contract.go +++ b/tokenomics/contract.go @@ -264,30 +264,30 @@ type ( ForceKYCForUserIds []string `json:"forceKYCForUserIds"` Enabled bool `json:"enabled"` } `json:"face-auth"` - WebFaceAuth struct { - Enabled bool `json:"enabled"` - } `json:"web-face-auth"` Social1KYC struct { DisabledVersions []string `json:"disabledVersions"` ForceKYCForUserIds []string `json:"forceKYCForUserIds"` Enabled bool `json:"enabled"` } `json:"social1-kyc"` - WebSocial1KYC struct { - Enabled bool `json:"enabled"` - } `json:"web-social1-kyc"` QuizKYC struct { DisabledVersions []string `json:"disabledVersions"` ForceKYCForUserIds []string `json:"forceKYCForUserIds"` Enabled bool `json:"enabled"` } `json:"quiz-kyc"` - WebQuizKYC struct { - Enabled bool `json:"enabled"` - } `json:"web-quiz-kyc"` Social2KYC struct { DisabledVersions []string `json:"disabledVersions"` ForceKYCForUserIds []string `json:"forceKYCForUserIds"` Enabled bool `json:"enabled"` } `json:"social2-kyc"` + WebFaceAuth struct { + Enabled bool `json:"enabled"` + } `json:"web-face-auth"` + WebSocial1KYC struct { + Enabled bool `json:"enabled"` + } `json:"web-social1-kyc"` + WebQuizKYC struct { + Enabled bool `json:"enabled"` + } `json:"web-quiz-kyc"` WebSocial2KYC struct { Enabled bool `json:"enabled"` } `json:"web-social2-kyc"` diff --git a/tokenomics/kyc.go b/tokenomics/kyc.go index 161cc61..cfcfe55 100644 --- a/tokenomics/kyc.go +++ b/tokenomics/kyc.go @@ -17,6 +17,7 @@ import ( "github.com/redis/go-redis/v9" "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/freezer/model" "github.com/ice-blockchain/wintr/connectors/storage/v3" "github.com/ice-blockchain/wintr/log" "github.com/ice-blockchain/wintr/terror" @@ -93,7 +94,7 @@ func (r *repository) validateKYC(ctx context.Context, state *getCurrentMiningSes return errors.Errorf("you can't skip kycStep:%v", skipKYCStep) } } - if err := r.overrideKYCStateWithEskimoKYCState(ctx, state.UserID, &state.KYCState, skipKYCSteps); err != nil { + if err := r.overrideKYCStateWithEskimoKYCState(ctx, state.UserID, state, skipKYCSteps); err != nil { return errors.Wrapf(err, "failed to overrideKYCStateWithEskimoKYCState for %#v", state) } if state.KYCStepBlocked == users.FacialRecognitionKYCStep && r.isKYCEnabled(ctx, state.LatestDevice, users.FacialRecognitionKYCStep) { @@ -365,24 +366,12 @@ func (r *repository) isKYCStepForced(state users.KYCStep, userID string) bool { return false } -func (kyc *KYCState) KYCStepNotAttempted(kycStep users.KYCStep) bool { - return !kyc.KYCStepAttempted(kycStep) -} - -func (kyc *KYCState) KYCStepAttempted(kycStep users.KYCStep) bool { - return kyc.KYCStepsLastUpdatedAt != nil && len(*kyc.KYCStepsLastUpdatedAt) >= int(kycStep) && !(*kyc.KYCStepsLastUpdatedAt)[kycStep-1].IsNil() -} - -func (kyc *KYCState) DelayPassedSinceLastKYCStepAttempt(kycStep users.KYCStep, duration stdlibtime.Duration) bool { - return kyc.KYCStepAttempted(kycStep) && time.Now().Sub(*(*kyc.KYCStepsLastUpdatedAt)[kycStep-1].Time) >= duration -} - /* Because existing users have empty KYCState in dragonfly cuz usersTableSource might not have updated it yet. And because we might need to reset any kyc steps for the user prior to starting to mine. So we need to call Eskimo for that, to be sure we have the valid kyc state for the user before starting to mine. */ -func (r *repository) overrideKYCStateWithEskimoKYCState(ctx context.Context, userID string, state *KYCState, skipKYCSteps []users.KYCStep) error { +func (r *repository) overrideKYCStateWithEskimoKYCState(ctx context.Context, userID string, state *getCurrentMiningSession, skipKYCSteps []users.KYCStep) error { request := req. SetContext(ctx). SetRetryCount(25). @@ -420,7 +409,20 @@ func (r *repository) overrideKYCStateWithEskimoKYCState(ctx context.Context, use } else if data, err2 := resp.ToBytes(); err2 != nil { return errors.Wrapf(err2, "failed to read body of eskimo user state request for userID:%v, skipKYCSteps:%#v", userID, skipKYCSteps) } else { - return errors.Wrapf(json.Unmarshal(data, state), "failed to unmarshal into %#v, data: %v, skipKYCSteps:%#v", state, string(data), skipKYCSteps) + var usr struct { + model.CountryField + model.MiningBlockchainAccountAddressField + model.KYCState + model.DeserializedUsersKey + } + if err3 := json.Unmarshal(data, &usr); err3 != nil { + return errors.Wrapf(err3, "failed to unmarshal into %#v, data: `%v`, skipKYCSteps:%#v", &usr, string(data), skipKYCSteps) + } else { + usr.DeserializedUsersKey = state.DeserializedUsersKey + state.KYCState = usr.KYCState + + return errors.Wrapf(storage.Set(ctx, r.db, &usr), "failed to db set partial state:%#v, userID:%v, skipKYCSteps:%#v", &usr, userID, skipKYCSteps) //nolint:lll // . + } } } diff --git a/tokenomics/mining.go b/tokenomics/mining.go index 94664c9..29c98d1 100644 --- a/tokenomics/mining.go +++ b/tokenomics/mining.go @@ -470,11 +470,7 @@ func (r *repository) calculateMintedPreStakingCoins( } func (r *repository) calculateMiningStreak(now, start, end *time.Time) uint64 { - if start.IsNil() || end.IsNil() || now.After(*end.Time) || now.Before(*start.Time) { - return 0 - } - - return uint64(now.Sub(*start.Time) / r.cfg.MiningSessionDuration.Max) + return model.CalculateMiningStreak(now, start, end, r.cfg.MiningSessionDuration.Max) } func (r *repository) calculateRemainingFreeMiningSessions(now, end *time.Time) uint64 { diff --git a/tokenomics/mining_sessions.go b/tokenomics/mining_sessions.go index bc37984..8baa8ce 100644 --- a/tokenomics/mining_sessions.go +++ b/tokenomics/mining_sessions.go @@ -31,15 +31,10 @@ type ( model.ReferralsCountChangeGuardUpdatedAtField model.DeserializedUsersKey } - KYCState struct { - model.KYCStepsCreatedAtField - model.KYCStepsLastUpdatedAtField - model.KYCStepPassedField - model.KYCStepBlockedField - } getCurrentMiningSession struct { - KYCState + model.ReferralsCountChangeGuardUpdatedAtField StartOrExtendMiningSession + model.KYCState model.LatestDeviceField model.UserIDField model.SlashingRateSoloField @@ -51,7 +46,6 @@ type ( model.IDTMinus1Field model.PreStakingAllocationField model.PreStakingBonusField - model.ReferralsCountChangeGuardUpdatedAtField } ) diff --git a/tokenomics/users.go b/tokenomics/users.go index f087c33..f96bf92 100644 --- a/tokenomics/users.go +++ b/tokenomics/users.go @@ -165,10 +165,11 @@ func (s *usersTableSource) replaceUser(ctx context.Context, usr *users.User) err } type ( user struct { - KYCState + model.KYCState model.UserIDField model.ProfilePictureNameField model.UsernameField + model.CountryField model.MiningBlockchainAccountAddressField model.BlockchainAccountAddressField model.BalanceForTMinus1Field @@ -190,6 +191,7 @@ func (s *usersTableSource) replaceUser(ctx context.Context, usr *users.User) err newPartialState.ID = internalID newPartialState.ProfilePictureName = s.pictureClient.StripDownloadURL(usr.ProfilePictureURL) newPartialState.Username = usr.Username + newPartialState.Country = usr.Country newPartialState.MiningBlockchainAccountAddress = usr.MiningBlockchainAccountAddress newPartialState.BlockchainAccountAddress = usr.BlockchainAccountAddress if usr.KYCStepPassed != nil { @@ -209,6 +211,7 @@ func (s *usersTableSource) replaceUser(ctx context.Context, usr *users.User) err newPartialState.HideRanking = s.hideRanking(usr) if newPartialState.ProfilePictureName != dbUser[0].ProfilePictureName || newPartialState.Username != dbUser[0].Username || + !strings.EqualFold(newPartialState.Country, dbUser[0].Country) || newPartialState.MiningBlockchainAccountAddress != dbUser[0].MiningBlockchainAccountAddress || newPartialState.BlockchainAccountAddress != dbUser[0].BlockchainAccountAddress || newPartialState.HideRanking != dbUser[0].HideRanking ||