cosmos-sdk/testutil/simsx/environment.go
2025-04-14 17:30:59 +00:00

478 lines
14 KiB
Go

package simsx
import (
"context"
"errors"
"math/rand"
"slices"
"time"
"cosmossdk.io/core/address"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
// helper type for simple bank access
type contextAwareBalanceSource struct {
ctx context.Context
bank BalanceSource
}
func (s contextAwareBalanceSource) SpendableCoins(accAddress sdk.AccAddress) sdk.Coins {
return s.bank.SpendableCoins(s.ctx, accAddress)
}
func (s contextAwareBalanceSource) IsSendEnabledDenom(denom string) bool {
return s.bank.IsSendEnabledDenom(s.ctx, denom)
}
// SimAccount is an extended simtypes.Account
type SimAccount struct {
simtypes.Account
r *rand.Rand
liquidBalance *SimsAccountBalance
bank contextAwareBalanceSource
}
// LiquidBalance spendable balance. This excludes not spendable amounts like staked or vested amounts.
func (a *SimAccount) LiquidBalance() *SimsAccountBalance {
if a.liquidBalance == nil {
a.liquidBalance = NewSimsAccountBalance(a, a.r, a.bank.SpendableCoins(a.Address))
}
return a.liquidBalance
}
// SimsAccountBalance is a helper type for common access methods to balance amounts.
type SimsAccountBalance struct {
sdk.Coins
owner *SimAccount
r *rand.Rand
}
// NewSimsAccountBalance constructor
func NewSimsAccountBalance(o *SimAccount, r *rand.Rand, coins sdk.Coins) *SimsAccountBalance {
return &SimsAccountBalance{Coins: coins, r: r, owner: o}
}
type CoinsFilter interface {
Accept(c sdk.Coins) bool // returns false to reject
}
type CoinsFilterFn func(c sdk.Coins) bool
func (f CoinsFilterFn) Accept(c sdk.Coins) bool {
return f(c)
}
func WithSendEnabledCoins() CoinsFilter {
return statefulCoinsFilterFn(func(s *SimAccount, coins sdk.Coins) bool {
for _, coin := range coins {
if !s.bank.IsSendEnabledDenom(coin.Denom) {
return false
}
}
return true
})
}
// filter with context of SimAccount
type statefulCoinsFilter struct {
s *SimAccount
do func(s *SimAccount, c sdk.Coins) bool
}
// constructor
func statefulCoinsFilterFn(f func(s *SimAccount, c sdk.Coins) bool) CoinsFilter {
return &statefulCoinsFilter{do: f}
}
func (f statefulCoinsFilter) Accept(c sdk.Coins) bool {
if f.s == nil {
panic("account not set")
}
return f.do(f.s, c)
}
func (f *statefulCoinsFilter) visit(s *SimAccount) {
f.s = s
}
var _ visitable = &statefulCoinsFilter{}
type visitable interface {
visit(s *SimAccount)
}
// RandSubsetCoins return random amounts from the current balance. When the coins are empty, skip is called on the reporter.
// The amounts are removed from the liquid balance.
func (b *SimsAccountBalance) RandSubsetCoins(reporter SimulationReporter, filters ...CoinsFilter) sdk.Coins {
amount := b.randomAmount(1, reporter, b.Coins, filters...)
b.Coins = b.Sub(amount...)
if amount.Empty() {
reporter.Skip("got empty amounts")
}
return amount
}
// RandSubsetCoin return random amount from the current balance. When the coins are empty, skip is called on the reporter.
// The amount is removed from the liquid balance.
func (b *SimsAccountBalance) RandSubsetCoin(reporter SimulationReporter, denom string, filters ...CoinsFilter) sdk.Coin {
ok, coin := b.Find(denom)
if !ok {
reporter.Skipf("no such coin: %s", denom)
return sdk.NewCoin(denom, math.ZeroInt())
}
amounts := b.randomAmount(1, reporter, sdk.Coins{coin}, filters...)
if amounts.Empty() {
reporter.Skip("empty coin")
return sdk.NewCoin(denom, math.ZeroInt())
}
b.BlockAmount(amounts[0])
return amounts[0]
}
// BlockAmount returns true when balance is > requested amount and subtracts the amount from the liquid balance
func (b *SimsAccountBalance) BlockAmount(amount sdk.Coin) bool {
ok, coin := b.Find(amount.Denom)
if !ok || !coin.IsPositive() || !coin.IsGTE(amount) {
return false
}
b.Coins = b.Sub(amount)
return true
}
func (b *SimsAccountBalance) randomAmount(retryCount int, reporter SimulationReporter, coins sdk.Coins, filters ...CoinsFilter) sdk.Coins {
if retryCount < 0 || b.Empty() {
reporter.Skip("failed to find matching amount")
return sdk.Coins{}
}
amount := simtypes.RandSubsetCoins(b.r, coins)
for _, filter := range filters {
if f, ok := filter.(visitable); ok {
f.visit(b.owner)
}
if !filter.Accept(amount) {
return b.randomAmount(retryCount-1, reporter, coins, filters...)
}
}
return amount
}
func (b *SimsAccountBalance) RandFees() sdk.Coins {
amount, err := simtypes.RandomFees(b.r, sdk.Context{}, b.Coins)
if err != nil {
return sdk.Coins{}
}
return amount
}
type SimAccountFilter interface {
// Accept returns true to accept the account or false to reject
Accept(a SimAccount) bool
}
type SimAccountFilterFn func(a SimAccount) bool
func (s SimAccountFilterFn) Accept(a SimAccount) bool {
return s(a)
}
func ExcludeAccounts(others ...SimAccount) SimAccountFilter {
return SimAccountFilterFn(func(a SimAccount) bool {
return !slices.ContainsFunc(others, func(o SimAccount) bool {
return a.Address.Equals(o.Address)
})
})
}
// UniqueAccounts returns a stateful filter that rejects duplicate accounts.
// It uses a map to keep track of accounts that have been processed.
// If an account exists in the map, the filter function returns false
// to reject a duplicate, else it adds the account to the map and returns true.
//
// Example usage:
//
// uniqueAccountsFilter := simsx.UniqueAccounts()
//
// for {
// from := testData.AnyAccount(reporter, uniqueAccountsFilter)
// //... rest of the loop
// }
func UniqueAccounts() SimAccountFilter {
idx := make(map[string]struct{})
return SimAccountFilterFn(func(a SimAccount) bool {
if _, ok := idx[a.AddressBech32]; ok {
return false
}
idx[a.AddressBech32] = struct{}{}
return true
})
}
func ExcludeAddresses(addrs ...string) SimAccountFilter {
return SimAccountFilterFn(func(a SimAccount) bool {
return !slices.Contains(addrs, a.AddressBech32)
})
}
func WithDenomBalance(denom string) SimAccountFilter {
return SimAccountFilterFn(func(a SimAccount) bool {
return a.LiquidBalance().AmountOf(denom).IsPositive()
})
}
func WithLiquidBalanceGTE(amount ...sdk.Coin) SimAccountFilter {
return SimAccountFilterFn(func(a SimAccount) bool {
return a.LiquidBalance().IsAllGTE(amount)
})
}
// WithSpendableBalance Filters for liquid token but send may not be enabled for all or any
func WithSpendableBalance() SimAccountFilter {
return SimAccountFilterFn(func(a SimAccount) bool {
return !a.LiquidBalance().Empty()
})
}
type ModuleAccountSource interface {
GetModuleAddress(moduleName string) sdk.AccAddress
}
// BalanceSource is an interface for retrieving balance-related information for a given account.
type BalanceSource interface {
SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins
IsSendEnabledDenom(ctx context.Context, denom string) bool
}
// ChainDataSource provides common sims test data and helper methods
type ChainDataSource struct {
r *rand.Rand
addressToAccountsPosIndex map[string]int
accounts []SimAccount
accountSource ModuleAccountSource
addressCodec address.Codec
bank contextAwareBalanceSource
}
// NewChainDataSource constructor
func NewChainDataSource(
ctx context.Context,
r *rand.Rand,
ak ModuleAccountSource,
bk BalanceSource,
codec address.Codec,
oldSimAcc ...simtypes.Account,
) *ChainDataSource {
if len(oldSimAcc) == 0 {
panic("empty accounts")
}
acc := make([]SimAccount, len(oldSimAcc))
index := make(map[string]int, len(oldSimAcc))
bank := contextAwareBalanceSource{ctx: ctx, bank: bk}
for i, a := range oldSimAcc {
acc[i] = SimAccount{Account: a, r: r, bank: bank}
index[a.AddressBech32] = i
if a.AddressBech32 == "" {
panic("test account has empty bech32 address")
}
}
return &ChainDataSource{
r: r,
accountSource: ak,
addressCodec: codec,
accounts: acc,
bank: bank,
addressToAccountsPosIndex: index,
}
}
// AnyAccount returns a random SimAccount matching the filter criteria. Module accounts are excluded.
// In case of an error or no matching account was found with 1 retry, the reporter is set to skip and an empty value is returned.
func (c *ChainDataSource) AnyAccount(r SimulationReporter, filters ...SimAccountFilter) SimAccount {
acc := c.AnyAccountN(1, r, filters...)
return acc
}
// AnyAccountN returns a random SimAccount matching the filter criteria with given number of retries. Module accounts are excluded.
// In case of an error or no matching account found, the reporter is set to skip and an empty value is returned.
func (c *ChainDataSource) AnyAccountN(retries int, r SimulationReporter, filters ...SimAccountFilter) SimAccount {
acc := c.randomAccount(r, retries, filters...)
return acc
}
// GetAccountbyAccAddr return SimAccount with given binary address. Reporter skip flag is set when not found.
func (c ChainDataSource) GetAccountbyAccAddr(reporter SimulationReporter, addr sdk.AccAddress) SimAccount {
if len(addr) == 0 {
reporter.Skip("can not find account for empty address")
return c.nullAccount()
}
addrStr, err := c.addressCodec.BytesToString(addr)
if err != nil {
reporter.Skipf("can not convert account address to string: %s", err)
return c.nullAccount()
}
return c.GetAccount(reporter, addrStr)
}
func (c ChainDataSource) HasAccount(addr string) bool {
_, ok := c.addressToAccountsPosIndex[addr]
return ok
}
// GetAccount return SimAccount with given bench32 address. Reporter skip flag is set when not found.
func (c ChainDataSource) GetAccount(reporter SimulationReporter, addr string) SimAccount {
pos, ok := c.addressToAccountsPosIndex[addr]
if !ok {
reporter.Skipf("no account: %s", addr)
return c.nullAccount()
}
return c.accounts[pos]
}
func (c *ChainDataSource) randomAccount(reporter SimulationReporter, retryCount int, filters ...SimAccountFilter) SimAccount {
if retryCount < 0 {
reporter.Skip("failed to find a matching account")
return c.nullAccount()
}
idx := c.r.Intn(len(c.accounts))
acc := c.accounts[idx]
for _, filter := range filters {
if !filter.Accept(acc) {
return c.randomAccount(reporter, retryCount-1, filters...)
}
}
return acc
}
// create null object
func (c ChainDataSource) nullAccount() SimAccount {
return SimAccount{
Account: simtypes.Account{},
r: c.r,
liquidBalance: &SimsAccountBalance{},
bank: c.accounts[0].bank,
}
}
func (c *ChainDataSource) ModuleAccountAddress(reporter SimulationReporter, moduleName string) string {
acc := c.accountSource.GetModuleAddress(moduleName)
if acc == nil {
reporter.Skipf("unknown module account: %s", moduleName)
return ""
}
res, err := c.addressCodec.BytesToString(acc)
if err != nil {
reporter.Skipf("failed to encode module address: %s", err)
return ""
}
return res
}
func (c *ChainDataSource) AddressCodec() address.Codec {
return c.addressCodec
}
func (c *ChainDataSource) Rand() *XRand {
return &XRand{c.r}
}
func (c *ChainDataSource) IsSendEnabledDenom(denom string) bool {
return c.bank.IsSendEnabledDenom(denom)
}
// AllAccounts returns all accounts in legacy format
func (c *ChainDataSource) AllAccounts() []simtypes.Account {
return Collect(c.accounts, func(a SimAccount) simtypes.Account { return a.Account })
}
func (c *ChainDataSource) AccountsCount() int {
return len(c.accounts)
}
// AccountAt return SimAccount within the accounts slice. Reporter skip flag is set when boundaries are exceeded.
func (c *ChainDataSource) AccountAt(reporter SimulationReporter, i int) SimAccount {
if i > len(c.accounts) {
reporter.Skipf("account index out of range: %d", i)
return c.nullAccount()
}
return c.accounts[i]
}
type XRand struct {
*rand.Rand
}
// NewXRand constructor
func NewXRand(rand *rand.Rand) *XRand {
return &XRand{Rand: rand}
}
func (r *XRand) StringN(max int) string {
return simtypes.RandStringOfLength(r.Rand, max)
}
func (r *XRand) SubsetCoins(src sdk.Coins) sdk.Coins {
return simtypes.RandSubsetCoins(r.Rand, src)
}
// Coin return one coin from the list
func (r *XRand) Coin(src sdk.Coins) sdk.Coin {
return src[r.Intn(len(src))]
}
func (r *XRand) DecN(max math.LegacyDec) math.LegacyDec {
return simtypes.RandomDecAmount(r.Rand, max)
}
func (r *XRand) IntInRange(min, max int) int {
return r.Intn(max-min) + min
}
// Uint64InRange returns a pseudo-random uint64 number in the range [min, max].
// It panics when min >= max
func (r *XRand) Uint64InRange(min, max uint64) uint64 {
if min >= max {
panic("min must be less than max")
}
return uint64(r.Int63n(int64(max-min)) + int64(min))
}
// Uint32InRange returns a pseudo-random uint32 number in the range [min, max].
// It panics when min >= max
func (r *XRand) Uint32InRange(min, max uint32) uint32 {
if min >= max {
panic("min must be less than max")
}
return uint32(r.Intn(int(max-min))) + min
}
func (r *XRand) PositiveSDKIntn(max math.Int) (math.Int, error) {
return simtypes.RandPositiveInt(r.Rand, max)
}
func (r *XRand) PositiveSDKIntInRange(min, max math.Int) (math.Int, error) {
diff := max.Sub(min)
if !diff.IsPositive() {
return math.Int{}, errors.New("min value must not be greater or equal to max")
}
result, err := r.PositiveSDKIntn(diff)
if err != nil {
return math.Int{}, err
}
return result.Add(min), nil
}
// Timestamp returns a timestamp between Jan 1, 2062 and Jan 1, 2262
func (r *XRand) Timestamp() time.Time {
return simtypes.RandTimestamp(r.Rand)
}
func (r *XRand) Bool() bool {
return r.Intn(100) > 50
}
func (r *XRand) Amount(balance math.Int) math.Int {
return simtypes.RandomAmount(r.Rand, balance)
}