mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 15:55:18 +00:00
Swap Genesis State (#960)
* wip: add swap state persistent to genesis * separate pool record constructors; add tests for json and yaml encoding of record structs * beef up validation checks for state records * fix integration with master - renamed method * add test coverage for basic state array validations * extra test around pool record reserve and id ordering to ensure no regressions in the future * add validations to ensure pool records and share records are unique within the collection types * test genesis json and yaml encoding * validate in genesis that the total shares owned for each pool is equal to the total shares of each pool * update alias * nit lint * test genesis init and export * add migration todo Co-authored-by: Nick DeLuca <nickdeluca08@gmail.com>
This commit is contained in:
parent
013093ecb5
commit
d45fa58f5c
@ -305,5 +305,6 @@ func loadStabilityComMembers() ([]sdk.AccAddress, error) {
|
||||
|
||||
// Swap introduces new v0.15 swap genesis state
|
||||
func Swap() v0_15swap.GenesisState {
|
||||
return v0_15swap.NewGenesisState(v0_15swap.DefaultParams())
|
||||
// TODO add swap genesis state
|
||||
return v0_15swap.DefaultGenesisState()
|
||||
}
|
||||
|
@ -25,7 +25,10 @@ const (
|
||||
ModuleAccountName = types.ModuleAccountName
|
||||
ModuleName = types.ModuleName
|
||||
QuerierRoute = types.QuerierRoute
|
||||
QueryGetDeposits = types.QueryGetDeposits
|
||||
QueryGetParams = types.QueryGetParams
|
||||
QueryGetPool = types.QueryGetPool
|
||||
QueryGetPools = types.QueryGetPools
|
||||
RouterKey = types.RouterKey
|
||||
StoreKey = types.StoreKey
|
||||
)
|
||||
@ -43,6 +46,7 @@ var (
|
||||
NewBasePoolWithExistingShares = types.NewBasePoolWithExistingShares
|
||||
NewDenominatedPool = types.NewDenominatedPool
|
||||
NewDenominatedPoolWithExistingShares = types.NewDenominatedPoolWithExistingShares
|
||||
NewDepositsQueryResult = types.NewDepositsQueryResult
|
||||
NewGenesisState = types.NewGenesisState
|
||||
NewMsgDeposit = types.NewMsgDeposit
|
||||
NewMsgSwapExactForTokens = types.NewMsgSwapExactForTokens
|
||||
@ -50,6 +54,10 @@ var (
|
||||
NewMsgWithdraw = types.NewMsgWithdraw
|
||||
NewParams = types.NewParams
|
||||
NewPoolRecord = types.NewPoolRecord
|
||||
NewPoolRecordFromPool = types.NewPoolRecordFromPool
|
||||
NewPoolStatsQueryResult = types.NewPoolStatsQueryResult
|
||||
NewQueryDepositsParams = types.NewQueryDepositsParams
|
||||
NewQueryPoolParams = types.NewQueryPoolParams
|
||||
NewShareRecord = types.NewShareRecord
|
||||
ParamKeyTable = types.ParamKeyTable
|
||||
PoolID = types.PoolID
|
||||
@ -59,6 +67,8 @@ var (
|
||||
|
||||
// variable aliases
|
||||
DefaultAllowedPools = types.DefaultAllowedPools
|
||||
DefaultPoolRecords = types.DefaultPoolRecords
|
||||
DefaultShareRecords = types.DefaultShareRecords
|
||||
DefaultSwapFee = types.DefaultSwapFee
|
||||
DepositorPoolSharesPrefix = types.DepositorPoolSharesPrefix
|
||||
ErrDeadlineExceeded = types.ErrDeadlineExceeded
|
||||
@ -86,6 +96,8 @@ type (
|
||||
AllowedPools = types.AllowedPools
|
||||
BasePool = types.BasePool
|
||||
DenominatedPool = types.DenominatedPool
|
||||
DepositsQueryResult = types.DepositsQueryResult
|
||||
DepositsQueryResults = types.DepositsQueryResults
|
||||
GenesisState = types.GenesisState
|
||||
MsgDeposit = types.MsgDeposit
|
||||
MsgSwapExactForTokens = types.MsgSwapExactForTokens
|
||||
@ -94,6 +106,13 @@ type (
|
||||
MsgWithdraw = types.MsgWithdraw
|
||||
Params = types.Params
|
||||
PoolRecord = types.PoolRecord
|
||||
PoolRecords = types.PoolRecords
|
||||
PoolStatsQueryResult = types.PoolStatsQueryResult
|
||||
PoolStatsQueryResults = types.PoolStatsQueryResults
|
||||
QueryDepositsParams = types.QueryDepositsParams
|
||||
QueryPoolParams = types.QueryPoolParams
|
||||
ShareRecord = types.ShareRecord
|
||||
ShareRecords = types.ShareRecords
|
||||
SupplyKeeper = types.SupplyKeeper
|
||||
SwapHooks = types.SwapHooks
|
||||
)
|
||||
|
@ -3,9 +3,9 @@ package swap
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/swap/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// InitGenesis initializes story state from genesis file
|
||||
@ -15,10 +15,18 @@ func InitGenesis(ctx sdk.Context, k Keeper, gs types.GenesisState) {
|
||||
}
|
||||
|
||||
k.SetParams(ctx, gs.Params)
|
||||
for _, pr := range gs.PoolRecords {
|
||||
k.SetPool(ctx, pr)
|
||||
}
|
||||
for _, sh := range gs.ShareRecords {
|
||||
k.SetDepositorShares(ctx, sh)
|
||||
}
|
||||
}
|
||||
|
||||
// ExportGenesis exports the genesis state
|
||||
func ExportGenesis(ctx sdk.Context, k Keeper) types.GenesisState {
|
||||
params := k.GetParams(ctx)
|
||||
return types.NewGenesisState(params)
|
||||
pools := k.GetAllPools(ctx)
|
||||
shares := k.GetAllDepositorShares(ctx)
|
||||
return types.NewGenesisState(params, pools, shares)
|
||||
}
|
||||
|
73
x/swap/genesis_test.go
Normal file
73
x/swap/genesis_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
package swap_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kava-labs/kava/x/swap"
|
||||
"github.com/kava-labs/kava/x/swap/testutil"
|
||||
"github.com/kava-labs/kava/x/swap/types"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
type genesisTestSuite struct {
|
||||
testutil.Suite
|
||||
}
|
||||
|
||||
func (suite *genesisTestSuite) Test_InitGenesis_ValidationPanic() {
|
||||
invalidState := types.NewGenesisState(
|
||||
types.Params{
|
||||
SwapFee: sdk.NewDec(-1),
|
||||
},
|
||||
types.PoolRecords{},
|
||||
types.ShareRecords{},
|
||||
)
|
||||
|
||||
suite.Panics(func() {
|
||||
swap.InitGenesis(suite.Ctx, suite.Keeper, invalidState)
|
||||
}, "expected init genesis to panic with invalid state")
|
||||
}
|
||||
|
||||
func (suite *genesisTestSuite) Test_InitAndExportGenesis() {
|
||||
depositor_1, err := sdk.AccAddressFromBech32("kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w")
|
||||
suite.Require().NoError(err)
|
||||
depositor_2, err := sdk.AccAddressFromBech32("kava1esagqd83rhqdtpy5sxhklaxgn58k2m3s3mnpea")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// slices are sorted by key as stored in the data store, so init and export can be compared with equal
|
||||
state := types.NewGenesisState(
|
||||
types.Params{
|
||||
AllowedPools: swap.AllowedPools{swap.NewAllowedPool("ukava", "usdx")},
|
||||
SwapFee: sdk.MustNewDecFromStr("0.00255"),
|
||||
},
|
||||
types.PoolRecords{
|
||||
swap.NewPoolRecord(sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(2e6))), sdk.NewInt(1e6)),
|
||||
swap.NewPoolRecord(sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6))), sdk.NewInt(3e6)),
|
||||
},
|
||||
types.ShareRecords{
|
||||
types.NewShareRecord(depositor_2, "hard/usdx", sdk.NewInt(1e6)),
|
||||
types.NewShareRecord(depositor_1, "ukava/usdx", sdk.NewInt(3e6)),
|
||||
},
|
||||
)
|
||||
|
||||
swap.InitGenesis(suite.Ctx, suite.Keeper, state)
|
||||
suite.Equal(state.Params, suite.Keeper.GetParams(suite.Ctx))
|
||||
|
||||
poolRecord1, _ := suite.Keeper.GetPool(suite.Ctx, "hard/usdx")
|
||||
suite.Equal(state.PoolRecords[0], poolRecord1)
|
||||
poolRecord2, _ := suite.Keeper.GetPool(suite.Ctx, "ukava/usdx")
|
||||
suite.Equal(state.PoolRecords[1], poolRecord2)
|
||||
|
||||
shareRecord1, _ := suite.Keeper.GetDepositorShares(suite.Ctx, depositor_2, "hard/usdx")
|
||||
suite.Equal(state.ShareRecords[0], shareRecord1)
|
||||
shareRecord2, _ := suite.Keeper.GetDepositorShares(suite.Ctx, depositor_1, "ukava/usdx")
|
||||
suite.Equal(state.ShareRecords[1], shareRecord2)
|
||||
|
||||
exportedState := swap.ExportGenesis(suite.Ctx, suite.Keeper)
|
||||
suite.Equal(state, exportedState)
|
||||
}
|
||||
|
||||
func TestGenesisTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(genesisTestSuite))
|
||||
}
|
@ -113,7 +113,7 @@ func (k Keeper) initializePool(ctx sdk.Context, poolID string, depositor sdk.Acc
|
||||
return sdk.Coins{}, sdk.ZeroInt(), err
|
||||
}
|
||||
|
||||
poolRecord := types.NewPoolRecord(pool)
|
||||
poolRecord := types.NewPoolRecordFromPool(pool)
|
||||
shareRecord := types.NewShareRecord(depositor, poolRecord.PoolID, pool.TotalShares())
|
||||
|
||||
k.SetPool(ctx, poolRecord)
|
||||
@ -132,7 +132,7 @@ func (k Keeper) addLiquidityToPool(ctx sdk.Context, record types.PoolRecord, dep
|
||||
|
||||
depositAmount, shares := pool.AddLiquidity(desiredAmount)
|
||||
|
||||
poolRecord := types.NewPoolRecord(pool)
|
||||
poolRecord := types.NewPoolRecordFromPool(pool)
|
||||
|
||||
shareRecord, sharesFound := k.GetDepositorShares(ctx, depositor, poolRecord.PoolID)
|
||||
if sharesFound {
|
||||
|
@ -88,7 +88,7 @@ func (suite *keeperTestSuite) TestPool_Persistance() {
|
||||
|
||||
pool, err := types.NewDenominatedPool(reserves)
|
||||
suite.Nil(err)
|
||||
record := types.NewPoolRecord(pool)
|
||||
record := types.NewPoolRecordFromPool(pool)
|
||||
|
||||
suite.Keeper.SetPool(suite.Ctx, record)
|
||||
|
||||
|
@ -70,7 +70,7 @@ func (suite *querierTestSuite) TestQueryPool() {
|
||||
|
||||
pool, err := types.NewDenominatedPool(sdk.NewCoins(coinA, coinB))
|
||||
suite.Nil(err)
|
||||
poolRecord := types.NewPoolRecord(pool)
|
||||
poolRecord := types.NewPoolRecordFromPool(pool)
|
||||
suite.Keeper.SetPool(suite.Ctx, poolRecord)
|
||||
|
||||
ctx := suite.Ctx.WithIsCheckTx(false)
|
||||
@ -101,12 +101,12 @@ func (suite *querierTestSuite) TestQueryPools() {
|
||||
|
||||
poolAB, err := types.NewDenominatedPool(sdk.NewCoins(coinA, coinB))
|
||||
suite.Nil(err)
|
||||
poolRecordAB := types.NewPoolRecord(poolAB)
|
||||
poolRecordAB := types.NewPoolRecordFromPool(poolAB)
|
||||
suite.Keeper.SetPool(suite.Ctx, poolRecordAB)
|
||||
|
||||
poolAC, err := types.NewDenominatedPool(sdk.NewCoins(coinA, coinC))
|
||||
suite.Nil(err)
|
||||
poolRecordAC := types.NewPoolRecord(poolAC)
|
||||
poolRecordAC := types.NewPoolRecordFromPool(poolAC)
|
||||
suite.Keeper.SetPool(suite.Ctx, poolRecordAC)
|
||||
|
||||
// Build a map of pools to compare to query results
|
||||
@ -142,7 +142,7 @@ func (suite *querierTestSuite) TestQueryDeposit() {
|
||||
coinB := sdk.NewCoin("usdx", sdk.NewInt(200))
|
||||
pool, err := types.NewDenominatedPool(sdk.NewCoins(coinA, coinB))
|
||||
suite.Nil(err)
|
||||
poolRecord := types.NewPoolRecord(pool)
|
||||
poolRecord := types.NewPoolRecordFromPool(pool)
|
||||
suite.Keeper.SetPool(suite.Ctx, poolRecord)
|
||||
|
||||
// Deposit into pool
|
||||
|
@ -96,7 +96,7 @@ func (k Keeper) commitSwap(
|
||||
feePaid sdk.Coin,
|
||||
exactDirection string,
|
||||
) error {
|
||||
k.SetPool(ctx, types.NewPoolRecord(pool))
|
||||
k.SetPool(ctx, types.NewPoolRecordFromPool(pool))
|
||||
|
||||
if err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, requester, types.ModuleAccountName, sdk.NewCoins(swapInput)); err != nil {
|
||||
return err
|
||||
|
@ -78,7 +78,7 @@ func (k Keeper) updatePool(ctx sdk.Context, poolID string, pool *types.Denominat
|
||||
if pool.TotalShares().IsZero() {
|
||||
k.DeletePool(ctx, poolID)
|
||||
} else {
|
||||
k.SetPool(ctx, types.NewPoolRecord(pool))
|
||||
k.SetPool(ctx, types.NewPoolRecordFromPool(pool))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,37 @@
|
||||
package types
|
||||
|
||||
import "bytes"
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
type poolShares struct {
|
||||
totalShares sdk.Int
|
||||
totalSharesOwned sdk.Int
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultPoolRecords is used to set default records in default genesis state
|
||||
DefaultPoolRecords = PoolRecords{}
|
||||
// DefaultShareRecords is used to set default records in default genesis state
|
||||
DefaultShareRecords = ShareRecords{}
|
||||
)
|
||||
|
||||
// GenesisState is the state that must be provided at genesis.
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
PoolRecords `json:"pool_records" yaml:"pool_records"`
|
||||
ShareRecords `json:"share_records" yaml:"share_records"`
|
||||
}
|
||||
|
||||
// NewGenesisState creates a new genesis state.
|
||||
func NewGenesisState(params Params) GenesisState {
|
||||
func NewGenesisState(params Params, poolRecords PoolRecords, shareRecords ShareRecords) GenesisState {
|
||||
return GenesisState{
|
||||
Params: params,
|
||||
Params: params,
|
||||
PoolRecords: poolRecords,
|
||||
ShareRecords: shareRecords,
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +40,38 @@ func (gs GenesisState) Validate() error {
|
||||
if err := gs.Params.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gs.PoolRecords.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gs.ShareRecords.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalShares := make(map[string]poolShares)
|
||||
for _, pr := range gs.PoolRecords {
|
||||
totalShares[pr.PoolID] = poolShares{
|
||||
totalShares: pr.TotalShares,
|
||||
totalSharesOwned: sdk.ZeroInt(),
|
||||
}
|
||||
}
|
||||
for _, sr := range gs.ShareRecords {
|
||||
if shares, found := totalShares[sr.PoolID]; found {
|
||||
shares.totalSharesOwned = shares.totalSharesOwned.Add(sr.SharesOwned)
|
||||
totalShares[sr.PoolID] = shares
|
||||
} else {
|
||||
totalShares[sr.PoolID] = poolShares{
|
||||
totalShares: sdk.ZeroInt(),
|
||||
totalSharesOwned: sr.SharesOwned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for poolID, ps := range totalShares {
|
||||
if !ps.totalShares.Equal(ps.totalSharesOwned) {
|
||||
return fmt.Errorf("total depositor shares %s not equal to pool '%s' total shares %s", ps.totalSharesOwned.String(), poolID, ps.totalShares.String())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -26,6 +79,8 @@ func (gs GenesisState) Validate() error {
|
||||
func DefaultGenesisState() GenesisState {
|
||||
return NewGenesisState(
|
||||
DefaultParams(),
|
||||
DefaultPoolRecords,
|
||||
DefaultShareRecords,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/kava-labs/kava/x/swap/types"
|
||||
@ -8,6 +9,7 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestGenesis_Default(t *testing.T) {
|
||||
@ -133,8 +135,8 @@ func TestGenesis_Equal(t *testing.T) {
|
||||
sdk.MustNewDecFromStr("0.85"),
|
||||
}
|
||||
|
||||
genesisA := types.GenesisState{params}
|
||||
genesisB := types.GenesisState{params}
|
||||
genesisA := types.GenesisState{params, types.DefaultPoolRecords, types.DefaultShareRecords}
|
||||
genesisB := types.GenesisState{params, types.DefaultPoolRecords, types.DefaultShareRecords}
|
||||
|
||||
assert.True(t, genesisA.Equal(genesisB))
|
||||
}
|
||||
@ -147,17 +149,17 @@ func TestGenesis_NotEqual(t *testing.T) {
|
||||
|
||||
// Base params
|
||||
genesisAParams := baseParams
|
||||
genesisA := types.GenesisState{genesisAParams}
|
||||
genesisA := types.GenesisState{genesisAParams, types.DefaultPoolRecords, types.DefaultShareRecords}
|
||||
|
||||
// Different swap fee
|
||||
genesisBParams := baseParams
|
||||
genesisBParams.SwapFee = sdk.MustNewDecFromStr("0.84")
|
||||
genesisB := types.GenesisState{genesisBParams}
|
||||
genesisB := types.GenesisState{genesisBParams, types.DefaultPoolRecords, types.DefaultShareRecords}
|
||||
|
||||
// Different pairs
|
||||
genesisCParams := baseParams
|
||||
genesisCParams.AllowedPools = types.NewAllowedPools(types.NewAllowedPool("ukava", "hard"))
|
||||
genesisC := types.GenesisState{genesisCParams}
|
||||
genesisC := types.GenesisState{genesisCParams, types.DefaultPoolRecords, types.DefaultShareRecords}
|
||||
|
||||
// A and B have different swap fees
|
||||
assert.False(t, genesisA.Equal(genesisB))
|
||||
@ -166,3 +168,229 @@ func TestGenesis_NotEqual(t *testing.T) {
|
||||
// A and B and different swap fees and pair token B denoms
|
||||
assert.False(t, genesisA.Equal(genesisB))
|
||||
}
|
||||
|
||||
func TestGenesis_JSONEncoding(t *testing.T) {
|
||||
raw := `{
|
||||
"params": {
|
||||
"allowed_pools": [
|
||||
{
|
||||
"token_a": "ukava",
|
||||
"token_b": "usdx"
|
||||
},
|
||||
{
|
||||
"token_a": "hard",
|
||||
"token_b": "busd"
|
||||
}
|
||||
],
|
||||
"swap_fee": "0.003000000000000000"
|
||||
},
|
||||
"pool_records": [
|
||||
{
|
||||
"pool_id": "ukava/usdx",
|
||||
"reserves_a": { "denom": "ukava", "amount": "1000000" },
|
||||
"reserves_b": { "denom": "usdx", "amount": "5000000" },
|
||||
"total_shares": "3000000"
|
||||
},
|
||||
{
|
||||
"pool_id": "hard/usdx",
|
||||
"reserves_a": { "denom": "ukava", "amount": "1000000" },
|
||||
"reserves_b": { "denom": "usdx", "amount": "2000000" },
|
||||
"total_shares": "2000000"
|
||||
}
|
||||
],
|
||||
"share_records": [
|
||||
{
|
||||
"depositor": "kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w",
|
||||
"pool_id": "ukava/usdx",
|
||||
"shares_owned": "100000"
|
||||
},
|
||||
{
|
||||
"depositor": "kava1esagqd83rhqdtpy5sxhklaxgn58k2m3s3mnpea",
|
||||
"pool_id": "hard/usdx",
|
||||
"shares_owned": "200000"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
var state types.GenesisState
|
||||
err := json.Unmarshal([]byte(raw), &state)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(state.Params.AllowedPools))
|
||||
assert.Equal(t, sdk.MustNewDecFromStr("0.003"), state.Params.SwapFee)
|
||||
assert.Equal(t, 2, len(state.PoolRecords))
|
||||
assert.Equal(t, 2, len(state.ShareRecords))
|
||||
}
|
||||
|
||||
func TestGenesis_YAMLEncoding(t *testing.T) {
|
||||
expected := `params:
|
||||
allowed_pools:
|
||||
- token_a: ukava
|
||||
token_b: usdx
|
||||
- token_a: hard
|
||||
token_b: busd
|
||||
swap_fee: "0.003000000000000000"
|
||||
pool_records:
|
||||
- pool_id: ukava/usdx
|
||||
reserves_a:
|
||||
denom: ukava
|
||||
amount: "1000000"
|
||||
reserves_b:
|
||||
denom: usdx
|
||||
amount: "5000000"
|
||||
total_shares: "3000000"
|
||||
- pool_id: hard/usdx
|
||||
reserves_a:
|
||||
denom: hard
|
||||
amount: "1000000"
|
||||
reserves_b:
|
||||
denom: usdx
|
||||
amount: "2000000"
|
||||
total_shares: "1500000"
|
||||
share_records:
|
||||
- depositor: kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w
|
||||
pool_id: ukava/usdx
|
||||
shares_owned: "100000"
|
||||
- depositor: kava1esagqd83rhqdtpy5sxhklaxgn58k2m3s3mnpea
|
||||
pool_id: hard/usdx
|
||||
shares_owned: "200000"
|
||||
`
|
||||
|
||||
depositor_1, err := sdk.AccAddressFromBech32("kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w")
|
||||
require.NoError(t, err)
|
||||
depositor_2, err := sdk.AccAddressFromBech32("kava1esagqd83rhqdtpy5sxhklaxgn58k2m3s3mnpea")
|
||||
require.NoError(t, err)
|
||||
|
||||
state := types.NewGenesisState(
|
||||
types.NewParams(
|
||||
types.NewAllowedPools(
|
||||
types.NewAllowedPool("ukava", "usdx"),
|
||||
types.NewAllowedPool("hard", "busd"),
|
||||
),
|
||||
sdk.MustNewDecFromStr("0.003"),
|
||||
),
|
||||
types.PoolRecords{
|
||||
types.NewPoolRecord(sdk.NewCoins(ukava(1e6), usdx(5e6)), i(3e6)),
|
||||
types.NewPoolRecord(sdk.NewCoins(hard(1e6), usdx(2e6)), i(15e5)),
|
||||
},
|
||||
types.ShareRecords{
|
||||
types.NewShareRecord(depositor_1, "ukava/usdx", i(1e5)),
|
||||
types.NewShareRecord(depositor_2, "hard/usdx", i(2e5)),
|
||||
},
|
||||
)
|
||||
|
||||
data, err := yaml.Marshal(state)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestGenesis_ValidatePoolRecords(t *testing.T) {
|
||||
invalidPoolRecord := types.NewPoolRecord(sdk.NewCoins(ukava(1e6), usdx(5e6)), i(-1))
|
||||
|
||||
state := types.NewGenesisState(
|
||||
types.DefaultParams(),
|
||||
types.PoolRecords{invalidPoolRecord},
|
||||
types.ShareRecords{},
|
||||
)
|
||||
|
||||
assert.Error(t, state.Validate())
|
||||
}
|
||||
|
||||
func TestGenesis_ValidateShareRecords(t *testing.T) {
|
||||
depositor, err := sdk.AccAddressFromBech32("kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w")
|
||||
require.NoError(t, err)
|
||||
|
||||
invalidShareRecord := types.NewShareRecord(depositor, "", i(-1))
|
||||
|
||||
state := types.NewGenesisState(
|
||||
types.DefaultParams(),
|
||||
types.PoolRecords{},
|
||||
types.ShareRecords{invalidShareRecord},
|
||||
)
|
||||
|
||||
assert.Error(t, state.Validate())
|
||||
}
|
||||
|
||||
func TestGenesis_Validate_PoolShareIntegration(t *testing.T) {
|
||||
depositor_1, err := sdk.AccAddressFromBech32("kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w")
|
||||
require.NoError(t, err)
|
||||
depositor_2, err := sdk.AccAddressFromBech32("kava1esagqd83rhqdtpy5sxhklaxgn58k2m3s3mnpea")
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
poolRecords types.PoolRecords
|
||||
shareRecords types.ShareRecords
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "single pool record, zero share records",
|
||||
poolRecords: types.PoolRecords{
|
||||
types.NewPoolRecord(sdk.NewCoins(ukava(1e6), usdx(5e6)), i(3e6)),
|
||||
},
|
||||
shareRecords: types.ShareRecords{},
|
||||
expectedErr: "total depositor shares 0 not equal to pool 'ukava/usdx' total shares 3000000",
|
||||
},
|
||||
{
|
||||
name: "zero pool records, one share record",
|
||||
poolRecords: types.PoolRecords{},
|
||||
shareRecords: types.ShareRecords{
|
||||
types.NewShareRecord(depositor_1, "ukava/usdx", i(5e6)),
|
||||
},
|
||||
expectedErr: "total depositor shares 5000000 not equal to pool 'ukava/usdx' total shares 0",
|
||||
},
|
||||
{
|
||||
name: "one pool record, one share record",
|
||||
poolRecords: types.PoolRecords{
|
||||
types.NewPoolRecord(sdk.NewCoins(ukava(1e6), usdx(5e6)), i(3e6)),
|
||||
},
|
||||
shareRecords: types.ShareRecords{
|
||||
types.NewShareRecord(depositor_1, "ukava/usdx", i(15e5)),
|
||||
},
|
||||
expectedErr: "total depositor shares 1500000 not equal to pool 'ukava/usdx' total shares 3000000",
|
||||
},
|
||||
{
|
||||
name: "more than one pool records, more than one share record",
|
||||
poolRecords: types.PoolRecords{
|
||||
types.NewPoolRecord(sdk.NewCoins(ukava(1e6), usdx(5e6)), i(3e6)),
|
||||
types.NewPoolRecord(sdk.NewCoins(hard(1e6), usdx(2e6)), i(2e6)),
|
||||
},
|
||||
shareRecords: types.ShareRecords{
|
||||
types.NewShareRecord(depositor_1, "ukava/usdx", i(15e5)),
|
||||
types.NewShareRecord(depositor_2, "ukava/usdx", i(15e5)),
|
||||
types.NewShareRecord(depositor_1, "hard/usdx", i(1e6)),
|
||||
},
|
||||
expectedErr: "total depositor shares 1000000 not equal to pool 'hard/usdx' total shares 2000000",
|
||||
},
|
||||
{
|
||||
name: "valid case with many pool records and share records",
|
||||
poolRecords: types.PoolRecords{
|
||||
types.NewPoolRecord(sdk.NewCoins(ukava(1e6), usdx(5e6)), i(3e6)),
|
||||
types.NewPoolRecord(sdk.NewCoins(hard(1e6), usdx(2e6)), i(2e6)),
|
||||
types.NewPoolRecord(sdk.NewCoins(hard(7e6), ukava(10e6)), i(8e6)),
|
||||
},
|
||||
shareRecords: types.ShareRecords{
|
||||
types.NewShareRecord(depositor_1, "ukava/usdx", i(15e5)),
|
||||
types.NewShareRecord(depositor_2, "ukava/usdx", i(15e5)),
|
||||
types.NewShareRecord(depositor_1, "hard/usdx", i(2e6)),
|
||||
types.NewShareRecord(depositor_1, "hard/ukava", i(3e6)),
|
||||
types.NewShareRecord(depositor_2, "hard/ukava", i(5e6)),
|
||||
},
|
||||
expectedErr: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
state := types.NewGenesisState(types.DefaultParams(), tc.poolRecords, tc.shareRecords)
|
||||
err := state.Validate()
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
@ -25,20 +27,32 @@ func PoolID(denomA string, denomB string) string {
|
||||
// and is used to store the state of a denominated pool
|
||||
type PoolRecord struct {
|
||||
// primary key
|
||||
PoolID string
|
||||
PoolID string `json:"pool_id" yaml:"pool_id"`
|
||||
ReservesA sdk.Coin `json:"reserves_a" yaml:"reserves_a"`
|
||||
ReservesB sdk.Coin `json:"reserves_b" yaml:"reserves_b"`
|
||||
TotalShares sdk.Int `json:"total_shares" yaml:"total_shares"`
|
||||
}
|
||||
|
||||
// Reserves returns the total reserves for a pool
|
||||
func (p PoolRecord) Reserves() sdk.Coins {
|
||||
return sdk.NewCoins(p.ReservesA, p.ReservesB)
|
||||
// NewPoolRecord takes reserve coins and total shares, returning
|
||||
// a new pool record with a id
|
||||
func NewPoolRecord(reserves sdk.Coins, totalShares sdk.Int) PoolRecord {
|
||||
if len(reserves) != 2 {
|
||||
panic("reserves must have two denominations")
|
||||
}
|
||||
|
||||
poolID := PoolIDFromCoins(reserves)
|
||||
|
||||
return PoolRecord{
|
||||
PoolID: poolID,
|
||||
ReservesA: reserves[0],
|
||||
ReservesB: reserves[1],
|
||||
TotalShares: totalShares,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPoolRecord takes a pointer to a denominated pool and returns a
|
||||
// NewPoolRecordFromPool takes a pointer to a denominated pool and returns a
|
||||
// pool record for storage in state.
|
||||
func NewPoolRecord(pool *DenominatedPool) PoolRecord {
|
||||
func NewPoolRecordFromPool(pool *DenominatedPool) PoolRecord {
|
||||
reserves := pool.Reserves()
|
||||
poolID := PoolIDFromCoins(reserves)
|
||||
|
||||
@ -50,9 +64,65 @@ func NewPoolRecord(pool *DenominatedPool) PoolRecord {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate performs basic validation checks of the record data
|
||||
func (p PoolRecord) Validate() error {
|
||||
if p.PoolID == "" {
|
||||
return errors.New("poolID must be set")
|
||||
}
|
||||
|
||||
tokens := strings.Split(p.PoolID, "/")
|
||||
if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" || tokens[1] < tokens[0] || tokens[0] == tokens[1] {
|
||||
return fmt.Errorf("poolID '%s' is invalid", p.PoolID)
|
||||
}
|
||||
if sdk.ValidateDenom(tokens[0]) != nil || sdk.ValidateDenom(tokens[1]) != nil {
|
||||
return fmt.Errorf("poolID '%s' is invalid", p.PoolID)
|
||||
}
|
||||
if tokens[0] != p.ReservesA.Denom || tokens[1] != p.ReservesB.Denom {
|
||||
return fmt.Errorf("poolID '%s' does not match reserves", p.PoolID)
|
||||
}
|
||||
|
||||
if !p.ReservesA.IsPositive() {
|
||||
return fmt.Errorf("pool '%s' has invalid reserves: %s", p.PoolID, p.ReservesA)
|
||||
}
|
||||
|
||||
if !p.ReservesB.IsPositive() {
|
||||
return fmt.Errorf("pool '%s' has invalid reserves: %s", p.PoolID, p.ReservesB)
|
||||
}
|
||||
|
||||
if !p.TotalShares.IsPositive() {
|
||||
return fmt.Errorf("pool '%s' has invalid total shares: %s", p.PoolID, p.TotalShares)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reserves returns the total reserves for a pool
|
||||
func (p PoolRecord) Reserves() sdk.Coins {
|
||||
return sdk.NewCoins(p.ReservesA, p.ReservesB)
|
||||
}
|
||||
|
||||
// PoolRecords is a slice of PoolRecord
|
||||
type PoolRecords []PoolRecord
|
||||
|
||||
// Validate performs basic validation checks on all records in the slice
|
||||
func (prs PoolRecords) Validate() error {
|
||||
seenPoolIDs := make(map[string]bool)
|
||||
|
||||
for _, p := range prs {
|
||||
if err := p.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if seenPoolIDs[p.PoolID] {
|
||||
return fmt.Errorf("duplicate poolID '%s'", p.PoolID)
|
||||
}
|
||||
|
||||
seenPoolIDs[p.PoolID] = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShareRecord stores the shares owned for a depositor and pool
|
||||
type ShareRecord struct {
|
||||
// primary key
|
||||
@ -72,5 +142,54 @@ func NewShareRecord(depositor sdk.AccAddress, poolID string, sharesOwned sdk.Int
|
||||
}
|
||||
}
|
||||
|
||||
// Validate performs basic validation checks of the record data
|
||||
func (sr ShareRecord) Validate() error {
|
||||
if sr.PoolID == "" {
|
||||
return errors.New("poolID must be set")
|
||||
}
|
||||
|
||||
tokens := strings.Split(sr.PoolID, "/")
|
||||
if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" || tokens[1] < tokens[0] || tokens[0] == tokens[1] {
|
||||
return fmt.Errorf("poolID '%s' is invalid", sr.PoolID)
|
||||
}
|
||||
if sdk.ValidateDenom(tokens[0]) != nil || sdk.ValidateDenom(tokens[1]) != nil {
|
||||
return fmt.Errorf("poolID '%s' is invalid", sr.PoolID)
|
||||
}
|
||||
|
||||
if sr.Depositor.Empty() {
|
||||
return fmt.Errorf("share record cannot have empty depositor address")
|
||||
}
|
||||
|
||||
if !sr.SharesOwned.IsPositive() {
|
||||
return fmt.Errorf("depositor '%s' and pool '%s' has invalid total shares: %s", sr.Depositor.String(), sr.PoolID, sr.SharesOwned.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShareRecords is a slice of ShareRecord
|
||||
type ShareRecords []ShareRecord
|
||||
|
||||
// Validate performs basic validation checks on all records in the slice
|
||||
func (srs ShareRecords) Validate() error {
|
||||
seenDepositors := make(map[string]map[string]bool)
|
||||
|
||||
for _, sr := range srs {
|
||||
if err := sr.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if seenPools, found := seenDepositors[sr.Depositor.String()]; found {
|
||||
if seenPools[sr.PoolID] {
|
||||
return fmt.Errorf("duplicate depositor '%s' and poolID '%s'", sr.Depositor.String(), sr.PoolID)
|
||||
}
|
||||
seenPools[sr.PoolID] = true
|
||||
} else {
|
||||
seenPools := make(map[string]bool)
|
||||
seenPools[sr.PoolID] = true
|
||||
seenDepositors[sr.Depositor.String()] = seenPools
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
types "github.com/kava-labs/kava/x/swap/types"
|
||||
@ -8,6 +9,7 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestState_PoolID(t *testing.T) {
|
||||
@ -37,17 +39,308 @@ func TestState_PoolID(t *testing.T) {
|
||||
|
||||
func TestState_NewPoolRecord(t *testing.T) {
|
||||
reserves := sdk.NewCoins(usdx(50e6), ukava(10e6))
|
||||
totalShares := sdk.NewInt(30e6)
|
||||
|
||||
poolRecord := types.NewPoolRecord(reserves, totalShares)
|
||||
|
||||
assert.Equal(t, reserves[0], poolRecord.ReservesA)
|
||||
assert.Equal(t, reserves[1], poolRecord.ReservesB)
|
||||
assert.Equal(t, reserves, poolRecord.Reserves())
|
||||
assert.Equal(t, totalShares, poolRecord.TotalShares)
|
||||
|
||||
assert.PanicsWithValue(t, "reserves must have two denominations", func() {
|
||||
reserves := sdk.NewCoins(ukava(10e6))
|
||||
_ = types.NewPoolRecord(reserves, totalShares)
|
||||
}, "expected panic with 1 coin in reserves")
|
||||
|
||||
assert.PanicsWithValue(t, "reserves must have two denominations", func() {
|
||||
reserves := sdk.NewCoins(ukava(10e6), hard(1e6), usdx(20e6))
|
||||
_ = types.NewPoolRecord(reserves, totalShares)
|
||||
}, "expected panic with 3 coins in reserves")
|
||||
}
|
||||
|
||||
func TestState_NewPoolRecordFromPool(t *testing.T) {
|
||||
reserves := sdk.NewCoins(usdx(50e6), ukava(10e6))
|
||||
|
||||
pool, err := types.NewDenominatedPool(reserves)
|
||||
require.NoError(t, err)
|
||||
|
||||
record := types.NewPoolRecord(pool)
|
||||
record := types.NewPoolRecordFromPool(pool)
|
||||
|
||||
assert.Equal(t, types.PoolID("ukava", "usdx"), record.PoolID)
|
||||
assert.Equal(t, ukava(10e6), record.ReservesA)
|
||||
assert.Equal(t, record.ReservesB, usdx(50e6))
|
||||
assert.Equal(t, pool.TotalShares(), record.TotalShares)
|
||||
assert.Equal(t, sdk.NewCoins(ukava(10e6), usdx(50e6)), record.Reserves())
|
||||
assert.Nil(t, record.Validate())
|
||||
}
|
||||
|
||||
func TestState_PoolRecord_JSONEncoding(t *testing.T) {
|
||||
raw := `{
|
||||
"pool_id": "ukava/usdx",
|
||||
"reserves_a": { "denom": "ukava", "amount": "1000000" },
|
||||
"reserves_b": { "denom": "usdx", "amount": "5000000" },
|
||||
"total_shares": "3000000"
|
||||
}`
|
||||
|
||||
var record types.PoolRecord
|
||||
err := json.Unmarshal([]byte(raw), &record)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "ukava/usdx", record.PoolID)
|
||||
assert.Equal(t, ukava(1e6), record.ReservesA)
|
||||
assert.Equal(t, usdx(5e6), record.ReservesB)
|
||||
assert.Equal(t, i(3e6), record.TotalShares)
|
||||
}
|
||||
|
||||
func TestState_PoolRecord_YamlEncoding(t *testing.T) {
|
||||
expected := `pool_id: ukava/usdx
|
||||
reserves_a:
|
||||
denom: ukava
|
||||
amount: "1000000"
|
||||
reserves_b:
|
||||
denom: usdx
|
||||
amount: "5000000"
|
||||
total_shares: "3000000"
|
||||
`
|
||||
record := types.NewPoolRecord(sdk.NewCoins(ukava(1e6), usdx(5e6)), i(3e6))
|
||||
data, err := yaml.Marshal(record)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestState_PoolRecord_Validations(t *testing.T) {
|
||||
validRecord := types.NewPoolRecord(
|
||||
sdk.NewCoins(usdx(500e6), ukava(100e6)),
|
||||
i(300e6),
|
||||
)
|
||||
testCases := []struct {
|
||||
name string
|
||||
poolID string
|
||||
reservesA sdk.Coin
|
||||
reservesB sdk.Coin
|
||||
totalShares sdk.Int
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "empty pool id",
|
||||
poolID: "",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID must be set",
|
||||
},
|
||||
{
|
||||
name: "no poolID tokens",
|
||||
poolID: "ukavausdx",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID 'ukavausdx' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID empty tokens",
|
||||
poolID: "/",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID '/' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID empty token a",
|
||||
poolID: "/usdx",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID '/usdx' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID empty token b",
|
||||
poolID: "ukava/",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID 'ukava/' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID is not sorted",
|
||||
poolID: "usdx/ukava",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID 'usdx/ukava' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID has invalid denom a",
|
||||
poolID: "UKAVA/usdx",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID 'UKAVA/usdx' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID has invalid denom b",
|
||||
poolID: "ukava/USDX",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID 'ukava/USDX' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID has duplicate denoms",
|
||||
poolID: "ukava/ukava",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID 'ukava/ukava' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID does not match reserve A",
|
||||
poolID: "ukava/usdx",
|
||||
reservesA: hard(5e6),
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID 'ukava/usdx' does not match reserves",
|
||||
},
|
||||
{
|
||||
name: "poolID does not match reserve B",
|
||||
poolID: "ukava/usdx",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: hard(5e6),
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "poolID 'ukava/usdx' does not match reserves",
|
||||
},
|
||||
{
|
||||
name: "negative reserve a",
|
||||
poolID: "ukava/usdx",
|
||||
reservesA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "pool 'ukava/usdx' has invalid reserves: -1ukava",
|
||||
},
|
||||
{
|
||||
name: "zero reserve a",
|
||||
poolID: "ukava/usdx",
|
||||
reservesA: sdk.Coin{Denom: "ukava", Amount: sdk.ZeroInt()},
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "pool 'ukava/usdx' has invalid reserves: 0ukava",
|
||||
},
|
||||
{
|
||||
name: "negative reserve b",
|
||||
poolID: "ukava/usdx",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: sdk.Coin{Denom: "usdx", Amount: sdk.NewInt(-1)},
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "pool 'ukava/usdx' has invalid reserves: -1usdx",
|
||||
},
|
||||
{
|
||||
name: "zero reserve b",
|
||||
poolID: "ukava/usdx",
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: sdk.Coin{Denom: "usdx", Amount: sdk.ZeroInt()},
|
||||
totalShares: validRecord.TotalShares,
|
||||
expectedErr: "pool 'ukava/usdx' has invalid reserves: 0usdx",
|
||||
},
|
||||
{
|
||||
name: "negative total shares",
|
||||
poolID: validRecord.PoolID,
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: sdk.NewInt(-1),
|
||||
expectedErr: "pool 'ukava/usdx' has invalid total shares: -1",
|
||||
},
|
||||
{
|
||||
name: "zero total shares",
|
||||
poolID: validRecord.PoolID,
|
||||
reservesA: validRecord.ReservesA,
|
||||
reservesB: validRecord.ReservesB,
|
||||
totalShares: sdk.ZeroInt(),
|
||||
expectedErr: "pool 'ukava/usdx' has invalid total shares: 0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
record := types.PoolRecord{
|
||||
PoolID: tc.poolID,
|
||||
ReservesA: tc.reservesA,
|
||||
ReservesB: tc.reservesB,
|
||||
TotalShares: tc.totalShares,
|
||||
}
|
||||
err := record.Validate()
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestState_PoolRecord_OrderedReserves(t *testing.T) {
|
||||
invalidOrder := types.NewPoolRecord(
|
||||
// force order to not be sorted
|
||||
sdk.Coins{usdx(500e6), ukava(100e6)},
|
||||
i(300e6),
|
||||
)
|
||||
assert.Error(t, invalidOrder.Validate())
|
||||
|
||||
validOrder := types.NewPoolRecord(
|
||||
// force order to not be sorted
|
||||
sdk.Coins{ukava(500e6), usdx(100e6)},
|
||||
i(300e6),
|
||||
)
|
||||
assert.NoError(t, validOrder.Validate())
|
||||
|
||||
record_1 := types.NewPoolRecord(sdk.NewCoins(usdx(500e6), ukava(100e6)), i(300e6))
|
||||
record_2 := types.NewPoolRecord(sdk.NewCoins(ukava(100e6), usdx(500e6)), i(300e6))
|
||||
// ensure no regresssions in NewCoins ordering
|
||||
assert.Equal(t, record_1, record_2)
|
||||
assert.Equal(t, "ukava/usdx", record_1.PoolID)
|
||||
assert.Equal(t, "ukava/usdx", record_2.PoolID)
|
||||
}
|
||||
|
||||
func TestState_PoolRecords_Validation(t *testing.T) {
|
||||
validRecord := types.NewPoolRecord(
|
||||
sdk.NewCoins(usdx(500e6), ukava(100e6)),
|
||||
i(300e6),
|
||||
)
|
||||
|
||||
invalidRecord := types.NewPoolRecord(
|
||||
sdk.NewCoins(usdx(500e6), ukava(100e6)),
|
||||
i(-1),
|
||||
)
|
||||
|
||||
records := types.PoolRecords{
|
||||
validRecord,
|
||||
}
|
||||
assert.NoError(t, records.Validate())
|
||||
|
||||
records = append(records, invalidRecord)
|
||||
err := records.Validate()
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "pool 'ukava/usdx' has invalid total shares: -1")
|
||||
}
|
||||
|
||||
func TestState_PoolRecords_ValidateUniquePools(t *testing.T) {
|
||||
record_1 := types.NewPoolRecord(
|
||||
sdk.NewCoins(usdx(500e6), ukava(100e6)),
|
||||
i(300e6),
|
||||
)
|
||||
|
||||
record_2 := types.NewPoolRecord(
|
||||
sdk.NewCoins(usdx(5000e6), ukava(1000e6)),
|
||||
i(3000e6),
|
||||
)
|
||||
|
||||
record_3 := types.NewPoolRecord(
|
||||
sdk.NewCoins(usdx(5000e6), hard(1000e6)),
|
||||
i(3000e6),
|
||||
)
|
||||
|
||||
validRecords := types.PoolRecords{record_1, record_3}
|
||||
assert.NoError(t, validRecords.Validate())
|
||||
|
||||
invalidRecords := types.PoolRecords{record_1, record_2}
|
||||
assert.EqualError(t, invalidRecords.Validate(), "duplicate poolID 'ukava/usdx'")
|
||||
}
|
||||
|
||||
func TestState_NewShareRecord(t *testing.T) {
|
||||
@ -61,3 +354,205 @@ func TestState_NewShareRecord(t *testing.T) {
|
||||
assert.Equal(t, poolID, record.PoolID)
|
||||
assert.Equal(t, shares, record.SharesOwned)
|
||||
}
|
||||
|
||||
func TestState_ShareRecord_JSONEncoding(t *testing.T) {
|
||||
raw := `{
|
||||
"depositor": "kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w",
|
||||
"pool_id": "ukava/usdx",
|
||||
"shares_owned": "3000000"
|
||||
}`
|
||||
|
||||
var record types.ShareRecord
|
||||
err := json.Unmarshal([]byte(raw), &record)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w", record.Depositor.String())
|
||||
assert.Equal(t, "ukava/usdx", record.PoolID)
|
||||
assert.Equal(t, i(3e6), record.SharesOwned)
|
||||
}
|
||||
|
||||
func TestState_ShareRecord_YamlEncoding(t *testing.T) {
|
||||
expected := `depositor: kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w
|
||||
pool_id: ukava/usdx
|
||||
shares_owned: "3000000"
|
||||
`
|
||||
depositor, err := sdk.AccAddressFromBech32("kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w")
|
||||
require.NoError(t, err)
|
||||
|
||||
record := types.NewShareRecord(depositor, "ukava/usdx", i(3e6))
|
||||
data, err := yaml.Marshal(record)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestState_InvalidShareRecordEmptyDepositor(t *testing.T) {
|
||||
record := types.ShareRecord{
|
||||
Depositor: sdk.AccAddress{},
|
||||
PoolID: types.PoolID("ukava", "usdx"),
|
||||
SharesOwned: sdk.NewInt(1e6),
|
||||
}
|
||||
require.Error(t, record.Validate())
|
||||
}
|
||||
|
||||
func TestState_InvalidShareRecordNegativeShares(t *testing.T) {
|
||||
record := types.ShareRecord{
|
||||
Depositor: sdk.AccAddress("some user"),
|
||||
PoolID: types.PoolID("ukava", "usdx"),
|
||||
SharesOwned: sdk.NewInt(-1e6),
|
||||
}
|
||||
require.Error(t, record.Validate())
|
||||
}
|
||||
|
||||
func TestState_ShareRecord_Validations(t *testing.T) {
|
||||
depositor, err := sdk.AccAddressFromBech32("kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w")
|
||||
require.NoError(t, err)
|
||||
validRecord := types.NewShareRecord(
|
||||
depositor,
|
||||
types.PoolID("ukava", "usdx"),
|
||||
i(30e6),
|
||||
)
|
||||
testCases := []struct {
|
||||
name string
|
||||
depositor sdk.AccAddress
|
||||
poolID string
|
||||
sharesOwned sdk.Int
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "empty pool id",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: "",
|
||||
sharesOwned: validRecord.SharesOwned,
|
||||
expectedErr: "poolID must be set",
|
||||
},
|
||||
{
|
||||
name: "no poolID tokens",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: "ukavausdx",
|
||||
sharesOwned: validRecord.SharesOwned,
|
||||
expectedErr: "poolID 'ukavausdx' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID empty tokens",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: "/",
|
||||
sharesOwned: validRecord.SharesOwned,
|
||||
expectedErr: "poolID '/' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID empty token a",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: "/usdx",
|
||||
sharesOwned: validRecord.SharesOwned,
|
||||
expectedErr: "poolID '/usdx' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID empty token b",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: "ukava/",
|
||||
sharesOwned: validRecord.SharesOwned,
|
||||
expectedErr: "poolID 'ukava/' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID is not sorted",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: "usdx/ukava",
|
||||
sharesOwned: validRecord.SharesOwned,
|
||||
expectedErr: "poolID 'usdx/ukava' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID has invalid denom a",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: "UKAVA/usdx",
|
||||
sharesOwned: validRecord.SharesOwned,
|
||||
expectedErr: "poolID 'UKAVA/usdx' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID has invalid denom b",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: "ukava/USDX",
|
||||
sharesOwned: validRecord.SharesOwned,
|
||||
expectedErr: "poolID 'ukava/USDX' is invalid",
|
||||
},
|
||||
{
|
||||
name: "poolID has duplicate denoms",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: "ukava/ukava",
|
||||
sharesOwned: validRecord.SharesOwned,
|
||||
expectedErr: "poolID 'ukava/ukava' is invalid",
|
||||
},
|
||||
{
|
||||
name: "negative total shares",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: validRecord.PoolID,
|
||||
sharesOwned: sdk.NewInt(-1),
|
||||
expectedErr: "depositor 'kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w' and pool 'ukava/usdx' has invalid total shares: -1",
|
||||
},
|
||||
{
|
||||
name: "zero total shares",
|
||||
depositor: validRecord.Depositor,
|
||||
poolID: validRecord.PoolID,
|
||||
sharesOwned: sdk.ZeroInt(),
|
||||
expectedErr: "depositor 'kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w' and pool 'ukava/usdx' has invalid total shares: 0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
record := types.ShareRecord{
|
||||
Depositor: tc.depositor,
|
||||
PoolID: tc.poolID,
|
||||
SharesOwned: tc.sharesOwned,
|
||||
}
|
||||
err := record.Validate()
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestState_ShareRecords_Validation(t *testing.T) {
|
||||
depositor, err := sdk.AccAddressFromBech32("kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w")
|
||||
require.NoError(t, err)
|
||||
|
||||
validRecord := types.NewShareRecord(
|
||||
depositor,
|
||||
"ukava/usdx",
|
||||
i(300e6),
|
||||
)
|
||||
|
||||
invalidRecord := types.NewShareRecord(
|
||||
depositor,
|
||||
"hard/usdx",
|
||||
i(-1),
|
||||
)
|
||||
|
||||
records := types.ShareRecords{
|
||||
validRecord,
|
||||
}
|
||||
assert.NoError(t, records.Validate())
|
||||
|
||||
records = append(records, invalidRecord)
|
||||
err = records.Validate()
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "depositor 'kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w' and pool 'hard/usdx' has invalid total shares: -1")
|
||||
}
|
||||
|
||||
func TestState_ShareRecords_ValidateUniqueShareRecords(t *testing.T) {
|
||||
depositor_1, err := sdk.AccAddressFromBech32("kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w")
|
||||
require.NoError(t, err)
|
||||
|
||||
depositor_2, err := sdk.AccAddressFromBech32("kava1esagqd83rhqdtpy5sxhklaxgn58k2m3s3mnpea")
|
||||
require.NoError(t, err)
|
||||
|
||||
record_1 := types.NewShareRecord(depositor_1, "ukava/usdx", i(20e6))
|
||||
record_2 := types.NewShareRecord(depositor_1, "ukava/usdx", i(10e6))
|
||||
record_3 := types.NewShareRecord(depositor_1, "hard/usdx", i(20e6))
|
||||
record_4 := types.NewShareRecord(depositor_2, "ukava/usdx", i(20e6))
|
||||
|
||||
validRecords := types.ShareRecords{record_1, record_3, record_4}
|
||||
assert.NoError(t, validRecords.Validate())
|
||||
|
||||
invalidRecords := types.ShareRecords{record_1, record_3, record_2, record_4}
|
||||
assert.EqualError(t, invalidRecords.Validate(), "duplicate depositor 'kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w' and poolID 'ukava/usdx'")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user