participation.gno
16.98 Kb · 493 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
12type ParticipationProcessStatus int
13
14const (
15 ParticipationProcessStatusInvalid ParticipationProcessStatus = iota
16 ParticipationProcessStatusOpen
17 ParticipationProcessStatusClosed
18 ParticipationProcessStatusCommitted
19 ParticipationProcessStatusCanceled
20)
21
22type ParticipationFundStatus int
23
24const (
25 ParticipationFundStatusInvalid ParticipationFundStatus = iota
26 ParticipationFundStatusHeld
27 ParticipationFundStatusCommitted
28 ParticipationFundStatusRefunded
29)
30
31type ParticipationProcess struct {
32 ID uint64
33 ProcessKey string
34 GameWorldID uint64
35 RewardPoolID uint64
36 ParticipationFee int64
37 Status ParticipationProcessStatus
38 HeldBalance int64
39 CommittedTotal int64
40 RefundedTotal int64
41 FundCount uint64
42}
43
44type ParticipationProcessView struct {
45 ID uint64
46 ProcessKey string
47 GameWorldID uint64
48 RewardPoolKey string
49 ParticipationFee int64
50 Status ParticipationProcessStatus
51 HeldBalance int64
52 CommittedTotal int64
53 RefundedTotal int64
54 FundCount uint64
55}
56
57type ParticipationFund struct {
58 ID uint64
59 ProcessID uint64
60 ParticipationKey string
61 PaymentKey string
62 Owner address
63 Amount int64
64 Refunded bool
65}
66
67func CreateParticipationProcess(cur realm, processKey string, gameWorldID uint64, rewardPoolKey string, participationFeeUgnot int64) uint64 {
68 accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator)
69 assertNoAttachedCoins()
70 assertKeyLength(processKey, "process key")
71 assertKeyLength(rewardPoolKey, "reward pool key")
72 if participationProcessIDs.Has(processKey) {
73 panic("participation process key already exists")
74 }
75 if participationFeeUgnot <= 0 {
76 panic("participation fee must be positive")
77 }
78 assertGameWorldExists(gameWorldID)
79 rewardPoolID := GetRewardPoolID(rewardPoolKey)
80 if rewardPoolID == 0 {
81 panic("reward pool not found: " + rewardPoolKey)
82 }
83 pool := GetRewardPool(rewardPoolID)
84 if pool.GameWorldID != gameWorldID {
85 panic("reward pool belongs to another game world")
86 }
87 if pool.Status != RewardPoolStatusOpen {
88 panic("reward pool is not open")
89 }
90 if latestParticipationProcessID == maxUint64 {
91 panic("participation process id exhausted")
92 }
93
94 latestParticipationProcessID++
95 processID := latestParticipationProcessID
96 treeSet(participationProcesses, processID, ParticipationProcess{
97 ID: processID,
98 ProcessKey: processKey,
99 GameWorldID: gameWorldID,
100 RewardPoolID: rewardPoolID,
101 ParticipationFee: participationFeeUgnot,
102 Status: ParticipationProcessStatusOpen,
103 })
104 participationProcessIDs.Set(processKey, processID)
105 getOrCreateIDSet(gameWorldParticipationProcessIDs, gameWorldID).Set(uint64Key(processID), true)
106 return processID
107}
108
109func CloseParticipationProcess(cur realm, processID uint64) {
110 accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator)
111 assertNoAttachedCoins()
112 process := GetParticipationProcess(processID)
113 if process.Status == ParticipationProcessStatusClosed {
114 return
115 }
116 if process.Status != ParticipationProcessStatusOpen {
117 panic("participation process cannot be closed")
118 }
119 process.Status = ParticipationProcessStatusClosed
120 treeSet(participationProcesses, processID, process)
121}
122
123func CancelParticipationProcess(cur realm, processID uint64) {
124 accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator)
125 assertNoAttachedCoins()
126 process := GetParticipationProcess(processID)
127 if process.Status == ParticipationProcessStatusCanceled {
128 return
129 }
130 if process.Status != ParticipationProcessStatusOpen && process.Status != ParticipationProcessStatusClosed {
131 panic("participation process cannot be canceled")
132 }
133 process.Status = ParticipationProcessStatusCanceled
134 treeSet(participationProcesses, processID, process)
135}
136
137func DepositParticipation(cur realm, processID uint64, participationKey, paymentKey string) uint64 {
138 owner := accesscontrol.MustGetUserCaller(0, cur)
139 assertKeyLength(participationKey, "participation key")
140 assertKeyLength(paymentKey, "payment key")
141 process := GetParticipationProcess(processID)
142 if process.Status != ParticipationProcessStatusOpen {
143 panic("participation process is not open")
144 }
145 paymentIndexKey := keyedOwnerKey(processID, owner, paymentKey)
146 if participationFundIDs.Has(paymentIndexKey) {
147 panic("payment key already processed")
148 }
149 assertExactUgnotPayment(process.ParticipationFee)
150 if latestParticipationFundID == maxUint64 || process.FundCount == maxUint64 {
151 panic("participation fund id exhausted")
152 }
153 if process.HeldBalance > maxInt64-process.ParticipationFee || accountedUgnotTotal > maxInt64-process.ParticipationFee {
154 panic("vault fund amount overflow")
155 }
156
157 latestParticipationFundID++
158 fundID := latestParticipationFundID
159 treeSet(participationFunds, fundID, ParticipationFund{
160 ID: fundID,
161 ProcessID: processID,
162 ParticipationKey: participationKey,
163 PaymentKey: paymentKey,
164 Owner: owner,
165 Amount: process.ParticipationFee,
166 })
167 participationFundIDs.Set(paymentIndexKey, fundID)
168 addStringSetID(participationOwnerFundIDs, ownerKey(processID, owner), fundID)
169 addStringSetID(participationGroupFundIDs, keyedOwnerKey(processID, owner, participationKey), fundID)
170 process.HeldBalance += process.ParticipationFee
171 process.FundCount++
172 treeSet(participationProcesses, processID, process)
173 accountedUgnotTotal += process.ParticipationFee
174 return fundID
175}
176
177func RefundParticipationFund(cur realm, fundID uint64, operationKey string) uint64 {
178 executor := accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator)
179 assertNoAttachedCoins()
180 assertNewOperationKey(operationKey)
181 fund := GetParticipationFund(fundID)
182 if fund.Refunded {
183 panic("participation fund already refunded")
184 }
185 process := GetParticipationProcess(fund.ProcessID)
186 if process.Status != ParticipationProcessStatusOpen && process.Status != ParticipationProcessStatusClosed && process.Status != ParticipationProcessStatusCanceled {
187 panic("participation fund cannot be refunded")
188 }
189 if process.HeldBalance < fund.Amount {
190 panic("insufficient participation fund balance")
191 }
192
193 process.HeldBalance -= fund.Amount
194 process.RefundedTotal += fund.Amount
195 treeSet(participationProcesses, process.ID, process)
196 fund.Refunded = true
197 treeSet(participationFunds, fund.ID, fund)
198 return recordAndSendRefund(cross(cur), process.RewardPoolID, operationKey, fund.PaymentKey, fund.Owner, fund.Amount, "participation", executor)
199}
200
201func CommitParticipationProcess(cur realm, processID uint64, operationKey string) {
202 accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator)
203 assertNoAttachedCoins()
204 assertNewOperationKey(operationKey)
205 process := GetParticipationProcess(processID)
206 if process.Status != ParticipationProcessStatusClosed {
207 panic("participation process must be closed before commit")
208 }
209 pool := GetRewardPool(process.RewardPoolID)
210 if pool.Status != RewardPoolStatusOpen {
211 panic("reward pool is not open")
212 }
213 amount := process.HeldBalance
214 if pool.Balance > maxInt64-amount || pool.ParticipationCommittedTotal > maxInt64-amount || process.CommittedTotal > maxInt64-amount {
215 panic("vault fund amount overflow")
216 }
217
218 process.HeldBalance = 0
219 process.CommittedTotal += amount
220 process.Status = ParticipationProcessStatusCommitted
221 treeSet(participationProcesses, processID, process)
222 pool.ParticipationCommittedTotal += amount
223 pool.Balance += amount
224 treeSet(rewardPools, pool.ID, pool)
225 processedOperationKeys.Set(operationKey, processID)
226}
227
228func GetParticipationProcessID(processKey string) uint64 {
229 assertKeyLength(processKey, "process key")
230 value, found := stringGet(participationProcessIDs, processKey)
231 if !found {
232 return 0
233 }
234 return value.(uint64)
235}
236
237func GetParticipationProcess(processID uint64) ParticipationProcess {
238 value, found := treeGet(participationProcesses, processID)
239 if !found {
240 panic("participation process not found: " + strconv.FormatUint(processID, 10))
241 }
242 return value.(ParticipationProcess)
243}
244
245func GetParticipationProcessByKey(processKey string) ParticipationProcessView {
246 processID := GetParticipationProcessID(processKey)
247 if processID == 0 {
248 panic("participation process not found: " + processKey)
249 }
250 process := GetParticipationProcess(processID)
251 return ParticipationProcessView{
252 ID: process.ID,
253 ProcessKey: process.ProcessKey,
254 GameWorldID: process.GameWorldID,
255 RewardPoolKey: GetRewardPool(process.RewardPoolID).PoolKey,
256 ParticipationFee: process.ParticipationFee,
257 Status: process.Status,
258 HeldBalance: process.HeldBalance,
259 CommittedTotal: process.CommittedTotal,
260 RefundedTotal: process.RefundedTotal,
261 FundCount: process.FundCount,
262 }
263}
264
265func ListParticipationProcessStates(processKeys ...string) []map[string]string {
266 if len(processKeys) > gameVaultList {
267 panic("count exceeds game vault list limit")
268 }
269
270 result := []map[string]string{}
271 for _, processKey := range processKeys {
272 processID := GetParticipationProcessID(processKey)
273 state := map[string]string{
274 "requestedProcessKey": processKey,
275 "processId": "0",
276 "processKey": "",
277 "gameWorldId": "0",
278 "rewardPoolKey": "",
279 "participationFee": "0",
280 "processStatus": "0",
281 "heldBalance": "0",
282 "committedTotal": "0",
283 "refundedTotal": "0",
284 "fundCount": "0",
285 }
286 if processID != 0 {
287 process := GetParticipationProcess(processID)
288 state["processId"] = strconv.FormatUint(process.ID, 10)
289 state["processKey"] = process.ProcessKey
290 state["gameWorldId"] = strconv.FormatUint(process.GameWorldID, 10)
291 state["rewardPoolKey"] = GetRewardPool(process.RewardPoolID).PoolKey
292 state["participationFee"] = strconv.FormatInt(process.ParticipationFee, 10)
293 state["processStatus"] = strconv.Itoa(int(process.Status))
294 state["heldBalance"] = strconv.FormatInt(process.HeldBalance, 10)
295 state["committedTotal"] = strconv.FormatInt(process.CommittedTotal, 10)
296 state["refundedTotal"] = strconv.FormatInt(process.RefundedTotal, 10)
297 state["fundCount"] = strconv.FormatUint(process.FundCount, 10)
298 }
299 result = append(result, state)
300 }
301 return result
302}
303
304func ListParticipationPaymentStates(processKeysCSV, ownersCSV, paymentKeysCSV string) []map[string]string {
305 result := []map[string]string{}
306 if processKeysCSV == "" && ownersCSV == "" && paymentKeysCSV == "" {
307 return result
308 }
309 processStart, ownerStart, paymentStart := 0, 0, 0
310 for {
311 processKey, processNext, processDone := readQueryCSVEntry(processKeysCSV, processStart)
312 ownerValue, ownerNext, ownerDone := readQueryCSVEntry(ownersCSV, ownerStart)
313 paymentKey, paymentNext, paymentDone := readQueryCSVEntry(paymentKeysCSV, paymentStart)
314 if processDone != ownerDone || ownerDone != paymentDone {
315 panic("participation payment value count mismatch")
316 }
317 if len(result) == gameVaultList {
318 panic("count exceeds game vault list limit")
319 }
320 processID := GetParticipationProcessID(processKey)
321 owner := address(ownerValue)
322 assertValidOwner(owner)
323 assertKeyLength(paymentKey, "payment key")
324 state := map[string]string{
325 "requestedProcessKey": processKey,
326 "owner": owner.String(),
327 "paymentKey": paymentKey,
328 "processId": "0",
329 "processKey": "",
330 "participationFee": "0",
331 "processStatus": "0",
332 "fundId": "0",
333 "fundProcessId": "0",
334 "participationKey": "",
335 "fundPaymentKey": "",
336 "fundOwner": "",
337 "fundAmount": "0",
338 "fundRefunded": "false",
339 "fundEffectiveStatus": "0",
340 }
341 if processID != 0 {
342 process := GetParticipationProcess(processID)
343 state["processId"] = strconv.FormatUint(process.ID, 10)
344 state["processKey"] = process.ProcessKey
345 state["participationFee"] = strconv.FormatInt(process.ParticipationFee, 10)
346 state["processStatus"] = strconv.Itoa(int(process.Status))
347 fundID, found := stringGet(participationFundIDs, keyedOwnerKey(processID, owner, paymentKey))
348 if found {
349 fund := GetParticipationFund(fundID.(uint64))
350 state["fundId"] = strconv.FormatUint(fund.ID, 10)
351 state["fundProcessId"] = strconv.FormatUint(fund.ProcessID, 10)
352 state["participationKey"] = fund.ParticipationKey
353 state["fundPaymentKey"] = fund.PaymentKey
354 state["fundOwner"] = fund.Owner.String()
355 state["fundAmount"] = strconv.FormatInt(fund.Amount, 10)
356 state["fundRefunded"] = strconv.FormatBool(fund.Refunded)
357 state["fundEffectiveStatus"] = strconv.Itoa(int(GetParticipationFundEffectiveStatus(fund.ID)))
358 }
359 }
360 result = append(result, state)
361 if processDone {
362 return result
363 }
364 processStart, ownerStart, paymentStart = processNext, ownerNext, paymentNext
365 }
366}
367
368func GetLatestParticipationProcessID() uint64 { return latestParticipationProcessID }
369
370func GetParticipationFund(fundID uint64) ParticipationFund {
371 value, found := treeGet(participationFunds, fundID)
372 if !found {
373 panic("participation fund not found: " + strconv.FormatUint(fundID, 10))
374 }
375 return value.(ParticipationFund)
376}
377
378func GetLatestParticipationFundID() uint64 { return latestParticipationFundID }
379
380func GetParticipationFundEffectiveStatus(fundID uint64) ParticipationFundStatus {
381 fund := GetParticipationFund(fundID)
382 if fund.Refunded {
383 return ParticipationFundStatusRefunded
384 }
385 if GetParticipationProcess(fund.ProcessID).Status == ParticipationProcessStatusCommitted {
386 return ParticipationFundStatusCommitted
387 }
388 return ParticipationFundStatusHeld
389}
390
391func GetParticipationFundID(processID uint64, owner address, paymentKey string) uint64 {
392 GetParticipationProcess(processID)
393 assertValidOwner(owner)
394 assertKeyLength(paymentKey, "payment key")
395 value, found := stringGet(participationFundIDs, keyedOwnerKey(processID, owner, paymentKey))
396 if !found {
397 return 0
398 }
399 return value.(uint64)
400}
401
402func ListParticipationFundStates(values ...string) []map[string]string {
403 if len(values)%3 != 0 {
404 panic("participation fund value count mismatch")
405 }
406 if len(values)/3 > gameVaultList {
407 panic("count exceeds game vault list limit")
408 }
409
410 result := []map[string]string{}
411 for index := 0; index < len(values); index += 3 {
412 processID, err := strconv.ParseUint(values[index], 10, 64)
413 if err != nil || processID == 0 {
414 panic("invalid participation process id")
415 }
416 owner := address(values[index+1])
417 paymentKey := values[index+2]
418 fundID := GetParticipationFundID(processID, owner, paymentKey)
419 state := map[string]string{
420 "processId": strconv.FormatUint(processID, 10),
421 "owner": owner.String(),
422 "paymentKey": paymentKey,
423 "fundId": "0",
424 "fundProcessId": "0",
425 "participationKey": "",
426 "fundPaymentKey": "",
427 "fundOwner": "",
428 "fundAmount": "0",
429 "fundRefunded": "false",
430 "fundEffectiveStatus": "0",
431 }
432 if fundID != 0 {
433 fund := GetParticipationFund(fundID)
434 state["fundId"] = strconv.FormatUint(fund.ID, 10)
435 state["fundProcessId"] = strconv.FormatUint(fund.ProcessID, 10)
436 state["participationKey"] = fund.ParticipationKey
437 state["fundPaymentKey"] = fund.PaymentKey
438 state["fundOwner"] = fund.Owner.String()
439 state["fundAmount"] = strconv.FormatInt(fund.Amount, 10)
440 state["fundRefunded"] = strconv.FormatBool(fund.Refunded)
441 state["fundEffectiveStatus"] = strconv.Itoa(int(GetParticipationFundEffectiveStatus(fundID)))
442 }
443 result = append(result, state)
444 }
445 return result
446}
447
448func GetParticipationFundCount(processID uint64, owner address, participationKey string) int {
449 GetParticipationProcess(processID)
450 assertValidOwner(owner)
451 assertKeyLength(participationKey, "participation key")
452 ids, found := getStringSet(participationGroupFundIDs, keyedOwnerKey(processID, owner, participationKey))
453 if !found {
454 return 0
455 }
456 return ids.Size()
457}
458
459func ListParticipationFundIDs(processID uint64, owner address, participationKey string, page, count int) []uint64 {
460 GetParticipationProcess(processID)
461 assertValidOwner(owner)
462 assertKeyLength(participationKey, "participation key")
463 ids, _ := getStringSet(participationGroupFundIDs, keyedOwnerKey(processID, owner, participationKey))
464 return listIDs(ids, page, count)
465}
466
467func GetParticipationFundCountByOwner(processID uint64, owner address) int {
468 GetParticipationProcess(processID)
469 assertValidOwner(owner)
470 ids, found := getStringSet(participationOwnerFundIDs, ownerKey(processID, owner))
471 if !found {
472 return 0
473 }
474 return ids.Size()
475}
476
477func ListParticipationFundIDsByOwner(processID uint64, owner address, page, count int) []uint64 {
478 GetParticipationProcess(processID)
479 assertValidOwner(owner)
480 ids, _ := getStringSet(participationOwnerFundIDs, ownerKey(processID, owner))
481 return listIDs(ids, page, count)
482}
483
484func assertValidOwner(owner address) {
485 if !owner.IsValid() {
486 panic("invalid participation owner")
487 }
488}
489
490func sendUgnot(cur realm, recipient address, amount int64) {
491 accountedUgnotTotal -= amount
492 banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(cur.Address(), recipient, chain.Coins{{"ugnot", amount}})
493}