/
rules.go
270 lines (224 loc) · 8.54 KB
/
rules.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
"github.com/provenance-io/provenance/x/reward/types"
)
// CacheContextWithHistory returns a new Context with the multi-store cached and a new
// EventManager with existing history. The cached context is written to the context when writeCache
// is called. Note, events are automatically emitted on the parent context's
// EventManager when the caller executes the write.
//
// This is similar to the ctx.CacheContext() function, but this keeps the history so we can act on it.
func CacheContextWithHistory(ctx sdk.Context) (cc sdk.Context, writeCache func()) {
cms := ctx.MultiStore().CacheMultiStore()
cc = ctx.WithMultiStore(cms).WithEventManager(sdk.NewEventManagerWithHistory(ctx.EventManager().GetABCIEventHistory()))
writeCache = func() {
ctx.EventManager().EmitEvents(cc.EventManager().Events())
cms.Write()
}
return cc, writeCache
}
// ProcessTransactions in the endblock
func (k Keeper) ProcessTransactions(origCtx sdk.Context) {
// Get all Active Reward Programs
rewardPrograms, err := k.GetAllActiveRewardPrograms(origCtx)
if err != nil {
origCtx.Logger().Error(err.Error())
return
}
// Grant shares for qualifying actions
for index := range rewardPrograms {
// Because this is designed for the EndBlocker, we don't always have auto-rollback.
// We don't partial state recorded if an error is encountered in the middle.
// So use a cache context and only write it if there wasn't an error.
ctx, writeCtx := CacheContextWithHistory(origCtx)
// Go through all the reward programs
actions, err := k.DetectQualifyingActions(ctx, &rewardPrograms[index])
if err != nil {
ctx.Logger().Error(err.Error())
continue
}
// Record any results
err = k.RewardShares(ctx, &rewardPrograms[index], actions)
if err != nil {
ctx.Logger().Error(err.Error())
continue
}
writeCtx()
}
}
// DetectQualifyingActions takes in the RewardProgram and checks if any of the qualifying actions is found in the event history
func (k Keeper) DetectQualifyingActions(ctx sdk.Context, program *types.RewardProgram) ([]types.EvaluationResult, error) {
ctx.Logger().Info(fmt.Sprintf("EvaluateRules for RewardProgram: %d", program.GetId()))
results := []types.EvaluationResult(nil)
// Check if any of the transactions are qualifying actions
for _, supportedAction := range program.GetQualifyingActions() {
// Get the appropriate RewardAction
// If it's not supported we skip it
action, err := supportedAction.GetRewardAction(ctx)
if err != nil {
ctx.Logger().Info(err.Error())
continue
}
// Build all the qualifying actions from the abci events
actions, err := k.FindQualifyingActions(ctx, action)
if err != nil {
return nil, err
}
// Process actions and get the results
actions = k.ProcessQualifyingActions(ctx, program, action, actions)
results = append(results, actions...)
}
return results, nil
}
// ProcessQualifyingActions process the detected qualifying actions.
func (k Keeper) ProcessQualifyingActions(ctx sdk.Context, program *types.RewardProgram, processor types.RewardAction, actions []types.EvaluationResult) []types.EvaluationResult {
successfulActions := []types.EvaluationResult(nil)
if program == nil || processor == nil || actions == nil {
return successfulActions
}
for _, action := range actions {
state, err := k.GetRewardAccountState(ctx, program.GetId(), program.GetCurrentClaimPeriod(), action.Address.String())
if err != nil {
continue
}
if state.Validate() != nil {
state = types.NewRewardAccountState(program.GetId(), program.GetCurrentClaimPeriod(), action.Address.String(), 0, []*types.ActionCounter{})
}
if !processor.PreEvaluate(ctx, k, state) {
k.SetRewardAccountState(ctx, state)
continue
}
if !processor.Evaluate(ctx, k, state, action) {
k.SetRewardAccountState(ctx, state)
continue
}
state.ActionCounter = types.IncrementActionCount(state.ActionCounter, processor.ActionType())
postEvalRes, evaluationResultFromPostEval := processor.PostEvaluate(ctx, k, state, action)
if !postEvalRes {
k.SetRewardAccountState(ctx, state)
continue
}
successfulActions = append(successfulActions, evaluationResultFromPostEval)
k.SetRewardAccountState(ctx, state)
}
return successfulActions
}
// RewardShares Sets shares for an account(i.e address) based on EvaluationResult
func (k Keeper) RewardShares(ctx sdk.Context, rewardProgram *types.RewardProgram, evaluateRes []types.EvaluationResult) error {
ctx.Logger().Info(fmt.Sprintf("Recording shares for for rewardProgramId=%d, claimPeriod=%d",
rewardProgram.GetId(), rewardProgram.GetCurrentClaimPeriod()))
if rewardProgram == nil {
return sdkerrors.ErrNotFound.Wrap("reward program cannot be nil")
}
// get the ClaimPeriodRewardDistribution
claimPeriodRewardDistribution, err := k.GetClaimPeriodRewardDistribution(ctx, rewardProgram.GetCurrentClaimPeriod(), rewardProgram.GetId())
if err != nil {
return err
}
if claimPeriodRewardDistribution.Validate() != nil {
return sdkerrors.ErrNotFound.Wrap("invalid claim period reward distribution")
}
for _, res := range evaluateRes {
state, err := k.GetRewardAccountState(ctx, rewardProgram.GetId(), rewardProgram.GetCurrentClaimPeriod(), res.Address.String())
if state.Validate() != nil {
ctx.Logger().Error(fmt.Sprintf("Account state does not exist for RewardProgram: %d, ClaimPeriod: %d, Address: %s. Skipping...",
rewardProgram.GetId(), rewardProgram.GetCurrentClaimPeriod(), res.Address.String()))
continue
}
if err != nil {
return err
}
state.SharesEarned += uint64(res.Shares)
k.SetRewardAccountState(ctx, state)
// we know the rewards, so update the claim period reward
claimPeriodRewardDistribution.TotalShares += res.Shares
}
// set total claim period rewards distribution.
k.SetClaimPeriodRewardDistribution(ctx, claimPeriodRewardDistribution)
return nil
}
// IterateABCIEvents Iterates through all the ABCIEvents that match the eventCriteria.
// Nil criteria means to iterate over everything.
func (k Keeper) IterateABCIEvents(ctx sdk.Context, criteria *types.EventCriteria, action func(string, *map[string][]byte) error) error {
for _, event := range ctx.EventManager().GetABCIEventHistory() {
event := event
// Event type must match the criteria
// nil criteria is considered to match everything
if criteria != nil && !criteria.MatchesEvent(event.Type) {
continue
}
// Convert the attributes into a map
attributes := make(map[string][]byte)
for _, attribute := range event.Attributes {
attributes[string(attribute.Key)] = attribute.Value
}
valid := true
if criteria != nil {
// Ensure each attribute matches the required criteria
// If a single attribute does not match then we don't continue with the event
eventCriteria := criteria.Events[event.Type]
for key := range eventCriteria.Attributes {
valid = eventCriteria.MatchesAttribute(key, attributes[key])
if !valid {
break
}
}
}
if !valid {
continue
}
err := action(event.Type, &attributes)
if err != nil {
return err
}
}
return nil
}
// FindQualifyingActions iterates event history and applies the RewardAction to them, adds them to the result if they qualify.
func (k Keeper) FindQualifyingActions(ctx sdk.Context, action types.RewardAction) ([]types.EvaluationResult, error) {
result := ([]types.EvaluationResult)(nil)
builder := action.GetBuilder()
err := k.IterateABCIEvents(ctx, builder.GetEventCriteria(), func(eventType string, attributes *map[string][]byte) error {
// Add the event to the builder
err := builder.AddEvent(eventType, attributes)
if err != nil {
return err
}
// Not finished building skip attempting to build
if !builder.CanBuild() {
return nil
}
// Attempt to build
action, err := builder.BuildAction()
if err != nil {
return err
}
result = append(result, action)
builder.Reset()
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
// GetAccountKeeper gets this Keeper's AccountKeeper.
func (k Keeper) GetAccountKeeper() types.AccountKeeper {
return k.authkeeper
}
// GetStakingKeeper gets this Keeper's StakingKeeper.
func (k Keeper) GetStakingKeeper() types.StakingKeeper {
return k.stakingKeeper
}
// SetStakingKeeper only used in tests
func (k *Keeper) SetStakingKeeper(newKeeper types.StakingKeeper) {
k.stakingKeeper = newKeeper
}
// SetAccountKeeper only used in tests
func (k *Keeper) SetAccountKeeper(newKeeper authkeeper.AccountKeeper) {
k.authkeeper = newKeeper
}