distribution.gno
8.59 Kb · 301 lines
1package game_vault
2
3import (
4 "chain"
5 "chain/banker"
6 "strconv"
7
8 "gno.land/p/akkadia/beta/accesscontrol"
9 "gno.land/r/akkadia/beta/admin"
10)
11
12const (
13 SetDistributionBatchMaxEvent = "SetDistributionBatchMax"
14 distributionBatchMaxDefault = 50
15 distributionBatchMaxUpperBound = 100
16)
17
18var distributionBatchMax int
19
20type RewardDistribution struct {
21 ID uint64
22 PoolID uint64
23 GameWorldID uint64
24 OperationKey string
25 RecipientCount uint32
26 TotalAmount int64
27 Executor address
28}
29
30type RewardDistributionView struct {
31 ID uint64
32 PoolKey string
33 GameWorldID uint64
34 OperationKey string
35 RecipientCount uint32
36 TotalAmount int64
37 Executor address
38}
39
40type VaultRefund struct {
41 ID uint64
42 PoolID uint64
43 OperationKey string
44 SourceKey string
45 Recipient address
46 Amount int64
47 Kind string
48 Executor address
49}
50
51func SetDistributionBatchMax(cur realm, value int) {
52 executor := accesscontrol.MustGetAdminCaller(0, cur, admin.IsAdmin)
53 assertNoAttachedCoins()
54 if value < 1 {
55 panic("limit must be at least 1")
56 }
57 if value > distributionBatchMaxUpperBound {
58 panic("distribution batch max exceeds limit")
59 }
60 if value == distributionBatchMax {
61 return
62 }
63 oldValue := distributionBatchMax
64 distributionBatchMax = value
65 chain.Emit(SetDistributionBatchMaxEvent, "executor", executor.String(), "oldValue", strconv.Itoa(oldValue), "newValue", strconv.Itoa(value))
66}
67
68func GetDistributionBatchMax() int { return distributionBatchMax }
69
70func DistributeReward(cur realm, poolKey string, operationKey, recipientCSV, ugnotAmountCSV string) uint64 {
71 executor := accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator)
72 assertNoAttachedCoins()
73 assertNewOperationKey(operationKey)
74 poolID, pool := mustGetRewardPoolByKey(poolKey)
75 if pool.Status != RewardPoolStatusLocked {
76 panic("reward pool must be locked before distribution")
77 }
78 if latestRewardDistributionID == maxUint64 {
79 panic("reward distribution id exhausted")
80 }
81 if recipientCSV == "" || ugnotAmountCSV == "" {
82 panic("payout list must not be empty")
83 }
84
85 count, total := validatePayouts(recipientCSV, ugnotAmountCSV, cur.Address(), pool.Balance)
86 if pool.DistributedTotal > maxInt64-total {
87 panic("reward pool distributed amount overflow")
88 }
89 if pool.DistributionCount == maxUint64 {
90 panic("reward pool distribution count overflow")
91 }
92
93 latestRewardDistributionID++
94 distributionID := latestRewardDistributionID
95 pool.Balance -= total
96 pool.DistributedTotal += total
97 pool.DistributionCount++
98 treeSet(rewardPools, poolID, pool)
99 treeSet(rewardDistributions, distributionID, RewardDistribution{
100 ID: distributionID,
101 PoolID: poolID,
102 GameWorldID: pool.GameWorldID,
103 OperationKey: operationKey,
104 RecipientCount: uint32(count),
105 TotalAmount: total,
106 Executor: executor,
107 })
108 rewardDistributionIDs.Set(operationKey, distributionID)
109 processedOperationKeys.Set(operationKey, distributionID)
110 getOrCreateIDSet(poolDistributionIDs, poolID).Set(uint64Key(distributionID), true)
111 accountedUgnotTotal -= total
112 sendPayouts(cross(cur), recipientCSV, ugnotAmountCSV)
113 return distributionID
114}
115
116func recordAndSendRefund(cur realm, poolID uint64, operationKey, sourceKey string, recipient address, amount int64, kind string, executor address) uint64 {
117 if latestVaultRefundID == maxUint64 {
118 panic("vault refund id exhausted")
119 }
120 latestVaultRefundID++
121 refundID := latestVaultRefundID
122 treeSet(vaultRefunds, refundID, VaultRefund{
123 ID: refundID,
124 PoolID: poolID,
125 OperationKey: operationKey,
126 SourceKey: sourceKey,
127 Recipient: recipient,
128 Amount: amount,
129 Kind: kind,
130 Executor: executor,
131 })
132 vaultRefundIDs.Set(operationKey, refundID)
133 processedOperationKeys.Set(operationKey, refundID)
134 if poolID != 0 {
135 getOrCreateIDSet(poolRefundIDs, poolID).Set(uint64Key(refundID), true)
136 }
137 sendUgnot(cross(cur), recipient, amount)
138 return refundID
139}
140
141func validatePayouts(recipientCSV, ugnotAmountCSV string, realmAddress address, balance int64) (int, int64) {
142 count := 0
143 total := int64(0)
144 recipientStart := 0
145 amountStart := 0
146 for {
147 _, nextRecipient, recipientDone := readPayoutRecipient(recipientCSV, recipientStart, realmAddress)
148 amount, nextAmount, amountDone := readPayoutAmount(ugnotAmountCSV, amountStart)
149 if recipientDone != amountDone {
150 panic("payout list item count mismatch")
151 }
152 count++
153 if count > distributionBatchMax {
154 panic("payout list exceeds limit")
155 }
156 if total > maxInt64-amount {
157 panic("distribution amount overflow")
158 }
159 total += amount
160 if total > balance {
161 panic("distribution amount exceeds reward pool balance")
162 }
163 if recipientDone {
164 return count, total
165 }
166 recipientStart = nextRecipient
167 amountStart = nextAmount
168 }
169}
170
171func sendPayouts(cur realm, recipientCSV, ugnotAmountCSV string) {
172 bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
173 recipientStart := 0
174 amountStart := 0
175 for {
176 recipient, nextRecipient, recipientDone := readPayoutRecipient(recipientCSV, recipientStart, cur.Address())
177 amount, nextAmount, amountDone := readPayoutAmount(ugnotAmountCSV, amountStart)
178 if recipientDone != amountDone {
179 panic("payout list item count mismatch")
180 }
181 bnk.SendCoins(cur.Address(), recipient, chain.Coins{{"ugnot", amount}})
182 if recipientDone {
183 return
184 }
185 recipientStart = nextRecipient
186 amountStart = nextAmount
187 }
188}
189
190func readPayoutRecipient(recipientCSV string, start int, realmAddress address) (address, int, bool) {
191 value, next, done := readPayoutCSVEntry(recipientCSV, start)
192 recipient := address(value)
193 if !recipient.IsValid() || recipient == realmAddress {
194 panic("invalid payout recipient")
195 }
196 return recipient, next, done
197}
198
199func readPayoutAmount(ugnotAmountCSV string, start int) (int64, int, bool) {
200 value, next, done := readPayoutCSVEntry(ugnotAmountCSV, start)
201 amount, err := strconv.ParseInt(value, 10, 64)
202 if err != nil || amount <= 0 {
203 panic("invalid payout amount")
204 }
205 return amount, next, done
206}
207
208func readPayoutCSVEntry(csv string, start int) (string, int, bool) {
209 end := start
210 for end < len(csv) && csv[end] != ',' {
211 end++
212 }
213 if end == start {
214 panic("payout entry must not be empty")
215 }
216 if end == len(csv) {
217 return csv[start:end], 0, true
218 }
219 next := end + 1
220 if next == len(csv) {
221 panic("payout entry must not be empty")
222 }
223 return csv[start:end], next, false
224}
225
226func GetRewardDistribution(distributionID uint64) RewardDistribution {
227 value, found := treeGet(rewardDistributions, distributionID)
228 if !found {
229 panic("reward distribution not found: " + strconv.FormatUint(distributionID, 10))
230 }
231 return value.(RewardDistribution)
232}
233
234func GetLatestRewardDistributionID() uint64 { return latestRewardDistributionID }
235
236func GetRewardDistributionByOperationKey(operationKey string) RewardDistributionView {
237 assertKeyLength(operationKey, "operation key")
238 value, found := stringGet(rewardDistributionIDs, operationKey)
239 if !found {
240 panic("reward distribution not found: " + operationKey)
241 }
242 distribution := GetRewardDistribution(value.(uint64))
243 return RewardDistributionView{
244 ID: distribution.ID,
245 PoolKey: GetRewardPool(distribution.PoolID).PoolKey,
246 GameWorldID: distribution.GameWorldID,
247 OperationKey: distribution.OperationKey,
248 RecipientCount: distribution.RecipientCount,
249 TotalAmount: distribution.TotalAmount,
250 Executor: distribution.Executor,
251 }
252}
253
254func GetRewardDistributionCountByPool(poolID uint64) int {
255 GetRewardPool(poolID)
256 ids, found := getIDSet(poolDistributionIDs, poolID)
257 if !found {
258 return 0
259 }
260 return ids.Size()
261}
262
263func ListRewardDistributionIDsByPool(poolID uint64, page, count int) []uint64 {
264 GetRewardPool(poolID)
265 ids, _ := getIDSet(poolDistributionIDs, poolID)
266 return listIDs(ids, page, count)
267}
268
269func GetVaultRefund(refundID uint64) VaultRefund {
270 value, found := treeGet(vaultRefunds, refundID)
271 if !found {
272 panic("vault refund not found: " + strconv.FormatUint(refundID, 10))
273 }
274 return value.(VaultRefund)
275}
276
277func GetLatestVaultRefundID() uint64 { return latestVaultRefundID }
278
279func GetVaultRefundByOperationKey(operationKey string) VaultRefund {
280 assertKeyLength(operationKey, "operation key")
281 value, found := stringGet(vaultRefundIDs, operationKey)
282 if !found {
283 panic("vault refund not found: " + operationKey)
284 }
285 return GetVaultRefund(value.(uint64))
286}
287
288func GetVaultRefundCountByPool(poolID uint64) int {
289 GetRewardPool(poolID)
290 ids, found := getIDSet(poolRefundIDs, poolID)
291 if !found {
292 return 0
293 }
294 return ids.Size()
295}
296
297func ListVaultRefundIDsByPool(poolID uint64, page, count int) []uint64 {
298 GetRewardPool(poolID)
299 ids, _ := getIDSet(poolRefundIDs, poolID)
300 return listIDs(ids, page, count)
301}