Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}