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

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}