Skip to content

Commit

Permalink
coin distribution preparation
Browse files Browse the repository at this point in the history
  • Loading branch information
ice-ares committed Dec 20, 2023
1 parent 9a8b37b commit 4037263
Show file tree
Hide file tree
Showing 12 changed files with 739 additions and 62 deletions.
9 changes: 8 additions & 1 deletion coin-distribution/DDL.sql
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ CREATE TABLE IF NOT EXISTS global (
WITH (FILLFACTOR = 70);
INSERT INTO global (key,value)
VALUES ('coin_distributer_enabled','true'),
('coin_collector_enabled','true')
('coin_collector_enabled','true'),
('coin_collector_forced_execution','false'),
('coin_collector_start_date','2023-12-20T10:54:20.657949659Z'),
('coin_collector_end_date','2024-10-24T10:54:20.657949659Z'),
('coin_collector_min_mining_streaks_required','0'),
('coin_collector_start_hour','0'),
('coin_collector_min_balance_required','0'),
('coin_collector_denied_countries','')
ON CONFLICT(key) DO NOTHING;

CREATE TABLE IF NOT EXISTS coin_distributions_by_earner (
Expand Down
13 changes: 12 additions & 1 deletion coin-distribution/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,20 @@ type (
CheckHealth(ctx context.Context) error
ReviewCoinDistributions(ctx context.Context, reviewerUserID string, decision string) error
NotifyCoinDistributionCollectionCycleEnded(ctx context.Context) error
GetCollectorStatus(ctx context.Context) (latestCollectingDate *time.Time, collectorEnabled bool, err error)
GetCollectorSettings(ctx context.Context) (*CollectorSettings, error)
CollectCoinDistributionsForReview(ctx context.Context, records []*ByEarnerForReview) error
}
CollectorSettings struct {
DeniedCountries map[string]struct{}
LatestDate *time.Time
StartDate *time.Time
EndDate *time.Time
MinBalanceRequired float64
StartHour int
MinMiningStreaksRequired uint64
Enabled bool
ForcedExecution bool
}

CoinDistributionsForReview struct {
Distributions []*PendingReview `json:"distributions"`
Expand Down
44 changes: 41 additions & 3 deletions coin-distribution/eligibility.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package coindistribution

import (
"strings"
stdlibtime "time"

"github.com/ice-blockchain/eskimo/users"
Expand All @@ -24,8 +25,7 @@ func CalculateEthereumDistributionICEBalance(
return standardBalance
}

//TODO: should this be fractional or natural?
return standardBalance / (float64(delta.Nanoseconds()) / float64(ethereumDistributionFrequencyMax.Nanoseconds()))
return standardBalance / float64(int64(delta/ethereumDistributionFrequencyMax)+1)
}

func IsEligibleForEthereumDistribution(
Expand All @@ -37,7 +37,7 @@ func IsEligibleForEthereumDistribution(
kycState model.KYCState,
miningSessionDuration, ethereumDistributionFrequencyMin, ethereumDistributionFrequencyMax stdlibtime.Duration) bool {
var countryAllowed bool
if _, countryDenied := distributionDeniedCountries[country]; country != "" && !countryDenied {
if _, countryDenied := distributionDeniedCountries[strings.ToLower(country)]; country != "" && !countryDenied {
countryAllowed = true
}

Expand All @@ -49,6 +49,44 @@ func IsEligibleForEthereumDistribution(
kycState.KYCStepPassedCorrectly(users.QuizKYCStep)
}

//nolint:funlen // .
func IsEligibleForCoinDistributionNow(id int64,
now, lastEthereumCoinDistributionProcessedAt, coinDistributionStartDate *time.Time,
ethereumDistributionFrequencyMin, ethereumDistributionFrequencyMax stdlibtime.Duration) bool {
var (
reservationIsTodayAndIsOutsideOfFirstCycle, secondReservationIsWithinFirstDistributionCycle bool
truncatedLastEthereumCoinDistributionProcessedAt *time.Time
ethereumDistributionStartDate = coinDistributionStartDate.Truncate(ethereumDistributionFrequencyMin) //nolint:lll // .
ethereumDistributionDayAfterStartDate = ethereumDistributionStartDate.Add(ethereumDistributionFrequencyMin) //nolint:lll // .
ethereumDistributionFirstCycleEndDate = ethereumDistributionDayAfterStartDate.Add(ethereumDistributionFrequencyMax) //nolint:lll // .
userReservedDayForEthereumCoinDistributionIndex = id % int64(ethereumDistributionFrequencyMax/ethereumDistributionFrequencyMin) //nolint:lll // .
today = now.Truncate(ethereumDistributionFrequencyMin)
neverDoneItBeforeAndTodayIsNotEthereumDistributionStartDateButReservationIsToday = userReservedDayForEthereumCoinDistributionIndex == int64((today.Sub(ethereumDistributionDayAfterStartDate)%ethereumDistributionFrequencyMax)/ethereumDistributionFrequencyMin) //nolint:lll // .
)
if !lastEthereumCoinDistributionProcessedAt.IsNil() {
truncatedLastEthereumCoinDistributionProcessedAt = time.New(lastEthereumCoinDistributionProcessedAt.Truncate(ethereumDistributionFrequencyMin))
reservationIsTodayAndIsOutsideOfFirstCycle = today.Sub(*truncatedLastEthereumCoinDistributionProcessedAt.Time)%ethereumDistributionFrequencyMax == 0 //nolint:lll // .
secondReservationIsWithinFirstDistributionCycle = userReservedDayForEthereumCoinDistributionIndex == int64((truncatedLastEthereumCoinDistributionProcessedAt.Add(ethereumDistributionFrequencyMin).Sub(ethereumDistributionDayAfterStartDate)%ethereumDistributionFrequencyMax)/ethereumDistributionFrequencyMin) //nolint:lll // .
}
switch {
case lastEthereumCoinDistributionProcessedAt.IsNil() && today.Equal(ethereumDistributionStartDate):
return true
case !lastEthereumCoinDistributionProcessedAt.IsNil() && today.Equal(ethereumDistributionStartDate):
return false
case lastEthereumCoinDistributionProcessedAt.IsNil() && !today.Equal(ethereumDistributionStartDate):
return neverDoneItBeforeAndTodayIsNotEthereumDistributionStartDateButReservationIsToday
case !lastEthereumCoinDistributionProcessedAt.IsNil() && !today.Equal(ethereumDistributionStartDate):
switch {
case today.Before(ethereumDistributionFirstCycleEndDate):
return secondReservationIsWithinFirstDistributionCycle
default:
return reservationIsTodayAndIsOutsideOfFirstCycle
}
default:
return false
}
}

func isEthereumAddressValid(ethAddress string) bool {
if ethAddress == "" {
return false
Expand Down
81 changes: 64 additions & 17 deletions coin-distribution/pending_review.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (r *repository) GetCoinDistributionsForReview(ctx context.Context, arg *Get
AND %[1]v
ORDER BY %[2]v
LIMIT $2 OFFSET $1`, strings.Join(append(conditions, "1=1"), " AND "), strings.Join(append(arg.orderBy(), "internal_id asc"), ", "))
result, err := storage.Select[struct {
result, err := storage.ExecMany[struct {
*PendingReview
Day stdlibtime.Time
InternalID uint64
Expand All @@ -71,7 +71,7 @@ func (r *repository) GetCoinDistributionsForReview(ctx context.Context, arg *Get
FROM coin_distributions_pending_review
WHERE 1=1
AND %[1]v`, strings.Join(append(conditions, "1=1"), " AND "))
total, err := storage.Get[struct {
total, err := storage.ExecOne[struct {
Rows uint64
Ice uint64
}](ctx, r.db, sql, whereArgs...)
Expand Down Expand Up @@ -172,7 +172,7 @@ func (r *repository) ReviewCoinDistributions(ctx context.Context, reviewerUserID
switch strings.ToLower(decision) {
case "approve":
return storage.DoInTransaction(ctx, r.db, func(conn storage.QueryExecer) error {
if _, err := storage.Get[struct{ Bogus bool }](ctx, conn, sqlToCheckIfAnythingNeedsApproving); err != nil {
if _, err := storage.ExecOne[struct{ Bogus bool }](ctx, conn, sqlToCheckIfAnythingNeedsApproving); err != nil {
if storage.IsErr(err, storage.ErrNotFound) {
err = nil
}
Expand All @@ -188,7 +188,7 @@ func (r *repository) ReviewCoinDistributions(ctx context.Context, reviewerUserID
})
case "deny":
return storage.DoInTransaction(ctx, r.db, func(conn storage.QueryExecer) error {
if _, err := storage.Get[struct{ Bogus bool }](ctx, conn, sqlToCheckIfAnythingNeedsApproving); err != nil {
if _, err := storage.ExecOne[struct{ Bogus bool }](ctx, conn, sqlToCheckIfAnythingNeedsApproving); err != nil {
if storage.IsErr(err, storage.ErrNotFound) {
err = nil
}
Expand All @@ -210,6 +210,9 @@ func (r *repository) ReviewCoinDistributions(ctx context.Context, reviewerUserID
}

func (r *repository) CollectCoinDistributionsForReview(ctx context.Context, records []*ByEarnerForReview) error {
if len(records) == 0 {
return nil
}
const columns = 9
values := make([]string, 0, len(records))
args := make([]any, 0, len(records)*columns)
Expand All @@ -229,7 +232,12 @@ func (r *repository) CollectCoinDistributionsForReview(ctx context.Context, reco
sql := fmt.Sprintf(`INSERT INTO coin_distributions_by_earner(created_at,day,internal_id,balance,username,referred_by_username,user_id,earner_user_id,eth_address)
VALUES %v
ON CONFLICT (day, user_id, earner_user_id) DO UPDATE
SET balance = EXCLUDED.balance`, strings.Join(values, ",\n"))
SET
created_at = EXCLUDED.created_at,
balance = EXCLUDED.balance,
username = EXCLUDED.username,
referred_by_username = EXCLUDED.referred_by_username,
eth_address = EXCLUDED.eth_address`, strings.Join(values, ",\n"))
_, err := storage.Exec(ctx, r.db, sql, args...)

return errors.Wrapf(err, "failed to insert into coin_distributions_by_earner %#v", records)
Expand All @@ -246,32 +254,71 @@ func generateValuesSQLParams(index, columns int) string {

func (r *repository) NotifyCoinDistributionCollectionCycleEnded(ctx context.Context) error {
sql := `INSERT INTO global(key,value)
VALUES ('latest_collecting_date',$1),
('new_coin_distributions_pending','true')
VALUES ('coin_collector_latest_collecting_date',$1),
('new_coin_distributions_pending','true'),
('coin_collector_forced_execution','false')
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value`
_, err := storage.Exec(ctx, r.db, sql, time.Now().Format(stdlibtime.DateOnly))

return errors.Wrap(err, "failed to update global.value for latest_collecting_date to now and mark new_coin_distributions_pending")
return errors.Wrap(err, "failed to update global.value for coin_collector_latest_collecting_date to now and mark new_coin_distributions_pending")
}

func (r *repository) GetCollectorStatus(ctx context.Context) (latestCollectingDate *time.Time, collectorEnabled bool, err error) {
sql := `SELECT (SELECT value::timestamp FROM global WHERE key = $1) AS latest_collecting_date,
coalesce((SELECT value::bool FROM global WHERE key = $2),false) AS coin_collector_enabled`
func (r *repository) GetCollectorSettings(ctx context.Context) (*CollectorSettings, error) {
sql := `SELECT (SELECT value::timestamp FROM global WHERE key = $1) AS coin_collector_latest_collecting_date,
(SELECT value::timestamp FROM global WHERE key = $2) AS coin_collector_start_date,
(SELECT value::timestamp FROM global WHERE key = $3) AS coin_collector_end_date,
coalesce((SELECT value::bool FROM global WHERE key = $4),false) AS coin_collector_enabled,
coalesce((SELECT value::bool FROM global WHERE key = $5),false) AS coin_collector_forced_execution,
coalesce((SELECT value::int FROM global WHERE key = $6),0) AS coin_collector_min_mining_streaks_required,
coalesce((SELECT value::int FROM global WHERE key = $7),0) AS coin_collector_start_hour,
coalesce((SELECT value::int FROM global WHERE key = $8),0) AS coin_collector_min_balance_required,
coalesce((SELECT value FROM global WHERE key = $9),'') AS coin_collector_denied_countries`
val, err := storage.ExecOne[struct {
LatestCollectingDate *time.Time
CoinCollectorEnabled bool
}](ctx, r.db, sql, "latest_collecting_date", "coin_collector_enabled")
CoinCollectorLatestCollectingDate *time.Time
CoinCollectorStartDate *time.Time
CoinCollectorEndDate *time.Time
CoinCollectorDeniedCountries string
CoinCollectorMinMiningStreaksRequired int
CoinCollectorStartHour int
CoinCollectorMinBalanceRequired int
CoinCollectorEnabled bool
CoinCollectorForcedExecution bool
}](ctx, r.db, sql,
"coin_collector_latest_collecting_date",
"coin_collector_start_date",
"coin_collector_end_date",
"coin_collector_enabled",
"coin_collector_forced_execution",
"coin_collector_min_mining_streaks_required",
"coin_collector_start_hour",
"coin_collector_min_balance_required",
"coin_collector_denied_countries")
if err != nil {
return nil, false, errors.Wrap(err, "failed to select info about latest_collecting_date, coin_collector_enabled")
return nil, errors.Wrap(err, "failed to select info about GetCollectorSettings")
}
countries := strings.Split(strings.ToLower(val.CoinCollectorDeniedCountries), ",")
mappedCountries := make(map[string]struct{}, len(countries))
for ix := range countries {
mappedCountries[countries[ix]] = struct{}{}
}

return val.LatestCollectingDate, val.CoinCollectorEnabled, nil
return &CollectorSettings{
DeniedCountries: mappedCountries,
LatestDate: val.CoinCollectorLatestCollectingDate,
StartDate: val.CoinCollectorStartDate,
EndDate: val.CoinCollectorEndDate,
MinBalanceRequired: float64(val.CoinCollectorMinBalanceRequired),
StartHour: val.CoinCollectorStartHour,
MinMiningStreaksRequired: uint64(val.CoinCollectorMinMiningStreaksRequired),
Enabled: val.CoinCollectorEnabled,
ForcedExecution: val.CoinCollectorForcedExecution,
}, nil
}

func tryPrepareCoinDistributionsForReview(ctx context.Context, db *storage.DB) error {
return storage.DoInTransaction(ctx, db, func(conn storage.QueryExecer) error {
if _, err := storage.Get[struct{ Bogus bool }](ctx, conn, "SELECT true AS bogus FROM global WHERE key = 'new_coin_distributions_pending' FOR UPDATE SKIP LOCKED"); err != nil {
if _, err := storage.ExecOne[struct{ Bogus bool }](ctx, conn, "SELECT true AS bogus FROM global WHERE key = 'new_coin_distributions_pending' FOR UPDATE SKIP LOCKED"); err != nil {
if storage.IsErr(err, storage.ErrNotFound) {
err = nil
}
Expand Down
6 changes: 5 additions & 1 deletion miner/.testdata/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ tokenomics:
miningSessionDuration:
max: 24h
extraBonuses:
duration: 24h
duration: 24h
miner:
ethereumDistributionFrequency:
min: 24h
max: 672h
66 changes: 52 additions & 14 deletions miner/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
stdlibtime "time"

dwh "github.com/ice-blockchain/freezer/bookkeeper/storage"
coindistribution "github.com/ice-blockchain/freezer/coin-distribution"
"github.com/ice-blockchain/freezer/model"
"github.com/ice-blockchain/freezer/tokenomics"
messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker"
Expand Down Expand Up @@ -55,6 +56,11 @@ type (
model.MiningSessionSoloEndedAtField
model.MiningSessionSoloPreviouslyEndedAtField
model.ExtraBonusStartedAtField
model.ReferralsCountChangeGuardUpdatedAtField
model.KYCState
model.MiningBlockchainAccountAddressField
model.CountryField
model.UsernameField
model.LatestDeviceField
model.UserIDField
UpdatedUser
Expand All @@ -67,7 +73,6 @@ type (
model.PreStakingAllocationField
model.ExtraBonusField
model.UTCOffsetField
model.ReferralsCountChangeGuardUpdatedAtField
}

UpdatedUser struct { // This is public only because we have to embed it, and it has to be if so.
Expand All @@ -76,6 +81,11 @@ type (
model.ResurrectSoloUsedAtField
model.ResurrectT0UsedAtField
model.ResurrectTMinus1UsedAtField
model.SoloLastEthereumCoinDistributionProcessedAtField
model.ForT0LastEthereumCoinDistributionProcessedAtField
model.ForTMinus1LastEthereumCoinDistributionProcessedAtField
model.BalanceT1EthereumPendingField
model.BalanceT2EthereumPendingField
model.DeserializedUsersKey
model.IDT0Field
model.IDTMinus1Field
Expand All @@ -92,6 +102,12 @@ type (
model.BalanceT2Field
model.BalanceForT0Field
model.BalanceForTMinus1Field
model.BalanceSoloEthereumField
model.BalanceT0EthereumField
model.BalanceT1EthereumField
model.BalanceT2EthereumField
model.BalanceForT0EthereumField
model.BalanceForTMinus1EthereumField
model.SlashingRateSoloField
model.SlashingRateT0Field
model.SlashingRateT1Field
Expand All @@ -112,9 +128,21 @@ type (
model.MiningSessionSoloEndedAtField
model.MiningSessionSoloPreviouslyEndedAtField
model.ResurrectSoloUsedAtField
model.SoloLastEthereumCoinDistributionProcessedAtField
model.UserIDField
model.CountryField
model.UsernameField
model.MiningBlockchainAccountAddressField
model.KYCState
model.IDT0Field
model.DeserializedUsersKey
model.BalanceTotalStandardField
model.BalanceSoloEthereumField
model.BalanceT0EthereumField
model.BalanceT1EthereumField
model.BalanceT2EthereumField
model.PreStakingAllocationField
model.PreStakingBonusField
}

referralCountGuardUpdatedUser struct {
Expand All @@ -128,20 +156,30 @@ type (
}

miner struct {
mb messagebroker.Client
db storage.DB
dwhClient dwh.Client
cancel context.CancelFunc
telemetry *telemetry
wg *sync.WaitGroup
extraBonusStartDate *time.Time
extraBonusIndicesDistribution map[uint16]map[uint16]uint16
coinDistributionStartedSignaler chan struct{}
coinDistributionEndedSignaler chan struct{}
stopCoinDistributionCollectionWorkerManager chan struct{}
coinDistributionWorkerMX *sync.Mutex
coinDistributionRepository coindistribution.Repository
mb messagebroker.Client
db storage.DB
dwhClient dwh.Client
cancel context.CancelFunc
telemetry *telemetry
wg *sync.WaitGroup
extraBonusStartDate *time.Time
extraBonusIndicesDistribution map[uint16]map[uint16]uint16
}
config struct {
disableAdvancedTeam *atomic.Pointer[[]string]
tokenomics.Config `mapstructure:",squash"` //nolint:tagliatelle // Nope.
Workers int64 `yaml:"workers"`
BatchSize int64 `yaml:"batchSize"`
Development bool `yaml:"development"`
disableAdvancedTeam *atomic.Pointer[[]string]
coinDistributionCollectorSettings *atomic.Pointer[coindistribution.CollectorSettings]
tokenomics.Config `mapstructure:",squash"` //nolint:tagliatelle // Nope.
EthereumDistributionFrequency struct {
Min stdlibtime.Duration `yaml:"min"`
Max stdlibtime.Duration `yaml:"max"`
} `yaml:"ethereumDistributionFrequency" mapstructure:"ethereumDistributionFrequency"`
Workers int64 `yaml:"workers"`
BatchSize int64 `yaml:"batchSize"`
Development bool `yaml:"development"`
}
)
Loading

0 comments on commit 4037263

Please sign in to comment.