Transaction

A80D0D2A6B4779…B4889CB0281E

Block 747,751 · index 0 · indexed

Summary

Hash
A80D0D2A6B4779D86631D5B31A394397042FED0938356F8D23E3B4889CB0281E
Block
747,751
Size
83437 bytes
Gas used
111,831,840 / 223,663,580
Fee
223664 ugnot
Status
success

Messages

#1AddPackagegno.land/r/akkadia/beta/game_vault24 arguments

Arguments · 24

  1. #1game_vault
  2. #2distribution.gno
  3. #3package game_vault import ( "chain" "chain/banker" "strconv" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/r/akkadia/beta/admin" ) const ( SetDistributionBatchMaxEvent = "SetDistributionBatchMax" distributionBatchMaxDefault = 50 distributionBatchMaxUpperBound = 100 ) var distributionBatchMax int type RewardDistribution struct { ID uint64 PoolID uint64 GameWorldID uint64 OperationKey string RecipientCount uint32 TotalAmount int64 Executor address } type RewardDistributionView struct { ID uint64 PoolKey string GameWorldID uint64 OperationKey string RecipientCount uint32 TotalAmount int64 Executor address } type VaultRefund struct { ID uint64 PoolID uint64 OperationKey string SourceKey string Recipient address Amount int64 Kind string Executor address } func SetDistributionBatchMax(cur realm, value int) { executor := accesscontrol.MustGetAdminCaller(0, cur, admin.IsAdmin) assertNoAttachedCoins() if value < 1 { panic("limit must be at least 1") } if value > distributionBatchMaxUpperBound { panic("distribution batch max exceeds limit") } if value == distributionBatchMax { return } oldValue := distributionBatchMax distributionBatchMax = value chain.Emit(SetDistributionBatchMaxEvent, "executor", executor.String(), "oldValue", strconv.Itoa(oldValue), "newValue", strconv.Itoa(value)) } func GetDistributionBatchMax() int { return distributionBatchMax } func DistributeReward(cur realm, poolKey string, operationKey, recipientCSV, ugnotAmountCSV string) uint64 { executor := accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() assertNewOperationKey(operationKey) poolID, pool := mustGetRewardPoolByKey(poolKey) if pool.Status != RewardPoolStatusLocked { panic("reward pool must be locked before distribution") } if latestRewardDistributionID == maxUint64 { panic("reward distribution id exhausted") } if recipientCSV == "" || ugnotAmountCSV == "" { panic("payout list must not be empty") } count, total := validatePayouts(recipientCSV, ugnotAmountCSV, cur.Address(), pool.Balance) if pool.DistributedTotal > maxInt64-total { panic("reward pool distributed amount overflow") } if pool.DistributionCount == maxUint64 { panic("reward pool distribution count overflow") } latestRewardDistributionID++ distributionID := latestRewardDistributionID pool.Balance -= total pool.DistributedTotal += total pool.DistributionCount++ treeSet(rewardPools, poolID, pool) treeSet(rewardDistributions, distributionID, RewardDistribution{ ID: distributionID, PoolID: poolID, GameWorldID: pool.GameWorldID, OperationKey: operationKey, RecipientCount: uint32(count), TotalAmount: total, Executor: executor, }) rewardDistributionIDs.Set(operationKey, distributionID) processedOperationKeys.Set(operationKey, distributionID) getOrCreateIDSet(poolDistributionIDs, poolID).Set(uint64Key(distributionID), true) accountedUgnotTotal -= total sendPayouts(cross(cur), recipientCSV, ugnotAmountCSV) return distributionID } func recordAndSendRefund(cur realm, poolID uint64, operationKey, sourceKey string, recipient address, amount int64, kind string, executor address) uint64 { if latestVaultRefundID == maxUint64 { panic("vault refund id exhausted") } latestVaultRefundID++ refundID := latestVaultRefundID treeSet(vaultRefunds, refundID, VaultRefund{ ID: refundID, PoolID: poolID, OperationKey: operationKey, SourceKey: sourceKey, Recipient: recipient, Amount: amount, Kind: kind, Executor: executor, }) vaultRefundIDs.Set(operationKey, refundID) processedOperationKeys.Set(operationKey, refundID) if poolID != 0 { getOrCreateIDSet(poolRefundIDs, poolID).Set(uint64Key(refundID), true) } sendUgnot(cross(cur), recipient, amount) return refundID } func validatePayouts(recipientCSV, ugnotAmountCSV string, realmAddress address, balance int64) (int, int64) { count := 0 total := int64(0) recipientStart := 0 amountStart := 0 for { _, nextRecipient, recipientDone := readPayoutRecipient(recipientCSV, recipientStart, realmAddress) amount, nextAmount, amountDone := readPayoutAmount(ugnotAmountCSV, amountStart) if recipientDone != amountDone { panic("payout list item count mismatch") } count++ if count > distributionBatchMax { panic("payout list exceeds limit") } if total > maxInt64-amount { panic("distribution amount overflow") } total += amount if total > balance { panic("distribution amount exceeds reward pool balance") } if recipientDone { return count, total } recipientStart = nextRecipient amountStart = nextAmount } } func sendPayouts(cur realm, recipientCSV, ugnotAmountCSV string) { bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur) recipientStart := 0 amountStart := 0 for { recipient, nextRecipient, recipientDone := readPayoutRecipient(recipientCSV, recipientStart, cur.Address()) amount, nextAmount, amountDone := readPayoutAmount(ugnotAmountCSV, amountStart) if recipientDone != amountDone { panic("payout list item count mismatch") } bnk.SendCoins(cur.Address(), recipient, chain.Coins{{"ugnot", amount}}) if recipientDone { return } recipientStart = nextRecipient amountStart = nextAmount } } func readPayoutRecipient(recipientCSV string, start int, realmAddress address) (address, int, bool) { value, next, done := readPayoutCSVEntry(recipientCSV, start) recipient := address(value) if !recipient.IsValid() || recipient == realmAddress { panic("invalid payout recipient") } return recipient, next, done } func readPayoutAmount(ugnotAmountCSV string, start int) (int64, int, bool) { value, next, done := readPayoutCSVEntry(ugnotAmountCSV, start) amount, err := strconv.ParseInt(value, 10, 64) if err != nil || amount <= 0 { panic("invalid payout amount") } return amount, next, done } func readPayoutCSVEntry(csv string, start int) (string, int, bool) { end := start for end < len(csv) && csv[end] != ',' { end++ } if end == start { panic("payout entry must not be empty") } if end == len(csv) { return csv[start:end], 0, true } next := end + 1 if next == len(csv) { panic("payout entry must not be empty") } return csv[start:end], next, false } func GetRewardDistribution(distributionID uint64) RewardDistribution { value, found := treeGet(rewardDistributions, distributionID) if !found { panic("reward distribution not found: " + strconv.FormatUint(distributionID, 10)) } return value.(RewardDistribution) } func GetLatestRewardDistributionID() uint64 { return latestRewardDistributionID } func GetRewardDistributionByOperationKey(operationKey string) RewardDistributionView { assertKeyLength(operationKey, "operation key") value, found := stringGet(rewardDistributionIDs, operationKey) if !found { panic("reward distribution not found: " + operationKey) } distribution := GetRewardDistribution(value.(uint64)) return RewardDistributionView{ ID: distribution.ID, PoolKey: GetRewardPool(distribution.PoolID).PoolKey, GameWorldID: distribution.GameWorldID, OperationKey: distribution.OperationKey, RecipientCount: distribution.RecipientCount, TotalAmount: distribution.TotalAmount, Executor: distribution.Executor, } } func GetRewardDistributionCountByPool(poolID uint64) int { GetRewardPool(poolID) ids, found := getIDSet(poolDistributionIDs, poolID) if !found { return 0 } return ids.Size() } func ListRewardDistributionIDsByPool(poolID uint64, page, count int) []uint64 { GetRewardPool(poolID) ids, _ := getIDSet(poolDistributionIDs, poolID) return listIDs(ids, page, count) } func GetVaultRefund(refundID uint64) VaultRefund { value, found := treeGet(vaultRefunds, refundID) if !found { panic("vault refund not found: " + strconv.FormatUint(refundID, 10)) } return value.(VaultRefund) } func GetLatestVaultRefundID() uint64 { return latestVaultRefundID } func GetVaultRefundByOperationKey(operationKey string) VaultRefund { assertKeyLength(operationKey, "operation key") value, found := stringGet(vaultRefundIDs, operationKey) if !found { panic("vault refund not found: " + operationKey) } return GetVaultRefund(value.(uint64)) } func GetVaultRefundCountByPool(poolID uint64) int { GetRewardPool(poolID) ids, found := getIDSet(poolRefundIDs, poolID) if !found { return 0 } return ids.Size() } func ListVaultRefundIDsByPool(poolID uint64, page, count int) []uint64 { GetRewardPool(poolID) ids, _ := getIDSet(poolRefundIDs, poolID) return listIDs(ids, page, count) }
  4. #4entry.gno
  5. #5package game_vault import ( "strconv" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/r/akkadia/beta/admin" ) type EntryProcessStatus int const ( EntryProcessStatusInvalid EntryProcessStatus = iota EntryProcessStatusOpen EntryProcessStatusClosed EntryProcessStatusCanceled ) type EntryFundStatus int const ( EntryFundStatusInvalid EntryFundStatus = iota EntryFundStatusHeld EntryFundStatusRefunded ) type EntryProcess struct { ID uint64 ProcessKey string GameWorldID uint64 EntryFee int64 Status EntryProcessStatus HeldBalance int64 RefundedTotal int64 FundCount uint64 } type EntryFund struct { ID uint64 ProcessID uint64 ParticipationKey string EntryKey string Owner address Amount int64 Refunded bool } func CreateEntryProcess(cur realm, processKey string, gameWorldID uint64, entryFeeUgnot int64) uint64 { accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() assertKeyLength(processKey, "process key") if entryProcessIDs.Has(processKey) { panic("entry process key already exists") } if entryFeeUgnot <= 0 { panic("entry fee must be positive") } assertGameWorldExists(gameWorldID) if latestEntryProcessID == maxUint64 { panic("entry process id exhausted") } latestEntryProcessID++ processID := latestEntryProcessID treeSet(entryProcesses, processID, EntryProcess{ ID: processID, ProcessKey: processKey, GameWorldID: gameWorldID, EntryFee: entryFeeUgnot, Status: EntryProcessStatusOpen, }) entryProcessIDs.Set(processKey, processID) getOrCreateIDSet(gameWorldEntryProcessIDs, gameWorldID).Set(uint64Key(processID), true) return processID } func CloseEntryProcess(cur realm, processID uint64) { accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() process := GetEntryProcess(processID) if process.Status == EntryProcessStatusClosed { return } if process.Status != EntryProcessStatusOpen { panic("entry process cannot be closed") } process.Status = EntryProcessStatusClosed treeSet(entryProcesses, processID, process) } func CancelEntryProcess(cur realm, processID uint64) { accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() process := GetEntryProcess(processID) if process.Status == EntryProcessStatusCanceled { return } if process.Status != EntryProcessStatusOpen && process.Status != EntryProcessStatusClosed { panic("entry process cannot be canceled") } process.Status = EntryProcessStatusCanceled treeSet(entryProcesses, processID, process) } func DepositEntry(cur realm, processID uint64, participationKey, entryKey string) uint64 { owner := accesscontrol.MustGetUserCaller(0, cur) assertKeyLength(participationKey, "participation key") assertKeyLength(entryKey, "entry key") process := GetEntryProcess(processID) if process.Status != EntryProcessStatusOpen { panic("entry process is not open") } entryIndexKey := keyedOwnerKey(processID, owner, entryKey) if entryFundIDs.Has(entryIndexKey) { panic("entry key already processed") } assertExactUgnotPayment(process.EntryFee) if latestEntryFundID == maxUint64 || process.FundCount == maxUint64 { panic("entry fund id exhausted") } if process.HeldBalance > maxInt64-process.EntryFee || accountedUgnotTotal > maxInt64-process.EntryFee { panic("vault fund amount overflow") } latestEntryFundID++ fundID := latestEntryFundID treeSet(entryFunds, fundID, EntryFund{ ID: fundID, ProcessID: processID, ParticipationKey: participationKey, EntryKey: entryKey, Owner: owner, Amount: process.EntryFee, }) entryFundIDs.Set(entryIndexKey, fundID) addStringSetID(entryOwnerFundIDs, ownerKey(processID, owner), fundID) addStringSetID(entryGroupFundIDs, keyedOwnerKey(processID, owner, participationKey), fundID) process.HeldBalance += process.EntryFee process.FundCount++ treeSet(entryProcesses, processID, process) accountedUgnotTotal += process.EntryFee return fundID } func RefundEntryFund(cur realm, fundID uint64, operationKey string) uint64 { executor := accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() assertNewOperationKey(operationKey) fund := GetEntryFund(fundID) if fund.Refunded { panic("entry fund already refunded") } process := GetEntryProcess(fund.ProcessID) if process.Status != EntryProcessStatusOpen && process.Status != EntryProcessStatusClosed && process.Status != EntryProcessStatusCanceled { panic("entry fund cannot be refunded") } if process.HeldBalance < fund.Amount { panic("insufficient entry fund balance") } process.HeldBalance -= fund.Amount process.RefundedTotal += fund.Amount treeSet(entryProcesses, process.ID, process) fund.Refunded = true treeSet(entryFunds, fund.ID, fund) return recordAndSendRefund(cross(cur), 0, operationKey, fund.EntryKey, fund.Owner, fund.Amount, "entry", executor) } func GetEntryProcessID(processKey string) uint64 { assertKeyLength(processKey, "process key") value, found := stringGet(entryProcessIDs, processKey) if !found { return 0 } return value.(uint64) } func GetEntryProcess(processID uint64) EntryProcess { value, found := treeGet(entryProcesses, processID) if !found { panic("entry process not found: " + strconv.FormatUint(processID, 10)) } return value.(EntryProcess) } func GetLatestEntryProcessID() uint64 { return latestEntryProcessID } func ListEntryRefundStates(processKeysCSV, ownersCSV, entryKeysCSV string) []map[string]string { result := []map[string]string{} if processKeysCSV == "" && ownersCSV == "" && entryKeysCSV == "" { return result } processStart, ownerStart, entryStart := 0, 0, 0 for { processKey, processNext, processDone := readQueryCSVEntry(processKeysCSV, processStart) ownerValue, ownerNext, ownerDone := readQueryCSVEntry(ownersCSV, ownerStart) entryKey, entryNext, entryDone := readQueryCSVEntry(entryKeysCSV, entryStart) if processDone != ownerDone || ownerDone != entryDone { panic("entry refund value count mismatch") } if len(result) == gameVaultList { panic("count exceeds game vault list limit") } owner := address(ownerValue) processID := GetEntryProcessID(processKey) assertValidOwner(owner) assertKeyLength(entryKey, "entry key") state := map[string]string{ "requestedProcessKey": processKey, "owner": owner.String(), "entryKey": entryKey, "processId": "0", "processKey": "", "entryFee": "0", "processStatus": "0", "fundId": "0", "fundProcessId": "0", "participationKey": "", "fundEntryKey": "", "fundOwner": "", "fundAmount": "0", "fundRefunded": "false", } if processID != 0 { process := GetEntryProcess(processID) state["processId"] = strconv.FormatUint(process.ID, 10) state["processKey"] = process.ProcessKey state["entryFee"] = strconv.FormatInt(process.EntryFee, 10) state["processStatus"] = strconv.Itoa(int(process.Status)) fundID, found := stringGet(entryFundIDs, keyedOwnerKey(processID, owner, entryKey)) if found { fund := GetEntryFund(fundID.(uint64)) state["fundId"] = strconv.FormatUint(fund.ID, 10) state["fundProcessId"] = strconv.FormatUint(fund.ProcessID, 10) state["participationKey"] = fund.ParticipationKey state["fundEntryKey"] = fund.EntryKey state["fundOwner"] = fund.Owner.String() state["fundAmount"] = strconv.FormatInt(fund.Amount, 10) state["fundRefunded"] = strconv.FormatBool(fund.Refunded) } } result = append(result, state) if processDone { return result } processStart, ownerStart, entryStart = processNext, ownerNext, entryNext } } func GetEntryFund(fundID uint64) EntryFund { value, found := treeGet(entryFunds, fundID) if !found { panic("entry fund not found: " + strconv.FormatUint(fundID, 10)) } return value.(EntryFund) } func GetLatestEntryFundID() uint64 { return latestEntryFundID } func GetEntryFundStatus(fundID uint64) EntryFundStatus { if GetEntryFund(fundID).Refunded { return EntryFundStatusRefunded } return EntryFundStatusHeld } func GetEntryFundID(processID uint64, owner address, entryKey string) uint64 { GetEntryProcess(processID) assertValidOwner(owner) assertKeyLength(entryKey, "entry key") value, found := stringGet(entryFundIDs, keyedOwnerKey(processID, owner, entryKey)) if !found { return 0 } return value.(uint64) } func GetEntryFundCount(processID uint64, owner address, participationKey string) int { GetEntryProcess(processID) assertValidOwner(owner) assertKeyLength(participationKey, "participation key") ids, found := getStringSet(entryGroupFundIDs, keyedOwnerKey(processID, owner, participationKey)) if !found { return 0 } return ids.Size() } func ListEntryFundIDs(processID uint64, owner address, participationKey string, page, count int) []uint64 { GetEntryProcess(processID) assertValidOwner(owner) assertKeyLength(participationKey, "participation key") ids, _ := getStringSet(entryGroupFundIDs, keyedOwnerKey(processID, owner, participationKey)) return listIDs(ids, page, count) } func GetEntryFundCountByOwner(processID uint64, owner address) int { GetEntryProcess(processID) assertValidOwner(owner) ids, found := getStringSet(entryOwnerFundIDs, ownerKey(processID, owner)) if !found { return 0 } return ids.Size() } func ListEntryFundIDsByOwner(processID uint64, owner address, page, count int) []uint64 { GetEntryProcess(processID) assertValidOwner(owner) ids, _ := getStringSet(entryOwnerFundIDs, ownerKey(processID, owner)) return listIDs(ids, page, count) }
  6. #6funding.gno
  7. #7package game_vault import ( "chain" "chain/banker" "strconv" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/r/akkadia/beta/admin" ) type SystemFunding struct { ID uint64 PoolID uint64 FundingKey string Depositor address Amount int64 Refunded bool } func DepositSystemFunding(cur realm, poolID uint64, fundingKey string) uint64 { depositor := accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertKeyLength(fundingKey, "funding key") if systemFundingIDs.Has(fundingKey) { panic("funding key already processed") } pool := GetRewardPool(poolID) if pool.Status != RewardPoolStatusOpen { panic("reward pool is not open") } amount := getAttachedUgnotDeposit() if latestSystemFundingID == maxUint64 { panic("system funding id exhausted") } if pool.Balance > maxInt64-amount || pool.SystemFundedTotal > maxInt64-amount || accountedUgnotTotal > maxInt64-amount { panic("vault fund amount overflow") } latestSystemFundingID++ fundingID := latestSystemFundingID treeSet(systemFundings, fundingID, SystemFunding{ ID: fundingID, PoolID: poolID, FundingKey: fundingKey, Depositor: depositor, Amount: amount, }) systemFundingIDs.Set(fundingKey, fundingID) getOrCreateIDSet(poolSystemFundingIDs, poolID).Set(uint64Key(fundingID), true) pool.SystemFundedTotal += amount pool.Balance += amount treeSet(rewardPools, poolID, pool) accountedUgnotTotal += amount return fundingID } func RefundSystemFunding(cur realm, poolID uint64, fundingKey, operationKey string) uint64 { executor := accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() assertNewOperationKey(operationKey) pool := GetRewardPool(poolID) if pool.Status != RewardPoolStatusCanceled { panic("reward pool must be canceled before refund") } funding := GetSystemFundingByKey(fundingKey) if funding.PoolID != poolID { panic("system funding belongs to another reward pool") } if funding.Refunded { panic("system funding already refunded") } if pool.Balance < funding.Amount { panic("insufficient system funding balance") } pool.Balance -= funding.Amount pool.SystemRefundedTotal += funding.Amount treeSet(rewardPools, poolID, pool) funding.Refunded = true treeSet(systemFundings, funding.ID, funding) return recordAndSendRefund(cross(cur), poolID, operationKey, fundingKey, funding.Depositor, funding.Amount, "system", executor) } func GetSystemFunding(fundingID uint64) SystemFunding { value, found := treeGet(systemFundings, fundingID) if !found { panic("system funding not found: " + strconv.FormatUint(fundingID, 10)) } return value.(SystemFunding) } func GetLatestSystemFundingID() uint64 { return latestSystemFundingID } func GetSystemFundingByKey(fundingKey string) SystemFunding { assertKeyLength(fundingKey, "funding key") value, found := stringGet(systemFundingIDs, fundingKey) if !found { panic("system funding not found: " + fundingKey) } return GetSystemFunding(value.(uint64)) } func GetSystemFundingCountByPool(poolID uint64) int { GetRewardPool(poolID) ids, found := getIDSet(poolSystemFundingIDs, poolID) if !found { return 0 } return ids.Size() } func ListSystemFundingIDsByPool(poolID uint64, page, count int) []uint64 { GetRewardPool(poolID) ids, _ := getIDSet(poolSystemFundingIDs, poolID) return listIDs(ids, page, count) } func GetAccountedUgnotTotal() int64 { return accountedUgnotTotal } func GetUnaccountedUgnotTotal() int64 { actual := banker.NewReadonlyBanker().GetCoins(chain.PackageAddress("gno.land/r/akkadia/beta/game_vault")).AmountOf("ugnot") if actual < accountedUgnotTotal { panic("vault fund accounting exceeds realm balance") } return actual - accountedUgnotTotal }
  8. #8gnomod.toml
  9. #9module = "gno.land/r/akkadia/beta/game_vault" gno = "0.9"
  10. #10participation.gno
  11. #11package game_vault import ( "chain" "chain/banker" "strconv" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/r/akkadia/beta/admin" ) type ParticipationProcessStatus int const ( ParticipationProcessStatusInvalid ParticipationProcessStatus = iota ParticipationProcessStatusOpen ParticipationProcessStatusClosed ParticipationProcessStatusCommitted ParticipationProcessStatusCanceled ) type ParticipationFundStatus int const ( ParticipationFundStatusInvalid ParticipationFundStatus = iota ParticipationFundStatusHeld ParticipationFundStatusCommitted ParticipationFundStatusRefunded ) type ParticipationProcess struct { ID uint64 ProcessKey string GameWorldID uint64 RewardPoolID uint64 ParticipationFee int64 Status ParticipationProcessStatus HeldBalance int64 CommittedTotal int64 RefundedTotal int64 FundCount uint64 } type ParticipationProcessView struct { ID uint64 ProcessKey string GameWorldID uint64 RewardPoolKey string ParticipationFee int64 Status ParticipationProcessStatus HeldBalance int64 CommittedTotal int64 RefundedTotal int64 FundCount uint64 } type ParticipationFund struct { ID uint64 ProcessID uint64 ParticipationKey string PaymentKey string Owner address Amount int64 Refunded bool } func CreateParticipationProcess(cur realm, processKey string, gameWorldID uint64, rewardPoolKey string, participationFeeUgnot int64) uint64 { accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() assertKeyLength(processKey, "process key") assertKeyLength(rewardPoolKey, "reward pool key") if participationProcessIDs.Has(processKey) { panic("participation process key already exists") } if participationFeeUgnot <= 0 { panic("participation fee must be positive") } assertGameWorldExists(gameWorldID) rewardPoolID := GetRewardPoolID(rewardPoolKey) if rewardPoolID == 0 { panic("reward pool not found: " + rewardPoolKey) } pool := GetRewardPool(rewardPoolID) if pool.GameWorldID != gameWorldID { panic("reward pool belongs to another game world") } if pool.Status != RewardPoolStatusOpen { panic("reward pool is not open") } if latestParticipationProcessID == maxUint64 { panic("participation process id exhausted") } latestParticipationProcessID++ processID := latestParticipationProcessID treeSet(participationProcesses, processID, ParticipationProcess{ ID: processID, ProcessKey: processKey, GameWorldID: gameWorldID, RewardPoolID: rewardPoolID, ParticipationFee: participationFeeUgnot, Status: ParticipationProcessStatusOpen, }) participationProcessIDs.Set(processKey, processID) getOrCreateIDSet(gameWorldParticipationProcessIDs, gameWorldID).Set(uint64Key(processID), true) return processID } func CloseParticipationProcess(cur realm, processID uint64) { accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() process := GetParticipationProcess(processID) if process.Status == ParticipationProcessStatusClosed { return } if process.Status != ParticipationProcessStatusOpen { panic("participation process cannot be closed") } process.Status = ParticipationProcessStatusClosed treeSet(participationProcesses, processID, process) } func CancelParticipationProcess(cur realm, processID uint64) { accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() process := GetParticipationProcess(processID) if process.Status == ParticipationProcessStatusCanceled { return } if process.Status != ParticipationProcessStatusOpen && process.Status != ParticipationProcessStatusClosed { panic("participation process cannot be canceled") } process.Status = ParticipationProcessStatusCanceled treeSet(participationProcesses, processID, process) } func DepositParticipation(cur realm, processID uint64, participationKey, paymentKey string) uint64 { owner := accesscontrol.MustGetUserCaller(0, cur) assertKeyLength(participationKey, "participation key") assertKeyLength(paymentKey, "payment key") process := GetParticipationProcess(processID) if process.Status != ParticipationProcessStatusOpen { panic("participation process is not open") } paymentIndexKey := keyedOwnerKey(processID, owner, paymentKey) if participationFundIDs.Has(paymentIndexKey) { panic("payment key already processed") } assertExactUgnotPayment(process.ParticipationFee) if latestParticipationFundID == maxUint64 || process.FundCount == maxUint64 { panic("participation fund id exhausted") } if process.HeldBalance > maxInt64-process.ParticipationFee || accountedUgnotTotal > maxInt64-process.ParticipationFee { panic("vault fund amount overflow") } latestParticipationFundID++ fundID := latestParticipationFundID treeSet(participationFunds, fundID, ParticipationFund{ ID: fundID, ProcessID: processID, ParticipationKey: participationKey, PaymentKey: paymentKey, Owner: owner, Amount: process.ParticipationFee, }) participationFundIDs.Set(paymentIndexKey, fundID) addStringSetID(participationOwnerFundIDs, ownerKey(processID, owner), fundID) addStringSetID(participationGroupFundIDs, keyedOwnerKey(processID, owner, participationKey), fundID) process.HeldBalance += process.ParticipationFee process.FundCount++ treeSet(participationProcesses, processID, process) accountedUgnotTotal += process.ParticipationFee return fundID } func RefundParticipationFund(cur realm, fundID uint64, operationKey string) uint64 { executor := accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() assertNewOperationKey(operationKey) fund := GetParticipationFund(fundID) if fund.Refunded { panic("participation fund already refunded") } process := GetParticipationProcess(fund.ProcessID) if process.Status != ParticipationProcessStatusOpen && process.Status != ParticipationProcessStatusClosed && process.Status != ParticipationProcessStatusCanceled { panic("participation fund cannot be refunded") } if process.HeldBalance < fund.Amount { panic("insufficient participation fund balance") } process.HeldBalance -= fund.Amount process.RefundedTotal += fund.Amount treeSet(participationProcesses, process.ID, process) fund.Refunded = true treeSet(participationFunds, fund.ID, fund) return recordAndSendRefund(cross(cur), process.RewardPoolID, operationKey, fund.PaymentKey, fund.Owner, fund.Amount, "participation", executor) } func CommitParticipationProcess(cur realm, processID uint64, operationKey string) { accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertNoAttachedCoins() assertNewOperationKey(operationKey) process := GetParticipationProcess(processID) if process.Status != ParticipationProcessStatusClosed { panic("participation process must be closed before commit") } pool := GetRewardPool(process.RewardPoolID) if pool.Status != RewardPoolStatusOpen { panic("reward pool is not open") } amount := process.HeldBalance if pool.Balance > maxInt64-amount || pool.ParticipationCommittedTotal > maxInt64-amount || process.CommittedTotal > maxInt64-amount { panic("vault fund amount overflow") } process.HeldBalance = 0 process.CommittedTotal += amount process.Status = ParticipationProcessStatusCommitted treeSet(participationProcesses, processID, process) pool.ParticipationCommittedTotal += amount pool.Balance += amount treeSet(rewardPools, pool.ID, pool) processedOperationKeys.Set(operationKey, processID) } func GetParticipationProcessID(processKey string) uint64 { assertKeyLength(processKey, "process key") value, found := stringGet(participationProcessIDs, processKey) if !found { return 0 } return value.(uint64) } func GetParticipationProcess(processID uint64) ParticipationProcess { value, found := treeGet(participationProcesses, processID) if !found { panic("participation process not found: " + strconv.FormatUint(processID, 10)) } return value.(ParticipationProcess) } func GetParticipationProcessByKey(processKey string) ParticipationProcessView { processID := GetParticipationProcessID(processKey) if processID == 0 { panic("participation process not found: " + processKey) } process := GetParticipationProcess(processID) return ParticipationProcessView{ ID: process.ID, ProcessKey: process.ProcessKey, GameWorldID: process.GameWorldID, RewardPoolKey: GetRewardPool(process.RewardPoolID).PoolKey, ParticipationFee: process.ParticipationFee, Status: process.Status, HeldBalance: process.HeldBalance, CommittedTotal: process.CommittedTotal, RefundedTotal: process.RefundedTotal, FundCount: process.FundCount, } } func ListParticipationProcessStates(processKeys ...string) []map[string]string { if len(processKeys) > gameVaultList { panic("count exceeds game vault list limit") } result := []map[string]string{} for _, processKey := range processKeys { processID := GetParticipationProcessID(processKey) state := map[string]string{ "requestedProcessKey": processKey, "processId": "0", "processKey": "", "gameWorldId": "0", "rewardPoolKey": "", "participationFee": "0", "processStatus": "0", "heldBalance": "0", "committedTotal": "0", "refundedTotal": "0", "fundCount": "0", } if processID != 0 { process := GetParticipationProcess(processID) state["processId"] = strconv.FormatUint(process.ID, 10) state["processKey"] = process.ProcessKey state["gameWorldId"] = strconv.FormatUint(process.GameWorldID, 10) state["rewardPoolKey"] = GetRewardPool(process.RewardPoolID).PoolKey state["participationFee"] = strconv.FormatInt(process.ParticipationFee, 10) state["processStatus"] = strconv.Itoa(int(process.Status)) state["heldBalance"] = strconv.FormatInt(process.HeldBalance, 10) state["committedTotal"] = strconv.FormatInt(process.CommittedTotal, 10) state["refundedTotal"] = strconv.FormatInt(process.RefundedTotal, 10) state["fundCount"] = strconv.FormatUint(process.FundCount, 10) } result = append(result, state) } return result } func ListParticipationPaymentStates(processKeysCSV, ownersCSV, paymentKeysCSV string) []map[string]string { result := []map[string]string{} if processKeysCSV == "" && ownersCSV == "" && paymentKeysCSV == "" { return result } processStart, ownerStart, paymentStart := 0, 0, 0 for { processKey, processNext, processDone := readQueryCSVEntry(processKeysCSV, processStart) ownerValue, ownerNext, ownerDone := readQueryCSVEntry(ownersCSV, ownerStart) paymentKey, paymentNext, paymentDone := readQueryCSVEntry(paymentKeysCSV, paymentStart) if processDone != ownerDone || ownerDone != paymentDone { panic("participation payment value count mismatch") } if len(result) == gameVaultList { panic("count exceeds game vault list limit") } processID := GetParticipationProcessID(processKey) owner := address(ownerValue) assertValidOwner(owner) assertKeyLength(paymentKey, "payment key") state := map[string]string{ "requestedProcessKey": processKey, "owner": owner.String(), "paymentKey": paymentKey, "processId": "0", "processKey": "", "participationFee": "0", "processStatus": "0", "fundId": "0", "fundProcessId": "0", "participationKey": "", "fundPaymentKey": "", "fundOwner": "", "fundAmount": "0", "fundRefunded": "false", "fundEffectiveStatus": "0", } if processID != 0 { process := GetParticipationProcess(processID) state["processId"] = strconv.FormatUint(process.ID, 10) state["processKey"] = process.ProcessKey state["participationFee"] = strconv.FormatInt(process.ParticipationFee, 10) state["processStatus"] = strconv.Itoa(int(process.Status)) fundID, found := stringGet(participationFundIDs, keyedOwnerKey(processID, owner, paymentKey)) if found { fund := GetParticipationFund(fundID.(uint64)) state["fundId"] = strconv.FormatUint(fund.ID, 10) state["fundProcessId"] = strconv.FormatUint(fund.ProcessID, 10) state["participationKey"] = fund.ParticipationKey state["fundPaymentKey"] = fund.PaymentKey state["fundOwner"] = fund.Owner.String() state["fundAmount"] = strconv.FormatInt(fund.Amount, 10) state["fundRefunded"] = strconv.FormatBool(fund.Refunded) state["fundEffectiveStatus"] = strconv.Itoa(int(GetParticipationFundEffectiveStatus(fund.ID))) } } result = append(result, state) if processDone { return result } processStart, ownerStart, paymentStart = processNext, ownerNext, paymentNext } } func GetLatestParticipationProcessID() uint64 { return latestParticipationProcessID } func GetParticipationFund(fundID uint64) ParticipationFund { value, found := treeGet(participationFunds, fundID) if !found { panic("participation fund not found: " + strconv.FormatUint(fundID, 10)) } return value.(ParticipationFund) } func GetLatestParticipationFundID() uint64 { return latestParticipationFundID } func GetParticipationFundEffectiveStatus(fundID uint64) ParticipationFundStatus { fund := GetParticipationFund(fundID) if fund.Refunded { return ParticipationFundStatusRefunded } if GetParticipationProcess(fund.ProcessID).Status == ParticipationProcessStatusCommitted { return ParticipationFundStatusCommitted } return ParticipationFundStatusHeld } func GetParticipationFundID(processID uint64, owner address, paymentKey string) uint64 { GetParticipationProcess(processID) assertValidOwner(owner) assertKeyLength(paymentKey, "payment key") value, found := stringGet(participationFundIDs, keyedOwnerKey(processID, owner, paymentKey)) if !found { return 0 } return value.(uint64) } func ListParticipationFundStates(values ...string) []map[string]string { if len(values)%3 != 0 { panic("participation fund value count mismatch") } if len(values)/3 > gameVaultList { panic("count exceeds game vault list limit") } result := []map[string]string{} for index := 0; index < len(values); index += 3 { processID, err := strconv.ParseUint(values[index], 10, 64) if err != nil || processID == 0 { panic("invalid participation process id") } owner := address(values[index+1]) paymentKey := values[index+2] fundID := GetParticipationFundID(processID, owner, paymentKey) state := map[string]string{ "processId": strconv.FormatUint(processID, 10), "owner": owner.String(), "paymentKey": paymentKey, "fundId": "0", "fundProcessId": "0", "participationKey": "", "fundPaymentKey": "", "fundOwner": "", "fundAmount": "0", "fundRefunded": "false", "fundEffectiveStatus": "0", } if fundID != 0 { fund := GetParticipationFund(fundID) state["fundId"] = strconv.FormatUint(fund.ID, 10) state["fundProcessId"] = strconv.FormatUint(fund.ProcessID, 10) state["participationKey"] = fund.ParticipationKey state["fundPaymentKey"] = fund.PaymentKey state["fundOwner"] = fund.Owner.String() state["fundAmount"] = strconv.FormatInt(fund.Amount, 10) state["fundRefunded"] = strconv.FormatBool(fund.Refunded) state["fundEffectiveStatus"] = strconv.Itoa(int(GetParticipationFundEffectiveStatus(fundID))) } result = append(result, state) } return result } func GetParticipationFundCount(processID uint64, owner address, participationKey string) int { GetParticipationProcess(processID) assertValidOwner(owner) assertKeyLength(participationKey, "participation key") ids, found := getStringSet(participationGroupFundIDs, keyedOwnerKey(processID, owner, participationKey)) if !found { return 0 } return ids.Size() } func ListParticipationFundIDs(processID uint64, owner address, participationKey string, page, count int) []uint64 { GetParticipationProcess(processID) assertValidOwner(owner) assertKeyLength(participationKey, "participation key") ids, _ := getStringSet(participationGroupFundIDs, keyedOwnerKey(processID, owner, participationKey)) return listIDs(ids, page, count) } func GetParticipationFundCountByOwner(processID uint64, owner address) int { GetParticipationProcess(processID) assertValidOwner(owner) ids, found := getStringSet(participationOwnerFundIDs, ownerKey(processID, owner)) if !found { return 0 } return ids.Size() } func ListParticipationFundIDsByOwner(processID uint64, owner address, page, count int) []uint64 { GetParticipationProcess(processID) assertValidOwner(owner) ids, _ := getStringSet(participationOwnerFundIDs, ownerKey(processID, owner)) return listIDs(ids, page, count) } func assertValidOwner(owner address) { if !owner.IsValid() { panic("invalid participation owner") } } func sendUgnot(cur realm, recipient address, amount int64) { accountedUgnotTotal -= amount banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(cur.Address(), recipient, chain.Coins{{"ugnot", amount}}) }
  12. #12refund_query.gno
  13. #13package game_vault import "strconv" // ListRefundScopeStates returns participation process and reward pool states for refund decisions. func ListRefundScopeStates(scopeKeys ...string) []map[string]string { if len(scopeKeys)%2 != 0 { panic("refund scope key count mismatch") } if len(scopeKeys)/2 > gameVaultList { panic("count exceeds game vault list limit") } result := []map[string]string{} for index := 0; index < len(scopeKeys); index += 2 { processKey := scopeKeys[index] rewardPoolKey := scopeKeys[index+1] assertKeyLength(processKey, "process key") assertKeyLength(rewardPoolKey, "pool key") state := map[string]string{ "requestedProcessKey": processKey, "requestedRewardPoolKey": rewardPoolKey, "processId": "0", "processKey": "", "processGameWorldId": "0", "processRewardPoolKey": "", "participationFee": "0", "processStatus": "0", "rewardPoolId": "0", "rewardPoolKey": "", "rewardPoolGameWorldId": "0", "rewardPoolStatus": "0", } processID := GetParticipationProcessID(processKey) if processID != 0 { process := GetParticipationProcess(processID) state["processId"] = strconv.FormatUint(process.ID, 10) state["processKey"] = process.ProcessKey state["processGameWorldId"] = strconv.FormatUint(process.GameWorldID, 10) state["processRewardPoolKey"] = GetRewardPool(process.RewardPoolID).PoolKey state["participationFee"] = strconv.FormatInt(process.ParticipationFee, 10) state["processStatus"] = strconv.Itoa(int(process.Status)) } rewardPoolID := GetRewardPoolID(rewardPoolKey) if rewardPoolID != 0 { pool := GetRewardPool(rewardPoolID) state["rewardPoolId"] = strconv.FormatUint(pool.ID, 10) state["rewardPoolKey"] = pool.PoolKey state["rewardPoolGameWorldId"] = strconv.FormatUint(pool.GameWorldID, 10) state["rewardPoolStatus"] = strconv.Itoa(int(pool.Status)) } result = append(result, state) } return result } // ListParticipationRefundStates returns fund, processed operation, and refund evidence for each participant. func ListParticipationRefundStates(values ...string) []map[string]string { if len(values)%4 != 0 { panic("participation refund value count mismatch") } if len(values)/4 > gameVaultList { panic("count exceeds game vault list limit") } result := []map[string]string{} for index := 0; index < len(values); index += 4 { processID, err := strconv.ParseUint(values[index], 10, 64) if err != nil || processID == 0 { panic("invalid participation process id") } owner := address(values[index+1]) paymentKey := values[index+2] operationKey := values[index+3] assertValidOwner(owner) assertKeyLength(paymentKey, "payment key") assertKeyLength(operationKey, "operation key") state := map[string]string{ "processId": strconv.FormatUint(processID, 10), "owner": owner.String(), "paymentKey": paymentKey, "operationKey": operationKey, "fundId": "0", "fundProcessId": "0", "participationKey": "", "fundPaymentKey": "", "fundOwner": "", "fundAmount": "0", "fundRefunded": "false", "fundEffectiveStatus": "0", "operationProcessed": strconv.FormatBool(HasProcessedOperationKey(operationKey)), "refundId": "0", "refundPoolId": "0", "refundOperationKey": "", "refundSourceKey": "", "refundRecipient": "", "refundAmount": "0", "refundKind": "", } fundID := GetParticipationFundID(processID, owner, paymentKey) if fundID != 0 { fund := GetParticipationFund(fundID) state["fundId"] = strconv.FormatUint(fund.ID, 10) state["fundProcessId"] = strconv.FormatUint(fund.ProcessID, 10) state["participationKey"] = fund.ParticipationKey state["fundPaymentKey"] = fund.PaymentKey state["fundOwner"] = fund.Owner.String() state["fundAmount"] = strconv.FormatInt(fund.Amount, 10) state["fundRefunded"] = strconv.FormatBool(fund.Refunded) state["fundEffectiveStatus"] = strconv.Itoa(int(GetParticipationFundEffectiveStatus(fundID))) } refundIDValue, found := stringGet(vaultRefundIDs, operationKey) if found { refund := GetVaultRefund(refundIDValue.(uint64)) state["refundId"] = strconv.FormatUint(refund.ID, 10) state["refundPoolId"] = strconv.FormatUint(refund.PoolID, 10) state["refundOperationKey"] = refund.OperationKey state["refundSourceKey"] = refund.SourceKey state["refundRecipient"] = refund.Recipient.String() state["refundAmount"] = strconv.FormatInt(refund.Amount, 10) state["refundKind"] = refund.Kind } result = append(result, state) } return result }
  14. #14refund_query_test.gno
  15. #15package game_vault import ( "chain" "strconv" "testing" "gno.land/p/nt/uassert/v0" ) func TestListRefundScopeStates(cur realm, t *testing.T) { vaultTest.Setup(cross(cur), t) vaultTest.CreatePool(cross(cur), "pool:refund-query") processID := vaultTest.CreateParticipationProcess(cross(cur), "process:refund-query", "pool:refund-query", 25) states := ListRefundScopeStates( "process:missing", "pool:missing", "process:refund-query", "pool:refund-query", ) uassert.Equal(t, 2, len(states)) uassert.Equal(t, "0", states[0]["processId"]) uassert.Equal(t, "0", states[0]["rewardPoolId"]) uassert.Equal(t, strconv.FormatUint(processID, 10), states[1]["processId"]) uassert.Equal(t, strconv.Itoa(int(ParticipationProcessStatusOpen)), states[1]["processStatus"]) uassert.Equal(t, strconv.Itoa(int(RewardPoolStatusOpen)), states[1]["rewardPoolStatus"]) } func TestListParticipationRefundStates(cur realm, t *testing.T) { vaultTest.Setup(cross(cur), t) poolID := vaultTest.CreatePool(cross(cur), "pool:participation-refund-query") processID := vaultTest.CreateParticipationProcess(cross(cur), "process:participation-refund-query", "pool:participation-refund-query", 25) fundID := vaultTest.DepositParticipation(cross(cur), vaultTest.Player1, processID, "participation:1", "payment:1", 25) before := ListParticipationRefundStates( strconv.FormatUint(processID, 10), vaultTest.Player1.String(), "payment:1", "refund:1", strconv.FormatUint(processID, 10), vaultTest.Player1.String(), "payment:missing", "refund:missing", ) uassert.Equal(t, strconv.FormatUint(fundID, 10), before[0]["fundId"]) uassert.Equal(t, strconv.Itoa(int(ParticipationFundStatusHeld)), before[0]["fundEffectiveStatus"]) uassert.Equal(t, "false", before[0]["operationProcessed"]) uassert.Equal(t, "0", before[1]["fundId"]) testing.SetOriginCaller(vaultTest.Operator) testing.SetRealm(testing.NewUserRealm(vaultTest.Operator)) testing.SetOriginSend(chain.Coins{}) RefundParticipationFund(cross(cur), fundID, "refund:1") after := ListParticipationRefundStates( strconv.FormatUint(processID, 10), vaultTest.Player1.String(), "payment:1", "refund:1", ) uassert.Equal(t, "true", after[0]["fundRefunded"]) uassert.Equal(t, "true", after[0]["operationProcessed"]) uassert.Equal(t, strconv.FormatUint(poolID, 10), after[0]["refundPoolId"]) uassert.Equal(t, "payment:1", after[0]["refundSourceKey"]) } func TestRefundStateQueriesRejectMismatchedValues(cur realm, t *testing.T) { uassert.PanicsWithMessage(t, cur, "refund scope key count mismatch", func() { ListRefundScopeStates("process:1") }) uassert.PanicsWithMessage(t, cur, "participation refund value count mismatch", func() { ListParticipationRefundStates("1", vaultTest.Player1.String(), "payment:1") }) scopeKeys := []string{} refundValues := []string{} for index := 0; index <= gameVaultList; index++ { scopeKeys = append(scopeKeys, "process:1", "pool:1") refundValues = append(refundValues, "1", vaultTest.Player1.String(), "payment:1", "refund:1") } uassert.PanicsWithMessage(t, cur, "count exceeds game vault list limit", func() { ListRefundScopeStates(scopeKeys...) }) uassert.PanicsWithMessage(t, cur, "count exceeds game vault list limit", func() { ListParticipationRefundStates(refundValues...) }) }
  16. #16security_test.gno
  17. #17package game_vault import ( "chain" "testing" "gno.land/p/nt/uassert/v0" ) func TestGameVaultCallerSecurity(cur realm, t *testing.T) { vaultTest.Setup(cross(cur), t) testing.SetOriginCaller(vaultTest.Player1) testing.SetRealm(testing.NewUserRealm(vaultTest.Player1)) testing.SetOriginSend(chain.Coins{}) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { CreateRewardPool(cross(cur), "pool:unauthorized", vaultTest.GameWorldID) }) processID := vaultTest.CreateEntryProcess(cross(cur), "entry:security", 10) testing.SetOriginCaller(vaultTest.Player1) testing.SetRealm(testing.NewCodeRealm("gno.land/r/example/vault_relay")) testing.SetOriginSend(chain.Coins{{"ugnot", 10}}) uassert.AbortsWithMessage(t, cur, "contract calls not allowed: function must be called directly by user", func() { DepositEntry(cross(cur), processID, "participant:1", "entry:relay") }) } func TestParticipationRefundExcludesCommittedAmount(cur realm, t *testing.T) { vaultTest.Setup(cross(cur), t) poolID := vaultTest.CreatePool(cross(cur), "pool:refund") processID := vaultTest.CreateParticipationProcess(cross(cur), "participation:refund", "pool:refund", 10) refundedFundID := vaultTest.DepositParticipation(cross(cur), vaultTest.Player1, processID, "participant:1", "payment:refund", 10) committedFundID := vaultTest.DepositParticipation(cross(cur), vaultTest.Player1, processID, "participant:1", "payment:commit", 10) testing.SetOriginCaller(vaultTest.Operator) testing.SetRealm(testing.NewUserRealm(vaultTest.Operator)) testing.SetOriginSend(chain.Coins{}) RefundParticipationFund(cross(cur), refundedFundID, "refund:participation") CloseParticipationProcess(cross(cur), processID) CommitParticipationProcess(cross(cur), processID, "commit:participation") uassert.Equal(t, int64(10), GetRewardPool(poolID).Balance) uassert.Equal(t, int(ParticipationFundStatusRefunded), int(GetParticipationFundEffectiveStatus(refundedFundID))) uassert.Equal(t, int(ParticipationFundStatusCommitted), int(GetParticipationFundEffectiveStatus(committedFundID))) uassert.Equal(t, 1, GetVaultRefundCountByPool(poolID)) uassert.AbortsWithMessage(t, cur, "participation fund cannot be refunded", func() { RefundParticipationFund(cross(cur), committedFundID, "refund:committed") }) } func TestSystemFundingCancellationRefundAndReplay(cur realm, t *testing.T) { vaultTest.Setup(cross(cur), t) poolID := vaultTest.CreatePool(cross(cur), "pool:system-refund") fundingID := vaultTest.DepositSystemFunding(cross(cur), poolID, "system:refund", 20) testing.SetOriginCaller(vaultTest.Operator) testing.SetRealm(testing.NewUserRealm(vaultTest.Operator)) testing.SetOriginSend(chain.Coins{}) CancelRewardPool(cross(cur), "pool:system-refund") refundID := RefundSystemFunding(cross(cur), poolID, "system:refund", "refund:system") uassert.True(t, GetSystemFunding(fundingID).Refunded) uassert.Equal(t, int64(0), GetRewardPool(poolID).Balance) uassert.Equal(t, refundID, GetVaultRefundByOperationKey("refund:system").ID) uassert.AbortsWithMessage(t, cur, "operation key already processed", func() { RefundSystemFunding(cross(cur), poolID, "system:refund", "refund:system") }) }
  18. #18setup_test.gno
  19. #19package game_vault import ( "chain" "testing" "gno.land/p/nt/testutils/v0" "gno.land/r/akkadia/beta/admin" gameworld "gno.land/r/akkadia/beta/game_world" ) var vaultTest = &vaultTestHelper{ Admin: testutils.TestAddress("vaultAdmin"), Operator: testutils.TestAddress("vaultOperator"), Owner: testutils.TestAddress("vaultOwner"), Player1: testutils.TestAddress("vaultPlayer1"), Player2: testutils.TestAddress("vaultPlayer2"), } type vaultTestHelper struct { Admin address Operator address Owner address Player1 address Player2 address GameWorldID uint64 setupDone bool } func (h *vaultTestHelper) Setup(cur realm, t *testing.T) { t.Helper() resetGameVaultState() testing.SetOriginSend(chain.Coins{}) if h.setupDone { return } originalAdmin := admin.GetAdmin() testing.SetOriginCaller(originalAdmin) testing.SetRealm(testing.NewUserRealm(originalAdmin)) admin.TransferAdmin(cross(cur), h.Admin) testing.SetOriginCaller(h.Admin) testing.SetRealm(testing.NewUserRealm(h.Admin)) admin.AcceptAdmin(cross(cur)) admin.SetOperator(cross(cur), h.Operator) testing.SetOriginCaller(h.Operator) testing.SetRealm(testing.NewUserRealm(h.Operator)) gameworld.SetGameTypePolicy(cross(cur), "vault_test", 0, gameworld.CreationModePublicCreator) testing.SetOriginCaller(h.Owner) testing.SetRealm(testing.NewUserRealm(h.Owner)) h.GameWorldID = gameworld.CreateGameWorld(cross(cur), "vault_test") h.setupDone = true } func (h *vaultTestHelper) CreatePool(cur realm, key string) uint64 { testing.SetOriginCaller(h.Operator) testing.SetRealm(testing.NewUserRealm(h.Operator)) testing.SetOriginSend(chain.Coins{}) return CreateRewardPool(cross(cur), key, h.GameWorldID) } func (h *vaultTestHelper) CreateParticipationProcess(cur realm, key, poolKey string, fee int64) uint64 { testing.SetOriginCaller(h.Operator) testing.SetRealm(testing.NewUserRealm(h.Operator)) testing.SetOriginSend(chain.Coins{}) return CreateParticipationProcess(cross(cur), key, h.GameWorldID, poolKey, fee) } func (h *vaultTestHelper) CreateEntryProcess(cur realm, key string, fee int64) uint64 { testing.SetOriginCaller(h.Operator) testing.SetRealm(testing.NewUserRealm(h.Operator)) testing.SetOriginSend(chain.Coins{}) return CreateEntryProcess(cross(cur), key, h.GameWorldID, fee) } func (h *vaultTestHelper) DepositParticipation(cur realm, owner address, processID uint64, participationKey, paymentKey string, amount int64) uint64 { testing.SetOriginCaller(owner) testing.SetRealm(testing.NewUserRealm(owner)) coins := chain.Coins{{"ugnot", amount}} testing.IssueCoins(chain.PackageAddress("gno.land/r/akkadia/beta/game_vault"), coins) testing.SetOriginSend(coins) return DepositParticipation(cross(cur), processID, participationKey, paymentKey) } func (h *vaultTestHelper) DepositEntry(cur realm, owner address, processID uint64, participationKey, entryKey string, amount int64) uint64 { testing.SetOriginCaller(owner) testing.SetRealm(testing.NewUserRealm(owner)) coins := chain.Coins{{"ugnot", amount}} testing.IssueCoins(chain.PackageAddress("gno.land/r/akkadia/beta/game_vault"), coins) testing.SetOriginSend(coins) return DepositEntry(cross(cur), processID, participationKey, entryKey) } func (h *vaultTestHelper) DepositSystemFunding(cur realm, poolID uint64, key string, amount int64) uint64 { testing.SetOriginCaller(h.Operator) testing.SetRealm(testing.NewUserRealm(h.Operator)) coins := chain.Coins{{"ugnot", amount}} testing.IssueCoins(chain.PackageAddress("gno.land/r/akkadia/beta/game_vault"), coins) testing.SetOriginSend(coins) return DepositSystemFunding(cross(cur), poolID, key) }
  20. #20store.gno
  21. #21package game_vault import ( "strconv" "gno.land/p/nt/bptree/v0" ) var ( rewardPools *bptree.BPTree rewardPoolIDs *bptree.BPTree gameWorldRewardPoolIDs *bptree.BPTree participationProcesses *bptree.BPTree participationProcessIDs *bptree.BPTree gameWorldParticipationProcessIDs *bptree.BPTree participationFunds *bptree.BPTree participationFundIDs *bptree.BPTree participationOwnerFundIDs *bptree.BPTree participationGroupFundIDs *bptree.BPTree entryProcesses *bptree.BPTree entryProcessIDs *bptree.BPTree gameWorldEntryProcessIDs *bptree.BPTree entryFunds *bptree.BPTree entryFundIDs *bptree.BPTree entryOwnerFundIDs *bptree.BPTree entryGroupFundIDs *bptree.BPTree systemFundings *bptree.BPTree systemFundingIDs *bptree.BPTree poolSystemFundingIDs *bptree.BPTree rewardDistributions *bptree.BPTree rewardDistributionIDs *bptree.BPTree poolDistributionIDs *bptree.BPTree vaultRefunds *bptree.BPTree vaultRefundIDs *bptree.BPTree poolRefundIDs *bptree.BPTree processedOperationKeys *bptree.BPTree latestRewardPoolID uint64 latestParticipationProcessID uint64 latestParticipationFundID uint64 latestEntryProcessID uint64 latestEntryFundID uint64 latestSystemFundingID uint64 latestRewardDistributionID uint64 latestVaultRefundID uint64 accountedUgnotTotal int64 ) func init() { resetGameVaultState() } func resetGameVaultState() { rewardPools = bptree.NewBPTree32() rewardPoolIDs = bptree.NewBPTree32() gameWorldRewardPoolIDs = bptree.NewBPTree32() participationProcesses = bptree.NewBPTree32() participationProcessIDs = bptree.NewBPTree32() gameWorldParticipationProcessIDs = bptree.NewBPTree32() participationFunds = bptree.NewBPTree32() participationFundIDs = bptree.NewBPTree32() participationOwnerFundIDs = bptree.NewBPTree32() participationGroupFundIDs = bptree.NewBPTree32() entryProcesses = bptree.NewBPTree32() entryProcessIDs = bptree.NewBPTree32() gameWorldEntryProcessIDs = bptree.NewBPTree32() entryFunds = bptree.NewBPTree32() entryFundIDs = bptree.NewBPTree32() entryOwnerFundIDs = bptree.NewBPTree32() entryGroupFundIDs = bptree.NewBPTree32() systemFundings = bptree.NewBPTree32() systemFundingIDs = bptree.NewBPTree32() poolSystemFundingIDs = bptree.NewBPTree32() rewardDistributions = bptree.NewBPTree32() rewardDistributionIDs = bptree.NewBPTree32() poolDistributionIDs = bptree.NewBPTree32() vaultRefunds = bptree.NewBPTree32() vaultRefundIDs = bptree.NewBPTree32() poolRefundIDs = bptree.NewBPTree32() processedOperationKeys = bptree.NewBPTree32() latestRewardPoolID = 0 latestParticipationProcessID = 0 latestParticipationFundID = 0 latestEntryProcessID = 0 latestEntryFundID = 0 latestSystemFundingID = 0 latestRewardDistributionID = 0 latestVaultRefundID = 0 accountedUgnotTotal = 0 distributionBatchMax = distributionBatchMaxDefault } func ownerKey(processID uint64, owner address) string { return strconv.FormatUint(processID, 10) + ":" + owner.String() } func keyedOwnerKey(processID uint64, owner address, key string) string { return ownerKey(processID, owner) + ":" + key } func uint64Key(id uint64) string { // Zero-pad so BPTree key order matches numeric ID order. s := strconv.FormatUint(id, 10) if len(s) >= 20 { return s } return "00000000000000000000"[len(s):] + s } func parseUint64Key(key string) uint64 { id, err := strconv.ParseUint(key, 10, 64) if err != nil { panic("invalid uint64 key: " + key) } return id } func treeGet(store *bptree.BPTree, id uint64) (any, bool) { key := uint64Key(id) if !store.Has(key) { return nil, false } return store.Get(key), true } func treeSet(store *bptree.BPTree, id uint64, value any) { store.Set(uint64Key(id), value) } func stringGet(store *bptree.BPTree, key string) (any, bool) { if !store.Has(key) { return nil, false } return store.Get(key), true } func addStringSetID(store *bptree.BPTree, key string, id uint64) { value := store.Get(key) var ids *bptree.BPTree if value != nil { ids = value.(*bptree.BPTree) } else { ids = bptree.NewBPTree32() store.Set(key, ids) } ids.Set(uint64Key(id), true) } func getStringSet(store *bptree.BPTree, key string) (*bptree.BPTree, bool) { value := store.Get(key) if value == nil { return nil, false } return value.(*bptree.BPTree), true } func getOrCreateIDSet(store *bptree.BPTree, key uint64) *bptree.BPTree { value := store.Get(uint64Key(key)) if value != nil { return value.(*bptree.BPTree) } ids := bptree.NewBPTree32() store.Set(uint64Key(key), ids) return ids } func getIDSet(store *bptree.BPTree, key uint64) (*bptree.BPTree, bool) { value := store.Get(uint64Key(key)) if value == nil { return nil, false } return value.(*bptree.BPTree), true } func listIDs(ids *bptree.BPTree, page, count int) []uint64 { assertPageCount(page, count) result := []uint64{} if ids == nil { return result } ids.ReverseIterateByOffset((page-1)*count, count, func(key string, _ any) bool { result = append(result, parseUint64Key(key)) return false }) return result }
  22. #22validation.gno
  23. #23package game_vault func readQueryCSVEntry(csv string, start int) (string, int, bool) { end := start for end < len(csv) && csv[end] != ',' { end++ } return csv[start:end], end + 1, end == len(csv) }
  24. #24vault.gno

Result log

msg:0,success:true,log:,events:[]