diff --git a/Makefile b/Makefile
index d2712d6f..7becb364 100644
--- a/Makefile
+++ b/Makefile
@@ -101,7 +101,7 @@ clean:
# This tool checks local markdown links as well.
# Set to exclude riot links as they trigger false positives
link-check:
- @go run github.com/raviqqe/liche -r . --exclude "^http://127.*|^https://riot.im/app*"
+ @go run github.com/raviqqe/liche -r . --exclude "^http://127.*|^https://riot.im/app*|^http://kava-testnet*|^https://testnet-dex*"
########################################
### Testing
@@ -112,19 +112,32 @@ link-check:
test-all: build
# basic app tests
@go test ./app -v
- # cli tests
- @go test ./cli_test -tags cli_test -v -p 4
- # basic simulation (seed "2" happens to not unbond all validators before reaching 100 blocks)
- @go test ./app -run TestFullAppSimulation -Enabled -Commit -NumBlocks=100 -BlockSize=200 -Seed 2 -v -timeout 24h
+ # basic simulation (seed "4" happens to not unbond all validators before reaching 100 blocks)
+ @go test ./app -run TestFullAppSimulation -Enabled -Commit -NumBlocks=100 -BlockSize=200 -Seed 4 -v -timeout 24h
# other sim tests
- @go test ./app -run TestAppImportExport -Enabled -Commit -NumBlocks=100 -BlockSize=200 -Seed 2 -v -timeout 24h
- @go test ./app -run TestAppSimulationAfterImport -Enabled -Commit -NumBlocks=100 -BlockSize=200 -Seed 2 -v -timeout 24h
+ @go test ./app -run TestAppImportExport -Enabled -Commit -NumBlocks=100 -BlockSize=200 -Seed 4 -v -timeout 24h
+ @go test ./app -run TestAppSimulationAfterImport -Enabled -Commit -NumBlocks=100 -BlockSize=200 -Seed 4 -v -timeout 24h
@# AppStateDeterminism does not use Seed flag
- @go test ./app -run TestAppStateDeterminism -Enabled -Commit -NumBlocks=100 -BlockSize=200 -v -timeout 24h
+ @go test ./app -run TestAppStateDeterminism -Enabled -Commit -NumBlocks=100 -BlockSize=200 -Seed 4 -v -timeout 24h
+
+# run module tests and short simulations
+test-basic: test
+ @go test ./app -run TestFullAppSimulation -Enabled -Commit -NumBlocks=5 -BlockSize=200 -Seed 4 -v -timeout 2m
+ # other sim tests
+ @go test ./app -run TestAppImportExport -Enabled -Commit -NumBlocks=5 -BlockSize=200 -Seed 4 -v -timeout 2m
+ @go test ./app -run TestAppSimulationAfterImport -Enabled -Commit -NumBlocks=5 -BlockSize=200 -Seed 4 -v -timeout 2m
+ @# AppStateDeterminism does not use Seed flag
+ @go test ./app -run TestAppStateDeterminism -Enabled -Commit -NumBlocks=5 -BlockSize=200 -Seed 4 -v -timeout 2m
test:
@go test ./...
+test-rest:
+ rest_test/./run_all_tests_from_make.sh
+
+test-cli:
+ @go test ./cli_test -tags cli_test -v -p 4
+
# Kick start lots of sims on an AWS cluster.
# This submits an AWS Batch job to run a lot of sims, each within a docker image. Results are uploaded to S3
start-remote-sims:
@@ -140,4 +153,4 @@ start-remote-sims:
-—job-definition kava-sim-master \
-—container-override environment=[{SIM_NAME=master-$(VERSION)}]
-.PHONY: all build-linux install clean build test test-all start-remote-sims
+.PHONY: all build-linux install clean build test test-cli test-all test-rest test-basic start-remote-sims
diff --git a/README.md b/README.md
index 0952e808..d481e32b 100644
--- a/README.md
+++ b/README.md
@@ -19,11 +19,9 @@
### [Telegram](https://t.me/kavalabs) | [Medium](https://medium.com/kava-labs) | [Validator Chat](https://riot.im/app/#/room/#kava-validators:matrix.org)
-### Participate in Kava testnets and [snag a founder badge](./docs/REWARDS.md)!
-
-Reference implementation of Kava, a blockchain for cross-chain DeFi. Built using the [comsos-sdk](https://github.com/cosmos/cosmos-sdk).
+Reference implementation of Kava, a blockchain for cross-chain DeFi. Built using the [cosmos-sdk](https://github.com/cosmos/cosmos-sdk).
## Mainnet
diff --git a/app/app.go b/app/app.go
index d1da18b4..5d628acf 100644
--- a/app/app.go
+++ b/app/app.go
@@ -4,13 +4,15 @@ import (
"io"
"os"
+
abci "github.com/tendermint/tendermint/abci/types"
- cmn "github.com/tendermint/tendermint/libs/common"
"github.com/tendermint/tendermint/libs/log"
+ tmos "github.com/tendermint/tendermint/libs/os"
dbm "github.com/tendermint/tm-db"
bam "github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
+ "github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/version"
@@ -19,6 +21,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/crisis"
distr "github.com/cosmos/cosmos-sdk/x/distribution"
+ "github.com/cosmos/cosmos-sdk/x/evidence"
"github.com/cosmos/cosmos-sdk/x/genutil"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/mint"
@@ -29,10 +32,14 @@ import (
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/kava-labs/kava/x/auction"
+ "github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/committee"
+ "github.com/kava-labs/kava/x/incentive"
+ "github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
+
)
const (
@@ -60,10 +67,14 @@ var (
crisis.AppModuleBasic{},
slashing.AppModuleBasic{},
supply.AppModuleBasic{},
+ evidence.AppModuleBasic{},
auction.AppModuleBasic{},
cdp.AppModuleBasic{},
pricefeed.AppModuleBasic{},
committee.AppModuleBasic{},
+ bep3.AppModuleBasic{},
+ kavadist.AppModuleBasic{},
+ incentive.AppModuleBasic{},
)
// module account permissions
@@ -78,10 +89,16 @@ var (
auction.ModuleName: nil,
cdp.ModuleName: {supply.Minter, supply.Burner},
cdp.LiquidatorMacc: {supply.Minter, supply.Burner},
+ cdp.SavingsRateMacc: {supply.Minter},
+ bep3.ModuleName: {supply.Minter, supply.Burner},
+ kavadist.ModuleName: {supply.Minter},
}
)
-// Extended ABCI application
+// Verify app interface at compile time
+var _ simapp.App = (*App)(nil)
+
+// App represents an extended ABCI application
type App struct {
*bam.BaseApp
cdc *codec.Codec
@@ -103,11 +120,15 @@ type App struct {
govKeeper gov.Keeper
crisisKeeper crisis.Keeper
paramsKeeper params.Keeper
+ evidenceKeeper evidence.Keeper
vvKeeper validatorvesting.Keeper
auctionKeeper auction.Keeper
cdpKeeper cdp.Keeper
pricefeedKeeper pricefeed.Keeper
committeeKeeper committee.Keeper
+ bep3Keeper bep3.Keeper
+ kavadistKeeper kavadist.Keeper
+ incentiveKeeper incentive.Keeper
// the module manager
mm *module.Manager
@@ -130,8 +151,9 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
keys := sdk.NewKVStoreKeys(
bam.MainStoreKey, auth.StoreKey, staking.StoreKey,
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
- gov.StoreKey, params.StoreKey, validatorvesting.StoreKey,
- auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, committee.StoreKey,
+ gov.StoreKey, params.StoreKey, evidence.StoreKey, validatorvesting.StoreKey,
+ auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, bep3.StoreKey,
+ kavadist.StoreKey, incentive.StoreKey, committee.StoreKey,
)
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
@@ -144,7 +166,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
}
// init params keeper and subspaces
- app.paramsKeeper = params.NewKeeper(app.cdc, keys[params.StoreKey], tkeys[params.TStoreKey], params.DefaultCodespace)
+ app.paramsKeeper = params.NewKeeper(app.cdc, keys[params.StoreKey], tkeys[params.TStoreKey])
authSubspace := app.paramsKeeper.Subspace(auth.DefaultParamspace)
bankSubspace := app.paramsKeeper.Subspace(bank.DefaultParamspace)
stakingSubspace := app.paramsKeeper.Subspace(staking.DefaultParamspace)
@@ -152,61 +174,83 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
distrSubspace := app.paramsKeeper.Subspace(distr.DefaultParamspace)
slashingSubspace := app.paramsKeeper.Subspace(slashing.DefaultParamspace)
govSubspace := app.paramsKeeper.Subspace(gov.DefaultParamspace).WithKeyTable(gov.ParamKeyTable())
+ evidenceSubspace := app.paramsKeeper.Subspace(evidence.DefaultParamspace)
crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace)
auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace)
cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace)
pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace)
+ bep3Subspace := app.paramsKeeper.Subspace(bep3.DefaultParamspace)
+ kavadistSubspace := app.paramsKeeper.Subspace(kavadist.DefaultParamspace)
+ incentiveSubspace := app.paramsKeeper.Subspace(incentive.DefaultParamspace)
// add keepers
app.accountKeeper = auth.NewAccountKeeper(
app.cdc,
keys[auth.StoreKey],
authSubspace,
- auth.ProtoBaseAccount)
+ auth.ProtoBaseAccount,
+ )
app.bankKeeper = bank.NewBaseKeeper(
app.accountKeeper,
bankSubspace,
- bank.DefaultCodespace,
- app.ModuleAccountAddrs())
+ app.ModuleAccountAddrs(),
+ )
app.supplyKeeper = supply.NewKeeper(
app.cdc,
keys[supply.StoreKey],
app.accountKeeper,
app.bankKeeper,
- mAccPerms)
+ mAccPerms,
+ )
stakingKeeper := staking.NewKeeper(
app.cdc,
keys[staking.StoreKey],
app.supplyKeeper,
stakingSubspace,
- staking.DefaultCodespace)
+ )
app.mintKeeper = mint.NewKeeper(
app.cdc,
keys[mint.StoreKey],
mintSubspace,
&stakingKeeper,
app.supplyKeeper,
- auth.FeeCollectorName)
+ auth.FeeCollectorName,
+ )
app.distrKeeper = distr.NewKeeper(
app.cdc,
keys[distr.StoreKey],
distrSubspace,
&stakingKeeper,
app.supplyKeeper,
- distr.DefaultCodespace,
auth.FeeCollectorName,
- app.ModuleAccountAddrs())
+ app.ModuleAccountAddrs(),
+ )
app.slashingKeeper = slashing.NewKeeper(
app.cdc,
keys[slashing.StoreKey],
&stakingKeeper,
slashingSubspace,
- slashing.DefaultCodespace)
+ )
app.crisisKeeper = crisis.NewKeeper(
crisisSubspace,
invCheckPeriod,
app.supplyKeeper,
- auth.FeeCollectorName)
+ auth.FeeCollectorName,
+ )
+
+ // create evidence keeper with router
+ evidenceKeeper := evidence.NewKeeper(
+ app.cdc,
+ keys[evidence.StoreKey],
+ evidenceSubspace,
+ &app.stakingKeeper,
+ app.slashingKeeper,
+ )
+ evidenceRouter := evidence.NewRouter()
+ evidenceKeeper.SetRouter(evidenceRouter)
+ app.evidenceKeeper = *evidenceKeeper
+
+ // create committee keeper with router
committeeGovRouter := gov.NewRouter()
committeeGovRouter.
AddRoute(gov.RouterKey, gov.ProposalHandler).
@@ -218,7 +262,10 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
app.cdc,
keys[committee.StoreKey],
committeeGovRouter,
- committee.DefaultCodespace) // TODO blacklist module addresses?)
+ committee.DefaultCodespace, // TODO blacklist module addresses?)
+ )
+
+ // create gov keeper with router
govRouter := gov.NewRouter()
govRouter.
AddRoute(gov.RouterKey, gov.ProposalHandler).
@@ -231,26 +278,28 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
govSubspace,
app.supplyKeeper,
&stakingKeeper,
- gov.DefaultCodespace,
- govRouter)
+ govRouter,
+ )
+
app.vvKeeper = validatorvesting.NewKeeper(
app.cdc,
keys[validatorvesting.StoreKey],
app.accountKeeper,
app.bankKeeper,
app.supplyKeeper,
- &stakingKeeper)
+ &stakingKeeper,
+ )
app.pricefeedKeeper = pricefeed.NewKeeper(
app.cdc,
keys[pricefeed.StoreKey],
pricefeedSubspace,
- pricefeed.DefaultCodespace)
- // NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType)
+ )
app.auctionKeeper = auction.NewKeeper(
app.cdc,
keys[auction.StoreKey],
app.supplyKeeper,
- auctionSubspace)
+ auctionSubspace,
+ )
app.cdpKeeper = cdp.NewKeeper(
app.cdc,
keys[cdp.StoreKey],
@@ -258,7 +307,28 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
app.pricefeedKeeper,
app.auctionKeeper,
app.supplyKeeper,
- cdp.DefaultCodespace)
+ app.accountKeeper,
+ )
+ app.bep3Keeper = bep3.NewKeeper(
+ app.cdc,
+ keys[bep3.StoreKey],
+ app.supplyKeeper,
+ bep3Subspace,
+ )
+ app.kavadistKeeper = kavadist.NewKeeper(
+ app.cdc,
+ keys[kavadist.StoreKey],
+ kavadistSubspace,
+ app.supplyKeeper,
+ )
+ app.incentiveKeeper = incentive.NewKeeper(
+ app.cdc,
+ keys[incentive.StoreKey],
+ incentiveSubspace,
+ app.supplyKeeper,
+ app.cdpKeeper,
+ app.accountKeeper,
+ )
// register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
@@ -273,36 +343,41 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
bank.NewAppModule(app.bankKeeper, app.accountKeeper),
crisis.NewAppModule(&app.crisisKeeper),
supply.NewAppModule(app.supplyKeeper, app.accountKeeper),
- distr.NewAppModule(app.distrKeeper, app.supplyKeeper),
- gov.NewAppModule(app.govKeeper, app.supplyKeeper),
+ gov.NewAppModule(app.govKeeper, app.accountKeeper, app.supplyKeeper),
mint.NewAppModule(app.mintKeeper),
- slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper),
+ slashing.NewAppModule(app.slashingKeeper, app.accountKeeper, app.stakingKeeper),
+ distr.NewAppModule(app.distrKeeper, app.accountKeeper, app.supplyKeeper, app.stakingKeeper),
staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper),
+ evidence.NewAppModule(app.evidenceKeeper),
validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper),
- auction.NewAppModule(app.auctionKeeper, app.supplyKeeper),
- cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper),
- pricefeed.NewAppModule(app.pricefeedKeeper),
+ auction.NewAppModule(app.auctionKeeper, app.accountKeeper, app.supplyKeeper),
+ cdp.NewAppModule(app.cdpKeeper, app.accountKeeper, app.pricefeedKeeper, app.supplyKeeper),
+ pricefeed.NewAppModule(app.pricefeedKeeper, app.accountKeeper),
+ bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper),
+ kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
+ incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
committee.NewAppModule(app.committeeKeeper),
)
// During begin block slashing happens after distr.BeginBlocker so that
// there is nothing left over in the validator fee pool, so as to keep the
// CanWithdrawInvariant invariant.
- // Auction.BeginBlocker will close out expired auctions and pay debt back to cdp. So it should be run before cdp.BeginBlocker which cancels out debt with stable and starts more auctions.
- app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, auction.ModuleName, cdp.ModuleName, committee.ModuleName)
+ // Auction.BeginBlocker will close out expired auctions and pay debt back to cdp.
+ // So it should be run before cdp.BeginBlocker which cancels out debt with stable and starts more auctions.
+ app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName, bep3.ModuleName, incentive.ModuleName committee.ModuleName)
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
- // Note: genutils must occur after staking so that pools are properly
- // initialized with tokens from genesis accounts.
- //
- // Note: Changing the order of the auth module and modules that use module accounts
- // results in subtle changes to the way accounts are loaded from genesis.
app.mm.SetOrderInitGenesis(
- auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName,
+ auth.ModuleName, // loads all accounts - should run before any module with a module account
+ validatorvesting.ModuleName, distr.ModuleName,
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
- gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName,
- pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, committee.ModuleName, // TODO is this order ok?
+ gov.ModuleName, mint.ModuleName, evidence.ModuleName,
+ pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName,
+ bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName
+ supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
+ crisis.ModuleName, // runs the invariants at genesis - should run after other modules
+ genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts.
)
app.mm.RegisterInvariants(&app.crisisKeeper)
@@ -317,15 +392,17 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper),
bank.NewAppModule(app.bankKeeper, app.accountKeeper),
supply.NewAppModule(app.supplyKeeper, app.accountKeeper),
- gov.NewAppModule(app.govKeeper, app.supplyKeeper),
+ gov.NewAppModule(app.govKeeper, app.accountKeeper, app.supplyKeeper),
mint.NewAppModule(app.mintKeeper),
- distr.NewAppModule(app.distrKeeper, app.supplyKeeper),
+ distr.NewAppModule(app.distrKeeper, app.accountKeeper, app.supplyKeeper, app.stakingKeeper),
staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper),
- slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper),
- cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), // TODO how is the order be decided here? Is this order correct?
- pricefeed.NewAppModule(app.pricefeedKeeper),
- auction.NewAppModule(app.auctionKeeper, app.supplyKeeper),
- // TODO committee
+ slashing.NewAppModule(app.slashingKeeper, app.accountKeeper, app.stakingKeeper),
+ pricefeed.NewAppModule(app.pricefeedKeeper, app.accountKeeper),
+ cdp.NewAppModule(app.cdpKeeper, app.accountKeeper, app.pricefeedKeeper, app.supplyKeeper),
+ auction.NewAppModule(app.auctionKeeper, app.accountKeeper, app.supplyKeeper),
+ bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper),
+ kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
+ incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
)
app.sm.RegisterStoreDecoders()
@@ -344,7 +421,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
if loadLatest {
err := app.LoadLatestVersion(app.keys[bam.MainStoreKey])
if err != nil {
- cmn.Exit(err.Error())
+ tmos.Exit(err.Error())
}
}
@@ -413,6 +490,11 @@ func (app *App) Codec() *codec.Codec {
return app.cdc
}
+// SimulationManager implements the SimulationApp interface
+func (app *App) SimulationManager() *module.SimulationManager {
+ return app.sm
+}
+
// GetMaccPerms returns a mapping of the application's module account permissions.
func GetMaccPerms() map[string][]string {
perms := make(map[string][]string)
diff --git a/app/export.go b/app/export.go
index 0b6c150c..e4a5725a 100644
--- a/app/export.go
+++ b/app/export.go
@@ -13,10 +13,9 @@ import (
"github.com/cosmos/cosmos-sdk/x/staking"
)
-// export the state of the app for a genesis file
-func (app *App) ExportAppStateAndValidators(forZeroHeight bool, jailWhiteList []string) (
- appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) {
-
+// ExportAppStateAndValidators export the state of the app for a genesis file
+func (app *App) ExportAppStateAndValidators(forZeroHeight bool, jailWhiteList []string,
+) (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) {
// as if they could withdraw from the start of the next block
ctx := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()})
@@ -25,7 +24,6 @@ func (app *App) ExportAppStateAndValidators(forZeroHeight bool, jailWhiteList []
}
genState := app.mm.ExportGenesis(ctx)
-
appState, err = codec.MarshalJSONIndent(app.cdc, genState)
if err != nil {
return nil, nil, err
@@ -35,6 +33,8 @@ func (app *App) ExportAppStateAndValidators(forZeroHeight bool, jailWhiteList []
}
// prepare for fresh start at zero height
+// NOTE zero height genesis is a temporary feature which will be deprecated
+// in favour of export at a block height
func (app *App) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []string) {
applyWhiteList := false
@@ -60,14 +60,24 @@ func (app *App) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []string
// withdraw all validator commission
app.stakingKeeper.IterateValidators(ctx, func(_ int64, val staking.ValidatorI) (stop bool) {
- _, _ = app.distrKeeper.WithdrawValidatorCommission(ctx, val.GetOperator())
+ accumCommission := app.distrKeeper.GetValidatorAccumulatedCommission(ctx, val.GetOperator())
+ if accumCommission.IsZero() {
+ return false
+ }
+ _, err := app.distrKeeper.WithdrawValidatorCommission(ctx, val.GetOperator())
+ if err != nil {
+ log.Fatal(err)
+ }
return false
})
// withdraw all delegator rewards
dels := app.stakingKeeper.GetAllDelegations(ctx)
for _, delegation := range dels {
- _, _ = app.distrKeeper.WithdrawDelegationRewards(ctx, delegation.DelegatorAddress, delegation.ValidatorAddress)
+ _, err := app.distrKeeper.WithdrawDelegationRewards(ctx, delegation.DelegatorAddress, delegation.ValidatorAddress)
+ if err != nil {
+ log.Fatal(err)
+ }
}
// clear validator slash events
@@ -86,7 +96,7 @@ func (app *App) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []string
// donate any unwithdrawn outstanding reward fraction tokens to the community pool
scraps := app.distrKeeper.GetValidatorOutstandingRewards(ctx, val.GetOperator())
feePool := app.distrKeeper.GetFeePool(ctx)
- feePool.CommunityPool = feePool.CommunityPool.Add(scraps)
+ feePool.CommunityPool = feePool.CommunityPool.Add(scraps...)
app.distrKeeper.SetFeePool(ctx, feePool)
app.distrKeeper.Hooks().AfterValidatorCreated(ctx, val.GetOperator())
@@ -128,7 +138,6 @@ func (app *App) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []string
iter := sdk.KVStoreReversePrefixIterator(store, staking.ValidatorsKey)
counter := int16(0)
- var valConsAddrs []sdk.ConsAddress
for ; iter.Valid(); iter.Next() {
addr := sdk.ValAddress(iter.Key()[1:])
validator, found := app.stakingKeeper.GetValidator(ctx, addr)
@@ -137,7 +146,6 @@ func (app *App) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []string
}
validator.UnbondingHeight = 0
- valConsAddrs = append(valConsAddrs, validator.ConsAddress())
if applyWhiteList && !whiteListMap[addr.String()] {
validator.Jailed = true
}
diff --git a/app/params/doc.go b/app/params/doc.go
new file mode 100644
index 00000000..c8efce8f
--- /dev/null
+++ b/app/params/doc.go
@@ -0,0 +1,19 @@
+/*
+Package params defines the simulation parameters for the Kava app.
+
+It contains the default weights used for each transaction used on the module's
+simulation. These weights define the chance for a transaction to be simulated at
+any gived operation.
+
+You can repace the default values for the weights by providing a params.json
+file with the weights defined for each of the transaction operations:
+
+ {
+ "op_weight_msg_send": 60,
+ "op_weight_msg_delegate": 100,
+ }
+
+In the example above, the `MsgSend` has 60% chance to be simulated, while the
+`MsgDelegate` will always be simulated.
+*/
+package params
diff --git a/app/params/params.go b/app/params/params.go
new file mode 100644
index 00000000..17138c3d
--- /dev/null
+++ b/app/params/params.go
@@ -0,0 +1,16 @@
+package params
+
+// Simulation parameter constants
+const (
+ StakePerAccount = "stake_per_account"
+ InitiallyBondedValidators = "initially_bonded_validators"
+)
+
+// Default simulation operation weights for messages and gov proposals
+const (
+ DefaultWeightMsgPlaceBid int = 100
+ DefaultWeightMsgCreateAtomicSwap int = 100
+ DefaultWeightMsgUpdatePrices int = 100
+ DefaultWeightMsgCdp int = 100
+ DefaultWeightMsgClaimReward int = 100
+)
diff --git a/app/sim_test.go b/app/sim_test.go
index ede0aaf1..4081978a 100644
--- a/app/sim_test.go
+++ b/app/sim_test.go
@@ -3,60 +3,35 @@ package app
import (
"encoding/json"
"fmt"
- "io/ioutil"
- "math/rand"
"os"
"testing"
"github.com/stretchr/testify/require"
-
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/simapp"
+ "github.com/cosmos/cosmos-sdk/simapp/helpers"
"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
- authsimops "github.com/cosmos/cosmos-sdk/x/auth/simulation/operations"
- banksimops "github.com/cosmos/cosmos-sdk/x/bank/simulation/operations"
distr "github.com/cosmos/cosmos-sdk/x/distribution"
- distrsimops "github.com/cosmos/cosmos-sdk/x/distribution/simulation/operations"
"github.com/cosmos/cosmos-sdk/x/gov"
- govsimops "github.com/cosmos/cosmos-sdk/x/gov/simulation/operations"
"github.com/cosmos/cosmos-sdk/x/mint"
"github.com/cosmos/cosmos-sdk/x/params"
- paramsimops "github.com/cosmos/cosmos-sdk/x/params/simulation/operations"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/cosmos/cosmos-sdk/x/slashing"
- slashingsimops "github.com/cosmos/cosmos-sdk/x/slashing/simulation/operations"
"github.com/cosmos/cosmos-sdk/x/staking"
- stakingsimops "github.com/cosmos/cosmos-sdk/x/staking/simulation/operations"
"github.com/cosmos/cosmos-sdk/x/supply"
)
-// Simulation parameter constants
-const (
- StakePerAccount = "stake_per_account"
- InitiallyBondedValidators = "initially_bonded_validators"
- OpWeightDeductFee = "op_weight_deduct_fee"
- OpWeightMsgSend = "op_weight_msg_send"
- OpWeightSingleInputMsgMultiSend = "op_weight_single_input_msg_multisend"
- OpWeightMsgSetWithdrawAddress = "op_weight_msg_set_withdraw_address"
- OpWeightMsgWithdrawDelegationReward = "op_weight_msg_withdraw_delegation_reward"
- OpWeightMsgWithdrawValidatorCommission = "op_weight_msg_withdraw_validator_commission"
- OpWeightSubmitVotingSlashingTextProposal = "op_weight_submit_voting_slashing_text_proposal"
- OpWeightSubmitVotingSlashingCommunitySpendProposal = "op_weight_submit_voting_slashing_community_spend_proposal"
- OpWeightSubmitVotingSlashingParamChangeProposal = "op_weight_submit_voting_slashing_param_change_proposal"
- OpWeightMsgDeposit = "op_weight_msg_deposit"
- OpWeightMsgCreateValidator = "op_weight_msg_create_validator"
- OpWeightMsgEditValidator = "op_weight_msg_edit_validator"
- OpWeightMsgDelegate = "op_weight_msg_delegate"
- OpWeightMsgUndelegate = "op_weight_msg_undelegate"
- OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate"
- OpWeightMsgUnjail = "op_weight_msg_unjail"
-)
+type StoreKeysPrefixes struct {
+ A sdk.StoreKey
+ B sdk.StoreKey
+ Prefixes [][]byte
+}
// TestMain runs setup and teardown code before all tests.
func TestMain(m *testing.M) {
@@ -66,207 +41,11 @@ func TestMain(m *testing.M) {
config.Seal()
// load the values from simulation specific flags
simapp.GetSimulatorFlags()
-
// run tests
exitCode := m.Run()
os.Exit(exitCode)
}
-func testAndRunTxs(app *App, config simulation.Config) []simulation.WeightedOperation {
- ap := make(simulation.AppParams)
-
- paramChanges := app.sm.GenerateParamChanges(config.Seed)
-
- if config.ParamsFile != "" {
- bz, err := ioutil.ReadFile(config.ParamsFile)
- if err != nil {
- panic(err)
- }
-
- app.cdc.MustUnmarshalJSON(bz, &ap)
- }
-
- // nolint: govet
- return []simulation.WeightedOperation{
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightDeductFee, &v, nil,
- func(_ *rand.Rand) {
- v = 5
- })
- return v
- }(nil),
- authsimops.SimulateDeductFee(app.accountKeeper, app.supplyKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgSend, &v, nil,
- func(_ *rand.Rand) {
- v = 100
- })
- return v
- }(nil),
- banksimops.SimulateMsgSend(app.accountKeeper, app.bankKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightSingleInputMsgMultiSend, &v, nil,
- func(_ *rand.Rand) {
- v = 10
- })
- return v
- }(nil),
- banksimops.SimulateSingleInputMsgMultiSend(app.accountKeeper, app.bankKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgSetWithdrawAddress, &v, nil,
- func(_ *rand.Rand) {
- v = 50
- })
- return v
- }(nil),
- distrsimops.SimulateMsgSetWithdrawAddress(app.distrKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgWithdrawDelegationReward, &v, nil,
- func(_ *rand.Rand) {
- v = 50
- })
- return v
- }(nil),
- distrsimops.SimulateMsgWithdrawDelegatorReward(app.distrKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgWithdrawValidatorCommission, &v, nil,
- func(_ *rand.Rand) {
- v = 50
- })
- return v
- }(nil),
- distrsimops.SimulateMsgWithdrawValidatorCommission(app.distrKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightSubmitVotingSlashingTextProposal, &v, nil,
- func(_ *rand.Rand) {
- v = 5
- })
- return v
- }(nil),
- govsimops.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, govsimops.SimulateTextProposalContent),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightSubmitVotingSlashingCommunitySpendProposal, &v, nil,
- func(_ *rand.Rand) {
- v = 5
- })
- return v
- }(nil),
- govsimops.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, distrsimops.SimulateCommunityPoolSpendProposalContent(app.distrKeeper)),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightSubmitVotingSlashingParamChangeProposal, &v, nil,
- func(_ *rand.Rand) {
- v = 5
- })
- return v
- }(nil),
- govsimops.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, paramsimops.SimulateParamChangeProposalContent(paramChanges)),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgDeposit, &v, nil,
- func(_ *rand.Rand) {
- v = 100
- })
- return v
- }(nil),
- govsimops.SimulateMsgDeposit(app.govKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgCreateValidator, &v, nil,
- func(_ *rand.Rand) {
- v = 100
- })
- return v
- }(nil),
- stakingsimops.SimulateMsgCreateValidator(app.accountKeeper, app.stakingKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgEditValidator, &v, nil,
- func(_ *rand.Rand) {
- v = 5
- })
- return v
- }(nil),
- stakingsimops.SimulateMsgEditValidator(app.stakingKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgDelegate, &v, nil,
- func(_ *rand.Rand) {
- v = 100
- })
- return v
- }(nil),
- stakingsimops.SimulateMsgDelegate(app.accountKeeper, app.stakingKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgUndelegate, &v, nil,
- func(_ *rand.Rand) {
- v = 100
- })
- return v
- }(nil),
- stakingsimops.SimulateMsgUndelegate(app.accountKeeper, app.stakingKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgBeginRedelegate, &v, nil,
- func(_ *rand.Rand) {
- v = 100
- })
- return v
- }(nil),
- stakingsimops.SimulateMsgBeginRedelegate(app.accountKeeper, app.stakingKeeper),
- },
- {
- func(_ *rand.Rand) int {
- var v int
- ap.GetOrGenerate(app.cdc, OpWeightMsgUnjail, &v, nil,
- func(_ *rand.Rand) {
- v = 100
- })
- return v
- }(nil),
- slashingsimops.SimulateMsgUnjail(app.slashingKeeper),
- },
- }
-}
-
// fauxMerkleModeOpt returns a BaseApp option to use a dbStoreAdapter instead of
// an IAVLStore for faster simulation speed.
func fauxMerkleModeOpt(bapp *baseapp.BaseApp) {
@@ -279,197 +58,96 @@ func interBlockCacheOpt() func(*baseapp.BaseApp) {
return baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager())
}
-// Profile with:
-// /usr/local/go/bin/go test -benchmem -run=^$ github.com/cosmos/cosmos-sdk/GaiaApp -bench ^BenchmarkFullAppSimulation$ -Commit=true -cpuprofile cpu.out
-func BenchmarkFullAppSimulation(b *testing.B) {
- logger := log.NewNopLogger()
- config := simapp.NewConfigFromFlags()
-
- var db dbm.DB
- dir, _ := ioutil.TempDir("", "goleveldb-app-sim")
- db, _ = sdk.NewLevelDB("Simulation", dir)
- defer func() {
- db.Close()
- _ = os.RemoveAll(dir)
- }()
-
- app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, interBlockCacheOpt())
-
- // Run randomized simulation
- // TODO: parameterize numbers, save for a later PR
- _, simParams, simErr := simulation.SimulateFromSeed(
- b, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.sm),
- testAndRunTxs(app, config), app.ModuleAccountAddrs(), config,
- )
-
- // export state and params before the simulation error is checked
- if config.ExportStatePath != "" {
- if err := ExportStateToJSON(app, config.ExportStatePath); err != nil {
- fmt.Println(err)
- b.Fail()
- }
- }
-
- if config.ExportParamsPath != "" {
- if err := simapp.ExportParamsToJSON(simParams, config.ExportParamsPath); err != nil {
- fmt.Println(err)
- b.Fail()
- }
- }
-
- if simErr != nil {
- fmt.Println(simErr)
- b.FailNow()
- }
-
- if config.Commit {
- fmt.Println("\nGoLevelDB Stats")
- fmt.Println(db.Stats()["leveldb.stats"])
- fmt.Println("GoLevelDB cached block size", db.Stats()["leveldb.cachedblock"])
- }
-}
-
-// TestFullAppSimulation runs a standard simulation of the app, modified by cmd line flag values.
func TestFullAppSimulation(t *testing.T) {
- if !simapp.FlagEnabledValue {
+ config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation")
+ if skip {
t.Skip("skipping application simulation")
}
-
- var logger log.Logger
- config := simapp.NewConfigFromFlags()
-
- if simapp.FlagVerboseValue {
- logger = log.TestingLogger()
- } else {
- logger = log.NewNopLogger()
- }
-
- var db dbm.DB
- dir, _ := ioutil.TempDir("", "goleveldb-app-sim")
- db, _ = sdk.NewLevelDB("Simulation", dir)
+ require.NoError(t, err, "simulation setup failed")
defer func() {
db.Close()
- _ = os.RemoveAll(dir)
+ require.NoError(t, os.RemoveAll(dir))
}()
app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt)
- require.Equal(t, "kava", app.Name())
+ require.Equal(t, appName, app.Name())
- // Run randomized simulation
+ // run randomized simulation
_, simParams, simErr := simulation.SimulateFromSeed(
- t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.sm),
- testAndRunTxs(app, config), app.ModuleAccountAddrs(), config,
- )
-
- // export state and params before the simulation error is checked
- if config.ExportStatePath != "" {
- err := ExportStateToJSON(app, config.ExportStatePath)
- require.NoError(t, err)
- }
-
- if config.ExportParamsPath != "" {
- err := simapp.ExportParamsToJSON(simParams, config.ExportParamsPath)
- require.NoError(t, err)
- }
-
- require.NoError(t, simErr)
-
- if config.Commit {
- // for memdb:
- // fmt.Println("Database Size", db.Stats()["database.size"])
- fmt.Println("\nGoLevelDB Stats")
- fmt.Println(db.Stats()["leveldb.stats"])
- fmt.Println("GoLevelDB cached block size", db.Stats()["leveldb.cachedblock"])
- }
-}
-
-// TestAppImportExport runs a simulation, exports the state, imports it, then checks the db state is same after import as it was before export.
-func TestAppImportExport(t *testing.T) {
- if !simapp.FlagEnabledValue {
- t.Skip("skipping application import/export simulation")
- }
-
- var logger log.Logger
- config := simapp.NewConfigFromFlags()
-
- if simapp.FlagVerboseValue {
- logger = log.TestingLogger()
- } else {
- logger = log.NewNopLogger()
- }
-
- var db dbm.DB
- dir, _ := ioutil.TempDir("", "goleveldb-app-sim")
- db, _ = sdk.NewLevelDB("Simulation", dir)
-
- defer func() {
- db.Close()
- _ = os.RemoveAll(dir)
- }()
-
- app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt)
- require.Equal(t, "kava", app.Name())
-
- // Run randomized simulation
- _, simParams, simErr := simulation.SimulateFromSeed(
- t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.sm),
- testAndRunTxs(app, config), app.ModuleAccountAddrs(), config,
+ t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()),
+ simapp.SimulationOperations(app, app.Codec(), config),
+ app.ModuleAccountAddrs(), config,
)
// export state and simParams before the simulation error is checked
- if config.ExportStatePath != "" {
- err := ExportStateToJSON(app, config.ExportStatePath)
- require.NoError(t, err)
- }
-
- if config.ExportParamsPath != "" {
- err := simapp.ExportParamsToJSON(simParams, config.ExportParamsPath)
- require.NoError(t, err)
- }
-
+ err = simapp.CheckExportSimulation(app, config, simParams)
+ require.NoError(t, err)
require.NoError(t, simErr)
if config.Commit {
- // for memdb:
- // fmt.Println("Database Size", db.Stats()["database.size"])
- fmt.Println("\nGoLevelDB Stats")
- fmt.Println(db.Stats()["leveldb.stats"])
- fmt.Println("GoLevelDB cached block size", db.Stats()["leveldb.cachedblock"])
+ simapp.PrintStats(db)
+ }
+}
+
+func TestAppImportExport(t *testing.T) {
+ config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation")
+ if skip {
+ t.Skip("skipping application import/export simulation")
+ }
+ require.NoError(t, err, "simulation setup failed")
+
+ defer func() {
+ db.Close()
+ require.NoError(t, os.RemoveAll(dir))
+ }()
+
+ app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt)
+ require.Equal(t, appName, app.Name())
+
+ // Run randomized simulation
+ _, simParams, simErr := simulation.SimulateFromSeed(
+ t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()),
+ simapp.SimulationOperations(app, app.Codec(), config),
+ app.ModuleAccountAddrs(), config,
+ )
+
+ // export state and simParams before the simulation error is checked
+ err = simapp.CheckExportSimulation(app, config, simParams)
+ require.NoError(t, err)
+ require.NoError(t, simErr)
+
+ if config.Commit {
+ simapp.PrintStats(db)
}
fmt.Printf("exporting genesis...\n")
appState, _, err := app.ExportAppStateAndValidators(false, []string{})
require.NoError(t, err)
+
fmt.Printf("importing genesis...\n")
- newDir, _ := ioutil.TempDir("", "goleveldb-app-sim-2")
- newDB, _ := sdk.NewLevelDB("Simulation-2", dir)
+ _, newDB, newDir, _, _, err := simapp.SetupSimulation("leveldb-app-sim-2", "Simulation-2")
+ require.NoError(t, err, "simulation setup failed")
defer func() {
newDB.Close()
- _ = os.RemoveAll(newDir)
+ require.NoError(t, os.RemoveAll(newDir))
}()
newApp := NewApp(log.NewNopLogger(), newDB, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt)
- require.Equal(t, "kava", newApp.Name())
+ require.Equal(t, appName, newApp.Name())
- var genesisState simapp.GenesisState
- err = app.cdc.UnmarshalJSON(appState, &genesisState)
+ var genesisState GenesisState
+ err = app.Codec().UnmarshalJSON(appState, &genesisState)
require.NoError(t, err)
+ ctxA := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()})
ctxB := newApp.NewContext(true, abci.Header{Height: app.LastBlockHeight()})
newApp.mm.InitGenesis(ctxB, genesisState)
fmt.Printf("comparing stores...\n")
- ctxA := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()})
-
- type StoreKeysPrefixes struct {
- A sdk.StoreKey
- B sdk.StoreKey
- Prefixes [][]byte
- }
storeKeysPrefixes := []StoreKeysPrefixes{
{app.keys[baseapp.MainStoreKey], newApp.keys[baseapp.MainStoreKey], [][]byte{}},
@@ -486,118 +164,84 @@ func TestAppImportExport(t *testing.T) {
{app.keys[gov.StoreKey], newApp.keys[gov.StoreKey], [][]byte{}},
}
- for _, storeKeysPrefix := range storeKeysPrefixes {
- storeKeyA := storeKeysPrefix.A
- storeKeyB := storeKeysPrefix.B
- prefixes := storeKeysPrefix.Prefixes
+ for _, skp := range storeKeysPrefixes {
+ storeA := ctxA.KVStore(skp.A)
+ storeB := ctxB.KVStore(skp.B)
- storeA := ctxA.KVStore(storeKeyA)
- storeB := ctxB.KVStore(storeKeyB)
-
- failedKVAs, failedKVBs := sdk.DiffKVStores(storeA, storeB, prefixes)
+ failedKVAs, failedKVBs := sdk.DiffKVStores(storeA, storeB, skp.Prefixes)
require.Equal(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare")
- fmt.Printf("compared %d key/value pairs between %s and %s\n", len(failedKVAs), storeKeyA, storeKeyB)
- require.Len(t, failedKVAs, 0, simapp.GetSimulationLog(storeKeyA.Name(), app.sm.StoreDecoders, app.cdc, failedKVAs, failedKVBs))
+ fmt.Printf("compared %d key/value pairs between %s and %s\n", len(failedKVAs), skp.A, skp.B)
+ require.Equal(t, len(failedKVAs), 0, simapp.GetSimulationLog(skp.A.Name(), app.SimulationManager().StoreDecoders, app.Codec(), failedKVAs, failedKVBs))
}
}
-// TestAppSimulationAfterImport runs a simulation, exports it, imports it and runs another simulation.
func TestAppSimulationAfterImport(t *testing.T) {
- if !simapp.FlagEnabledValue {
+ config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation")
+ if skip {
t.Skip("skipping application simulation after import")
}
-
- var logger log.Logger
- config := simapp.NewConfigFromFlags()
-
- if simapp.FlagVerboseValue {
- logger = log.TestingLogger()
- } else {
- logger = log.NewNopLogger()
- }
-
- dir, _ := ioutil.TempDir("", "goleveldb-app-sim")
- db, _ := sdk.NewLevelDB("Simulation", dir)
+ require.NoError(t, err, "simulation setup failed")
defer func() {
db.Close()
- _ = os.RemoveAll(dir)
+ require.NoError(t, os.RemoveAll(dir))
}()
app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt)
- require.Equal(t, "kava", app.Name())
+ require.Equal(t, appName, app.Name())
- // Run randomized simulation
// Run randomized simulation
stopEarly, simParams, simErr := simulation.SimulateFromSeed(
- t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.sm),
- testAndRunTxs(app, config), app.ModuleAccountAddrs(), config,
+ t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()),
+ simapp.SimulationOperations(app, app.Codec(), config),
+ app.ModuleAccountAddrs(), config,
)
- // export state and params before the simulation error is checked
- if config.ExportStatePath != "" {
- err := ExportStateToJSON(app, config.ExportStatePath)
- require.NoError(t, err)
- }
-
- if config.ExportParamsPath != "" {
- err := simapp.ExportParamsToJSON(simParams, config.ExportParamsPath)
- require.NoError(t, err)
- }
-
+ // export state and simParams before the simulation error is checked
+ err = simapp.CheckExportSimulation(app, config, simParams)
+ require.NoError(t, err)
require.NoError(t, simErr)
if config.Commit {
- // for memdb:
- // fmt.Println("Database Size", db.Stats()["database.size"])
- fmt.Println("\nGoLevelDB Stats")
- fmt.Println(db.Stats()["leveldb.stats"])
- fmt.Println("GoLevelDB cached block size", db.Stats()["leveldb.cachedblock"])
+ simapp.PrintStats(db)
}
if stopEarly {
- // we can't export or import a zero-validator genesis
- fmt.Printf("We can't export or import a zero-validator genesis, exiting test...\n")
+ fmt.Println("can't export or import a zero-validator genesis, exiting test...")
return
}
- fmt.Printf("Exporting genesis...\n")
+ fmt.Printf("exporting genesis...\n")
appState, _, err := app.ExportAppStateAndValidators(true, []string{})
- if err != nil {
- panic(err)
- }
+ require.NoError(t, err)
- fmt.Printf("Importing genesis...\n")
+ fmt.Printf("importing genesis...\n")
- newDir, _ := ioutil.TempDir("", "goleveldb-app-sim-2")
- newDB, _ := sdk.NewLevelDB("Simulation-2", dir)
+ _, newDB, newDir, _, _, err := simapp.SetupSimulation("leveldb-app-sim-2", "Simulation-2")
+ require.NoError(t, err, "simulation setup failed")
defer func() {
newDB.Close()
- _ = os.RemoveAll(newDir)
+ require.NoError(t, os.RemoveAll(newDir))
}()
- newApp := NewApp(log.NewNopLogger(), newDB, nil, true, 0, fauxMerkleModeOpt)
- require.Equal(t, "kava", newApp.Name())
+ newApp := NewApp(log.NewNopLogger(), newDB, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt)
+ require.Equal(t, appName, newApp.Name())
newApp.InitChain(abci.RequestInitChain{
AppStateBytes: appState,
})
- // Run randomized simulation on imported app
_, _, err = simulation.SimulateFromSeed(
- t, os.Stdout, newApp.BaseApp, simapp.AppStateFn(app.Codec(), app.sm),
- testAndRunTxs(newApp, config), newApp.ModuleAccountAddrs(), config,
+ t, os.Stdout, newApp.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()),
+ simapp.SimulationOperations(newApp, newApp.Codec(), config),
+ newApp.ModuleAccountAddrs(), config,
)
-
require.NoError(t, err)
}
-// TODO: Make another test for the fuzzer itself, which just has noOp txs
-// and doesn't depend on the application.
-// TestAppStateDeterminism runs several sims with the same seed and checks the states are equal.
func TestAppStateDeterminism(t *testing.T) {
if !simapp.FlagEnabledValue {
t.Skip("skipping application simulation")
@@ -608,99 +252,36 @@ func TestAppStateDeterminism(t *testing.T) {
config.ExportParamsPath = ""
config.OnOperation = false
config.AllInvariants = false
+ config.ChainID = helpers.SimAppChainID
- numSeeds := 3
- numTimesToRunPerSeed := 5
+ numTimesToRunPerSeed := 2
appHashList := make([]json.RawMessage, numTimesToRunPerSeed)
- for i := 0; i < numSeeds; i++ {
- config.Seed = rand.Int63()
+ for j := 0; j < numTimesToRunPerSeed; j++ {
+ logger := log.NewNopLogger()
+ db := dbm.NewMemDB()
+ app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, interBlockCacheOpt())
- for j := 0; j < numTimesToRunPerSeed; j++ {
- logger := log.NewNopLogger()
- db := dbm.NewMemDB()
- app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, interBlockCacheOpt())
+ fmt.Printf(
+ "running non-determinism simulation; seed %d: attempt: %d/%d\n",
+ config.Seed, j+1, numTimesToRunPerSeed,
+ )
- fmt.Printf(
- "running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n",
- config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed,
+ _, _, err := simulation.SimulateFromSeed(
+ t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()),
+ simapp.SimulationOperations(app, app.Codec(), config),
+ app.ModuleAccountAddrs(), config,
+ )
+ require.NoError(t, err)
+
+ appHash := app.LastCommitID().Hash
+ appHashList[j] = appHash
+
+ if j != 0 {
+ require.Equal(
+ t, appHashList[0], appHashList[j],
+ "non-determinism in seed %d: attempt: %d/%d\n", config.Seed, j+1, numTimesToRunPerSeed,
)
-
- _, _, err := simulation.SimulateFromSeed(
- t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.sm),
- testAndRunTxs(app, config), app.ModuleAccountAddrs(), config,
- )
- require.NoError(t, err)
-
- appHash := app.LastCommitID().Hash
- appHashList[j] = appHash
-
- if j != 0 {
- require.Equal(
- t, appHashList[0], appHashList[j],
- "non-determinism in seed %d: %d/%d, attempt: %d/%d\n", config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed,
- )
- }
}
}
-}
-
-func BenchmarkInvariants(b *testing.B) {
- logger := log.NewNopLogger()
-
- config := simapp.NewConfigFromFlags()
- config.AllInvariants = false
-
- dir, _ := ioutil.TempDir("", "goleveldb-app-invariant-bench")
- db, _ := sdk.NewLevelDB("simulation", dir)
-
- defer func() {
- db.Close()
- os.RemoveAll(dir)
- }()
-
- app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, interBlockCacheOpt())
-
- // 2. Run parameterized simulation (w/o invariants)
- _, simParams, simErr := simulation.SimulateFromSeed(
- b, ioutil.Discard, app.BaseApp, simapp.AppStateFn(app.Codec(), app.sm),
- testAndRunTxs(app, config), app.ModuleAccountAddrs(), config,
- )
-
- // export state and params before the simulation error is checked
- if config.ExportStatePath != "" {
- if err := ExportStateToJSON(app, config.ExportStatePath); err != nil {
- fmt.Println(err)
- b.Fail()
- }
- }
-
- if config.ExportParamsPath != "" {
- if err := simapp.ExportParamsToJSON(simParams, config.ExportParamsPath); err != nil {
- fmt.Println(err)
- b.Fail()
- }
- }
-
- if simErr != nil {
- fmt.Println(simErr)
- b.FailNow()
- }
-
- ctx := app.NewContext(true, abci.Header{Height: app.LastBlockHeight() + 1})
-
- // 3. Benchmark each invariant separately
- //
- // NOTE: We use the crisis keeper as it has all the invariants registered with
- // their respective metadata which makes it useful for testing/benchmarking.
- for _, cr := range app.crisisKeeper.Routes() {
- b.Run(fmt.Sprintf("%s/%s", cr.ModuleName, cr.Route), func(b *testing.B) {
- for n := 0; n < b.N; n++ {
- if res, stop := cr.Invar(ctx); stop {
- fmt.Printf("broken invariant at block %d of %d\n%s", ctx.BlockHeight()-1, config.NumBlocks, res)
- b.FailNow()
- }
- }
- })
- }
-}
+}
\ No newline at end of file
diff --git a/app/test_common.go b/app/test_common.go
index 3d687a5a..f0d4b0e7 100644
--- a/app/test_common.go
+++ b/app/test_common.go
@@ -28,8 +28,11 @@ import (
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/kava-labs/kava/x/auction"
+ "github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/committee"
+ "github.com/kava-labs/kava/x/incentive"
+ "github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
)
@@ -49,6 +52,10 @@ type TestApp struct {
}
func NewTestApp() TestApp {
+ config := sdk.GetConfig()
+ SetBech32AddressPrefixes(config)
+ SetBip44CoinType(config)
+
db := tmdb.NewMemDB()
app := NewApp(log.NewNopLogger(), db, nil, true, 0)
return TestApp{App: *app}
@@ -68,6 +75,9 @@ func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper
func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper }
func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper }
func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper }
+func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keeper }
+func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper }
+func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper }
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
diff --git a/cli_test/test_helpers.go b/cli_test/test_helpers.go
index 05470c99..1d82792f 100644
--- a/cli_test/test_helpers.go
+++ b/cli_test/test_helpers.go
@@ -14,7 +14,6 @@ import (
tmtypes "github.com/tendermint/tendermint/types"
- "github.com/cosmos/cosmos-sdk/client"
clientkeys "github.com/cosmos/cosmos-sdk/client/keys"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/crypto/keys"
@@ -208,7 +207,7 @@ func (f *Fixtures) UnsafeResetAll(flags ...string) {
// NOTE: GDInit sets the ChainID for the Fixtures instance
func (f *Fixtures) GDInit(moniker string, flags ...string) {
cmd := fmt.Sprintf("%s init -o --home=%s %s", f.GaiadBinary, f.GaiadHome, moniker)
- _, stderr := tests.ExecuteT(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ _, stderr := tests.ExecuteT(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
var chainID string
var initRes map[string]json.RawMessage
@@ -231,13 +230,13 @@ func (f *Fixtures) AddGenesisAccount(address sdk.AccAddress, coins sdk.Coins, fl
// GenTx is gaiad gentx
func (f *Fixtures) GenTx(name string, flags ...string) {
cmd := fmt.Sprintf("%s gentx --name=%s --home=%s --home-client=%s", f.GaiadBinary, name, f.GaiadHome, f.GaiacliHome)
- executeWriteCheckErr(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ executeWriteCheckErr(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// CollectGenTxs is gaiad collect-gentxs
func (f *Fixtures) CollectGenTxs(flags ...string) {
cmd := fmt.Sprintf("%s collect-gentxs --home=%s", f.GaiadBinary, f.GaiadHome)
- executeWriteCheckErr(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ executeWriteCheckErr(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// GDStart runs gaiad start with the appropriate flags and returns a process
@@ -276,19 +275,19 @@ func (f *Fixtures) KeysDelete(name string, flags ...string) {
// KeysAdd is gaiacli keys add
func (f *Fixtures) KeysAdd(name string, flags ...string) {
cmd := fmt.Sprintf("%s keys add --home=%s %s", f.GaiacliBinary, f.GaiacliHome, name)
- executeWriteCheckErr(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ executeWriteCheckErr(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// KeysAddRecover prepares gaiacli keys add --recover
func (f *Fixtures) KeysAddRecover(name, mnemonic string, flags ...string) (exitSuccess bool, stdout, stderr string) {
cmd := fmt.Sprintf("%s keys add --home=%s --recover %s", f.GaiacliBinary, f.GaiacliHome, name)
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass, mnemonic)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass, mnemonic)
}
// KeysAddRecoverHDPath prepares gaiacli keys add --recover --account --index
func (f *Fixtures) KeysAddRecoverHDPath(name, mnemonic string, account uint32, index uint32, flags ...string) {
cmd := fmt.Sprintf("%s keys add --home=%s --recover %s --account %d --index %d", f.GaiacliBinary, f.GaiacliHome, name, account, index)
- executeWriteCheckErr(f.T, addFlags(cmd, flags), client.DefaultKeyPass, mnemonic)
+ executeWriteCheckErr(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass, mnemonic)
}
// KeysShow is gaiacli keys show
@@ -324,25 +323,25 @@ func (f *Fixtures) CLIConfig(key, value string, flags ...string) {
// TxSend is gaiacli tx send
func (f *Fixtures) TxSend(from string, to sdk.AccAddress, amount sdk.Coin, flags ...string) (bool, string, string) {
cmd := fmt.Sprintf("%s tx send %s %s %s %v", f.GaiacliBinary, from, to, amount, f.Flags())
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// TxSign is gaiacli tx sign
func (f *Fixtures) TxSign(signer, fileName string, flags ...string) (bool, string, string) {
cmd := fmt.Sprintf("%s tx sign %v --from=%s %v", f.GaiacliBinary, f.Flags(), signer, fileName)
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// TxBroadcast is gaiacli tx broadcast
func (f *Fixtures) TxBroadcast(fileName string, flags ...string) (bool, string, string) {
cmd := fmt.Sprintf("%s tx broadcast %v %v", f.GaiacliBinary, f.Flags(), fileName)
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// TxEncode is gaiacli tx encode
func (f *Fixtures) TxEncode(fileName string, flags ...string) (bool, string, string) {
cmd := fmt.Sprintf("%s tx encode %v %v", f.GaiacliBinary, f.Flags(), fileName)
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// TxMultisign is gaiacli tx multisign
@@ -364,13 +363,13 @@ func (f *Fixtures) TxStakingCreateValidator(from, consPubKey string, amount sdk.
cmd += fmt.Sprintf(" --amount=%v --moniker=%v --commission-rate=%v", amount, from, "0.05")
cmd += fmt.Sprintf(" --commission-max-rate=%v --commission-max-change-rate=%v", "0.20", "0.10")
cmd += fmt.Sprintf(" --min-self-delegation=%v", "1")
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// TxStakingUnbond is gaiacli tx staking unbond
func (f *Fixtures) TxStakingUnbond(from, shares string, validator sdk.ValAddress, flags ...string) bool {
cmd := fmt.Sprintf("%s tx staking unbond %s %v --from=%s %v", f.GaiacliBinary, validator, shares, from, f.Flags())
- return executeWrite(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWrite(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
//___________________________________________________________________________________
@@ -380,19 +379,19 @@ func (f *Fixtures) TxStakingUnbond(from, shares string, validator sdk.ValAddress
func (f *Fixtures) TxGovSubmitProposal(from, typ, title, description string, deposit sdk.Coin, flags ...string) (bool, string, string) {
cmd := fmt.Sprintf("%s tx gov submit-proposal %v --from=%s --type=%s", f.GaiacliBinary, f.Flags(), from, typ)
cmd += fmt.Sprintf(" --title=%s --description=%s --deposit=%s", title, description, deposit)
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// TxGovDeposit is gaiacli tx gov deposit
func (f *Fixtures) TxGovDeposit(proposalID int, from string, amount sdk.Coin, flags ...string) (bool, string, string) {
cmd := fmt.Sprintf("%s tx gov deposit %d %s --from=%s %v", f.GaiacliBinary, proposalID, amount, from, f.Flags())
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// TxGovVote is gaiacli tx gov vote
func (f *Fixtures) TxGovVote(proposalID int, option gov.VoteOption, from string, flags ...string) (bool, string, string) {
cmd := fmt.Sprintf("%s tx gov vote %d %s --from=%s %v", f.GaiacliBinary, proposalID, option, from, f.Flags())
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// TxGovSubmitParamChangeProposal executes a CLI parameter change proposal
@@ -406,7 +405,7 @@ func (f *Fixtures) TxGovSubmitParamChangeProposal(
f.GaiacliBinary, proposalPath, from, f.Flags(),
)
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
// TxGovSubmitCommunityPoolSpendProposal executes a CLI community pool spend proposal
@@ -420,7 +419,7 @@ func (f *Fixtures) TxGovSubmitCommunityPoolSpendProposal(
f.GaiacliBinary, proposalPath, from, f.Flags(),
)
- return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass)
+ return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass)
}
//___________________________________________________________________________________
diff --git a/cmd/kvcli/main.go b/cmd/kvcli/main.go
index 9873421f..8700f0ac 100644
--- a/cmd/kvcli/main.go
+++ b/cmd/kvcli/main.go
@@ -12,6 +12,7 @@ import (
"github.com/tendermint/tendermint/libs/cli"
"github.com/cosmos/cosmos-sdk/client"
+ "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/lcd"
"github.com/cosmos/cosmos-sdk/client/rpc"
sdk "github.com/cosmos/cosmos-sdk/types"
@@ -48,7 +49,7 @@ func main() {
}
// Add --chain-id to persistent flags and mark it required
- rootCmd.PersistentFlags().String(client.FlagChainID, "", "Chain ID of tendermint node")
+ rootCmd.PersistentFlags().String(flags.FlagChainID, "", "Chain ID of tendermint node")
rootCmd.PersistentPreRunE = func(_ *cobra.Command, _ []string) error {
return initConfig(rootCmd)
}
@@ -59,13 +60,13 @@ func main() {
client.ConfigCmd(app.DefaultCLIHome),
queryCmd(cdc),
txCmd(cdc),
- client.LineBreak,
+ flags.LineBreak,
lcd.ServeCommand(cdc, registerRoutes),
- client.LineBreak,
+ flags.LineBreak,
getModifiedKeysCmd(),
- client.LineBreak,
+ flags.LineBreak,
version.Cmd,
- client.NewCompletionCmd(rootCmd, true),
+ flags.NewCompletionCmd(rootCmd, true),
)
// Add flags and prefix all env exposed with KA
@@ -87,12 +88,12 @@ func queryCmd(cdc *amino.Codec) *cobra.Command {
queryCmd.AddCommand(
authcmd.GetAccountCmd(cdc),
- client.LineBreak,
+ flags.LineBreak,
rpc.ValidatorCommand(cdc),
rpc.BlockCommand(),
authcmd.QueryTxsByEventsCmd(cdc),
authcmd.QueryTxCmd(cdc),
- client.LineBreak,
+ flags.LineBreak,
)
// add modules' query commands
@@ -109,14 +110,14 @@ func txCmd(cdc *amino.Codec) *cobra.Command {
txCmd.AddCommand(
bankcmd.SendTxCmd(cdc),
- client.LineBreak,
+ flags.LineBreak,
authcmd.GetSignCommand(cdc),
authcmd.GetMultiSignCommand(cdc),
- client.LineBreak,
+ flags.LineBreak,
authcmd.GetBroadcastCommand(cdc),
authcmd.GetEncodeCommand(cdc),
authcmd.GetDecodeCommand(cdc),
- client.LineBreak,
+ flags.LineBreak,
)
// add modules' tx commands
@@ -156,7 +157,7 @@ func initConfig(cmd *cobra.Command) error {
return err
}
}
- if err := viper.BindPFlag(client.FlagChainID, cmd.PersistentFlags().Lookup(client.FlagChainID)); err != nil {
+ if err := viper.BindPFlag(flags.FlagChainID, cmd.PersistentFlags().Lookup(flags.FlagChainID)); err != nil {
return err
}
if err := viper.BindPFlag(cli.EncodingFlag, cmd.PersistentFlags().Lookup(cli.EncodingFlag)); err != nil {
diff --git a/cmd/kvd/main.go b/cmd/kvd/main.go
index bef94402..148f3ba8 100644
--- a/cmd/kvd/main.go
+++ b/cmd/kvd/main.go
@@ -14,7 +14,7 @@ import (
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/baseapp"
- "github.com/cosmos/cosmos-sdk/client"
+ "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/server"
"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
@@ -59,7 +59,7 @@ func main() {
app.DefaultCLIHome))
rootCmd.AddCommand(genutilcli.ValidateGenesisCmd(ctx, cdc, app.ModuleBasics))
rootCmd.AddCommand(AddGenesisAccountCmd(ctx, cdc, app.DefaultNodeHome, app.DefaultCLIHome))
- rootCmd.AddCommand(client.NewCompletionCmd(rootCmd, true))
+ rootCmd.AddCommand(flags.NewCompletionCmd(rootCmd, true))
server.AddCommands(ctx, cdc, rootCmd, newApp, exportAppStateAndTMValidators)
diff --git a/contrib/README.md b/contrib/README.md
index 30a4e0db..cf15df14 100644
--- a/contrib/README.md
+++ b/contrib/README.md
@@ -1,4 +1,19 @@
-
# Contrib
-Resources and examples for running and interacting with the kava blockchain.
+Resources and examples for running and interacting with the kava blockchain. `contrib` contains sample genesis files, example governance proposals, and rest-server request examples for different versions of kava.
+
+## testnet-4000
+
+kava-testnet-4000 introduces the cdp and auction modules, allowing users to create CDPs and mint usdx as well as participate in auctions.
+
+Configuration and rest-server interaction files are available in the [testnet-4000](./testnet-4000/README.md) directory.
+
+## testnet-5000
+
+kava-testnet-5000 introduces the bep3 modules, allowing users to transfer BNB between the bnbchain testnet and kava.
+
+Configuration and rest-server interaction files are available in the [testnet-5000](./testnet-5000/README.md) directory.
+
+## dev
+
+During development new features offer require modifications to the genesis file. The dev directory houses these genesis files.
diff --git a/contrib/dev/genesis_examples/genesis_savings_rate.json b/contrib/dev/genesis_examples/genesis_savings_rate.json
new file mode 100644
index 00000000..538d5b72
--- /dev/null
+++ b/contrib/dev/genesis_examples/genesis_savings_rate.json
@@ -0,0 +1,352 @@
+{
+ "genesis_time": "2020-03-07T18:27:07.837213082Z",
+ "chain_id": "testing",
+ "consensus_params": {
+ "block": {
+ "max_bytes": "22020096",
+ "max_gas": "-1",
+ "time_iota_ms": "1000"
+ },
+ "evidence": {
+ "max_age": "100000"
+ },
+ "validator": {
+ "pub_key_types": [
+ "ed25519"
+ ]
+ }
+ },
+ "app_hash": "",
+ "app_state": {
+ "cdp": {
+ "cdps": [],
+ "debt_denom": "debt",
+ "deposits": [],
+ "gov_denom": "ukava",
+ "previous_distribution_time": "1970-01-01T00:00:00Z",
+ "params": {
+ "circuit_breaker": false,
+ "collateral_params": [
+ {
+ "auction_size": "5000000000",
+ "conversion_factor": "6",
+ "debt_limit": [
+ {
+ "amount": "10000000",
+ "denom": "usdx"
+ }
+ ],
+ "denom": "xrp",
+ "liquidation_penalty": "0.05",
+ "liquidation_ratio": "2.0",
+ "market_id": "xrp:usd",
+ "prefix": 0,
+ "stability_fee": "1.000000001547126"
+ },
+ {
+ "auction_size": "10000000",
+ "conversion_factor": "8",
+ "debt_limit": [
+ {
+ "amount": "10000000",
+ "denom": "usdx"
+ }
+ ],
+ "denom": "btc",
+ "liquidation_penalty": "0.05",
+ "liquidation_ratio": "1.5",
+ "market_id": "btc:usd",
+ "prefix": 1,
+ "stability_fee": "1.0000000007829977"
+ }
+ ],
+ "debt_auction_threshold": "1000000000",
+ "debt_params": [
+ {
+ "conversion_factor": "6",
+ "debt_floor": "10000000",
+ "debt_limit": [
+ {
+ "amount": "2000000000000",
+ "denom": "usdx"
+ }
+ ],
+ "denom": "usdx",
+ "reference_asset": "usd",
+ "savings_rate": "0.95"
+ }
+ ],
+ "global_debt_limit": [
+ {
+ "amount": "2000000000000",
+ "denom": "usdx"
+ }
+ ],
+ "savings_distribution_frequency": "120000000000",
+ "surplus_auction_threshold": "1000000000"
+ },
+ "previous_block_time": "1970-01-01T00:00:00Z",
+ "starting_cdp_id": "1"
+ },
+ "bank": {
+ "send_enabled": true
+ },
+ "params": null,
+ "bep3": {
+ "params": {
+ "bnb_deputy_address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
+ "min_block_lock": "80",
+ "max_block_lock": "600",
+ "supported_assets": [
+ {
+ "denom": "ukava",
+ "coin_id": "459",
+ "limit": "1",
+ "active": false
+ }
+ ]
+ },
+ "atomic_swaps": [],
+ "assets_supplies": []
+ },
+ "pricefeed": {
+ "params": {
+ "markets": [
+ {
+ "active": true,
+ "base_asset": "xrp",
+ "market_id": "xrp:usd",
+ "oracles": [
+ "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "kava1xq4cspcgl9thzmn6lkvd6dlx28wsr63zw4mlmf"
+ ],
+ "quote_asset": "usd"
+ },
+ {
+ "active": true,
+ "base_asset": "btc",
+ "market_id": "btc:usd",
+ "oracles": [
+ "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"
+ ],
+ "quote_asset": "usd"
+ }
+ ]
+ },
+ "posted_prices": [
+ {
+ "expiry": "2050-01-01T00:00:00Z",
+ "market_id": "btc:usd",
+ "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "price": "8700.0"
+ },
+ {
+ "expiry": "2050-01-01T00:00:00Z",
+ "market_id": "xrp:usd",
+ "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "price": "0.25"
+ }
+ ]
+ },
+ "gov": {
+ "starting_proposal_id": "1",
+ "deposits": null,
+ "votes": null,
+ "proposals": null,
+ "deposit_params": {
+ "min_deposit": [
+ {
+ "denom": "ukava",
+ "amount": "10000000"
+ }
+ ],
+ "max_deposit_period": "172800000000000"
+ },
+ "voting_params": {
+ "voting_period": "172800000000000"
+ },
+ "tally_params": {
+ "quorum": "0.334000000000000000",
+ "threshold": "0.500000000000000000",
+ "veto": "0.334000000000000000"
+ }
+ },
+ "staking": {
+ "params": {
+ "unbonding_time": "1814400000000000",
+ "max_validators": 100,
+ "max_entries": 7,
+ "bond_denom": "ukava"
+ },
+ "last_total_power": "0",
+ "last_validator_powers": null,
+ "validators": null,
+ "delegations": null,
+ "unbonding_delegations": null,
+ "redelegations": null,
+ "exported": false
+ },
+ "mint": {
+ "minter": {
+ "inflation": "0.130000000000000000",
+ "annual_provisions": "0.000000000000000000"
+ },
+ "params": {
+ "mint_denom": "ukava",
+ "inflation_rate_change": "0.130000000000000000",
+ "inflation_max": "0.200000000000000000",
+ "inflation_min": "0.070000000000000000",
+ "goal_bonded": "0.670000000000000000",
+ "blocks_per_year": "6311520"
+ }
+ },
+ "auth": {
+ "params": {
+ "max_memo_characters": "256",
+ "tx_sig_limit": "7",
+ "tx_size_cost_per_byte": "10",
+ "sig_verify_cost_ed25519": "590",
+ "sig_verify_cost_secp256k1": "1000"
+ },
+ "accounts": [
+ {
+ "type": "cosmos-sdk/Account",
+ "value": {
+ "address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "coins": [
+ {
+ "denom": "ukava",
+ "amount": "1000000000000"
+ },
+ {
+ "denom": "btc",
+ "amount": "10000000000"
+ },
+ {
+ "denom": "xrp",
+ "amount": "1000000000000"
+ }
+ ],
+ "public_key": null,
+ "account_number": "0",
+ "sequence": "0"
+ }
+ },
+ {
+ "type": "cosmos-sdk/Account",
+ "value": {
+ "address": "kava19dp7luf32mlqnw6mhpgk0g37ule7wm2g8gck8a",
+ "coins": [
+ {
+ "denom": "ukava",
+ "amount": "1000000000000"
+ }
+ ],
+ "public_key": null,
+ "account_number": "0",
+ "sequence": "0"
+ }
+ }
+ ]
+ },
+ "auction": {
+ "next_auction_id": "1",
+ "params": {
+ "max_auction_duration": "172800000000000",
+ "bid_duration": "3600000000000"
+ },
+ "auctions": []
+ },
+ "validatorvesting": {
+ "previous_block_time": "1970-01-01T00:00:00Z"
+ },
+ "supply": {
+ "supply": []
+ },
+ "crisis": {
+ "constant_fee": {
+ "denom": "ukava",
+ "amount": "1000"
+ }
+ },
+ "distribution": {
+ "fee_pool": {
+ "community_pool": []
+ },
+ "community_tax": "0.020000000000000000",
+ "base_proposer_reward": "0.010000000000000000",
+ "bonus_proposer_reward": "0.040000000000000000",
+ "withdraw_addr_enabled": true,
+ "delegator_withdraw_infos": [],
+ "previous_proposer": "",
+ "outstanding_rewards": [],
+ "validator_accumulated_commissions": [],
+ "validator_historical_rewards": [],
+ "validator_current_rewards": [],
+ "delegator_starting_infos": [],
+ "validator_slash_events": []
+ },
+ "genutil": {
+ "gentxs": [
+ {
+ "type": "cosmos-sdk/StdTx",
+ "value": {
+ "msg": [
+ {
+ "type": "cosmos-sdk/MsgCreateValidator",
+ "value": {
+ "description": {
+ "moniker": "kava-tester",
+ "identity": "",
+ "website": "",
+ "security_contact": "",
+ "details": ""
+ },
+ "commission": {
+ "rate": "0.100000000000000000",
+ "max_rate": "0.200000000000000000",
+ "max_change_rate": "0.010000000000000000"
+ },
+ "min_self_delegation": "1",
+ "delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte",
+ "pubkey": "kavavalconspub1zcjduepq3qyhcxfpa0u2efeu3htuxjz0248khl2cqfcm8jz2n0dr2e2a6tuqqafg2g",
+ "value": {
+ "denom": "ukava",
+ "amount": "10000000000"
+ }
+ }
+ }
+ ],
+ "fee": {
+ "amount": [],
+ "gas": "200000"
+ },
+ "signatures": [
+ {
+ "pub_key": {
+ "type": "tendermint/PubKeySecp256k1",
+ "value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43"
+ },
+ "signature": "ZIzw2qsaJzsNuROW1JYYH1ZOA3jrc4ZCHvrCxirWNlEZqIvnyC42nBQLIPQ+d+PIcpldLVy0KAkb8NBXj9G0nQ=="
+ }
+ ],
+ "memo": "6fff8e9b327f0811e7a25c1419781167f82ec7b3@172.31.40.66:26656"
+ }
+ }
+ ]
+ },
+ "slashing": {
+ "params": {
+ "max_evidence_age": "120000000000",
+ "signed_blocks_window": "100",
+ "min_signed_per_window": "0.500000000000000000",
+ "downtime_jail_duration": "600000000000",
+ "slash_fraction_double_sign": "0.050000000000000000",
+ "slash_fraction_downtime": "0.010000000000000000"
+ },
+ "signing_infos": {},
+ "missed_blocks": {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/contrib/genesis_examples/genesis_savings_rate.json b/contrib/genesis_examples/genesis_savings_rate.json
new file mode 100644
index 00000000..538d5b72
--- /dev/null
+++ b/contrib/genesis_examples/genesis_savings_rate.json
@@ -0,0 +1,352 @@
+{
+ "genesis_time": "2020-03-07T18:27:07.837213082Z",
+ "chain_id": "testing",
+ "consensus_params": {
+ "block": {
+ "max_bytes": "22020096",
+ "max_gas": "-1",
+ "time_iota_ms": "1000"
+ },
+ "evidence": {
+ "max_age": "100000"
+ },
+ "validator": {
+ "pub_key_types": [
+ "ed25519"
+ ]
+ }
+ },
+ "app_hash": "",
+ "app_state": {
+ "cdp": {
+ "cdps": [],
+ "debt_denom": "debt",
+ "deposits": [],
+ "gov_denom": "ukava",
+ "previous_distribution_time": "1970-01-01T00:00:00Z",
+ "params": {
+ "circuit_breaker": false,
+ "collateral_params": [
+ {
+ "auction_size": "5000000000",
+ "conversion_factor": "6",
+ "debt_limit": [
+ {
+ "amount": "10000000",
+ "denom": "usdx"
+ }
+ ],
+ "denom": "xrp",
+ "liquidation_penalty": "0.05",
+ "liquidation_ratio": "2.0",
+ "market_id": "xrp:usd",
+ "prefix": 0,
+ "stability_fee": "1.000000001547126"
+ },
+ {
+ "auction_size": "10000000",
+ "conversion_factor": "8",
+ "debt_limit": [
+ {
+ "amount": "10000000",
+ "denom": "usdx"
+ }
+ ],
+ "denom": "btc",
+ "liquidation_penalty": "0.05",
+ "liquidation_ratio": "1.5",
+ "market_id": "btc:usd",
+ "prefix": 1,
+ "stability_fee": "1.0000000007829977"
+ }
+ ],
+ "debt_auction_threshold": "1000000000",
+ "debt_params": [
+ {
+ "conversion_factor": "6",
+ "debt_floor": "10000000",
+ "debt_limit": [
+ {
+ "amount": "2000000000000",
+ "denom": "usdx"
+ }
+ ],
+ "denom": "usdx",
+ "reference_asset": "usd",
+ "savings_rate": "0.95"
+ }
+ ],
+ "global_debt_limit": [
+ {
+ "amount": "2000000000000",
+ "denom": "usdx"
+ }
+ ],
+ "savings_distribution_frequency": "120000000000",
+ "surplus_auction_threshold": "1000000000"
+ },
+ "previous_block_time": "1970-01-01T00:00:00Z",
+ "starting_cdp_id": "1"
+ },
+ "bank": {
+ "send_enabled": true
+ },
+ "params": null,
+ "bep3": {
+ "params": {
+ "bnb_deputy_address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
+ "min_block_lock": "80",
+ "max_block_lock": "600",
+ "supported_assets": [
+ {
+ "denom": "ukava",
+ "coin_id": "459",
+ "limit": "1",
+ "active": false
+ }
+ ]
+ },
+ "atomic_swaps": [],
+ "assets_supplies": []
+ },
+ "pricefeed": {
+ "params": {
+ "markets": [
+ {
+ "active": true,
+ "base_asset": "xrp",
+ "market_id": "xrp:usd",
+ "oracles": [
+ "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "kava1xq4cspcgl9thzmn6lkvd6dlx28wsr63zw4mlmf"
+ ],
+ "quote_asset": "usd"
+ },
+ {
+ "active": true,
+ "base_asset": "btc",
+ "market_id": "btc:usd",
+ "oracles": [
+ "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"
+ ],
+ "quote_asset": "usd"
+ }
+ ]
+ },
+ "posted_prices": [
+ {
+ "expiry": "2050-01-01T00:00:00Z",
+ "market_id": "btc:usd",
+ "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "price": "8700.0"
+ },
+ {
+ "expiry": "2050-01-01T00:00:00Z",
+ "market_id": "xrp:usd",
+ "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "price": "0.25"
+ }
+ ]
+ },
+ "gov": {
+ "starting_proposal_id": "1",
+ "deposits": null,
+ "votes": null,
+ "proposals": null,
+ "deposit_params": {
+ "min_deposit": [
+ {
+ "denom": "ukava",
+ "amount": "10000000"
+ }
+ ],
+ "max_deposit_period": "172800000000000"
+ },
+ "voting_params": {
+ "voting_period": "172800000000000"
+ },
+ "tally_params": {
+ "quorum": "0.334000000000000000",
+ "threshold": "0.500000000000000000",
+ "veto": "0.334000000000000000"
+ }
+ },
+ "staking": {
+ "params": {
+ "unbonding_time": "1814400000000000",
+ "max_validators": 100,
+ "max_entries": 7,
+ "bond_denom": "ukava"
+ },
+ "last_total_power": "0",
+ "last_validator_powers": null,
+ "validators": null,
+ "delegations": null,
+ "unbonding_delegations": null,
+ "redelegations": null,
+ "exported": false
+ },
+ "mint": {
+ "minter": {
+ "inflation": "0.130000000000000000",
+ "annual_provisions": "0.000000000000000000"
+ },
+ "params": {
+ "mint_denom": "ukava",
+ "inflation_rate_change": "0.130000000000000000",
+ "inflation_max": "0.200000000000000000",
+ "inflation_min": "0.070000000000000000",
+ "goal_bonded": "0.670000000000000000",
+ "blocks_per_year": "6311520"
+ }
+ },
+ "auth": {
+ "params": {
+ "max_memo_characters": "256",
+ "tx_sig_limit": "7",
+ "tx_size_cost_per_byte": "10",
+ "sig_verify_cost_ed25519": "590",
+ "sig_verify_cost_secp256k1": "1000"
+ },
+ "accounts": [
+ {
+ "type": "cosmos-sdk/Account",
+ "value": {
+ "address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "coins": [
+ {
+ "denom": "ukava",
+ "amount": "1000000000000"
+ },
+ {
+ "denom": "btc",
+ "amount": "10000000000"
+ },
+ {
+ "denom": "xrp",
+ "amount": "1000000000000"
+ }
+ ],
+ "public_key": null,
+ "account_number": "0",
+ "sequence": "0"
+ }
+ },
+ {
+ "type": "cosmos-sdk/Account",
+ "value": {
+ "address": "kava19dp7luf32mlqnw6mhpgk0g37ule7wm2g8gck8a",
+ "coins": [
+ {
+ "denom": "ukava",
+ "amount": "1000000000000"
+ }
+ ],
+ "public_key": null,
+ "account_number": "0",
+ "sequence": "0"
+ }
+ }
+ ]
+ },
+ "auction": {
+ "next_auction_id": "1",
+ "params": {
+ "max_auction_duration": "172800000000000",
+ "bid_duration": "3600000000000"
+ },
+ "auctions": []
+ },
+ "validatorvesting": {
+ "previous_block_time": "1970-01-01T00:00:00Z"
+ },
+ "supply": {
+ "supply": []
+ },
+ "crisis": {
+ "constant_fee": {
+ "denom": "ukava",
+ "amount": "1000"
+ }
+ },
+ "distribution": {
+ "fee_pool": {
+ "community_pool": []
+ },
+ "community_tax": "0.020000000000000000",
+ "base_proposer_reward": "0.010000000000000000",
+ "bonus_proposer_reward": "0.040000000000000000",
+ "withdraw_addr_enabled": true,
+ "delegator_withdraw_infos": [],
+ "previous_proposer": "",
+ "outstanding_rewards": [],
+ "validator_accumulated_commissions": [],
+ "validator_historical_rewards": [],
+ "validator_current_rewards": [],
+ "delegator_starting_infos": [],
+ "validator_slash_events": []
+ },
+ "genutil": {
+ "gentxs": [
+ {
+ "type": "cosmos-sdk/StdTx",
+ "value": {
+ "msg": [
+ {
+ "type": "cosmos-sdk/MsgCreateValidator",
+ "value": {
+ "description": {
+ "moniker": "kava-tester",
+ "identity": "",
+ "website": "",
+ "security_contact": "",
+ "details": ""
+ },
+ "commission": {
+ "rate": "0.100000000000000000",
+ "max_rate": "0.200000000000000000",
+ "max_change_rate": "0.010000000000000000"
+ },
+ "min_self_delegation": "1",
+ "delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte",
+ "pubkey": "kavavalconspub1zcjduepq3qyhcxfpa0u2efeu3htuxjz0248khl2cqfcm8jz2n0dr2e2a6tuqqafg2g",
+ "value": {
+ "denom": "ukava",
+ "amount": "10000000000"
+ }
+ }
+ }
+ ],
+ "fee": {
+ "amount": [],
+ "gas": "200000"
+ },
+ "signatures": [
+ {
+ "pub_key": {
+ "type": "tendermint/PubKeySecp256k1",
+ "value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43"
+ },
+ "signature": "ZIzw2qsaJzsNuROW1JYYH1ZOA3jrc4ZCHvrCxirWNlEZqIvnyC42nBQLIPQ+d+PIcpldLVy0KAkb8NBXj9G0nQ=="
+ }
+ ],
+ "memo": "6fff8e9b327f0811e7a25c1419781167f82ec7b3@172.31.40.66:26656"
+ }
+ }
+ ]
+ },
+ "slashing": {
+ "params": {
+ "max_evidence_age": "120000000000",
+ "signed_blocks_window": "100",
+ "min_signed_per_window": "0.500000000000000000",
+ "downtime_jail_duration": "600000000000",
+ "slash_fraction_double_sign": "0.050000000000000000",
+ "slash_fraction_downtime": "0.010000000000000000"
+ },
+ "signing_infos": {},
+ "missed_blocks": {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/contrib/testnet-4000/README.md b/contrib/testnet-4000/README.md
index c579bb26..b1e45544 100644
--- a/contrib/testnet-4000/README.md
+++ b/contrib/testnet-4000/README.md
@@ -6,7 +6,7 @@ Resources and examples for running and interacting with kava-testnet-4000
### Setup
- Before making a request, query account information for the signing account. Note the 'accountnumber' and 'sequence' fields, we'll need them later in order to send our request:
+Before making a request, query account information for the signing account. Note the 'accountnumber' and 'sequence' fields, we'll need them later in order to send our request:
```bash
kvcli q auth account $(kvcli keys show accB -a)
@@ -22,41 +22,49 @@ Now we'll create an unsigned request, sign it, and broadcast it to the Kava bloc
### Create CDP example request
- Format the base request in create-cdp.json. You'll need to update the 'from', 'chain-id', 'account_number', 'sequence', and 'gas' as appropriate. Then, populate the CDP creation request's params 'owner', 'collateral', and 'principal'. An example formatted request can be found in `example-create-cdp.json`.
+Format the base request in create-cdp.json. You'll need to update the 'from', 'chain-id', 'account_number', 'sequence', and 'gas' as appropriate. Then, populate the CDP creation request's params 'owner', 'collateral', and 'principal'. An example formatted request can be found in `example-create-cdp.json`.
```bash
# Create an unsigned request
- curl -H "Content-Type: application/json" -X PUT -d @./contrib/requests/create-cdp.json http://127.0.0.1:1317/cdp | jq > ./contrib/requests/create-cdp-unsigned.json
+ curl -H "Content-Type: application/json" -X PUT -d @./contrib/testnet-4000/rest_examples/cdp/create-cdp.json http://127.0.0.1:1317/cdp | jq > ./contrib/testnet-4000/rest_examples/cdp/create-cdp-unsigned.json
# Sign the request
- kvcli tx sign ./contrib/requests/create-cdp-unsigned.json --from accB --offline --chain-id testing --sequence 1 --account-number 2 | jq > ./contrib/requests/broadcast-create-cdp.json
+ kvcli tx sign ./contrib/testnet-4000/rest_examples/cdp/create-cdp-unsigned.json --from accB --offline --chain-id testing --sequence 1 --account-number 2 | jq > ./contrib/testnet-4000/rest_examples/cdp/broadcast-create-cdp.json
# Broadcast the request
- kvcli tx broadcast ./contrib/requests/broadcast-create-cdp.json
+ kvcli tx broadcast ./contrib/testnet-4000/rest_examples/cdp/broadcast-create-cdp.json
```
Congratulations, you've just created a CDP on Kava using the rest server!
### Post market price example request
- Note that only market oracles can post prices, other senders will have their transactions rejected by Kava.
+Note that only market oracles can post prices, other senders will have their transactions rejected by Kava.
- Format the base request in post-price.json. You'll need to update the 'from', 'chain-id', 'account_number', 'sequence', and 'gas' as appropriate. Then, populate the post price request's params 'from', 'market_id', 'price', and 'expiry'. An example formatted request can be found in `example-post-price.json`.
+Format the base request in post-price.json. You'll need to update the 'from', 'chain-id', 'account_number', 'sequence', and 'gas' as appropriate. Then, populate the post price request's params 'from', 'market_id', 'price', and 'expiry'. An example formatted request can be found in `example-post-price.json`.
```bash
# Create an unsigned request
- curl -H "Content-Type: application/json" -X PUT -d @./contrib/requests/post-price.json http://127.0.0.1:1317/pricefeed/postprice | jq > ./contrib/requests/post-price-unsigned.json
+ curl -H "Content-Type: application/json" -X PUT -d @./contrib/testnet-4000/rest_examples/pricefeed/post-price.json http://127.0.0.1:1317/pricefeed/postprice | jq > ./contrib/testnet-4000/rest_examples/pricefeed/post-price-unsigned.json
# Sign the request
- kvcli tx sign ./contrib/requests/post-price-unsigned.json --from validator --offline --chain-id testing --sequence 96 --account-number 0 | jq > ./contrib/requests/broadcast-post-price.json
+ kvcli tx sign ./contrib/testnet-4000/rest_examples/pricefeed/post-price-unsigned.json --from validator --offline --chain-id testing --sequence 96 --account-number 0 | jq > ./contrib/testnet-4000/rest_examples/pricefeed/broadcast-post-price.json
# Broadcast the request
- kvcli tx broadcast ./contrib/requests/broadcast-post-price.json
+ kvcli tx broadcast ./contrib/testnet-4000/rest_examples/pricefeed/broadcast-post-price.json
```
Congratulations, you've just posted a current market price on Kava using the rest server!
## Governance proposals
-Example governance proposals are located in `/proposal_examples`.
\ No newline at end of file
+Example governance proposals are located in `/proposal_examples`.
+
+## Dredd automated REST API testing
+
+To run the dredd tests to hit the endpoints and test the api run the following command from the `kava` folder:
+
+```bash
+ make test_dredd
+```
\ No newline at end of file
diff --git a/contrib/testnet-4000/requests/broadcast-create-cdp.json b/contrib/testnet-4000/rest_examples/cdp/broadcast-create-cdp.json
similarity index 100%
rename from contrib/testnet-4000/requests/broadcast-create-cdp.json
rename to contrib/testnet-4000/rest_examples/cdp/broadcast-create-cdp.json
diff --git a/contrib/testnet-4000/requests/broadcast-deposit-cdp.json b/contrib/testnet-4000/rest_examples/cdp/broadcast-deposit-cdp.json
similarity index 100%
rename from contrib/testnet-4000/requests/broadcast-deposit-cdp.json
rename to contrib/testnet-4000/rest_examples/cdp/broadcast-deposit-cdp.json
diff --git a/contrib/testnet-4000/requests/create-cdp-unsigned.json b/contrib/testnet-4000/rest_examples/cdp/create-cdp-unsigned.json
similarity index 100%
rename from contrib/testnet-4000/requests/create-cdp-unsigned.json
rename to contrib/testnet-4000/rest_examples/cdp/create-cdp-unsigned.json
diff --git a/contrib/testnet-4000/requests/create-cdp.json b/contrib/testnet-4000/rest_examples/cdp/create-cdp.json
similarity index 100%
rename from contrib/testnet-4000/requests/create-cdp.json
rename to contrib/testnet-4000/rest_examples/cdp/create-cdp.json
diff --git a/contrib/testnet-4000/requests/deposit-cdp.json b/contrib/testnet-4000/rest_examples/cdp/deposit-cdp.json
similarity index 100%
rename from contrib/testnet-4000/requests/deposit-cdp.json
rename to contrib/testnet-4000/rest_examples/cdp/deposit-cdp.json
diff --git a/contrib/testnet-4000/requests/example-create-cdp.json b/contrib/testnet-4000/rest_examples/cdp/example-create-cdp.json
similarity index 100%
rename from contrib/testnet-4000/requests/example-create-cdp.json
rename to contrib/testnet-4000/rest_examples/cdp/example-create-cdp.json
diff --git a/contrib/testnet-4000/requests/broadcast-post-price.json b/contrib/testnet-4000/rest_examples/pricefeed/broadcast-post-price.json
similarity index 100%
rename from contrib/testnet-4000/requests/broadcast-post-price.json
rename to contrib/testnet-4000/rest_examples/pricefeed/broadcast-post-price.json
diff --git a/contrib/testnet-4000/requests/example-post-price.json b/contrib/testnet-4000/rest_examples/pricefeed/example-post-price.json
similarity index 100%
rename from contrib/testnet-4000/requests/example-post-price.json
rename to contrib/testnet-4000/rest_examples/pricefeed/example-post-price.json
diff --git a/contrib/testnet-4000/requests/post-price-unsigned.json b/contrib/testnet-4000/rest_examples/pricefeed/post-price-unsigned.json
similarity index 100%
rename from contrib/testnet-4000/requests/post-price-unsigned.json
rename to contrib/testnet-4000/rest_examples/pricefeed/post-price-unsigned.json
diff --git a/contrib/testnet-4000/requests/post-price.json b/contrib/testnet-4000/rest_examples/pricefeed/post-price.json
similarity index 100%
rename from contrib/testnet-4000/requests/post-price.json
rename to contrib/testnet-4000/rest_examples/pricefeed/post-price.json
diff --git a/contrib/testnet-5000/README.md b/contrib/testnet-5000/README.md
new file mode 100644
index 00000000..f85311ad
--- /dev/null
+++ b/contrib/testnet-5000/README.md
@@ -0,0 +1,125 @@
+# Testnet-5000
+
+Testnet-5000 introduces transfers between Kava and Bnbchain via BEP3.
+
+This guide will walk you through interacting with the blockchains and transferring coins via the rest server. To send transactions, we'll create an unsigned request, sign it, and broadcast it to the Kava blockchain.
+
+## Rest server requests
+
+### Setup
+
+We'll be using Kava's CLI to build, sign, and broadcast the transactions:
+
+```bash
+ # Download kvcli
+ make install
+```
+
+Before making a request, query account information for the signing account. Note the 'accountnumber' and 'sequence' fields, we'll need them later in order to send our request:
+
+```bash
+ kvcli q auth account $(kvcli keys show testuser -a)
+```
+
+### Create swap
+
+Use the example file in `rest_examples/create-swap.json` to format the request. First, update the header parameters 'from', 'chain-id', 'account_number', 'sequence'.
+
+Next, we'll update the swap's creation parameters. For that, we need a unique random number that will be used to claim the funds.
+
+WARNING: Don't use `calc-rnh` for the generation of secrets in production. These values should be generated client-side for the safety of user funds.
+
+```bash
+ # Generate a sample random number, timestamp, and random number hash
+ kvcli q bep3 calc-rnh now
+
+ # Expected output:
+ # Random number: 110802331073994018312675691928205725441742309715720953510374321628333109608728
+ # Timestamp: 1585203985
+ # Random number hash: 4644fc2d9a2389c60e621785b873ae187e320eaded1687edaa120961428eba9e
+```
+
+In the same json file, populate each of the following parameters
+
+- from
+- to
+- recipient_other_chain
+- sender_other_chain
+- random_number_hash
+- timestamp
+- amount
+- expected_income
+- height_span
+- cross_chain
+
+Once each parameter is populated, it's time to create our swap:
+
+```bash
+ # Create an unsigned request
+ curl -H "Content-Type: application/json" -X POST -d @./contrib/testnet-5000/rest_examples/create-swap.json http://127.0.0.1:1317/bep3/swap/create | jq > ./contrib/testnet-5000/rest_examples/create-swap-unsigned.json
+
+ # Sign the request
+ kvcli tx sign ./contrib/testnet-5000/rest_examples/create-swap-unsigned.json --from testnetdeputy --offline --chain-id testing --sequence 0 --account-number 5 | jq > ./contrib/testnet-5000/rest_examples/broadcast-create-swap.json
+
+ # Broadcast the request
+ kvcli tx broadcast ./contrib/testnet-5000/rest_examples/broadcast-create-swap.json
+```
+
+The tx broadcast will log information in the terminal, including the txhash. This tx hash can be used to get information about the transaction, including the swap creation event that includes the swap's ID:
+
+```bash
+ # Get information about the transaction
+ curl -H "Content-Type: application/json" -X GET http://localhost:1317/txs/81A1955216F6D985ECB4770E29B9BCED8F73A42D0C0FD566372CF673CCB81587
+```
+
+Congratulations, you've just created a swap on Kava! The swap will be automatically relayed over to Bnbchain where it it can be claimed using the secret random number from above.
+
+# Claim swap
+
+Only unexpired swaps can be claimed. To claim a swap, we'll use the secret random number that matches this swap's timestamp and random number hash.
+
+Generally, claimable swaps must be created on Bnbchain.
+// TODO: add link to Bnbchain document with interaction steps
+
+Use the example file in `rest_examples/claim-swap.json` to format the request. Again, update the header parameters 'from', 'account_number', 'sequence'. Check your account using the command from above to ensure that the parameters match the blockchain's state.
+
+In the same json file, populate each listed parameter:
+
+- swap_id
+- random_number
+
+Once the `swap_id` parameter is populated, it's time to claim our swap:
+
+```bash
+ # Create an unsigned request
+ curl -H "Content-Type: application/json" -X POST -d @./contrib/testnet-5000/rest_examples/claim-swap.json http://127.0.0.1:1317/bep3/swap/claim | jq > ./contrib/testnet-5000/rest_examples/claim-swap-unsigned.json
+
+ # Sign the request
+ kvcli tx sign ./contrib/testnet-5000/rest_examples/claim-swap-unsigned.json --from user --offline --chain-id testing --sequence 0 --account-number 1 | jq > ./contrib/testnet-5000/rest_examples/broadcast-claim-swap.json
+
+ # Broadcast the request
+ kvcli tx broadcast ./contrib/testnet-5000/rest_examples/broadcast-claim-swap.json
+```
+
+# Refund swap
+
+Only expired swaps may be refunded.
+
+Use the example file in `rest_examples/refund-swap.json` to format the request. Again, update the header parameters 'from', 'account_number', 'sequence'. Check your account using the command from above to ensure that the parameters match the blockchain's state.
+
+In the same json file, populate each parameter:
+
+- swap_id
+
+Once the `swap_id` parameter is populated, it's time to refund our swap:
+
+```bash
+ # Create an unsigned request
+ curl -H "Content-Type: application/json" -X POST -d @./contrib/testnet-5000/rest_examples/refund-swap.json http://127.0.0.1:1317/bep3/swap/refund | jq > ./contrib/testnet-5000/rest_examples/refund-swap-unsigned.json
+
+ # Sign the request
+ kvcli tx sign ./contrib/testnet-5000/rest_examples/refund-swap-unsigned.json --from user --offline --chain-id testing --sequence 0 --account-number 1 | jq > ./contrib/testnet-5000/rest_examples/broadcast-refund-swap.json
+
+ # Broadcast the request
+ kvcli tx broadcast ./contrib/testnet-5000/rest_examples/broadcast-refund-swap.json
+```
diff --git a/contrib/testnet-5000/genesis_examples/internal_proposed_genesis.json b/contrib/testnet-5000/genesis_examples/internal_proposed_genesis.json
new file mode 100644
index 00000000..4853c045
--- /dev/null
+++ b/contrib/testnet-5000/genesis_examples/internal_proposed_genesis.json
@@ -0,0 +1,244 @@
+{
+ "genesis_time": "2020-03-27T21:00:00Z",
+ "chain_id": "testing",
+ "consensus_params": {
+ "block": {
+ "max_bytes": "200000",
+ "max_gas": "2000000",
+ "time_iota_ms": "1000"
+ },
+ "evidence": {
+ "max_age": "1000000"
+ },
+ "validator": {
+ "pub_key_types": ["ed25519"]
+ }
+ },
+ "app_hash": "",
+ "app_state": {
+ "auth": {
+ "accounts": [],
+ "params": {
+ "max_memo_characters": "256",
+ "sig_verify_cost_ed25519": "590",
+ "sig_verify_cost_secp256k1": "1000",
+ "tx_sig_limit": "7",
+ "tx_size_cost_per_byte": "10"
+ }
+ },
+ "kavadist": {
+ "params": {
+ "active": true,
+ "periods": [
+ {
+ "start": "2020-03-28T15:20:00Z",
+ "end": "2021-03-28T15:20:00Z",
+ "inflation": "1.000000003022265980"
+ }
+ ]
+ },
+ "previous_block_time": "1970-01-01T00:00:00Z"
+ },
+ "slashing": {
+ "missed_blocks": {},
+ "params": {
+ "downtime_jail_duration": "600000000000",
+ "max_evidence_age": "3600000000000",
+ "min_signed_per_window": "0.010000000000000000",
+ "signed_blocks_window": "1000",
+ "slash_fraction_double_sign": "0.050000000000000000",
+ "slash_fraction_downtime": "0.000100000000000000"
+ },
+ "signing_infos": {}
+ },
+ "bank": {
+ "send_enabled": true
+ },
+ "distribution": {
+ "base_proposer_reward": "0.010000000000000000",
+ "bonus_proposer_reward": "0.040000000000000000",
+ "community_tax": "0.000000000000000000",
+ "delegator_starting_infos": [],
+ "delegator_withdraw_infos": [],
+ "fee_pool": {
+ "community_pool": []
+ },
+ "outstanding_rewards": [],
+ "previous_proposer": "",
+ "validator_accumulated_commissions": [],
+ "validator_current_rewards": [],
+ "validator_historical_rewards": [],
+ "validator_slash_events": [],
+ "withdraw_addr_enabled": true
+ },
+ "mint": {
+ "minter": {
+ "annual_provisions": "0.000000000000000000",
+ "inflation": "0.020000000000000000"
+ },
+ "params": {
+ "blocks_per_year": "6311520",
+ "goal_bonded": "0.670000000000000000",
+ "inflation_max": "0.130000000000000000",
+ "inflation_min": "0.010000000000000000",
+ "inflation_rate_change": "0.130000000000000000",
+ "mint_denom": "ukava"
+ }
+ },
+ "cdp": {
+ "cdps": [],
+ "debt_denom": "debt",
+ "deposits": [],
+ "gov_denom": "ukava",
+ "params": {
+ "circuit_breaker": false,
+ "collateral_params": [
+ {
+ "auction_size": "50000000000",
+ "conversion_factor": "8",
+ "debt_limit": [
+ {
+ "amount": "2000000000000",
+ "denom": "usdx"
+ }
+ ],
+ "denom": "bnb",
+ "liquidation_penalty": "0.05",
+ "liquidation_ratio": "2.0",
+ "market_id": "bnb:usd",
+ "prefix": 1,
+ "stability_fee": "1.0000000007829977"
+ }
+ ],
+ "debt_auction_threshold": "10000000000",
+ "debt_params": [
+ {
+ "conversion_factor": "6",
+ "debt_floor": "10000000",
+ "debt_limit": [
+ {
+ "amount": "2000000000000",
+ "denom": "usdx"
+ }
+ ],
+ "denom": "usdx",
+ "reference_asset": "usd",
+ "savings_rate": "0.95"
+ }
+ ],
+ "global_debt_limit": [
+ {
+ "amount": "2000000000000",
+ "denom": "usdx"
+ }
+ ],
+ "savings_distribution_frequency": "120000000000",
+ "surplus_auction_threshold": "1000000000"
+ },
+ "previous_block_time": "1970-01-01T00:00:00Z",
+ "previous_distribution_time": "1970-01-01T00:00:00Z",
+ "starting_cdp_id": "1"
+ },
+ "gov": {
+ "deposit_params": {
+ "max_deposit_period": "600000000000",
+ "min_deposit": [
+ {
+ "amount": "200000000",
+ "denom": "ukava"
+ }
+ ]
+ },
+ "deposits": null,
+ "proposals": null,
+ "starting_proposal_id": "1",
+ "tally_params": {
+ "quorum": "0.334000000000000000",
+ "threshold": "0.500000000000000000",
+ "veto": "0.334000000000000000"
+ },
+ "votes": null,
+ "voting_params": {
+ "voting_period": "3600000000000"
+ }
+ },
+ "staking": {
+ "delegations": null,
+ "exported": false,
+ "last_total_power": "0",
+ "last_validator_powers": null,
+ "params": {
+ "bond_denom": "ukava",
+ "max_entries": 7,
+ "max_validators": 100,
+ "unbonding_time": "3600000000000"
+ },
+ "redelegations": null,
+ "unbonding_delegations": null,
+ "validators": null
+ },
+ "supply": {
+ "supply": []
+ },
+ "crisis": {
+ "constant_fee": {
+ "amount": "1333000000",
+ "denom": "ukava"
+ }
+ },
+ "pricefeed": {
+ "params": {
+ "markets": [
+ {
+ "active": true,
+ "base_asset": "bnb",
+ "market_id": "bnb:usd",
+ "oracles": ["kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"],
+ "quote_asset": "usd"
+ }
+ ]
+ },
+ "posted_prices": [
+ {
+ "expiry": "2020-04-20T00:00:00Z",
+ "market_id": "bnb:usd",
+ "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "price": "12.2639061184"
+ }
+ ]
+ },
+ "auction": {
+ "auctions": [],
+ "next_auction_id": "0",
+ "params": {
+ "bid_duration": "600000000000",
+ "max_auction_duration": "172800000000000",
+ "increment_surplus": "0.01",
+ "increment_debt": "0.01",
+ "increment_collateral": "0.01"
+ }
+ },
+ "bep3": {
+ "params": {
+ "bnb_deputy_address": "kava15wmww3chakqllq4n3ksm37lx36qz067gxe6f0k",
+ "min_block_lock": "80",
+ "max_block_lock": "600",
+ "supported_assets": [
+ {
+ "denom": "bnb",
+ "coin_id": "714",
+ "limit": "100000000000",
+ "active": true
+ }
+ ]
+ }
+ },
+ "genutil": {
+ "gentxs": []
+ },
+ "validatorvesting": {
+ "previous_block_time": "1970-01-01T00:00:00Z"
+ },
+ "params": null
+ }
+}
diff --git a/contrib/testnet-5000/rest_examples/broadcast-claim-swap.json b/contrib/testnet-5000/rest_examples/broadcast-claim-swap.json
new file mode 100644
index 00000000..e0ca95bf
--- /dev/null
+++ b/contrib/testnet-5000/rest_examples/broadcast-claim-swap.json
@@ -0,0 +1,29 @@
+{
+ "type": "cosmos-sdk/StdTx",
+ "value": {
+ "msg": [
+ {
+ "type": "bep3/MsgClaimAtomicSwap",
+ "value": {
+ "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "swap_id": "3CA20F0152F03B0AABE73E7AA1DDF78B9048EDE5A9A73846E1EF53BEBBFA4185",
+ "random_number": "E3D0A98B459F72231DA69C3BD771C1E721FEF4BE83C14B80DC805BA71019EEBE"
+ }
+ }
+ ],
+ "fee": {
+ "amount": [],
+ "gas": "500000"
+ },
+ "signatures": [
+ {
+ "pub_key": {
+ "type": "tendermint/PubKeySecp256k1",
+ "value": "AsGjurKnae7kuQQawGO3FLzrjeLCRO2W6orV74fekVYo"
+ },
+ "signature": "aOM9ui+LOSw9GDHe2cKWXPno8Oa1dInphCCJE4WVC5Q/3Kv0j4a6fkfT7sR9uXEQX5rDN7CAH2WrmWxE7E6P7Q=="
+ }
+ ],
+ "memo": ""
+ }
+}
diff --git a/contrib/testnet-5000/rest_examples/broadcast-create-swap.json b/contrib/testnet-5000/rest_examples/broadcast-create-swap.json
new file mode 100644
index 00000000..b37d8457
--- /dev/null
+++ b/contrib/testnet-5000/rest_examples/broadcast-create-swap.json
@@ -0,0 +1,41 @@
+{
+ "type": "cosmos-sdk/StdTx",
+ "value": {
+ "msg": [
+ {
+ "type": "bep3/MsgCreateAtomicSwap",
+ "value": {
+ "from": "kava1sl8glhaa9f9tep0d9h8gdcfmwcatghtdrfcd2x",
+ "to": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "recipient_other_chain": "tbnb1etjxt3t5r9899w3tf7dnktdue5ngu4ncdwckvt",
+ "sender_other_chain": "tbnb10uypsspvl6jlxcx5xse02pag39l8xpe7a3468h",
+ "random_number_hash": "C0544B7F4B890A673EA3F61BDB4650FBFE2F3E56BDA1B397D6D592FCA7163C8C",
+ "timestamp": "1585252531",
+ "amount": [
+ {
+ "denom": "bnb",
+ "amount": "555555"
+ }
+ ],
+ "expected_income": "555555bnb",
+ "height_span": "360",
+ "cross_chain": true
+ }
+ }
+ ],
+ "fee": {
+ "amount": [],
+ "gas": "500000"
+ },
+ "signatures": [
+ {
+ "pub_key": {
+ "type": "tendermint/PubKeySecp256k1",
+ "value": "A6ExM8g1WQtVD7U5hg+whlWvPza6p24ABFqv6ofv0lQn"
+ },
+ "signature": "1bC1pe1oH20pScI2FimQ98VfvUA/galjDBNkaXcbZKYsE3ig7oGr322PraO2tO44OlY0AiZ1LCVZ15kCxnXO3w=="
+ }
+ ],
+ "memo": ""
+ }
+}
diff --git a/contrib/testnet-5000/rest_examples/broadcast-refund-swap.json b/contrib/testnet-5000/rest_examples/broadcast-refund-swap.json
new file mode 100644
index 00000000..d3fc0070
--- /dev/null
+++ b/contrib/testnet-5000/rest_examples/broadcast-refund-swap.json
@@ -0,0 +1,28 @@
+{
+ "type": "cosmos-sdk/StdTx",
+ "value": {
+ "msg": [
+ {
+ "type": "bep3/MsgRefundAtomicSwap",
+ "value": {
+ "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "swap_id": "1B00244021EC239867E5B8C44BCD98E40F3148806A8C0A8FD3418872986BECBA"
+ }
+ }
+ ],
+ "fee": {
+ "amount": [],
+ "gas": "500000"
+ },
+ "signatures": [
+ {
+ "pub_key": {
+ "type": "tendermint/PubKeySecp256k1",
+ "value": "AsGjurKnae7kuQQawGO3FLzrjeLCRO2W6orV74fekVYo"
+ },
+ "signature": "y/T0zLHuvk7oRgTqU9bc6wnWm5JHzhnEk/U1xSWVkxVD2NvC9MvI6lHjXy1iBzyn388x4U8DQ+sxsT/BBA1ehg=="
+ }
+ ],
+ "memo": ""
+ }
+}
diff --git a/contrib/testnet-5000/rest_examples/claim-swap-unsigned.json b/contrib/testnet-5000/rest_examples/claim-swap-unsigned.json
new file mode 100644
index 00000000..eeed7deb
--- /dev/null
+++ b/contrib/testnet-5000/rest_examples/claim-swap-unsigned.json
@@ -0,0 +1,21 @@
+{
+ "type": "cosmos-sdk/StdTx",
+ "value": {
+ "msg": [
+ {
+ "type": "bep3/MsgClaimAtomicSwap",
+ "value": {
+ "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "swap_id": "3CA20F0152F03B0AABE73E7AA1DDF78B9048EDE5A9A73846E1EF53BEBBFA4185",
+ "random_number": "E3D0A98B459F72231DA69C3BD771C1E721FEF4BE83C14B80DC805BA71019EEBE"
+ }
+ }
+ ],
+ "fee": {
+ "amount": [],
+ "gas": "500000"
+ },
+ "signatures": null,
+ "memo": ""
+ }
+}
diff --git a/contrib/testnet-5000/rest_examples/claim-swap.json b/contrib/testnet-5000/rest_examples/claim-swap.json
new file mode 100644
index 00000000..54db1637
--- /dev/null
+++ b/contrib/testnet-5000/rest_examples/claim-swap.json
@@ -0,0 +1,15 @@
+{
+ "base_req": {
+ "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "memo": "",
+ "chain_id": "testing",
+ "account_number": "1",
+ "sequence": "0",
+ "gas": "500000",
+ "gas_adjustment": "1.0",
+ "simulate": false
+ },
+ "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "swap_id": "3ca20f0152f03b0aabe73e7aa1ddf78b9048ede5a9a73846e1ef53bebbfa4185",
+ "random_number": "e3d0a98b459f72231da69c3bd771c1e721fef4be83c14b80dc805ba71019eebe"
+}
diff --git a/contrib/testnet-5000/rest_examples/create-swap-unsigned.json b/contrib/testnet-5000/rest_examples/create-swap-unsigned.json
new file mode 100644
index 00000000..c610123b
--- /dev/null
+++ b/contrib/testnet-5000/rest_examples/create-swap-unsigned.json
@@ -0,0 +1,33 @@
+{
+ "type": "cosmos-sdk/StdTx",
+ "value": {
+ "msg": [
+ {
+ "type": "bep3/MsgCreateAtomicSwap",
+ "value": {
+ "from": "kava1sl8glhaa9f9tep0d9h8gdcfmwcatghtdrfcd2x",
+ "to": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "recipient_other_chain": "tbnb1etjxt3t5r9899w3tf7dnktdue5ngu4ncdwckvt",
+ "sender_other_chain": "tbnb10uypsspvl6jlxcx5xse02pag39l8xpe7a3468h",
+ "random_number_hash": "C0544B7F4B890A673EA3F61BDB4650FBFE2F3E56BDA1B397D6D592FCA7163C8C",
+ "timestamp": "1585252531",
+ "amount": [
+ {
+ "denom": "bnb",
+ "amount": "555555"
+ }
+ ],
+ "expected_income": "555555bnb",
+ "height_span": "360",
+ "cross_chain": true
+ }
+ }
+ ],
+ "fee": {
+ "amount": [],
+ "gas": "500000"
+ },
+ "signatures": null,
+ "memo": ""
+ }
+}
diff --git a/contrib/testnet-5000/rest_examples/create-swap.json b/contrib/testnet-5000/rest_examples/create-swap.json
new file mode 100644
index 00000000..c891d1e8
--- /dev/null
+++ b/contrib/testnet-5000/rest_examples/create-swap.json
@@ -0,0 +1,27 @@
+{
+ "base_req": {
+ "from": "kava1sl8glhaa9f9tep0d9h8gdcfmwcatghtdrfcd2x",
+ "memo": "",
+ "chain_id": "testing",
+ "account_number": "5",
+ "sequence": "0",
+ "gas": "500000",
+ "gas_adjustment": "1.0",
+ "simulate": false
+ },
+ "from": "kava1sl8glhaa9f9tep0d9h8gdcfmwcatghtdrfcd2x",
+ "to": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "recipient_other_chain": "tbnb1etjxt3t5r9899w3tf7dnktdue5ngu4ncdwckvt",
+ "sender_other_chain": "tbnb10uypsspvl6jlxcx5xse02pag39l8xpe7a3468h",
+ "random_number_hash": "c0544b7f4b890a673ea3f61bdb4650fbfe2f3e56bda1b397d6d592fca7163c8c",
+ "timestamp": "1585252531",
+ "amount": [
+ {
+ "denom": "bnb",
+ "amount": "555555"
+ }
+ ],
+ "expected_income": "555555bnb",
+ "height_span": "360",
+ "cross_chain": true
+}
diff --git a/contrib/testnet-5000/rest_examples/refund-swap-unsigned.json b/contrib/testnet-5000/rest_examples/refund-swap-unsigned.json
new file mode 100644
index 00000000..faa176c2
--- /dev/null
+++ b/contrib/testnet-5000/rest_examples/refund-swap-unsigned.json
@@ -0,0 +1,20 @@
+{
+ "type": "cosmos-sdk/StdTx",
+ "value": {
+ "msg": [
+ {
+ "type": "bep3/MsgRefundAtomicSwap",
+ "value": {
+ "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "swap_id": "1B00244021EC239867E5B8C44BCD98E40F3148806A8C0A8FD3418872986BECBA"
+ }
+ }
+ ],
+ "fee": {
+ "amount": [],
+ "gas": "500000"
+ },
+ "signatures": null,
+ "memo": ""
+ }
+}
diff --git a/contrib/testnet-5000/rest_examples/refund-swap.json b/contrib/testnet-5000/rest_examples/refund-swap.json
new file mode 100644
index 00000000..4cbf7ff1
--- /dev/null
+++ b/contrib/testnet-5000/rest_examples/refund-swap.json
@@ -0,0 +1,14 @@
+{
+ "base_req": {
+ "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "memo": "",
+ "chain_id": "testing",
+ "account_number": "1",
+ "sequence": "0",
+ "gas": "500000",
+ "gas_adjustment": "1.0",
+ "simulate": false
+ },
+ "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p",
+ "swap_id": "1b00244021ec239867e5b8c44bcd98e40f3148806a8c0a8fd3418872986becba"
+}
diff --git a/contrib/testnet-6000/genesis_examples/wip_genesis.json b/contrib/testnet-6000/genesis_examples/wip_genesis.json
new file mode 100644
index 00000000..c921ef73
--- /dev/null
+++ b/contrib/testnet-6000/genesis_examples/wip_genesis.json
@@ -0,0 +1,259 @@
+{
+ "genesis_time": "2020-04-23T16:32:31.393515Z",
+ "chain_id": "testing",
+ "consensus_params": {
+ "block": {
+ "max_bytes": "22020096",
+ "max_gas": "-1",
+ "time_iota_ms": "1000"
+ },
+ "evidence": {
+ "max_age_num_blocks": "100000",
+ "max_age_duration": "172800000000000"
+ },
+ "validator": {
+ "pub_key_types": [
+ "ed25519"
+ ]
+ }
+ },
+ "app_hash": "",
+ "app_state": {
+ "auction": {
+ "auctions": [],
+ "next_auction_id": "1",
+ "params": {
+ "bid_duration": "600000000000",
+ "increment_collateral": "0.010000000000000000",
+ "increment_debt": "0.010000000000000000",
+ "increment_surplus": "0.010000000000000000",
+ "max_auction_duration": "172800000000000"
+ }
+ },
+ "auth": {
+ "accounts": [],
+ "params": {
+ "max_memo_characters": "256",
+ "sig_verify_cost_ed25519": "590",
+ "sig_verify_cost_secp256k1": "1000",
+ "tx_sig_limit": "7",
+ "tx_size_cost_per_byte": "10"
+ }
+ },
+ "bank": {
+ "send_enabled": true
+ },
+ "bep3": {
+ "assets_supplies": [],
+ "atomic_swaps": [],
+ "params": {
+ "bnb_deputy_address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
+ "max_block_lock": "600",
+ "min_block_lock": "80",
+ "supported_assets": [
+ {
+ "active": true,
+ "coin_id": "714",
+ "denom": "bnb",
+ "limit": "100000000000"
+ }
+ ]
+ }
+ },
+ "cdp": {
+ "cdps": [],
+ "debt_denom": "debt",
+ "deposits": [],
+ "gov_denom": "ukava",
+ "params": {
+ "circuit_breaker": false,
+ "collateral_params": [
+ {
+ "auction_size": "50000000000",
+ "conversion_factor": "8",
+ "debt_limit": [
+ {
+ "amount": "2000000000000",
+ "denom": "usdx"
+ }
+ ],
+ "denom": "bnb",
+ "liquidation_penalty": "0.05",
+ "liquidation_ratio": "2.0",
+ "market_id": "bnb:usd",
+ "prefix": 1,
+ "stability_fee": "1.0000000007829977"
+ }
+ ],
+ "debt_auction_threshold": "10000000000",
+ "debt_params": [
+ {
+ "conversion_factor": "6",
+ "debt_floor": "10000000",
+ "debt_limit": [
+ {
+ "amount": "2000000000000",
+ "denom": "usdx"
+ }
+ ],
+ "denom": "usdx",
+ "reference_asset": "usd",
+ "savings_rate": "0.95"
+ }
+ ],
+ "global_debt_limit": [
+ {
+ "amount": "2000000000000",
+ "denom": "usdx"
+ }
+ ],
+ "savings_distribution_frequency": "120000000000",
+ "surplus_auction_threshold": "1000000000"
+ },
+ "previous_block_time": "1970-01-01T00:00:00Z",
+ "previous_distribution_time": "1970-01-01T00:00:00Z",
+ "starting_cdp_id": "1"
+ },
+ "crisis": {
+ "constant_fee": {
+ "amount": "1333000000",
+ "denom": "ukava"
+ }
+ },
+ "distribution": {
+ "delegator_starting_infos": [],
+ "delegator_withdraw_infos": [],
+ "fee_pool": {
+ "community_pool": []
+ },
+ "outstanding_rewards": [],
+ "params": {
+ "base_proposer_reward": "0.010000000000000000",
+ "bonus_proposer_reward": "0.040000000000000000",
+ "community_tax": "0.000000000000000000",
+ "withdraw_addr_enabled": true
+ },
+ "previous_proposer": "",
+ "validator_accumulated_commissions": [],
+ "validator_current_rewards": [],
+ "validator_historical_rewards": [],
+ "validator_slash_events": []
+ },
+ "evidence": {
+ "evidence": [],
+ "params": {
+ "max_evidence_age": "120000000000"
+ }
+ },
+ "genutil": {
+ "gentxs": []
+ },
+ "gov": {
+ "deposit_params": {
+ "max_deposit_period": "600000000000",
+ "min_deposit": [
+ {
+ "amount": "200000000",
+ "denom": "ukava"
+ }
+ ]
+ },
+ "deposits": null,
+ "proposals": null,
+ "starting_proposal_id": "1",
+ "tally_params": {
+ "quorum": "0.334000000000000000",
+ "threshold": "0.500000000000000000",
+ "veto": "0.334000000000000000"
+ },
+ "votes": null,
+ "voting_params": {
+ "voting_period": "600000000000"
+ }
+ },
+ "kavadist": {
+ "params": {
+ "active": true,
+ "periods": [
+ {
+ "end": "2021-03-28T15:20:00Z",
+ "inflation": "1.000000003022265980",
+ "start": "2020-03-28T15:20:00Z"
+ }
+ ]
+ },
+ "previous_block_time": "1970-01-01T00:00:00Z"
+ },
+ "mint": {
+ "minter": {
+ "annual_provisions": "0.000000000000000000",
+ "inflation": "0.020000000000000000"
+ },
+ "params": {
+ "blocks_per_year": "6311520",
+ "goal_bonded": "0.670000000000000000",
+ "inflation_max": "0.130000000000000000",
+ "inflation_min": "0.010000000000000000",
+ "inflation_rate_change": "0.130000000000000000",
+ "mint_denom": "ukava"
+ }
+ },
+ "params": null,
+ "pricefeed": {
+ "params": {
+ "markets": [
+ {
+ "active": true,
+ "base_asset": "bnb",
+ "market_id": "bnb:usd",
+ "oracles": [
+ "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"
+ ],
+ "quote_asset": "usd"
+ }
+ ]
+ },
+ "posted_prices": [
+ {
+ "expiry": "2021-04-20T00:00:00Z",
+ "market_id": "bnb:usd",
+ "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
+ "price": "12.2639061184"
+ }
+ ]
+ },
+ "slashing": {
+ "missed_blocks": {},
+ "params": {
+ "downtime_jail_duration": "600000000000",
+ "min_signed_per_window": "0.010000000000000000",
+ "signed_blocks_window": "1000",
+ "slash_fraction_double_sign": "0.050000000000000000",
+ "slash_fraction_downtime": "0.000100000000000000"
+ },
+ "signing_infos": {}
+ },
+ "staking": {
+ "delegations": null,
+ "exported": false,
+ "last_total_power": "0",
+ "last_validator_powers": null,
+ "params": {
+ "bond_denom": "ukava",
+ "historical_entries": 0,
+ "max_entries": 7,
+ "max_validators": 100,
+ "unbonding_time": "3600000000000"
+ },
+ "redelegations": null,
+ "unbonding_delegations": null,
+ "validators": null
+ },
+ "supply": {
+ "supply": []
+ },
+ "validatorvesting": {
+ "previous_block_time": "1970-01-01T00:00:00Z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index ffba0067..2b057cfa 100644
--- a/go.mod
+++ b/go.mod
@@ -3,14 +3,15 @@ module github.com/kava-labs/kava
go 1.13
require (
- github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1
+ github.com/btcsuite/btcd v0.20.1-beta // indirect
+ github.com/cosmos/cosmos-sdk v0.38.3
github.com/gorilla/mux v1.7.3
- github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75 // indirect
- github.com/spf13/cobra v0.0.5
- github.com/spf13/viper v1.4.0
- github.com/stretchr/testify v1.4.0
- github.com/tendermint/go-amino v0.15.0
- github.com/tendermint/tendermint v0.32.7
- github.com/tendermint/tm-db v0.2.0
- gopkg.in/yaml.v2 v2.2.4
+ github.com/spf13/cobra v0.0.6
+ github.com/spf13/viper v1.6.2
+ github.com/stretchr/testify v1.5.1
+ github.com/tendermint/go-amino v0.15.1
+ github.com/tendermint/tendermint v0.33.3
+ github.com/tendermint/tm-db v0.5.0
+ golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 // indirect
+ gopkg.in/yaml.v2 v2.2.8
)
diff --git a/go.sum b/go.sum
index cb578ad0..d69e9d6c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,56 +1,84 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-github.com/99designs/keyring v1.1.2 h1:JJauROcU6x6Nh9uZb+8JgXFvyo0GUESLo1ixhpA0Kmw=
-github.com/99designs/keyring v1.1.2/go.mod h1:657DQuMrBZRtuL/voxVyiyb6zpMehlm5vLB9Qwrv904=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/99designs/keyring v1.1.3 h1:mEV3iyZWjkxQ7R8ia8GcG97vCX5zQQ7n4o8R2BylwQY=
+github.com/99designs/keyring v1.1.3/go.mod h1:657DQuMrBZRtuL/voxVyiyb6zpMehlm5vLB9Qwrv904=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/ChainSafe/go-schnorrkel v0.0.0-20200102211924-4bcbc698314f h1:4O1om+UVU+Hfcihr1timk8YNXHxzZWgCo7ofnrZRApw=
+github.com/ChainSafe/go-schnorrkel v0.0.0-20200102211924-4bcbc698314f/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
-github.com/Workiva/go-datastructures v1.0.50/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA=
+github.com/Workiva/go-datastructures v1.0.52/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
+github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
+github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
+github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d h1:1aAija9gr0Hyv4KfQcRcwlmFIrhkDmIj2dz5bkg/s/8=
github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d/go.mod h1:icNx/6QdFblhsEjZehARqbNumymUT/ydwlLojFdv7Sk=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw=
github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0=
+github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw=
+github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoAT34XS/kLu40vC0+po/UfxrBBA4qZE=
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
+github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
+github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
+github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
+github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cosmos/cosmos-sdk v0.34.4-0.20190925161702-9d0bed8f4f4e h1:V8WpJTIAjajE2PE+1wWCG5LUYkWQal+aH6uqPUiZ9Qc=
-github.com/cosmos/cosmos-sdk v0.34.4-0.20190925161702-9d0bed8f4f4e/go.mod h1:gwKdI16dOjylNYJkaHbcx0TcEIHyRs1xyc5qROmjCJE=
-github.com/cosmos/cosmos-sdk v0.34.4-0.20191010155330-64a27412505c h1:LM81MVa0CG0Q118dOp6f2Q0MoYIMRTi26zoyqZiL8sY=
-github.com/cosmos/cosmos-sdk v0.34.4-0.20191010155330-64a27412505c/go.mod h1:FxjFo2Y2ZuqUczTcVfnDUG413OgofBbxFNXA72eaUR4=
-github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1 h1:yb+E8HGzFnO0YwLS6OCBIAVWtN8KfCYoKeO9mgAmQN0=
-github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1/go.mod h1:IGBhkbOK1ebLqMWjtgo99zUxWHsA5IOb6N9CI8nHs0Y=
-github.com/cosmos/cosmos-sdk v0.37.1 h1:mz5W3Au32VIPPtrY65dheVYeVDSFfS3eSSmuIj+cXsI=
-github.com/cosmos/cosmos-sdk v0.37.4 h1:1ioXxkpiS+wOgaUbROeDIyuF7hciU5nti0TSyBmV2Ok=
-github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8 h1:Iwin12wRQtyZhH6FV3ykFcdGNlYEzoeR0jN8Vn+JWsI=
-github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y=
-github.com/cosmos/ledger-cosmos-go v0.10.3 h1:Qhi5yTR5Pg1CaTpd00pxlGwNl4sFRdtK1J96OTjeFFc=
-github.com/cosmos/ledger-cosmos-go v0.10.3/go.mod h1:J8//BsAGTo3OC/vDLjMRFLW6q0WAaXvHnVc7ZmE8iUY=
+github.com/cosmos/cosmos-sdk v0.38.3 h1:qIBTiw+2T9POaSUJ5rvbBbXeq8C8btBlJxnSegPBd3Y=
+github.com/cosmos/cosmos-sdk v0.38.3/go.mod h1:rzWOofbKfRt3wxiylmYWEFHnxxGj0coyqgWl2I9obAw=
+github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU=
+github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y=
+github.com/cosmos/ledger-cosmos-go v0.11.1 h1:9JIYsGnXP613pb2vPjFeMMjBI5lEDsEaF6oYorTy6J4=
+github.com/cosmos/ledger-cosmos-go v0.11.1/go.mod h1:J8//BsAGTo3OC/vDLjMRFLW6q0WAaXvHnVc7ZmE8iUY=
github.com/cosmos/ledger-go v0.9.2 h1:Nnao/dLwaVTk1Q5U9THldpUMMXU94BOTWPddSmVB6pI=
github.com/cosmos/ledger-go v0.9.2/go.mod h1:oZJ2hHAZROdlHiwTg4t7kP+GKIIkBT+o6c9QWFanOyI=
-github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU=
github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -59,11 +87,18 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
-github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU=
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
-github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
+github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
@@ -72,72 +107,126 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
-github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
+github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-kit/kit v0.6.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo=
+github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
+github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/gogo/protobuf v1.3.0 h1:G8O7TerXerS4F6sx9OV7/nRfJdnXgHZu/S/7F2SN+UE=
-github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1-0.20190508161146-9fa652df1129 h1:tT8iWCYw4uOem71yYA3htfH+LNopJvcqZQshm56G5L4=
github.com/golang/mock v1.3.1-0.20190508161146-9fa652df1129/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
-github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=
-github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f h1:8N8XWLZelZNibkhM1FuF+3Ad3YIbgirjdMiVA0eUkaM=
+github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
+github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=
+github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=
+github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
+github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U=
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM=
github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
@@ -145,153 +234,197 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
-github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
-github.com/klauspost/compress v1.8.5 h1:2ucYeik+NtUTg+IAiNQtoFC5ZGs5mIVidI7Ome0Br3Y=
-github.com/klauspost/compress v1.8.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
-github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
-github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/libp2p/go-buffer-pool v0.0.1 h1:9Rrn/H46cXjaA2HQ5Y8lyhOS1NhTkZ4yuEs2r3Eechg=
-github.com/libp2p/go-buffer-pool v0.0.1/go.mod h1:xtyIz9PMobb13WaxR6Zo1Pd1zXJKYg0a8KiIvDp3TzQ=
github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs=
github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM=
-github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
+github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
+github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
+github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
-github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
-github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
-github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
-github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0=
+github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
+github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
+github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
+github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
+github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
+github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
+github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
+github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
-github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
-github.com/pelletier/go-toml v1.5.0 h1:5BakdOZdtKJ1FFk6QdL8iSGrMWsXgchNJcrnarjbmJQ=
-github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
+github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
+github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
+github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
+github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
+github.com/prometheus/client_golang v1.5.0 h1:Ctq0iGpCmr3jeP77kbF2UxgvRwzWWz+4Bh9/vJTyg1A=
+github.com/prometheus/client_golang v1.5.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
+github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
+github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
+github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
+github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs=
github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
-github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75 h1:o0pGzJnfjk0E+CZg6jxQrADfk9WYO9fMuLOtSP4owtE=
-github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75/go.mod h1:/L9q8uCsB8BOWdzLK+6WIwkAlcMfKhFCZY0n8/CLHRY=
-github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk=
-github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
-github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
-github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
-github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
-github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPHbB5mVY2iO9KV3pTt/QbIkGaO8gQ2WrDbP4=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
-github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=
+github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
-github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
-github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
-github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E=
+github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
+github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stumble/gorocksdb v0.0.3 h1:9UU+QA1pqFYJuf9+5p7z1IqdE5k0mma4UAeu2wmX8kA=
-github.com/stumble/gorocksdb v0.0.3/go.mod h1:v6IHdFBXk5DJ1K4FZ0xi+eY737quiiBxYtSWXadLybY=
-github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 h1:1oFLiOyVl+W7bnBzGhf7BbIv9loSFQcieWWYIjLqcAw=
-github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs=
+github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA=
+github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok=
+github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8=
github.com/tendermint/btcd v0.1.1 h1:0VcxPfflS2zZ3RiOAHkBiFUcPvbtRj5O7zHmcJWHV7s=
github.com/tendermint/btcd v0.1.1/go.mod h1:DC6/m53jtQzr/NFmMNEu0rxf18/ktVoVtMrnDD5pN+U=
-github.com/tendermint/crypto v0.0.0-20190823183015-45b1026d81ae h1:AOXNM7c2Vvo45SjAgeWF8Wy+NS7/NCqzRNpUc+HPAec=
-github.com/tendermint/crypto v0.0.0-20190823183015-45b1026d81ae/go.mod h1:z4YtwM70uOnk8h0pjJYlj3zdYwi9l03By6iAIF5j/Pk=
+github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 h1:hqAk8riJvK4RMWx1aInLzndwxKalgi5rTqgfXxOxbEI=
+github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15/go.mod h1:z4YtwM70uOnk8h0pjJYlj3zdYwi9l03By6iAIF5j/Pk=
github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso=
-github.com/tendermint/go-amino v0.15.0 h1:TC4e66P59W7ML9+bxio17CPKnxW3nKIRAYskntMAoRk=
-github.com/tendermint/go-amino v0.15.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME=
-github.com/tendermint/iavl v0.12.4 h1:hd1woxUGISKkfUWBA4mmmTwOua6PQZTJM/F0FDrmMV8=
-github.com/tendermint/iavl v0.12.4/go.mod h1:8LHakzt8/0G3/I8FUU0ReNx98S/EP6eyPJkAUvEXT/o=
-github.com/tendermint/tendermint v0.32.1/go.mod h1:jmPDAKuNkev9793/ivn/fTBnfpA9mGBww8MPRNPNxnU=
-github.com/tendermint/tendermint v0.32.3 h1:GEnWpGQ795h5oTFNbfBLsY0LW/CW2j6p6HtiYNfxsgg=
-github.com/tendermint/tendermint v0.32.3/go.mod h1:ZK2c29jl1QRYznIRyRWRDsmm1yvtPzBRT00x4t1JToY=
-github.com/tendermint/tendermint v0.32.5 h1:2hCLwuzfCKZxXSe/+iMEl+ChJWKJx6g/Wcvq3NMxVN4=
-github.com/tendermint/tendermint v0.32.5/go.mod h1:D2+A3pNjY+Po72X0mTfaXorFhiVI8dh/Zg640FGyGtE=
-github.com/tendermint/tendermint v0.32.6 h1:HozXi0USWvKrWuEh5ratnJV10ykkTy4nwXUi0UvPVzg=
-github.com/tendermint/tendermint v0.32.6/go.mod h1:D2+A3pNjY+Po72X0mTfaXorFhiVI8dh/Zg640FGyGtE=
-github.com/tendermint/tendermint v0.32.7 h1:Szu5Fm1L3pvn3t4uQxPAcP+7ndZEQKgLie/yokM56rU=
-github.com/tendermint/tendermint v0.32.7/go.mod h1:D2+A3pNjY+Po72X0mTfaXorFhiVI8dh/Zg640FGyGtE=
-github.com/tendermint/tm-db v0.1.1 h1:G3Xezy3sOk9+ekhjZ/kjArYIs1SmwV+1OUgNkj7RgV0=
-github.com/tendermint/tm-db v0.1.1/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6CpFrKzgw=
-github.com/tendermint/tm-db v0.2.0 h1:rJxgdqn6fIiVJZy4zLpY1qVlyD0TU6vhkT4kEf71TQQ=
-github.com/tendermint/tm-db v0.2.0/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6CpFrKzgw=
+github.com/tendermint/go-amino v0.15.1 h1:D2uk35eT4iTsvJd9jWIetzthE5C0/k2QmMFkCN+4JgQ=
+github.com/tendermint/go-amino v0.15.1/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME=
+github.com/tendermint/iavl v0.13.2 h1:O1m08/Ciy53l9IYmf75uIRVvrNsfjEbre8u/yCu/oqk=
+github.com/tendermint/iavl v0.13.2/go.mod h1:vE1u0XAGXYjHykd4BLp8p/yivrw2PF1TuoljBcsQoGA=
+github.com/tendermint/tendermint v0.33.2/go.mod h1:25DqB7YvV1tN3tHsjWoc2vFtlwICfrub9XO6UBO+4xk=
+github.com/tendermint/tendermint v0.33.3 h1:6lMqjEoCGejCzAghbvfQgmw87snGSqEhDTo/jw+W8CI=
+github.com/tendermint/tendermint v0.33.3/go.mod h1:25DqB7YvV1tN3tHsjWoc2vFtlwICfrub9XO6UBO+4xk=
+github.com/tendermint/tendermint v0.33.4 h1:NM3G9618yC5PaaxGrcAySc5ylc1PAANeIx42u2Re/jo=
+github.com/tendermint/tm-db v0.4.1/go.mod h1:JsJ6qzYkCGiGwm5GHl/H5GLI9XLb6qZX7PRe425dHAY=
+github.com/tendermint/tm-db v0.5.0 h1:qtM5UTr1dlRnHtDY6y7MZO5Di8XAE2j3lc/pCnKJ5hQ=
+github.com/tendermint/tm-db v0.5.0/go.mod h1:lSq7q5WRR/njf1LnhiZ/lIJHk2S8Y1Zyq5oP/3o9C2U=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
-github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
-github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
-github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.5.0 h1:dhq+O9pmNZFF6qAXpasMO1xSm7dL4qEz2ylfZN8BG9w=
-github.com/valyala/fasthttp v1.5.0/go.mod h1:eriCz9OhZjKCGfJ185a/IDgNl0bg9IbzfpcslMZXU1c=
-github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zondax/hid v0.9.0 h1:eiT3P6vNxAEVxXMw66eZUAAnU2zD33JBkfG/EnfAKl8=
@@ -299,90 +432,157 @@ github.com/zondax/hid v0.9.0/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWp
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
+go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
+go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U=
-golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
+golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 h1:6KET3Sqa7fkVfD63QnAM81ZeYg5n4HwApOJkufONnHA=
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 h1:67iHsV9djwGdZpdZNbLuQj6FOzCaZe3w+vhLjn5AcFA=
-google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw=
-google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk=
+google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
+gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
+gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
diff --git a/kava-logo.svg b/kava-logo.svg
index 01a6c6f5..4012cec7 100644
--- a/kava-logo.svg
+++ b/kava-logo.svg
@@ -1,44 +1,21 @@
-
-
-
-
+
+
+
diff --git a/rest_test/how_to_run_dredd_tests.md b/rest_test/how_to_run_dredd_tests.md
new file mode 100644
index 00000000..96c3ebe9
--- /dev/null
+++ b/rest_test/how_to_run_dredd_tests.md
@@ -0,0 +1,25 @@
+///////////////////////////////////////////////////////////////////////////////////////
+///// IF YOU FOLLOW THESE INSTRUCTIONS ALL THE TESTS SHOULD PASS THE FIRST TIME ///////
+///////////////////////////////////////////////////////////////////////////////////////
+
+Instructions on how to run the `dredd` tests
+
+(Prerequisite) Make sure that you have the latest versions of `node` and `npm` and `npx` installed. Then install `dredd` globally using the following command folder:
+
+`npm install dredd --global`
+
+(Running tests) Run `make test_dredd` from the `kava` directory.
+
+This builds the `test.go` file, creates the genesis state for the blockchain, starts the blockchain, starts the rest server, sends the required transactions to the blockchain, runs all the `dredd` tests, shuts
+down the blockchain, cleans up, and propagates up an error code if the tests do not all pass.
+**IMPORTANT** - It takes about 3 minutes for the script to run and complete:
+**When you run the script 62 tests should pass and zero should fail**
+
+(Shutdown) If the script fails or you stop it midway using `ctrl-c` then you should manually stop the blockchain and rest server using the following script. If you let it complete
+it will automatically shut down the blockchain, rest server and clean up.
+
+`./stopchain.sh`
+
+**DEBUGGING NOTE**: If you start getting `Validator set is different` errors then you need to try starting the chain from scratch (do NOT just use `unsafe-reset-all`, instead use `./stopchain.sh` and then `./run_all_tests.sh`)
+
+
diff --git a/rest_test/run_all_tests_from_make.sh b/rest_test/run_all_tests_from_make.sh
new file mode 100755
index 00000000..d1e01f50
--- /dev/null
+++ b/rest_test/run_all_tests_from_make.sh
@@ -0,0 +1,124 @@
+#! /bin/bash
+
+# kill kava if it is already running
+pgrep kvd | xargs kill
+pgrep kvcli | xargs kill
+
+# TODO import from development environment in envkey
+password="password"
+validatorMnemonic="equip town gesture square tomorrow volume nephew minute witness beef rich gadget actress egg sing secret pole winter alarm law today check violin uncover"
+# address: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
+# address: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
+faucet="chief access utility giant burger winner jar false naive mobile often perfect advice village love enroll embark bacon under flock harbor render father since"
+# address: kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz
+# address: kavavaloper1ls82zzghsx0exkpr52m8vht5jqs3un0c5j2c04
+# variables for home directories for kvd and kvcli
+kvdHome=/tmp/kvdHome
+kvcliHome=/tmp/kvcliHome
+genesis=$kvdHome/config/genesis.json
+swaggerFile=swagger-ui/testnet-4000/swagger-testnet-4000.yaml
+# Remove any existing data directory
+rm -rf $kvdHome
+rm -rf $kvcliHome
+# make the directories
+mkdir /tmp/kvdHome
+mkdir /tmp/kvcliHome
+# create validator key
+printf "$password\n$validatorMnemonic\n" | kvcli keys add vlad --recover --home $kvcliHome
+# create faucet key
+printf "$password\n$faucet\n" | kvcli --home $kvcliHome keys add faucet --recover --home $kvcliHome
+# function used to show that it is still loading
+showLoading() {
+ mypid=$!
+ loadingText=$1
+ echo -ne "$loadingText\r"
+ while kill -0 $mypid 2>/dev/null; do
+ echo -ne "$loadingText.\r"
+ sleep 0.5
+ echo -ne "$loadingText..\r"
+ sleep 0.5
+ echo -ne "$loadingText...\r"
+ sleep 0.5
+ echo -ne "\r\033[K"
+ echo -ne "$loadingText\r"
+ sleep 0.5
+ done
+ echo "$loadingText...finished"
+}
+# Create new data directory
+{
+kvd --home $kvdHome init --chain-id=testing vlad # doesn't need to be the same as the validator
+} > /dev/null 2>&1
+kvcli --home $kvcliHome config chain-id testing # or set trust-node true
+# add validator account to genesis
+kvd --home $kvdHome add-genesis-account $(kvcli --home $kvcliHome keys show vlad -a) 10000000000000stake,1000000000000xrp,100000000000btc
+# add faucet account to genesis
+kvd --home $kvdHome add-genesis-account $(kvcli --home $kvcliHome keys show faucet -a) 10000000000000stake,1000000000000xrp,100000000000btc
+# Create a delegation tx for the validator and add to genesis
+printf "$password\n" | kvd --home $kvdHome gentx --name vlad --home-client $kvcliHome
+{
+kvd --home $kvdHome collect-gentxs
+} > /dev/null 2>&1
+# set blocktime to ~1s
+jq '.app_state.mint.params.blocks_per_year = "31540000"' $genesis > $genesis.tmp && mv $genesis.tmp $genesis
+# update pricefeed information
+jq '.app_state.pricefeed.params.markets += [{"active": true, "base_asset": "xrp", "market_id": "xrp:usd", "oracles": ["kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c"], "quote_asset": "usd"}, {"active": true, "base_asset": "btc", "market_id": "btc:usd", "oracles": ["kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c"], "quote_asset": "usd"}]' $genesis > $genesis.tmp && mv $genesis.tmp $genesis
+jq '.app_state.pricefeed.posted_prices += [{"expiry": "2050-01-01T00:00:00Z", "market_id": "btc:usd", "oracle_address": "kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c", "price": "8700.0"}, {"expiry": "2050-01-01T00:00:00Z", "market_id": "xrp:usd", "oracle_address": "kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c", "price": "0.25"}]' $genesis > $genesis.tmp && mv $genesis.tmp $genesis
+# now update cdp params
+jq '.app_state.cdp.params = { "circuit_breaker": false, "collateral_params": [ { "auction_size": "10000000000", "conversion_factor": "8", "debt_limit": [ { "amount": "1000000000", "denom": "usdx" } ], "denom": "btc", "liquidation_penalty": "0.05", "liquidation_ratio": "1.5", "market_id": "btc:usd", "prefix": 0, "stability_fee": "1.0000000007829977" }, { "auction_size": "100000000", "conversion_factor": "6", "debt_limit": [ { "amount": "10000000", "denom": "usdx" } ], "denom": "xrp", "liquidation_penalty": "0.1", "liquidation_ratio": "2.0", "market_id": "xrp:usd", "prefix": 1, "stability_fee": "1.0000000007829977"} ], "debt_auction_threshold": "9000000", "debt_params": [ { "conversion_factor": "6", "debt_floor": "10000000", "debt_limit": [ { "amount": "2000000000000", "denom": "usdx" } ], "denom": "usdx", "reference_asset": "usd", "savings_rate": "0.95" } ], "global_debt_limit": [ { "amount": "2000000000000", "denom": "usdx" } ], "surplus_auction_threshold": "9000000", "savings_distribution_frequency": "120000000000" }' $genesis > $genesis.tmp && mv $genesis.tmp $genesis
+# start the blockchain in the background, wait until it starts making blocks
+{
+kvd start --home $kvdHome & kvdPid="$!"
+} > /dev/null 2>&1
+printf "\n"
+sleep 10 & showLoading "Starting rest server, please wait"
+# start the rest server. Use ./stopchain.sh to stop both rest server and the blockchain
+{
+kvcli rest-server --laddr tcp://127.0.0.1:1317 --chain-id=testing --home $kvcliHome & kvcliPid="$!"
+} > /dev/null 2>&1
+printf "\n"
+sleep 10 & showLoading "Preparing blockchain setup transactions, please wait"
+printf "\n"
+# build the go setup test file
+rm -f rest_test/setuptest
+go build rest_test/setup/setuptest.go & showLoading "Building go test file, please wait"
+# run the go code to send transactions to the chain and set it up correctly
+./setuptest $kvcliHome & showLoading "Sending messages to blockchain"
+printf "\n"
+printf "Blockchain setup completed"
+printf "\n\n"
+############################
+# Now run the dredd tests
+############################
+dredd $swaggerFile localhost:1317 2>&1 | tee output & showLoading "Running dredd tests"
+########################################################
+# Now run the check the return code from the dredd command.
+# If 0 then all test passed OK, otherwise some failed and propagate the error
+########################################################
+# check that the error code was zero
+if [ $? -eq 0 ]
+then
+ # check that all the tests passed (ie zero failing)
+ if [[ $(cat output | grep "0 failing") ]]
+ then
+ # check for no errors
+ if [[ $(cat output | grep "0 errors") ]]
+ then
+ echo "Success"
+ rm setuptest & showLoading "Cleaning up go binary"
+ # kill the kvd and kvcli processes (blockchain and rest api)
+ pgrep kvd | xargs kill
+ pgrep kvcli | xargs kill & showLoading "Stopping blockchain"
+ rm -f output
+ exit 0
+ fi
+ fi
+fi
+# otherwise return an error code and redirect stderr to stdout so user sees the error output
+echo "Failure" >&2
+rm setuptest & showLoading "Cleaning up go binary"
+# kill the kvd and kvcli processes (blockchain and rest api)
+pgrep kvd | xargs kill
+pgrep kvcli | xargs kill & showLoading "Stopping blockchain"
+rm -f output
+exit 1
diff --git a/rest_test/setup/setuptest.go b/rest_test/setup/setuptest.go
new file mode 100644
index 00000000..49267821
--- /dev/null
+++ b/rest_test/setup/setuptest.go
@@ -0,0 +1,481 @@
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/client/keys"
+ crkeys "github.com/cosmos/cosmos-sdk/crypto/keys"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkrest "github.com/cosmos/cosmos-sdk/types/rest"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest"
+ authclient "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ "github.com/cosmos/cosmos-sdk/x/bank"
+ "github.com/cosmos/cosmos-sdk/x/gov"
+ "github.com/cosmos/cosmos-sdk/x/gov/types"
+ "github.com/cosmos/cosmos-sdk/x/staking"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/cdp"
+ "github.com/kava-labs/kava/x/pricefeed"
+ "github.com/tendermint/go-amino"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+func init() {
+ config := sdk.GetConfig()
+ app.SetBech32AddressPrefixes(config)
+ app.SetBip44CoinType(config)
+ config.Seal()
+}
+
+func main() {
+ if len(os.Args) < 2 {
+ fmt.Printf("Please include the kvcli home directory as a command line argument\n")
+ fmt.Printf("For example: ./setuptest /tmp/kvcliHome\n")
+ fmt.Printf("Exiting...goodbye!\n")
+ return
+ }
+
+ // setup messages send to blockchain so it is in the correct state for testing
+ sendProposal()
+ sendDeposit()
+ sendVote()
+ sendDelegation()
+ sendUndelegation()
+ sendCoins()
+
+ sendProposal()
+ sendDeposit()
+ sendVote()
+ sendDelegation()
+ sendUndelegation()
+
+ sendCoins()
+
+ // create an XRP cdp and send to blockchain
+ sendXrpCdp()
+
+ // create a BTC cdp and send to blockchain
+ sendBtcCdp()
+
+ // reduce the price of BTC to trigger an auction
+ sendMsgPostPrice()
+}
+
+// lower the price of xrp to trigger an auction
+func sendMsgPostPrice() {
+ // get the address
+ address := getTestAddress()
+ // get the keyname and password
+ keyname, password := getKeynameAndPassword()
+
+ addr, err := sdk.AccAddressFromBech32(address) // validator address
+ if err != nil {
+ panic(err)
+ }
+
+ price, err := sdk.NewDecFromStr("1")
+ if err != nil {
+ panic(err)
+ }
+ // set the expiry time
+ expiry := tmtime.Now().Add(time.Second * 100000)
+
+ // create a cdp message to send to the blockchain
+ // from, assetcode, price, expiry
+ msg := pricefeed.NewMsgPostPrice(
+ addr,
+ "btc:usd",
+ price,
+ expiry,
+ )
+
+ // helper methods for transactions
+ cdc := app.MakeCodec() // make codec for the app
+
+ // get the keybase
+ keybase := getKeybase()
+
+ // cast to the generic msg type
+ msgToSend := []sdk.Msg{msg}
+
+ // send the message to the blockchain
+ sendMsgToBlockchain(cdc, address, keyname, password, msgToSend, keybase)
+
+}
+
+func sendBtcCdp() {
+ // get the address
+ address := getTestAddress()
+ // get the keyname and password
+ keyname, password := getKeynameAndPassword()
+
+ addr, err := sdk.AccAddressFromBech32(address) // validator address
+ if err != nil {
+ panic(err)
+ }
+
+ // create a cdp message to send to the blockchain
+ // sender, collateral, principal
+ msg := cdp.NewMsgCreateCDP(
+ addr,
+ sdk.NewCoins(sdk.NewInt64Coin("btc", 200000000)),
+ sdk.NewCoins(sdk.NewInt64Coin("usdx", 10000000)),
+ )
+
+ // helper methods for transactions
+ cdc := app.MakeCodec() // make codec for the app
+
+ // get the keybase
+ keybase := getKeybase()
+
+ // cast to the generic msg type
+ msgToSend := []sdk.Msg{msg}
+
+ // send the message to the blockchain
+ sendMsgToBlockchain(cdc, address, keyname, password, msgToSend, keybase)
+
+}
+
+func sendXrpCdp() {
+ // get the address
+ address := getTestAddress()
+ // get the keyname and password
+ keyname, password := getKeynameAndPassword()
+
+ addr, err := sdk.AccAddressFromBech32(address) // validator address
+ if err != nil {
+ panic(err)
+ }
+
+ // create a cdp message to send to the blockchain
+ // sender, collateral, principal
+ msg := cdp.NewMsgCreateCDP(
+ addr,
+ sdk.NewCoins(sdk.NewInt64Coin("xrp", 200000000)),
+ sdk.NewCoins(sdk.NewInt64Coin("usdx", 10000000)),
+ )
+
+ // helper methods for transactions
+ cdc := app.MakeCodec() // make codec for the app
+
+ // get the keybase
+ keybase := getKeybase()
+
+ // cast to the generic msg type
+ msgToSend := []sdk.Msg{msg}
+
+ // send the message to the blockchain
+ sendMsgToBlockchain(cdc, address, keyname, password, msgToSend, keybase)
+
+}
+
+func sendProposal() {
+ // get the address
+ address := getTestAddress()
+ // get the keyname and password
+ keyname, password := getKeynameAndPassword()
+
+ proposalContent := gov.ContentFromProposalType("A Test Title", "A test description on this proposal.", gov.ProposalTypeText)
+ addr, err := sdk.AccAddressFromBech32(address) // validator address
+ if err != nil {
+ panic(err)
+ }
+
+ // create a message to send to the blockchain
+ msg := gov.NewMsgSubmitProposal(
+ proposalContent,
+ sdk.NewCoins(sdk.NewInt64Coin("stake", 1000)),
+ addr,
+ )
+
+ // helper methods for transactions
+ cdc := app.MakeCodec() // make codec for the app
+
+ // get the keybase
+ keybase := getKeybase()
+
+ // SEND THE PROPOSAL
+ // cast to the generic msg type
+ msgToSend := []sdk.Msg{msg}
+
+ // send the PROPOSAL message to the blockchain
+ sendMsgToBlockchain(cdc, address, keyname, password, msgToSend, keybase)
+
+}
+
+func sendDeposit() {
+ // get the address
+ address := getTestAddress()
+ // get the keyname and password
+ keyname, password := getKeynameAndPassword()
+
+ addr, err := sdk.AccAddressFromBech32(address) // validator
+ if err != nil {
+ panic(err)
+ }
+
+ // helper methods for transactions
+ cdc := app.MakeCodec() // make codec for the app
+
+ // get the keybase
+ keybase := getKeybase()
+
+ // NOW SEND THE DEPOSIT
+
+ // create a deposit transaction to send to the proposal
+ amount := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 10000000))
+ deposit := gov.NewMsgDeposit(addr, 2, amount) // TODO IMPORTANT '2' must match 'x-example' in swagger.yaml
+ depositToSend := []sdk.Msg{deposit}
+
+ sendMsgToBlockchain(cdc, address, keyname, password, depositToSend, keybase)
+
+}
+
+func sendVote() {
+ // get the address
+ address := getTestAddress()
+ // get the keyname and password
+ keyname, password := getKeynameAndPassword()
+
+ addr, err := sdk.AccAddressFromBech32(address) // validator
+ if err != nil {
+ panic(err)
+ }
+
+ // helper methods for transactions
+ cdc := app.MakeCodec() // make codec for the app
+
+ // get the keybase
+ keybase := getKeybase()
+
+ // NOW SEND THE VOTE
+
+ // create a vote on a proposal to send to the blockchain
+ vote := gov.NewMsgVote(addr, uint64(2), types.OptionYes) // TODO IMPORTANT '2' must match 'x-example' in swagger.yaml
+
+ // send a vote to the blockchain
+ voteToSend := []sdk.Msg{vote}
+ sendMsgToBlockchain(cdc, address, keyname, password, voteToSend, keybase)
+
+}
+
+// this should send coins from one address to another
+func sendCoins() {
+ // get the address
+ address := getTestAddress()
+ // get the keyname and password
+ keyname, password := getKeynameAndPassword()
+
+ addrFrom, err := sdk.AccAddressFromBech32(address) // validator
+ if err != nil {
+ panic(err)
+ }
+
+ addrTo, err := sdk.AccAddressFromBech32("kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz") // TODO IMPORTANT this is the faucet address
+ if err != nil {
+ panic(err)
+ }
+
+ // helper methods for transactions
+ cdc := app.MakeCodec() // make codec for the app
+
+ // get the keybase
+ keybase := getKeybase()
+
+ // create coins
+ amount := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 2000000))
+
+ coins := bank.NewMsgSend(addrFrom, addrTo, amount) // TODO IMPORTANT '2' must match 'x-example' in swagger.yaml
+ coinsToSend := []sdk.Msg{coins}
+
+ // NOW SEND THE COINS
+
+ // send the coin message to the blockchain
+ sendMsgToBlockchain(cdc, address, keyname, password, coinsToSend, keybase)
+
+}
+
+func getTestAddress() (address string) {
+ // the test address - TODO IMPORTANT make sure this lines up with startchain.sh
+ address = "kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c"
+ return address
+}
+
+func getKeynameAndPassword() (keyname string, password string) {
+ keyname = "vlad" // TODO - IMPORTANT this must match the keys in the startchain.sh script
+ password = "password" // TODO - IMPORTANT this must match the keys in the startchain.sh script
+ return keyname, password
+}
+
+// this should send a delegation
+func sendDelegation() {
+ // get the address
+ address := getTestAddress()
+ // get the keyname and password
+ keyname, password := getKeynameAndPassword()
+
+ addrFrom, err := sdk.AccAddressFromBech32(address) // validator
+ if err != nil {
+ panic(err)
+ }
+
+ // helper methods for transactions
+ cdc := app.MakeCodec() // make codec for the app
+
+ // get the keybase
+ keybase := getKeybase()
+
+ // get the validator address for delegation
+ valAddr, err := sdk.ValAddressFromBech32("kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0") // **FAUCET**
+ if err != nil {
+ panic(err)
+ }
+
+ // create delegation amount
+ delAmount := sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000)
+ delegation := staking.NewMsgDelegate(addrFrom, valAddr, delAmount)
+ delegationToSend := []sdk.Msg{delegation}
+
+ // send the delegation to the blockchain
+ sendMsgToBlockchain(cdc, address, keyname, password, delegationToSend, keybase)
+}
+
+// this should send a MsgUndelegate
+func sendUndelegation() {
+ // get the address
+ address := getTestAddress()
+ // get the keyname and password
+ keyname, password := getKeynameAndPassword()
+
+ addrFrom, err := sdk.AccAddressFromBech32(address) // validator
+ if err != nil {
+ panic(err)
+ }
+
+ // helper methods for transactions
+ cdc := app.MakeCodec() // make codec for the app
+
+ // get the keybase
+ keybase := getKeybase()
+
+ // get the validator address for delegation
+ valAddr, err := sdk.ValAddressFromBech32("kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0") // **FAUCET**
+ if err != nil {
+ panic(err)
+ }
+
+ // create delegation amount
+ undelAmount := sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000)
+ undelegation := staking.NewMsgUndelegate(addrFrom, valAddr, undelAmount)
+ delegationToSend := []sdk.Msg{undelegation}
+
+ // send the delegation to the blockchain
+ sendMsgToBlockchain(cdc, address, keyname, password, delegationToSend, keybase)
+
+}
+
+func getKeybase() crkeys.Keybase {
+ // create a keybase
+ // IMPORTANT - TAKE THIS FROM COMMAND LINE PARAMETER and does NOT work with tilde i.e. ~/ does NOT work
+ keybase, err := keys.NewKeyBaseFromDir(os.Args[1])
+ if err != nil {
+ panic(err)
+ }
+
+ return keybase
+}
+
+// sendMsgToBlockchain sends a message to the blockchain via the rest api
+func sendMsgToBlockchain(cdc *amino.Codec, address string, keyname string,
+ password string, msg []sdk.Msg, keybase crkeys.Keybase) {
+
+ // get the account number and sequence number
+ accountNumber, sequenceNumber := getAccountNumberAndSequenceNumber(cdc, address)
+ inBuf := bufio.NewReader(os.Stdin)
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).
+ WithTxEncoder(authclient.GetTxEncoder(cdc)).WithChainID("testing").
+ WithKeybase(keybase).WithAccountNumber(accountNumber).
+ WithSequence(sequenceNumber).WithGas(500000)
+
+ // build and sign the transaction
+ // this is the *Amino* encoded version of the transaction
+ // fmt.Printf("%+v", txBldr.Keybase())
+ txBytes, err := txBldr.BuildAndSign("vlad", "password", msg)
+ if err != nil {
+ panic(err)
+ }
+ // fmt.Printf("txBytes: %s", txBytes)
+
+ // need to convert the Amino encoded version back to an actual go struct
+ var tx auth.StdTx
+ cdc.UnmarshalBinaryLengthPrefixed(txBytes, &tx) // might be unmarshal binary bare
+
+ // now we re-marshall it again into json
+ jsonBytes, err := cdc.MarshalJSON(
+ authrest.BroadcastReq{
+ Tx: tx,
+ Mode: "block",
+ },
+ )
+
+ fmt.Printf("%s", bytes.NewBuffer(jsonBytes))
+
+ if err != nil {
+ panic(err)
+ }
+ // fmt.Println("post body: ", string(jsonBytes))
+
+ resp, err := http.Post("http://localhost:1317/txs", "application/json", bytes.NewBuffer(jsonBytes))
+ if err != nil {
+ panic(err)
+ }
+
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("\n\nBody:\n\n")
+ fmt.Println(string(body))
+
+}
+
+// getAccountNumberAndSequenceNumber gets an account number and sequence number from the blockchain
+func getAccountNumberAndSequenceNumber(cdc *amino.Codec, address string) (accountNumber uint64, sequenceNumber uint64) {
+
+ // we need to setup the account number and sequence in order to have a valid transaction
+ resp, err := http.Get("http://localhost:1317/auth/accounts/" + address)
+ if err != nil {
+ panic(err)
+ }
+
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ panic(err)
+ }
+
+ var bodyUnmarshalled sdkrest.ResponseWithHeight
+ err = cdc.UnmarshalJSON(body, &bodyUnmarshalled)
+ if err != nil {
+ panic(err)
+ }
+
+ var account authexported.Account
+ err = cdc.UnmarshalJSON(bodyUnmarshalled.Result, &account)
+ if err != nil {
+ panic(err)
+ }
+
+ return account.GetAccountNumber(), account.GetSequence()
+
+}
diff --git a/rest_test/stopchain.sh b/rest_test/stopchain.sh
new file mode 100755
index 00000000..d392bbbd
--- /dev/null
+++ b/rest_test/stopchain.sh
@@ -0,0 +1,7 @@
+#! /bin/bash
+
+# THIS IS A BASH SCRIPT THAT STOPS THE BLOCKCHAIN AND THE REST API
+echo "Stopping blockchain"
+pgrep kvd | xargs kill
+pgrep kvcli | xargs kill
+echo "COMPLETED"
\ No newline at end of file
diff --git a/simulations/README.md b/simulations/README.md
index 69cbdab9..c4260146 100644
--- a/simulations/README.md
+++ b/simulations/README.md
@@ -41,3 +41,5 @@ AWS Batch allows for "array jobs" which are a way of specifying many duplicates
- click on the compute environment name, to get details, then click the link ECS Cluster Name to get details on the actual machines running
- for array jobs, click the job name to get details of the individual jobs
+
+## Sims - TODO
\ No newline at end of file
diff --git a/swagger-ui/swagger.yaml b/swagger-ui/swagger.yaml
index e3b526d1..9d564f22 100644
--- a/swagger-ui/swagger.yaml
+++ b/swagger-ui/swagger.yaml
@@ -152,7 +152,7 @@
description: Block height
required: true
type: number
- x-example: 1
+ x-example: 2
responses:
200:
description: The block at a specific height
@@ -231,7 +231,7 @@
description: Tx hash
required: true
type: string
- x-example: BCBE20E8D46758B96AE5883B792858296AC06E51435490FBDCAE25A72B3CC76B
+ x-example: B901DB8552BC5F5F82E5709EC37AAD8C0F8005CDB3608E5580055CC4D0D9B275
responses:
200:
description: Tx with the provided hash
@@ -256,8 +256,8 @@
- in: query
name: message.sender
type: string
- description: "transaction tags with sender: 'GET /txs?message.action=send&message.sender=kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv'"
- x-example: "kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv"
+ description: "transaction tags with sender: 'GET /txs?message.action=send&message.sender=kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9'"
+ x-example: "kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9"
- in: query
name: page
description: Page number
@@ -268,6 +268,11 @@
description: Maximum number of items per page
type: integer
x-example: 1
+ - in: query
+ name: txheight # this should actually be tx.height but dredd doesn't handle periods in get parameter names so no period allows the test to pass
+ description: Transaction height
+ type: integer
+ x-example: 1
responses:
200:
description: All txs matching the provided events
@@ -295,10 +300,10 @@
type: object
properties:
tx:
- $ref: "#/definitions/StdTx"
+ $ref: "#/definitions/PostStdTx"
mode:
type: string
- example: sync
+ example: block
responses:
200:
description: Tx broadcasting result
@@ -317,15 +322,13 @@
produces:
- application/json
parameters:
- - in: body
- name: tx
- description: The tx to encode
+ - description: ""
+ name: EncodeBody
+ in: body
required: true
schema:
type: object
- properties:
- tx:
- $ref: "#/definitions/StdTx"
+ $ref: "#/definitions/EncodeTx"
responses:
200:
description: The tx was successfully decoded and re-encoded
@@ -336,7 +339,7 @@
type: string
example: The base64-encoded Amino-serialized bytes for the tx
400:
- description: The tx was malformated
+ description: The tx was malformatted
500:
description: Server internal error
/txs/decode:
@@ -359,7 +362,7 @@
properties:
tx:
type: string
- example: SvBiXe4KPqijYZoKFFHEzJ8c2HPAfv2EFUcIhx0yPagwEhTy0vPA+GGhCEslKXa4Af0uB+mfShoMCgVzdGFrZRIDMTAwEgQQwJoM
+ example: ogEoKBapCjyoo2GaChRKWendsRagTF1ACC1nxzjVxW3xJBIU/A6hCReBn5NYI6K2dl10kCEeTfgaCgoFc3Rha2USATESEAoKCgVzdGFrZRIBMRDAmgwaQhJAdlC1HbNw+ux6lRrK3mNdmaH62NE3ThD8SswlDcnhFex7pKSNhaxE4m6TgDhosoK6EyU0LnOZKutXKECNSvO+WCIIdGVzdG1lbW8=
responses:
200:
description: The tx was successfully decoded
@@ -382,14 +385,18 @@
description: Account address in bech32 format
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
responses:
200:
description: Account balances
schema:
- type: array
- items:
- $ref: "#/definitions/Coin"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/Coin"
500:
description: Server internal error
/bank/accounts/{address}/transfers:
@@ -407,7 +414,7 @@
description: Account address in bech32 format
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
- in: body
name: account
description: The sender and tx information
@@ -422,8 +429,8 @@
items:
$ref: "#/definitions/Coin"
responses:
- 202:
- description: Tx was succesfully generated
+ 200:
+ description: Tx was successfully generated
schema:
$ref: "#/definitions/StdTx"
400:
@@ -443,7 +450,7 @@
description: Account address
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
responses:
200:
description: Account information on the blockchain
@@ -476,7 +483,7 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
get:
summary: Get all delegations from a delegator
tags:
@@ -487,47 +494,55 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/Delegation"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/Delegation"
400:
description: Invalid delegator address
500:
description: Internal Server Error
- post:
- summary: Submit delegation
- parameters:
- - in: body
- name: delegation
- description: The password of the account to remove from the KMS
- schema:
- type: object
- properties:
- base_req:
- $ref: "#/definitions/BaseReq"
- delegator_address:
- $ref: "#/definitions/Address"
- validator_address:
- $ref: "#/definitions/ValidatorAddress"
- delegation:
- $ref: "#/definitions/Coin"
- tags:
- - Staking
- consumes:
- - application/json
- produces:
- - application/json
- responses:
- 200:
- description: OK
- schema:
- $ref: "#/definitions/BroadcastTxCommitResult"
- 400:
- description: Invalid delegator address or delegation request body
- 401:
- description: Key password is wrong
- 500:
- description: Internal Server Error
+
+ # THE BELOW ENDPOINT IS NOT IMPLEMENTED IN COSMOS, SEE:
+ # https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/rest/query.go
+
+ # post:
+ # summary: Submit delegation
+ # parameters:
+ # - in: body
+ # name: delegation
+ # description: The password of the account to remove from the KMS
+ # schema:
+ # type: object
+ # properties:
+ # base_req:
+ # $ref: "#/definitions/BaseReq"
+ # delegator_address:
+ # $ref: "#/definitions/Address"
+ # validator_address:
+ # $ref: "#/definitions/ValidatorAddress"
+ # delegation:
+ # $ref: "#/definitions/Coin"
+ # tags:
+ # - Staking
+ # consumes:
+ # - application/json
+ # produces:
+ # - application/json
+ # responses:
+ # 200:
+ # description: OK
+ # schema:
+ # $ref: "#/definitions/BroadcastTxCommitResult"
+ # 400:
+ # description: Invalid delegator address or delegation request body
+ # 401:
+ # description: Key password is wrong
+ # 500:
+ # description: Internal Server Error
/staking/delegators/{delegatorAddr}/delegations/{validatorAddr}:
parameters:
- in: path
@@ -535,13 +550,13 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: validatorAddr
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Query the current delegation between a delegator and a validator
tags:
@@ -564,7 +579,7 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
get:
summary: Get all unbonding delegations from a delegator
tags:
@@ -575,48 +590,57 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/UnbondingDelegation"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/UnbondingDelegation"
+
400:
description: Invalid delegator address
500:
description: Internal Server Error
- post:
- summary: Submit an unbonding delegation
- parameters:
- - in: body
- name: delegation
- description: The password of the account to remove from the KMS
- schema:
- type: object
- properties:
- base_req:
- $ref: "#/definitions/BaseReq"
- delegator_address:
- $ref: "#/definitions/Address"
- validator_address:
- $ref: "#/definitions/ValidatorAddress"
- shares:
- type: string
- example: "100"
- tags:
- - Staking
- consumes:
- - application/json
- produces:
- - application/json
- responses:
- 200:
- description: OK
- schema:
- $ref: "#/definitions/BroadcastTxCommitResult"
- 400:
- description: Invalid delegator address or unbonding delegation request body
- 401:
- description: Key password is wrong
- 500:
- description: Internal Server Error
+
+ # THE BELOW ENDPOINT IS NOT IMPLEMENTED IN COSMOS, SEE:
+ # https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/rest/query.go
+
+ # post:
+ # summary: Submit an unbonding delegation
+ # parameters:
+ # - in: body
+ # name: delegation
+ # description: The password of the account to remove from the KMS
+ # schema:
+ # type: object
+ # properties:
+ # base_req:
+ # $ref: "#/definitions/BaseReq"
+ # delegator_address:
+ # $ref: "#/definitions/Address"
+ # validator_address:
+ # $ref: "#/definitions/ValidatorAddress"
+ # shares:
+ # type: string
+ # example: "100"
+ # tags:
+ # - Staking
+ # consumes:
+ # - application/json
+ # produces:
+ # - application/json
+ # responses:
+ # 200:
+ # description: OK
+ # schema:
+ # $ref: "#/definitions/BroadcastTxCommitResult"
+ # 400:
+ # description: Invalid delegator address or unbonding delegation request body
+ # 401:
+ # description: Key password is wrong
+ # 500:
+ # description: Internal Server Error
/staking/delegators/{delegatorAddr}/unbonding_delegations/{validatorAddr}:
parameters:
- in: path
@@ -624,13 +648,13 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: validatorAddr
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Query all unbonding delegations between a delegator and a validator
tags:
@@ -672,55 +696,63 @@
responses:
200:
description: OK
- schema:
- type: array
- items:
- $ref: "#/definitions/Redelegation"
- 500:
- description: Internal Server Error
- /staking/delegators/{delegatorAddr}/redelegations:
- parameters:
- - in: path
- name: delegatorAddr
- description: Bech32 AccAddress of Delegator
- required: true
- type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
- post:
- summary: Submit a redelegation
- parameters:
- - in: body
- name: delegation
- description: The sender and tx information
schema:
type: object
properties:
- base_req:
- $ref: "#/definitions/BaseReq"
- delegator_address:
- $ref: "#/definitions/Address"
- validator_src_addressess:
- $ref: "#/definitions/ValidatorAddress"
- validator_dst_address:
- $ref: "#/definitions/ValidatorAddress"
- shares:
+ height:
type: string
- example: "100"
- tags:
- - Staking
- consumes:
- - application/json
- produces:
- - application/json
- responses:
- 200:
- description: Tx was succesfully generated
- schema:
- $ref: "#/definitions/StdTx"
- 400:
- description: Invalid delegator address or redelegation request body
+ result:
+ items:
+ $ref: "#/definitions/Redelegation"
500:
description: Internal Server Error
+
+ # THE BELOW ENDPOINT IS NOT IMPLEMENTED IN COSMOS, SEE:
+ # https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/rest/query.go
+
+ # /staking/delegators/{delegatorAddr}/redelegations:
+ # parameters:
+ # - in: path
+ # name: delegatorAddr
+ # description: Bech32 AccAddress of Delegator
+ # required: true
+ # type: string
+ # x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
+ # post:
+ # summary: Submit a redelegation
+ # parameters:
+ # - in: body
+ # name: delegation
+ # description: The sender and tx information
+ # schema:
+ # type: object
+ # properties:
+ # base_req:
+ # $ref: "#/definitions/BaseReq"
+ # delegator_address:
+ # $ref: "#/definitions/Address"
+ # validator_src_addresses:
+ # $ref: "#/definitions/ValidatorAddress"
+ # validator_dst_address:
+ # $ref: "#/definitions/ValidatorAddress"
+ # shares:
+ # type: string
+ # example: "100"
+ # tags:
+ # - Staking
+ # consumes:
+ # - application/json
+ # produces:
+ # - application/json
+ # responses:
+ # 200:
+ # description: Tx was successfully generated
+ # schema:
+ # $ref: "#/definitions/StdTx"
+ # 400:
+ # description: Invalid delegator address or redelegation request body
+ # 500:
+ # description: Internal Server Error
/staking/delegators/{delegatorAddr}/validators:
parameters:
- in: path
@@ -728,7 +760,7 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
get:
summary: Query all validators that a delegator is bonded to
tags:
@@ -739,9 +771,13 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/Validator"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/Validator"
400:
description: Invalid delegator address
500:
@@ -753,13 +789,13 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: validatorAddr
description: Bech32 ValAddress of Delegator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Query a validator that a delegator is bonded to
tags:
@@ -802,9 +838,13 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/Validator"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/Validator"
500:
description: Internal Server Error
/staking/validators/{validatorAddr}:
@@ -814,7 +854,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Query the information from a single validator
tags:
@@ -825,7 +865,14 @@
200:
description: OK
schema:
- $ref: "#/definitions/Validator"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ # type: array // TODO what if there are multiple validators? should it be an array?
+ items:
+ $ref: "#/definitions/Validator"
400:
description: Invalid validator address
500:
@@ -837,7 +884,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1q53rwutgpzx7szcrgzqguxyccjpzt9j44jzrtj
get:
summary: Get all delegations from a validator
tags:
@@ -848,9 +895,13 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/Delegation"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/Delegation"
400:
description: Invalid validator address
500:
@@ -862,7 +913,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1q53rwutgpzx7szcrgzqguxyccjpzt9j44jzrtj
get:
summary: Get all unbonding delegations from a validator
tags:
@@ -873,9 +924,13 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/UnbondingDelegation"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/UnbondingDelegation"
400:
description: Invalid validator address
500:
@@ -986,9 +1041,14 @@
200:
description: OK
schema:
- type: array
items:
- $ref: "#/definitions/SigningInfo"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/SigningInfo"
400:
description: Invalid validator public key for one of the validators
500:
@@ -1009,7 +1069,7 @@
name: validatorAddr
required: true
in: path
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1q53rwutgpzx7szcrgzqguxyccjpzt9j44jzrtj
- description: ""
name: UnjailBody
in: body
@@ -1018,10 +1078,10 @@
type: object
properties:
base_req:
- $ref: "#/definitions/StdTx"
+ $ref: "#/definitions/BaseReq"
responses:
200:
- description: Tx was succesfully generated
+ description: Tx was successfully generated
schema:
$ref: "#/definitions/BroadcastTxCommitResult"
400:
@@ -1079,8 +1139,10 @@
$ref: "#/definitions/BaseReq"
title:
type: string
+ example: Test_title
description:
type: string
+ example: my_test_description
proposal_type:
type: string
example: "text"
@@ -1092,7 +1154,7 @@
$ref: "#/definitions/Coin"
responses:
200:
- description: Tx was succesfully generated
+ description: Tx was successfully generated
schema:
$ref: "#/definitions/StdTx"
400:
@@ -1126,9 +1188,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/TextProposal"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/TextProposal"
400:
description: Invalid query parameters
500:
@@ -1155,10 +1222,10 @@
$ref: "#/definitions/BaseReq"
title:
type: string
- x-example: "Param Change"
+ example: Param_Change
description:
type: string
- x-example: "Update max validators"
+ example: Update_max_validators
proposer:
$ref: "#/definitions/Address"
deposit:
@@ -1171,7 +1238,7 @@
$ref: "#/definitions/ParamChange"
responses:
200:
- description: The transaction was succesfully generated
+ description: The transaction was successfully generated
schema:
$ref: "#/definitions/StdTx"
400:
@@ -1242,9 +1309,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/Deposit"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/Deposit"
400:
description: Invalid proposal id
500:
@@ -1311,7 +1383,7 @@
name: depositor
required: true
in: path
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
responses:
200:
description: OK
@@ -1342,9 +1414,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/Vote"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/Vote"
400:
description: Invalid proposal id
500:
@@ -1410,7 +1487,7 @@
name: voter
required: true
in: path
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
responses:
200:
description: OK
@@ -1530,7 +1607,7 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava167w96tdvmazakdwkw2u57227eduula2cy572lf
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
get:
summary: Get the total rewards balance from all delegations
description: Get the sum of all the rewards earned by delegations by a single delegator
@@ -1581,13 +1658,13 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: validatorAddr
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Query a delegation reward
description: Query a single delegation reward by a delegator
@@ -1599,9 +1676,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/Coin"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/Coin"
400:
description: Invalid delegator address
500:
@@ -1640,7 +1722,7 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava167w96tdvmazakdwkw2u57227eduula2cy572lf
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
get:
summary: Get the rewards withdrawal address
description: Get the delegations' rewards withdrawal address. This is the address in which the user will receive the reward funds
@@ -1652,7 +1734,13 @@
200:
description: OK
schema:
- $ref: "#/definitions/Address"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ $ref: "#/definitions/Address"
+
400:
description: Invalid delegator address
500:
@@ -1693,7 +1781,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Validator distribution information
description: Query the distribution information of a single validator
@@ -1717,7 +1805,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Fee distribution outstanding rewards of a single validator
tags:
@@ -1728,9 +1816,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/Coin"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/Coin"
500:
description: Internal Server Error
/distribution/validators/{validatorAddr}/rewards:
@@ -1740,7 +1833,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Commission and self-delegation rewards of a single validator
description: Query the commission and self-delegation rewards of validator.
@@ -1752,9 +1845,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/Coin"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/Coin"
400:
description: Invalid validator address
500:
@@ -1797,9 +1895,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: "#/definitions/Coin"
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/Coin"
500:
description: Internal Server Error
/distribution/parameters:
@@ -1859,7 +1962,7 @@
200:
description: OK
schema:
- type: string
+ $ref: "#/definitions/StandardResponse"
500:
description: Internal Server Error
/minting/annual-provisions:
@@ -1873,7 +1976,7 @@
200:
description: OK
schema:
- type: string
+ $ref: "#/definitions/StandardResponse"
500:
description: Internal Server Error
/supply/total:
@@ -1908,7 +2011,7 @@
200:
description: OK
schema:
- type: string
+ $ref: "#/definitions/StandardResponse"
400:
description: Invalid coin denomination
500:
@@ -1982,7 +2085,7 @@
hash:
$ref: "#/definitions/Hash"
height:
- type: integer
+ type: string
KVPair:
type: object
properties:
@@ -1992,8 +2095,6 @@
type: string
Msg:
type: object
- required:
- - from
properties:
type:
type: string
@@ -2001,26 +2102,72 @@
value:
type: object
properties:
- from:
+ from_address:
type: string
- example: kava1mzqz3jfs9nzfp6v7qp647rv0afxlu2csl0txmq
+ example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
+ to_address:
+ type: string
+ example: kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz
+ amount:
+ type: array
+ items:
+ $ref: "#/definitions/Coin"
+ PostMsg:
+ type: object
+ properties:
+ type:
+ type: string
+ example: "cosmos-sdk/MsgSend"
+ value:
+ type: object
+ properties:
+ from_address:
+ type: string
+ example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
+ to_address:
+ type: string
+ example: kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz
+ amount:
+ type: array
+ items:
+ $ref: "#/definitions/PostCoin1"
+
+
Address:
type: string
description: bech32 encoded address
- example: kava1depk54cuajgkzea6zpgkq36tnjwdzv4afc3d27
+ example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
ValidatorAddress:
type: string
description: bech32 encoded address
- example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ example: kavavaloper1q53rwutgpzx7szcrgzqguxyccjpzt9j44jzrtj
Coin:
type: object
properties:
denom:
type: string
- example: ukava
+ example: stake
amount:
type: string
- example: "50"
+ example: "1"
+ PostCoin1:
+ type: object
+ properties:
+ denom:
+ type: string
+ example: stake
+ amount:
+ type: string
+ example: "2000000"
+ PostCoin2:
+ type: object
+ properties:
+ denom:
+ type: string
+ example: stake
+ amount:
+ type: string
+ example: "2000"
Hash:
type: string
example: EE5F3404034C524501629B56E0DDC38FAD651F04
@@ -2029,7 +2176,7 @@
properties:
signature:
type: string
- example: "W1HfKcc4F0rCSoxheZ7fsrB5nGK58U4gKysuzsmUwhloVnCxmbCx289uVMMvQN6tOcQsz7hMVTJrXSA1xzevvw=="
+ example: "dlC1HbNw+ux6lRrK3mNdmaH62NE3ThD8SswlDcnhFex7pKSNhaxE4m6TgDhosoK6EyU0LnOZKutXKECNSvO+WA=="
pubkey:
type: object
properties:
@@ -2038,7 +2185,24 @@
example: "tendermint/PubKeySecp256k1"
value:
type: string
- example: "Agey31h/NYpcy0sYm4liHMrXJMzbQUrgV4uHd/w09CXN"
+ example: "AmWAim83Qp+kIcj3RT7i327b3l0EHwzCrGVGXusb70B7"
+
+ PostSignature:
+ type: object
+ properties:
+ signature:
+ type: string
+ example: "KzBeQp/2+47oiqc16BImnOYidSAesZ3kgR3S8Fy+baFlvDlDv7goU/+rm4c7+woudNte3uZAG0CuUeHsF+Ld8Q=="
+ pubkey:
+ type: object
+ properties:
+ type:
+ type: string
+ example: "tendermint/PubKeySecp256k1"
+ value:
+ type: string
+ example: "AmWAim83Qp+kIcj3RT7i327b3l0EHwzCrGVGXusb70B7"
+
TxQuery:
type: object
properties:
@@ -2046,8 +2210,8 @@
type: string
example: "D085138D913993919295FF4B0A9107F1F2CDE0D37A87CE0644E217CBF3B49656"
height:
- type: number
- example: 368
+ type: string
+ example: "368"
tx:
$ref: "#/definitions/StdTx"
result:
@@ -2069,20 +2233,20 @@
type: object
properties:
total_count:
- type: number
- example: 1
+ type: string
+ example: "1"
count:
- type: number
- example: 1
+ type: string
+ example: "1"
page_number:
- type: number
- example: 1
+ type: string
+ example: "1"
page_total:
- type: number
- example: 1
+ type: string
+ example: "1"
limit:
- type: number
- example: 30
+ type: string
+ example: "30"
txs:
type: array
items:
@@ -2099,16 +2263,53 @@
properties:
gas:
type: string
+ example: "200000"
amount:
type: array
items:
$ref: "#/definitions/Coin"
memo:
type: string
+ example: "testmemo"
signatures:
type: array
items:
$ref: "#/definitions/Signature"
+
+ PostStdTx:
+ type: object
+ properties:
+ msg:
+ type: array
+ items:
+ $ref: "#/definitions/PostMsg"
+ fee:
+ type: object
+ properties:
+ gas:
+ type: string
+ example: "200000"
+ amount:
+ type: array
+ items:
+ $ref: "#/definitions/PostCoin2"
+ memo:
+ type: string
+ example: ""
+ signatures:
+ type: array
+ items:
+ $ref: "#/definitions/PostSignature"
+
+ EncodeTx:
+ type: object
+ properties:
+ type:
+ type: string
+ example: "cosmos-sdk/StdTx"
+ value:
+ type: object
+ $ref: "#/definitions/StdTx"
BlockID:
type: object
properties:
@@ -2118,8 +2319,8 @@
type: object
properties:
total:
- type: number
- example: 0
+ type: string
+ example: "0"
hash:
$ref: "#/definitions/Hash"
BlockHeader:
@@ -2129,19 +2330,19 @@
type: string
example: kavahub-2
height:
- type: number
- example: 1
+ type: string
+ example: "1"
time:
type: string
example: "2017-12-30T05:53:09.287+01:00"
num_txs:
- type: number
- example: 0
+ type: string
+ example: "0"
last_block_id:
$ref: "#/definitions/BlockID"
total_txs:
- type: number
- example: 35
+ type: string
+ example: "35"
last_commit_hash:
$ref: "#/definitions/Hash"
data_hash:
@@ -2174,46 +2375,54 @@
properties:
header:
$ref: "#/definitions/BlockHeader"
- txs:
- type: array
- items:
- type: string
+ data:
+ type: object
+ properties:
+ txs:
+ type: array
+ x-nullable: true
evidence:
- type: array
- items:
- type: string
+ type: object
+ properties:
+ evidence:
+ type: object
+ x-nullable: true
last_commit:
type: object
properties:
block_id:
$ref: "#/definitions/BlockID"
precommits:
- type: array
- items:
- type: object
- properties:
- validator_address:
- type: string
- validator_index:
- type: string
- example: "0"
- height:
- type: string
- example: "0"
- round:
- type: string
- example: "0"
- timestamp:
- type: string
- example: "2017-12-30T05:53:09.287+01:00"
- type:
- type: number
- example: 2
- block_id:
- $ref: "#/definitions/BlockID"
- signature:
- type: string
- example: "7uTC74QlknqYWEwg7Vn6M8Om7FuZ0EO4bjvuj6rwH1mTUJrRuMMZvAAqT9VjNgP0RA/TDp6u/92AqrZfXJSpBQ=="
+ # have to use an 'object' type for precommits as it is nullable
+ $ref: '#/definitions/Precommits'
+ Precommits:
+ type: array
+ items:
+ type: object
+ properties:
+ validator_address:
+ type: string
+ validator_index:
+ type: string
+ example: "0"
+ height:
+ type: string
+ example: "2"
+ round:
+ type: string
+ example: "0"
+ timestamp:
+ type: string
+ example: "2017-12-30T05:53:09.287+01:00"
+ type:
+ type: number
+ example: 2
+ block_id:
+ $ref: "#/definitions/BlockID"
+ signature:
+ type: string
+ example: "7uTC74QlknqYWEwg7Vn6M8Om7FuZ0EO4bjvuj6rwH1mTUJrRuMMZvAAqT9VjNgP0RA/TDp6u/92AqrZfXJSpBQ=="
+
BlockQuery:
type: object
properties:
@@ -2251,11 +2460,11 @@
properties:
from:
type: string
- example: "kava1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc"
+ example: "kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9"
description: Sender address or Keybase name to generate a transaction
memo:
type: string
- example: "Sent via Cosmos Voyager 🚀"
+ example: Sent_via_Kava
chain_id:
type: string
example: "Cosmos-Hub"
@@ -2539,4 +2748,11 @@
total:
type: array
items:
- $ref: "#/definitions/Coin"
\ No newline at end of file
+ $ref: "#/definitions/Coin"
+ StandardResponse:
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: string
\ No newline at end of file
diff --git a/swagger-ui/testnet-4000/swagger-testnet-4000.yaml b/swagger-ui/testnet-4000/swagger-testnet-4000.yaml
index 78954f36..bf369a13 100644
--- a/swagger-ui/testnet-4000/swagger-testnet-4000.yaml
+++ b/swagger-ui/testnet-4000/swagger-testnet-4000.yaml
@@ -237,12 +237,12 @@
description: Tx hash
required: true
type: string
- x-example: BCBE20E8D46758B96AE5883B792858296AC06E51435490FBDCAE25A72B3CC76B
+ x-example: 4B2C3449FF2647BD51B54C32761FE4EBE7AE024BBE2A9898972A69BEB82D97C2
responses:
200:
description: Tx with the provided hash
schema:
- $ref: '#/definitions/TxQuery'
+ $ref: "#/definitions/TxQuery"
500:
description: Internal Server Error
/txs:
@@ -258,12 +258,12 @@
name: message.action
type: string
description: "transaction events such as 'message.action=send' which results in the following endpoint: 'GET /txs?message.action=send'. note that each module documents its own events. look for xx_events.md in the corresponding cosmos-sdk/docs/spec directory"
- x-example: 'send'
+ x-example: "send"
- in: query
name: message.sender
type: string
- description: "transaction tags with sender: 'GET /txs?message.action=send&message.sender=kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv'"
- x-example: 'kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv'
+ description: "transaction tags with sender: 'GET /txs?message.action=send&message.sender=kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9'"
+ x-example: "kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9"
- in: query
name: page
description: Page number
@@ -274,11 +274,16 @@
description: Maximum number of items per page
type: integer
x-example: 1
+ - in: query
+ name: txheight # this should actually be tx.height but dredd doesn't handle periods in get parameter names so no period allows the test to pass
+ description: Transaction height
+ type: integer
+ x-example: 1
responses:
200:
description: All txs matching the provided events
schema:
- $ref: '#/definitions/PaginatedQueryTxs'
+ $ref: "#/definitions/PaginatedQueryTxs"
400:
description: Invalid search events
500:
@@ -301,15 +306,15 @@
type: object
properties:
tx:
- $ref: '#/definitions/StdTx'
+ $ref: "#/definitions/PostStdTx"
mode:
type: string
- example: sync
+ example: block
responses:
200:
description: Tx broadcasting result
schema:
- $ref: '#/definitions/BroadcastTxCommitResult'
+ $ref: "#/definitions/BroadcastTxCommitResult"
500:
description: Internal Server Error
/txs/encode:
@@ -323,15 +328,13 @@
produces:
- application/json
parameters:
- - in: body
- name: tx
- description: The tx to encode
+ - description: ""
+ name: EncodeBody
+ in: body
required: true
schema:
type: object
- properties:
- tx:
- $ref: '#/definitions/StdTx'
+ $ref: "#/definitions/EncodeTx"
responses:
200:
description: The tx was successfully decoded and re-encoded
@@ -342,7 +345,7 @@
type: string
example: The base64-encoded Amino-serialized bytes for the tx
400:
- description: The tx was malformated
+ description: The tx was malformatted
500:
description: Server internal error
/txs/decode:
@@ -365,12 +368,12 @@
properties:
tx:
type: string
- example: SvBiXe4KPqijYZoKFFHEzJ8c2HPAfv2EFUcIhx0yPagwEhTy0vPA+GGhCEslKXa4Af0uB+mfShoMCgVzdGFrZRIDMTAwEgQQwJoM
+ example: ogEoKBapCjyoo2GaChRKWendsRagTF1ACC1nxzjVxW3xJBIU/A6hCReBn5NYI6K2dl10kCEeTfgaCgoFc3Rha2USATESEAoKCgVzdGFrZRIBMRDAmgwaQhJAdlC1HbNw+ux6lRrK3mNdmaH62NE3ThD8SswlDcnhFex7pKSNhaxE4m6TgDhosoK6EyU0LnOZKutXKECNSvO+WCIIdGVzdG1lbW8=
responses:
200:
description: The tx was successfully decoded
schema:
- $ref: '#/definitions/StdTx'
+ $ref: "#/definitions/StdTx"
400:
description: The tx was malformated
500:
@@ -426,7 +429,7 @@
description: Owner address in bech32 format
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: denom
description: Collateral denom
@@ -472,7 +475,7 @@
description: Owner address in bech32 format
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: denom
description: Collateral denom
@@ -518,7 +521,7 @@
description: Owner address in bech32 format
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: denom
description: Collateral denom
@@ -562,7 +565,7 @@
description: Owner address in bech32 format
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: denom
description: Collateral denom
@@ -642,7 +645,7 @@
description: Owner address in bech32 format
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: denom
description: Collateral denom
@@ -674,9 +677,15 @@
200:
description: All CDPs with the input collateral denom
schema:
- type: array
- items:
- $ref: '#/definitions/CdpResponse'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ x-nullable: true
+ items:
+ $ref: '#/definitions/CdpResponse'
500:
description: Server internal error
/cdp/cdps/ratio/{denom}/{ratio}:
@@ -698,14 +707,20 @@
description: Collateralization ratio
required: true
type: string
- x-example: 2.0
+ x-example: "2.0"
responses:
200:
description: All CDPs with the input collateral denom and collateralization ratio less than the input ratio
schema:
- type: array
- items:
- $ref: '#/definitions/CdpResponse'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ x-nullable: true
+ items:
+ $ref: '#/definitions/CdpResponse'
500:
description: Server internal error
/cdp/cdps/cdp/deposits/{owner}/{denom}:
@@ -721,7 +736,7 @@
description: Owner address in bech32 format
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: denom
description: Collateral denom
@@ -732,9 +747,14 @@
200:
description: Deposits associated with the cdp
schema:
- type: array
- items:
- $ref: '#/definitions/CdpDepositResponse'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: '#/definitions/CdpDepositResponse'
500:
description: Server internal error
/auction/auctions/{id}/bids:
@@ -752,7 +772,16 @@
description: Auction id
required: true
type: string
- x-example: 1
+ x-example: "1"
+ - in: body
+ name: Auction bid request body
+ schema:
+ properties:
+ base_req:
+ $ref: '#/definitions/BaseReq'
+ amount:
+ $ref: '#/definitions/CoinBid'
+
responses:
200:
description: The transaction was successfully generated
@@ -788,9 +817,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/AuctionResponse'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: '#/definitions/AuctionResponse'
500:
description: Internal Server Error
/auction/auctions/{id}:
@@ -807,7 +841,7 @@
description: Auction id
required: true
type: string
- x-example: 2
+ x-example: "1"
responses:
200:
description: OK
@@ -818,7 +852,7 @@
500:
description: Internal Server Error
/pricefeed/postprice:
- put:
+ post:
summary: Generate post price transaction
consumes:
- application/json
@@ -844,7 +878,7 @@
example: '0.298464000000000007'
expiry:
type: string
- example: '2020-02-12T23:20:00Z'
+ example: '158551630291'
responses:
200:
description: The transaction was successfully generated
@@ -879,9 +913,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Market'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: '#/definitions/Market'
500:
description: Internal Server Error
/pricefeed/oracles/{market_id}:
@@ -902,9 +941,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Address'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: '#/definitions/Address'
500:
description: Internal Server Error
/pricefeed/rawprices/{market_id}:
@@ -925,9 +969,15 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/PostedPrice'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: '#/definitions/PostedPrice'
+
500:
description: Internal Server Error
/pricefeed/price/{market_id}:
@@ -948,7 +998,7 @@
200:
description: OK
schema:
- $ref: '#/definitions/Prices'
+ $ref: '#/definitions/Price'
500:
description: Internal Server Error
/bank/balances/{address}:
@@ -964,16 +1014,21 @@
description: Account address in bech32 format
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
responses:
200:
description: Account balances
schema:
- type: array
- items:
- $ref: '#/definitions/Coin'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/Coin"
500:
description: Server internal error
+
/bank/accounts/{address}/transfers:
post:
summary: Send coins from one account to another
@@ -989,7 +1044,7 @@
description: Account address in bech32 format
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
- in: body
name: account
description: The sender and tx information
@@ -998,16 +1053,16 @@
type: object
properties:
base_req:
- $ref: '#/definitions/BaseReq'
+ $ref: "#/definitions/BaseReq"
amount:
type: array
items:
- $ref: '#/definitions/Coin'
+ $ref: "#/definitions/Coin"
responses:
- 202:
+ 200:
description: Tx was successfully generated
schema:
- $ref: '#/definitions/StdTx'
+ $ref: "#/definitions/StdTx"
400:
description: Invalid request
500:
@@ -1025,7 +1080,7 @@
description: Account address
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
responses:
200:
description: Account information on the blockchain
@@ -1044,13 +1099,13 @@
coins:
type: array
items:
- $ref: '#/definitions/Coin'
+ $ref: "#/definitions/Coin"
public_key:
- $ref: '#/definitions/PublicKey'
+ $ref: "#/definitions/PublicKey"
sequence:
type: string
500:
- description: Server internel error
+ description: Server internal error
/staking/delegators/{delegatorAddr}/delegations:
parameters:
- in: path
@@ -1058,7 +1113,7 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
get:
summary: Get all delegations from a delegator
tags:
@@ -1069,47 +1124,55 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Delegation'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/Delegation"
400:
description: Invalid delegator address
500:
description: Internal Server Error
- post:
- summary: Submit delegation
- parameters:
- - in: body
- name: delegation
- description: The password of the account to remove from the KMS
- schema:
- type: object
- properties:
- base_req:
- $ref: '#/definitions/BaseReq'
- delegator_address:
- $ref: '#/definitions/Address'
- validator_address:
- $ref: '#/definitions/ValidatorAddress'
- delegation:
- $ref: '#/definitions/Coin'
- tags:
- - Staking
- consumes:
- - application/json
- produces:
- - application/json
- responses:
- 200:
- description: OK
- schema:
- $ref: '#/definitions/BroadcastTxCommitResult'
- 400:
- description: Invalid delegator address or delegation request body
- 401:
- description: Key password is wrong
- 500:
- description: Internal Server Error
+
+ # THE BELOW ENDPOINT IS NOT IMPLEMENTED IN COSMOS, SEE:
+ # https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/rest/query.go
+
+ # post:
+ # summary: Submit delegation
+ # parameters:
+ # - in: body
+ # name: delegation
+ # description: The password of the account to remove from the KMS
+ # schema:
+ # type: object
+ # properties:
+ # base_req:
+ # $ref: "#/definitions/BaseReq"
+ # delegator_address:
+ # $ref: "#/definitions/Address"
+ # validator_address:
+ # $ref: "#/definitions/ValidatorAddress"
+ # delegation:
+ # $ref: "#/definitions/Coin"
+ # tags:
+ # - Staking
+ # consumes:
+ # - application/json
+ # produces:
+ # - application/json
+ # responses:
+ # 200:
+ # description: OK
+ # schema:
+ # $ref: "#/definitions/BroadcastTxCommitResult"
+ # 400:
+ # description: Invalid delegator address or delegation request body
+ # 401:
+ # description: Key password is wrong
+ # 500:
+ # description: Internal Server Error
/staking/delegators/{delegatorAddr}/delegations/{validatorAddr}:
parameters:
- in: path
@@ -1117,13 +1180,13 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: validatorAddr
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Query the current delegation between a delegator and a validator
tags:
@@ -1134,7 +1197,7 @@
200:
description: OK
schema:
- $ref: '#/definitions/Delegation'
+ $ref: "#/definitions/Delegation"
400:
description: Invalid delegator address or validator address
500:
@@ -1146,7 +1209,7 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
get:
summary: Get all unbonding delegations from a delegator
tags:
@@ -1157,48 +1220,57 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/UnbondingDelegation'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/UnbondingDelegation"
+
400:
description: Invalid delegator address
500:
description: Internal Server Error
- post:
- summary: Submit an unbonding delegation
- parameters:
- - in: body
- name: delegation
- description: The password of the account to remove from the KMS
- schema:
- type: object
- properties:
- base_req:
- $ref: '#/definitions/BaseReq'
- delegator_address:
- $ref: '#/definitions/Address'
- validator_address:
- $ref: '#/definitions/ValidatorAddress'
- shares:
- type: string
- example: '100'
- tags:
- - Staking
- consumes:
- - application/json
- produces:
- - application/json
- responses:
- 200:
- description: OK
- schema:
- $ref: '#/definitions/BroadcastTxCommitResult'
- 400:
- description: Invalid delegator address or unbonding delegation request body
- 401:
- description: Key password is wrong
- 500:
- description: Internal Server Error
+
+ # THE BELOW ENDPOINT IS NOT IMPLEMENTED IN COSMOS, SEE:
+ # https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/rest/query.go
+
+ # post:
+ # summary: Submit an unbonding delegation
+ # parameters:
+ # - in: body
+ # name: delegation
+ # description: The password of the account to remove from the KMS
+ # schema:
+ # type: object
+ # properties:
+ # base_req:
+ # $ref: "#/definitions/BaseReq"
+ # delegator_address:
+ # $ref: "#/definitions/Address"
+ # validator_address:
+ # $ref: "#/definitions/ValidatorAddress"
+ # shares:
+ # type: string
+ # example: "100"
+ # tags:
+ # - Staking
+ # consumes:
+ # - application/json
+ # produces:
+ # - application/json
+ # responses:
+ # 200:
+ # description: OK
+ # schema:
+ # $ref: "#/definitions/BroadcastTxCommitResult"
+ # 400:
+ # description: Invalid delegator address or unbonding delegation request body
+ # 401:
+ # description: Key password is wrong
+ # 500:
+ # description: Internal Server Error
/staking/delegators/{delegatorAddr}/unbonding_delegations/{validatorAddr}:
parameters:
- in: path
@@ -1206,13 +1278,13 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: validatorAddr
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Query all unbonding delegations between a delegator and a validator
tags:
@@ -1223,7 +1295,7 @@
200:
description: OK
schema:
- $ref: '#/definitions/UnbondingDelegationPair'
+ $ref: "#/definitions/UnbondingDelegationPair"
400:
description: Invalid delegator address or validator address
500:
@@ -1254,55 +1326,63 @@
responses:
200:
description: OK
- schema:
- type: array
- items:
- $ref: '#/definitions/Redelegation'
- 500:
- description: Internal Server Error
- /staking/delegators/{delegatorAddr}/redelegations:
- parameters:
- - in: path
- name: delegatorAddr
- description: Bech32 AccAddress of Delegator
- required: true
- type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
- post:
- summary: Submit a redelegation
- parameters:
- - in: body
- name: delegation
- description: The sender and tx information
schema:
type: object
properties:
- base_req:
- $ref: '#/definitions/BaseReq'
- delegator_address:
- $ref: '#/definitions/Address'
- validator_src_addressess:
- $ref: '#/definitions/ValidatorAddress'
- validator_dst_address:
- $ref: '#/definitions/ValidatorAddress'
- shares:
+ height:
type: string
- example: '100'
- tags:
- - Staking
- consumes:
- - application/json
- produces:
- - application/json
- responses:
- 200:
- description: Tx was successfully generated
- schema:
- $ref: '#/definitions/StdTx'
- 400:
- description: Invalid delegator address or redelegation request body
+ result:
+ items:
+ $ref: "#/definitions/Redelegation"
500:
description: Internal Server Error
+
+ # THE BELOW ENDPOINT IS NOT IMPLEMENTED IN COSMOS, SEE:
+ # https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/rest/query.go
+
+ # /staking/delegators/{delegatorAddr}/redelegations:
+ # parameters:
+ # - in: path
+ # name: delegatorAddr
+ # description: Bech32 AccAddress of Delegator
+ # required: true
+ # type: string
+ # x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
+ # post:
+ # summary: Submit a redelegation
+ # parameters:
+ # - in: body
+ # name: delegation
+ # description: The sender and tx information
+ # schema:
+ # type: object
+ # properties:
+ # base_req:
+ # $ref: "#/definitions/BaseReq"
+ # delegator_address:
+ # $ref: "#/definitions/Address"
+ # validator_src_addresses:
+ # $ref: "#/definitions/ValidatorAddress"
+ # validator_dst_address:
+ # $ref: "#/definitions/ValidatorAddress"
+ # shares:
+ # type: string
+ # example: "100"
+ # tags:
+ # - Staking
+ # consumes:
+ # - application/json
+ # produces:
+ # - application/json
+ # responses:
+ # 200:
+ # description: Tx was successfully generated
+ # schema:
+ # $ref: "#/definitions/StdTx"
+ # 400:
+ # description: Invalid delegator address or redelegation request body
+ # 500:
+ # description: Internal Server Error
/staking/delegators/{delegatorAddr}/validators:
parameters:
- in: path
@@ -1310,7 +1390,7 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
get:
summary: Query all validators that a delegator is bonded to
tags:
@@ -1321,9 +1401,13 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Validator'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/Validator"
400:
description: Invalid delegator address
500:
@@ -1335,13 +1419,13 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: validatorAddr
description: Bech32 ValAddress of Delegator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Query a validator that a delegator is bonded to
tags:
@@ -1352,7 +1436,7 @@
200:
description: OK
schema:
- $ref: '#/definitions/Validator'
+ $ref: "#/definitions/Validator"
400:
description: Invalid delegator address or validator address
500:
@@ -1384,9 +1468,13 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Validator'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/Validator"
500:
description: Internal Server Error
/staking/validators/{validatorAddr}:
@@ -1396,7 +1484,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Query the information from a single validator
tags:
@@ -1419,7 +1507,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1q53rwutgpzx7szcrgzqguxyccjpzt9j44jzrtj
get:
summary: Get all delegations from a validator
tags:
@@ -1430,9 +1518,13 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Delegation'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/Delegation"
400:
description: Invalid validator address
500:
@@ -1444,7 +1536,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1q53rwutgpzx7szcrgzqguxyccjpzt9j44jzrtj
get:
summary: Get all unbonding delegations from a validator
tags:
@@ -1455,9 +1547,13 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/UnbondingDelegation'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/UnbondingDelegation"
400:
description: Invalid validator address
500:
@@ -1568,9 +1664,14 @@
200:
description: OK
schema:
- type: array
items:
- $ref: '#/definitions/SigningInfo'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ items:
+ $ref: "#/definitions/SigningInfo"
400:
description: Invalid validator public key for one of the validators
500:
@@ -1591,8 +1692,8 @@
name: validatorAddr
required: true
in: path
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
- - description: ''
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
+ - description: ""
name: UnjailBody
in: body
required: true
@@ -1600,12 +1701,12 @@
type: object
properties:
base_req:
- $ref: '#/definitions/StdTx'
+ $ref: "#/definitions/BaseReq"
responses:
200:
description: Tx was successfully generated
schema:
- $ref: '#/definitions/BroadcastTxCommitResult'
+ $ref: "#/definitions/BroadcastTxCommitResult"
400:
description: Invalid validator address or base_req
500:
@@ -1658,25 +1759,34 @@
type: object
properties:
base_req:
- $ref: '#/definitions/BaseReq'
+ $ref: "#/definitions/BaseReq"
title:
type: string
+ example: Test_title
description:
type: string
+ example: my_test_description
proposal_type:
type: string
- example: 'text'
+ example: "text"
proposer:
- $ref: '#/definitions/Address'
+ $ref: "#/definitions/Address"
initial_deposit:
type: array
items:
- $ref: '#/definitions/Coin'
+ $ref: "#/definitions/Coin"
responses:
200:
description: Tx was successfully generated
schema:
- $ref: '#/definitions/StdTx'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/TextProposal"
400:
description: Invalid proposal body
500:
@@ -1706,11 +1816,16 @@
type: string
responses:
200:
- description: OK
+ description: Tx was successfully generated
schema:
- type: array
- items:
- $ref: '#/definitions/TextProposal'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/TextProposal"
400:
description: Invalid query parameters
500:
@@ -1734,23 +1849,23 @@
type: object
properties:
base_req:
- $ref: '#/definitions/BaseReq'
+ $ref: "#/definitions/BaseReq"
title:
type: string
- x-example: 'Param Change'
+ example: Param_Change
description:
type: string
- x-example: 'Update max validators'
+ example: Update_max_validators
proposer:
- $ref: '#/definitions/Address'
+ $ref: "#/definitions/Address"
deposit:
type: array
items:
- $ref: '#/definitions/Coin'
+ $ref: "#/definitions/Coin"
changes:
type: array
items:
- $ref: '#/definitions/ParamChange'
+ $ref: "#/definitions/ParamChange"
responses:
200:
description: The transaction was successfully generated
@@ -1824,9 +1939,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Deposit'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: '#/definitions/Deposit'
400:
description: Invalid proposal id
500:
@@ -1887,13 +2007,13 @@
name: proposalId
required: true
in: path
- x-example: '2'
+ x-example: "2"
- type: string
description: Bech32 depositor address
name: depositor
required: true
in: path
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
responses:
200:
description: OK
@@ -1924,9 +2044,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Vote'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: '#/definitions/Vote'
400:
description: Invalid proposal id
500:
@@ -1992,7 +2117,7 @@
name: voter
required: true
in: path
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
responses:
200:
description: OK
@@ -2112,7 +2237,7 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava167w96tdvmazakdwkw2u57227eduula2cy572lf
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
get:
summary: Get the total rewards balance from all delegations
description: Get the sum of all the rewards earned by delegations by a single delegator
@@ -2124,7 +2249,12 @@
200:
description: OK
schema:
- $ref: '#/definitions/DelegatorTotalRewards'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ $ref: '#/definitions/DelegatorTotalRewards'
400:
description: Invalid delegator address
500:
@@ -2163,13 +2293,13 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv
+ x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
- in: path
name: validatorAddr
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Query a delegation reward
description: Query a single delegation reward by a delegator
@@ -2181,9 +2311,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Coin'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/Coin"
400:
description: Invalid delegator address
500:
@@ -2222,7 +2357,7 @@
description: Bech32 AccAddress of Delegator
required: true
type: string
- x-example: kava167w96tdvmazakdwkw2u57227eduula2cy572lf
+ x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
get:
summary: Get the rewards withdrawal address
description: Get the delegations' rewards withdrawal address. This is the address in which the user will receive the reward funds
@@ -2234,7 +2369,12 @@
200:
description: OK
schema:
- $ref: '#/definitions/Address'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ $ref: "#/definitions/Address"
400:
description: Invalid delegator address
500:
@@ -2275,7 +2415,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Validator distribution information
description: Query the distribution information of a single validator
@@ -2299,7 +2439,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Fee distribution outstanding rewards of a single validator
tags:
@@ -2310,9 +2450,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Coin'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: '#/definitions/Coin'
500:
description: Internal Server Error
/distribution/validators/{validatorAddr}/rewards:
@@ -2322,7 +2467,7 @@
description: Bech32 OperatorAddress of validator
required: true
type: string
- x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
get:
summary: Commission and self-delegation rewards of a single validator
description: Query the commission and self-delegation rewards of validator.
@@ -2332,11 +2477,16 @@
- application/json
responses:
200:
- description: OK
+ description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Coin'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: '#/definitions/Coin'
400:
description: Invalid validator address
500:
@@ -2379,9 +2529,14 @@
200:
description: OK
schema:
- type: array
- items:
- $ref: '#/definitions/Coin'
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: array
+ items:
+ $ref: "#/definitions/Coin"
500:
description: Internal Server Error
/distribution/parameters:
@@ -2441,7 +2596,7 @@
200:
description: OK
schema:
- type: string
+ $ref: "#/definitions/StandardResponse"
500:
description: Internal Server Error
/minting/annual-provisions:
@@ -2455,7 +2610,7 @@
200:
description: OK
schema:
- type: string
+ $ref: "#/definitions/StandardResponse"
500:
description: Internal Server Error
/supply/total:
@@ -2490,7 +2645,7 @@
200:
description: OK
schema:
- type: string
+ $ref: "#/definitions/StandardResponse"
400:
description: Invalid coin denomination
500:
@@ -2564,7 +2719,7 @@
hash:
$ref: '#/definitions/Hash'
height:
- type: integer
+ type: string
KVPair:
type: object
properties:
@@ -2574,26 +2729,31 @@
type: string
Msg:
type: object
- required:
- - from
properties:
type:
type: string
- example: 'string'
+ example: "cosmos-sdk/MsgSend"
value:
type: object
properties:
- from:
+ from_address:
type: string
- example: kava1mzqz3jfs9nzfp6v7qp647rv0afxlu2csl0txmq
+ example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
+ to_address:
+ type: string
+ example: kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz
+ amount:
+ type: array
+ items:
+ $ref: "#/definitions/Coin"
Address:
type: string
description: bech32 encoded address
- example: kava1depk54cuajgkzea6zpgkq36tnjwdzv4afc3d27
+ example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
ValidatorAddress:
type: string
description: bech32 encoded address
- example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l
+ example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
Coin:
type: object
properties:
@@ -2608,7 +2768,7 @@
properties:
denom:
type: string
- example: bnb
+ example: "xrp"
amount:
type: string
example: '10000000000'
@@ -2617,10 +2777,19 @@
properties:
denom:
type: string
- example: usdx
+ example: "usdx"
amount:
type: string
example: '10000000'
+ CoinBid:
+ type: object
+ properties:
+ denom:
+ type: string
+ example: "usdx"
+ amount:
+ type: string
+ example: '100000000'
Hash:
type: string
example: EE5F3404034C524501629B56E0DDC38FAD651F04
@@ -2646,8 +2815,8 @@
type: string
example: 'D085138D913993919295FF4B0A9107F1F2CDE0D37A87CE0644E217CBF3B49656'
height:
- type: number
- example: 368
+ type: string
+ example: "368"
tx:
$ref: '#/definitions/StdTx'
result:
@@ -2669,20 +2838,20 @@
type: object
properties:
total_count:
- type: number
- example: 1
+ type: string
+ example: "1"
count:
- type: number
- example: 1
+ type: string
+ example: "1"
page_number:
- type: number
- example: 1
+ type: string
+ example: "1"
page_total:
- type: number
- example: 1
+ type: string
+ example: "1"
limit:
- type: number
- example: 30
+ type: string
+ example: "30"
txs:
type: array
items:
@@ -2693,16 +2862,17 @@
msg:
type: array
items:
- $ref: '#/definitions/Msg'
+ $ref: "#/definitions/Msg"
fee:
type: object
properties:
gas:
type: string
+ example: "200000"
amount:
type: array
items:
- $ref: '#/definitions/Coin'
+ $ref: "#/definitions/Coin"
memo:
type: string
signatures:
@@ -2718,8 +2888,8 @@
type: object
properties:
total:
- type: number
- example: 0
+ type: string
+ example: "0"
hash:
$ref: '#/definitions/Hash'
BlockHeader:
@@ -2727,21 +2897,21 @@
properties:
chain_id:
type: string
- example: kavahub-2
+ example: testing
height:
- type: number
- example: 1
+ type: string
+ example: "1"
time:
type: string
example: '2017-12-30T05:53:09.287+01:00'
num_txs:
- type: number
- example: 0
+ type: string
+ example: "0"
last_block_id:
$ref: '#/definitions/BlockID'
total_txs:
- type: number
- example: 35
+ type: string
+ example: "35"
last_commit_hash:
$ref: '#/definitions/Hash'
data_hash:
@@ -2779,9 +2949,10 @@
items:
type: string
evidence:
- type: array
- items:
- type: string
+ type: object
+ # type: array
+ # items:
+ # type: string
last_commit:
type: object
properties:
@@ -2789,6 +2960,7 @@
$ref: '#/definitions/BlockID'
precommits:
type: array
+ x-nullable: true
items:
type: object
properties:
@@ -2851,20 +3023,20 @@
properties:
from:
type: string
- example: 'kava1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc'
+ example: 'kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c'
description: Sender address or Keybase name to generate a transaction
memo:
type: string
example: 'Sent via Cosmos Voyager 🚀'
chain_id:
type: string
- example: 'kava-1'
+ example: 'testing'
account_number:
type: string
example: '0'
sequence:
type: string
- example: '1'
+ example: '5'
gas:
type: string
example: '200000'
@@ -2877,7 +3049,7 @@
$ref: '#/definitions/Coin'
simulate:
type: boolean
- example: false
+ example: true
description: Estimate gas for a transaction (cannot be used in conjunction with generate_only)
TendermintValidator:
type: object
@@ -2901,7 +3073,7 @@
example: 1
owner:
type: string
- example: kava1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc
+ example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
collateral:
$ref: '#/definitions/CoinCollateral'
principal:
@@ -2924,9 +3096,11 @@
example: 1
depositor:
type: string
- example: kava1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc
+ example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9
amount:
- $ref: '#/definitions/CoinCollateral'
+ type: array
+ items:
+ $ref: '#/definitions/CoinCollateral'
PricefeedParameters:
type: object
properties:
@@ -2957,7 +3131,7 @@
example: '0.298758999999999997'
expiry:
type: string
- example: '2020-02-12T23:10:00Z'
+ example: '2021-02-12T23:10:00Z'
Market:
type: object
properties:
@@ -2972,6 +3146,7 @@
example: 'usd'
oracles:
type: array
+ x-nullable: true
items:
$ref: '#/definitions/Address'
active:
@@ -3307,3 +3482,96 @@
type: array
items:
$ref: '#/definitions/Coin'
+ StandardResponse:
+ type: object
+ properties:
+ height:
+ type: string
+ result:
+ type: string
+ PostStdTx:
+ type: object
+ properties:
+ msg:
+ type: array
+ items:
+ $ref: "#/definitions/PostMsg"
+ fee:
+ type: object
+ properties:
+ gas:
+ type: string
+ example: "200000"
+ amount:
+ type: array
+ items:
+ $ref: "#/definitions/PostCoin2"
+ memo:
+ type: string
+ example: ""
+ signatures:
+ type: array
+ items:
+ $ref: "#/definitions/PostSignature"
+
+ PostMsg:
+ type: object
+ properties:
+ type:
+ type: string
+ example: "cosmos-sdk/MsgSend"
+ value:
+ type: object
+ properties:
+ from_address:
+ type: string
+ example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
+ to_address:
+ type: string
+ example: kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz
+ amount:
+ type: array
+ items:
+ $ref: "#/definitions/PostCoin1"
+ PostCoin1:
+ type: object
+ properties:
+ denom:
+ type: string
+ example: stake
+ amount:
+ type: string
+ example: "2000000"
+ PostCoin2:
+ type: object
+ properties:
+ denom:
+ type: string
+ example: stake
+ amount:
+ type: string
+ example: "2000"
+ PostSignature:
+ type: object
+ properties:
+ signature:
+ type: string
+ example: "KzBeQp/2+47oiqc16BImnOYidSAesZ3kgR3S8Fy+baFlvDlDv7goU/+rm4c7+woudNte3uZAG0CuUeHsF+Ld8Q=="
+ pubkey:
+ type: object
+ properties:
+ type:
+ type: string
+ example: "tendermint/PubKeySecp256k1"
+ value:
+ type: string
+ example: "AmWAim83Qp+kIcj3RT7i327b3l0EHwzCrGVGXusb70B7"
+ EncodeTx:
+ type: object
+ properties:
+ type:
+ type: string
+ example: "cosmos-sdk/StdTx"
+ value:
+ type: object
+ $ref: "#/definitions/StdTx"
diff --git a/x/auction/alias.go b/x/auction/alias.go
index 1aed46a7..0ac1a7dd 100644
--- a/x/auction/alias.go
+++ b/x/auction/alias.go
@@ -1,8 +1,3 @@
-// nolint
-// autogenerated code using github.com/rigelrozanski/multitool
-// aliases generated for the following subdirectories:
-// ALIASGEN: github.com/kava-labs/kava/x/auction/keeper
-// ALIASGEN: github.com/kava-labs/kava/x/auction/types
package auction
import (
@@ -10,22 +5,13 @@ import (
"github.com/kava-labs/kava/x/auction/types"
)
+// nolint
+// autogenerated code using github.com/rigelrozanski/multitool
+// aliases generated for the following subdirectories:
+// ALIASGEN: github.com/kava-labs/kava/x/auction/keeper
+// ALIASGEN: github.com/kava-labs/kava/x/auction/types
+
const (
- DefaultCodespace = types.DefaultCodespace
- CodeInvalidInitialAuctionID = types.CodeInvalidInitialAuctionID
- CodeInvalidModulePermissions = types.CodeInvalidModulePermissions
- CodeUnrecognizedAuctionType = types.CodeUnrecognizedAuctionType
- CodeAuctionNotFound = types.CodeAuctionNotFound
- CodeAuctionHasNotExpired = types.CodeAuctionHasNotExpired
- CodeAuctionHasExpired = types.CodeAuctionHasExpired
- CodeInvalidBidDenom = types.CodeInvalidBidDenom
- CodeInvalidLotDenom = types.CodeInvalidLotDenom
- CodeBidTooSmall = types.CodeBidTooSmall
- CodeBidTooLarge = types.CodeBidTooLarge
- CodeLotTooSmall = types.CodeLotTooSmall
- CodeLotTooLarge = types.CodeLotTooLarge
- CodeCollateralAuctionIsInReversePhase = types.CodeCollateralAuctionIsInReversePhase
- CodeCollateralAuctionIsInForwardPhase = types.CodeCollateralAuctionIsInForwardPhase
EventTypeAuctionStart = types.EventTypeAuctionStart
EventTypeAuctionBid = types.EventTypeAuctionBid
EventTypeAuctionClose = types.EventTypeAuctionClose
@@ -55,6 +41,7 @@ var (
// functions aliases
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
+ RegisterInvariants = keeper.RegisterInvariants
NewSurplusAuction = types.NewSurplusAuction
NewDebtAuction = types.NewDebtAuction
NewCollateralAuction = types.NewCollateralAuction
diff --git a/x/auction/client/cli/query.go b/x/auction/client/cli/query.go
index 6d41ef3f..59ee6620 100644
--- a/x/auction/client/cli/query.go
+++ b/x/auction/client/cli/query.go
@@ -6,8 +6,8 @@ import (
"github.com/spf13/cobra"
- "github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/kava-labs/kava/x/auction/types"
@@ -21,7 +21,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
Short: "Querying commands for the auction module",
}
- auctionQueryCmd.AddCommand(client.GetCommands(
+ auctionQueryCmd.AddCommand(flags.GetCommands(
QueryGetAuctionCmd(queryRoute, cdc),
QueryGetAuctionsCmd(queryRoute, cdc),
QueryParamsCmd(queryRoute, cdc),
diff --git a/x/auction/client/cli/tx.go b/x/auction/client/cli/tx.go
index 4f143d7a..c70c3d90 100644
--- a/x/auction/client/cli/tx.go
+++ b/x/auction/client/cli/tx.go
@@ -1,14 +1,15 @@
package cli
import (
+ "bufio"
"fmt"
"strconv"
"strings"
"github.com/spf13/cobra"
- "github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
@@ -25,7 +26,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
Short: "auction transactions subcommands",
}
- auctionTxCmd.AddCommand(client.PostCommands(
+ auctionTxCmd.AddCommand(flags.PostCommands(
GetCmdPlaceBid(cdc),
)...)
@@ -45,8 +46,9 @@ $ %s tx %s bid 34 1000usdx --from myKeyName
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
- txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
id, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
diff --git a/x/auction/genesis.go b/x/auction/genesis.go
index eedcdf1e..10862b5d 100644
--- a/x/auction/genesis.go
+++ b/x/auction/genesis.go
@@ -22,7 +22,7 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper
for _, a := range gs.Auctions {
keeper.SetAuction(ctx, a)
// find the total coins that should be present in the module account
- totalAuctionCoins.Add(a.GetModuleAccountCoins())
+ totalAuctionCoins = totalAuctionCoins.Add(a.GetModuleAccountCoins()...)
}
// check if the module account exists
diff --git a/x/auction/genesis_test.go b/x/auction/genesis_test.go
index a7352c52..78ecf1f9 100644
--- a/x/auction/genesis_test.go
+++ b/x/auction/genesis_test.go
@@ -29,6 +29,12 @@ func TestInitGenesis(t *testing.T) {
tApp := app.NewTestApp()
keeper := tApp.GetAuctionKeeper()
ctx := tApp.NewContext(true, abci.Header{})
+ // setup module account
+ supplyKeeper := tApp.GetSupplyKeeper()
+ moduleAcc := supplyKeeper.GetModuleAccount(ctx, auction.ModuleName)
+ require.NoError(t, moduleAcc.SetCoins(testAuction.GetModuleAccountCoins()))
+ supplyKeeper.SetModuleAccount(ctx, moduleAcc)
+
// create genesis
gs := auction.NewGenesisState(
10,
@@ -38,7 +44,7 @@ func TestInitGenesis(t *testing.T) {
// run init
require.NotPanics(t, func() {
- auction.InitGenesis(ctx, keeper, tApp.GetSupplyKeeper(), gs)
+ auction.InitGenesis(ctx, keeper, supplyKeeper, gs)
})
// check state is as expected
@@ -59,7 +65,7 @@ func TestInitGenesis(t *testing.T) {
return false
})
})
- t.Run("invalid", func(t *testing.T) {
+ t.Run("invalid (invalid nextAuctionID)", func(t *testing.T) {
// setup keepers
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{})
@@ -71,6 +77,24 @@ func TestInitGenesis(t *testing.T) {
auction.GenesisAuctions{testAuction},
)
+ // check init fails
+ require.Panics(t, func() {
+ auction.InitGenesis(ctx, tApp.GetAuctionKeeper(), tApp.GetSupplyKeeper(), gs)
+ })
+ })
+ t.Run("invalid (missing mod account coins)", func(t *testing.T) {
+ // setup keepers
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{})
+
+ // create invalid genesis
+ gs := auction.NewGenesisState(
+ 10,
+ auction.DefaultParams(),
+ auction.GenesisAuctions{testAuction},
+ )
+ // invalid as there is no module account setup
+
// check init fails
require.Panics(t, func() {
auction.InitGenesis(ctx, tApp.GetAuctionKeeper(), tApp.GetSupplyKeeper(), gs)
diff --git a/x/auction/handler.go b/x/auction/handler.go
index 2ac42e36..26056f38 100644
--- a/x/auction/handler.go
+++ b/x/auction/handler.go
@@ -1,32 +1,31 @@
package auction
import (
- "fmt"
-
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/auction/types"
)
// NewHandler returns a function to handle all "auction" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
- return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
+ return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
ctx = ctx.WithEventManager(sdk.NewEventManager())
switch msg := msg.(type) {
case MsgPlaceBid:
return handleMsgPlaceBid(ctx, keeper, msg)
default:
- return sdk.ErrUnknownRequest(fmt.Sprintf("Unrecognized auction msg type: %T", msg)).Result()
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
}
}
}
-func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) sdk.Result {
+func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) (*sdk.Result, error) {
err := keeper.PlaceBid(ctx, msg.AuctionID, msg.Bidder, msg.Amount)
if err != nil {
- return err.Result()
+ return nil, err
}
ctx.EventManager().EmitEvent(
@@ -37,7 +36,7 @@ func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) sdk.Resu
),
)
- return sdk.Result{
+ return &sdk.Result{
Events: ctx.EventManager().Events(),
- }
+ }, nil
}
diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go
index 025c3df1..c1722dee 100644
--- a/x/auction/keeper/auctions.go
+++ b/x/auction/keeper/auctions.go
@@ -5,18 +5,19 @@ import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/kava-labs/kava/x/auction/types"
)
// StartSurplusAuction starts a new surplus (forward) auction.
-func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) {
-
+func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, error) {
auction := types.NewSurplusAuction(
seller,
lot,
bidDenom,
- types.DistantFuture)
+ types.DistantFuture,
+ )
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
if err != nil {
@@ -41,7 +42,7 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin
}
// StartDebtAuction starts a new debt (reverse) auction.
-func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, sdk.Error) {
+func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, error) {
auction := types.NewDebtAuction(
buyer,
@@ -53,7 +54,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in
// This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker.
macc := k.supplyKeeper.GetModuleAccount(ctx, buyer)
if !macc.HasPermission(supply.Minter) {
- return 0, types.ErrInvalidModulePermissions(k.codespace, supply.Minter)
+ return 0, sdkerrors.Wrap(types.ErrInvalidModulePermissions, supply.Minter)
}
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, buyer, types.ModuleName, sdk.NewCoins(debt))
@@ -79,8 +80,10 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in
}
// StartCollateralAuction starts a new collateral (2-phase) auction.
-func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin) (uint64, sdk.Error) {
-
+func (k Keeper) StartCollateralAuction(
+ ctx sdk.Context, seller string, lot, maxBid sdk.Coin,
+ lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin,
+) (uint64, error) {
weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights)
if err != nil {
return 0, err
@@ -91,7 +94,8 @@ func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.C
types.DistantFuture,
maxBid,
weightedAddresses,
- debt)
+ debt,
+ )
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
if err != nil {
@@ -120,41 +124,40 @@ func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.C
}
// PlaceBid places a bid on any auction.
-func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddress, newAmount sdk.Coin) sdk.Error {
+func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddress, newAmount sdk.Coin) error {
auction, found := k.GetAuction(ctx, auctionID)
if !found {
- return types.ErrAuctionNotFound(k.codespace, auctionID)
+ return sdkerrors.Wrapf(types.ErrAuctionNotFound, "%d", auctionID)
}
// validation common to all auctions
if ctx.BlockTime().After(auction.GetEndTime()) {
- return types.ErrAuctionHasExpired(k.codespace, auctionID)
+ return sdkerrors.Wrapf(types.ErrAuctionHasExpired, "%d", auctionID)
}
// move coins and return updated auction
- var err sdk.Error
- var updatedAuction types.Auction
+ var (
+ err error
+ updatedAuction types.Auction
+ )
switch a := auction.(type) {
case types.SurplusAuction:
- if updatedAuction, err = k.PlaceBidSurplus(ctx, a, bidder, newAmount); err != nil {
- return err
- }
+ updatedAuction, err = k.PlaceBidSurplus(ctx, a, bidder, newAmount)
case types.DebtAuction:
- if updatedAuction, err = k.PlaceBidDebt(ctx, a, bidder, newAmount); err != nil {
- return err
- }
+ updatedAuction, err = k.PlaceBidDebt(ctx, a, bidder, newAmount)
case types.CollateralAuction:
if !a.IsReversePhase() {
updatedAuction, err = k.PlaceForwardBidCollateral(ctx, a, bidder, newAmount)
} else {
updatedAuction, err = k.PlaceReverseBidCollateral(ctx, a, bidder, newAmount)
}
- if err != nil {
- return err
- }
default:
- return types.ErrUnrecognizedAuctionType(k.codespace)
+ err = sdkerrors.Wrap(types.ErrUnrecognizedAuctionType, auction.GetType())
+ }
+
+ if err != nil {
+ return err
}
k.SetAuction(ctx, updatedAuction)
@@ -163,10 +166,10 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres
}
// PlaceBidSurplus places a forward bid on a surplus auction, moving coins and returning the updated auction.
-func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.SurplusAuction, sdk.Error) {
+func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.SurplusAuction, error) {
// Validate new bid
if bid.Denom != a.Bid.Denom {
- return a, types.ErrInvalidBidDenom(k.codespace, bid.Denom, a.Bid.Denom)
+ return a, sdkerrors.Wrapf(types.ErrInvalidBidDenom, "%s ≠ %s)", bid.Denom, a.Bid.Denom)
}
minNewBidAmt := a.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost
sdk.MaxInt(
@@ -175,7 +178,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder
),
)
if bid.Amount.LT(minNewBidAmt) {
- return a, types.ErrBidTooSmall(k.codespace, bid, sdk.NewCoin(a.Bid.Denom, minNewBidAmt))
+ return a, sdkerrors.Wrapf(types.ErrBidTooSmall, "%s ≤ %s%s", bid, minNewBidAmt, a.Bid.Denom)
}
// New bidder pays back old bidder
@@ -223,13 +226,13 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder
}
// PlaceForwardBidCollateral places a forward bid on a collateral auction, moving coins and returning the updated auction.
-func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.CollateralAuction, sdk.Error) {
+func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.CollateralAuction, error) {
// Validate new bid
if bid.Denom != a.Bid.Denom {
- return a, types.ErrInvalidBidDenom(k.codespace, bid.Denom, a.Bid.Denom)
+ return a, sdkerrors.Wrapf(types.ErrInvalidBidDenom, "%s ≠ %s", bid.Denom, a.Bid.Denom)
}
if a.IsReversePhase() {
- return a, types.ErrCollateralAuctionIsInReversePhase(k.codespace, a.ID)
+ return a, sdkerrors.Wrapf(types.ErrCollateralAuctionIsInReversePhase, "%d", a.ID)
}
minNewBidAmt := a.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost
sdk.MaxInt(
@@ -239,10 +242,10 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc
)
minNewBidAmt = sdk.MinInt(minNewBidAmt, a.MaxBid.Amount) // allow new bids to hit MaxBid even though it may be less than the increment %
if bid.Amount.LT(minNewBidAmt) {
- return a, types.ErrBidTooSmall(k.codespace, bid, sdk.NewCoin(a.Bid.Denom, minNewBidAmt))
+ return a, sdkerrors.Wrapf(types.ErrBidTooSmall, "%s ≤ %s%s", bid, minNewBidAmt, a.Bid.Denom)
}
if a.MaxBid.IsLT(bid) {
- return a, types.ErrBidTooLarge(k.codespace, bid, a.MaxBid)
+ return a, sdkerrors.Wrapf(types.ErrBidTooLarge, "%s > %s", bid, a.MaxBid)
}
// New bidder pays back old bidder
@@ -299,13 +302,13 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc
}
// PlaceReverseBidCollateral places a reverse bid on a collateral auction, moving coins and returning the updated auction.
-func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.CollateralAuction, sdk.Error) {
+func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.CollateralAuction, error) {
// Validate new bid
if lot.Denom != a.Lot.Denom {
- return a, types.ErrInvalidLotDenom(k.codespace, lot.Denom, a.Lot.Denom)
+ return a, sdkerrors.Wrapf(types.ErrInvalidLotDenom, lot.Denom, a.Lot.Denom)
}
if !a.IsReversePhase() {
- return a, types.ErrCollateralAuctionIsInForwardPhase(k.codespace, a.ID)
+ return a, sdkerrors.Wrapf(types.ErrCollateralAuctionIsInForwardPhase, "%d", a.ID)
}
maxNewLotAmt := a.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost
sdk.MaxInt(
@@ -314,10 +317,10 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc
),
)
if lot.Amount.GT(maxNewLotAmt) {
- return a, types.ErrLotTooLarge(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, maxNewLotAmt))
+ return a, sdkerrors.Wrapf(types.ErrLotTooLarge, "%s > %s%s", lot, maxNewLotAmt, a.Lot.Denom)
}
if lot.IsNegative() {
- return a, types.ErrLotTooSmall(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, sdk.ZeroInt()))
+ return a, sdkerrors.Wrapf(types.ErrLotTooSmall, "%s ≤ %s%s", lot, sdk.ZeroInt(), a.Lot.Denom)
}
// New bidder pays back old bidder
@@ -368,10 +371,10 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc
}
// PlaceBidDebt places a reverse bid on a debt auction, moving coins and returning the updated auction.
-func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.DebtAuction, sdk.Error) {
+func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.DebtAuction, error) {
// Validate new bid
if lot.Denom != a.Lot.Denom {
- return a, types.ErrInvalidLotDenom(k.codespace, lot.Denom, a.Lot.Denom)
+ return a, sdkerrors.Wrapf(types.ErrInvalidLotDenom, lot.Denom, a.Lot.Denom)
}
maxNewLotAmt := a.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost
sdk.MaxInt(
@@ -380,10 +383,10 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac
),
)
if lot.Amount.GT(maxNewLotAmt) {
- return a, types.ErrLotTooLarge(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, maxNewLotAmt))
+ return a, sdkerrors.Wrapf(types.ErrLotTooLarge, "%s > %s%s", lot, maxNewLotAmt, a.Lot.Denom)
}
if lot.IsNegative() {
- return a, types.ErrLotTooSmall(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, sdk.ZeroInt()))
+ return a, sdkerrors.Wrapf(types.ErrLotTooSmall, "%s ≤ %s%s", lot, sdk.ZeroInt(), a.Lot.Denom)
}
// New bidder pays back old bidder
@@ -434,15 +437,15 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac
}
// CloseAuction closes an auction and distributes funds to the highest bidder.
-func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error {
+func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) error {
auction, found := k.GetAuction(ctx, auctionID)
if !found {
- return types.ErrAuctionNotFound(k.codespace, auctionID)
+ return sdkerrors.Wrapf(types.ErrAuctionNotFound, "%d", auctionID)
}
if ctx.BlockTime().Before(auction.GetEndTime()) {
- return types.ErrAuctionHasNotExpired(k.codespace, ctx.BlockTime(), auction.GetEndTime())
+ return sdkerrors.Wrapf(types.ErrAuctionHasNotExpired, "block time %s, auction end time %s", ctx.BlockTime().UTC(), auction.GetEndTime().UTC())
}
// payout to the last bidder
@@ -460,7 +463,7 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error {
return err
}
default:
- return types.ErrUnrecognizedAuctionType(k.codespace)
+ return sdkerrors.Wrap(types.ErrUnrecognizedAuctionType, auc.GetType())
}
k.DeleteAuction(ctx, auctionID)
@@ -475,7 +478,7 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error {
}
// PayoutDebtAuction pays out the proceeds for a debt auction, first minting the coins.
-func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) sdk.Error {
+func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) error {
err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot))
if err != nil {
return err
@@ -494,7 +497,7 @@ func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) sdk.Erro
}
// PayoutSurplusAuction pays out the proceeds for a surplus auction.
-func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) sdk.Error {
+func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil {
return err
@@ -503,7 +506,7 @@ func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) sd
}
// PayoutCollateralAuction pays out the proceeds for a collateral auction.
-func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAuction) sdk.Error {
+func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAuction) error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil {
return err
@@ -518,7 +521,7 @@ func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAucti
}
// CloseExpiredAuctions finds all auctions that are past (or at) their ending times and closes them, paying out to the highest bidder.
-func (k Keeper) CloseExpiredAuctions(ctx sdk.Context) sdk.Error {
+func (k Keeper) CloseExpiredAuctions(ctx sdk.Context) error {
var expiredAuctions []uint64
k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool {
expiredAuctions = append(expiredAuctions, id)
@@ -542,10 +545,10 @@ func earliestTime(t1, t2 time.Time) time.Time {
}
// splitCoinIntoWeightedBuckets divides up some amount of coins according to some weights.
-func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, sdk.Error) {
+func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, error) {
for _, bucket := range buckets {
if bucket.IsNegative() {
- return nil, sdk.ErrInternal("cannot split coin into bucket with negative weight")
+ return nil, fmt.Errorf("cannot split %s into bucket with negative weight (%s)", coin.String(), bucket.String())
}
}
amounts := splitIntIntoWeightedBuckets(coin.Amount, buckets)
diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go
index 3813b6df..c8c19c1b 100644
--- a/x/auction/keeper/auctions_test.go
+++ b/x/auction/keeper/auctions_test.go
@@ -267,30 +267,31 @@ func TestStartSurplusAuction(t *testing.T) {
blockTime time.Time
args args
expectPass bool
+ expPanic bool
}{
{
"normal",
someTime,
args{cdp.LiquidatorMacc, c("stable", 10), "gov"},
- true,
+ true, false,
},
{
"no module account",
someTime,
args{"nonExistentModule", c("stable", 10), "gov"},
- false,
+ false, true,
},
{
"not enough coins",
someTime,
args{cdp.LiquidatorMacc, c("stable", 101), "gov"},
- false,
+ false, false,
},
{
"incorrect denom",
someTime,
args{cdp.LiquidatorMacc, c("notacoin", 10), "gov"},
- false,
+ false, false,
},
}
for _, tc := range testCases {
@@ -308,7 +309,15 @@ func TestStartSurplusAuction(t *testing.T) {
keeper := tApp.GetAuctionKeeper()
// run function under test
- id, err := keeper.StartSurplusAuction(ctx, tc.args.seller, tc.args.lot, tc.args.bidDenom)
+ var (
+ id uint64
+ err error
+ )
+ if tc.expPanic {
+ require.Panics(t, func() { _, _ = keeper.StartSurplusAuction(ctx, tc.args.seller, tc.args.lot, tc.args.bidDenom) }, tc.name)
+ } else {
+ id, err = keeper.StartSurplusAuction(ctx, tc.args.seller, tc.args.lot, tc.args.bidDenom)
+ }
// check
sk := tApp.GetSupplyKeeper()
@@ -316,11 +325,11 @@ func TestStartSurplusAuction(t *testing.T) {
actualAuc, found := keeper.GetAuction(ctx, id)
if tc.expectPass {
- require.NoError(t, err)
+ require.NoError(t, err, tc.name)
// check coins moved
- require.Equal(t, initialLiquidatorCoins.Sub(cs(tc.args.lot)), liquidatorCoins)
+ require.Equal(t, initialLiquidatorCoins.Sub(cs(tc.args.lot)), liquidatorCoins, tc.name)
// check auction in store and is correct
- require.True(t, found)
+ require.True(t, found, tc.name)
expectedAuction := types.Auction(types.SurplusAuction{BaseAuction: types.BaseAuction{
ID: id,
Initiator: tc.args.seller,
@@ -331,13 +340,13 @@ func TestStartSurplusAuction(t *testing.T) {
EndTime: types.DistantFuture,
MaxEndTime: types.DistantFuture,
}})
- require.Equal(t, expectedAuction, actualAuc)
- } else {
- require.Error(t, err)
+ require.Equal(t, expectedAuction, actualAuc, tc.name)
+ } else if !tc.expPanic && !tc.expectPass {
+ require.Error(t, err, tc.name)
// check coins not moved
- require.Equal(t, initialLiquidatorCoins, liquidatorCoins)
+ require.Equal(t, initialLiquidatorCoins, liquidatorCoins, tc.name)
// check auction not in store
- require.False(t, found)
+ require.False(t, found, tc.name)
}
})
}
diff --git a/x/auction/keeper/bidding_test.go b/x/auction/keeper/bidding_test.go
index 40d2d164..9a34cabc 100644
--- a/x/auction/keeper/bidding_test.go
+++ b/x/auction/keeper/bidding_test.go
@@ -1,6 +1,7 @@
package keeper_test
import (
+ "errors"
"strings"
"testing"
"time"
@@ -55,319 +56,347 @@ func TestAuctionBidding(t *testing.T) {
auctionArgs auctionArgs
setupBids []bidArgs
bidArgs bidArgs
- expectedError sdk.CodeType
+ expectedError error
expectedEndTime time.Time
expectedBidder sdk.AccAddress
expectedBid sdk.Coin
- expectpass bool
+ expectPass bool
+ expectPanic bool
}{
{
"basic: auction doesn't exist",
auctionArgs{Surplus, "", c("token1", 1), c("token2", 1), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
nil,
bidArgs{buyer, c("token2", 10)},
- types.CodeAuctionNotFound,
+ types.ErrAuctionNotFound,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
false,
+ true,
},
{
"basic: closed auction",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
nil,
bidArgs{buyer, c("token2", 10)},
- types.CodeAuctionHasExpired,
+ types.ErrAuctionHasExpired,
types.DistantFuture,
nil,
c("token2", 0),
false,
+ false,
},
{
"surplus: normal",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
nil,
bidArgs{buyer, c("token2", 10)},
- sdk.CodeType(0),
+ nil,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
true,
+ false,
},
{
"surplus: second bidder",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
[]bidArgs{{buyer, c("token2", 10)}},
bidArgs{secondBuyer, c("token2", 11)},
- sdk.CodeType(0),
+ nil,
someTime.Add(types.DefaultBidDuration),
secondBuyer,
c("token2", 11),
true,
+ false,
},
{
"surplus: invalid bid denom",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
nil,
bidArgs{buyer, c("badtoken", 10)},
- types.CodeInvalidBidDenom,
+ types.ErrInvalidBidDenom,
types.DistantFuture,
nil, // surplus auctions are created with initial bidder as a nil address
c("token2", 0),
false,
+ false,
},
{
"surplus: invalid bid (less than)",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
[]bidArgs{{buyer, c("token2", 100)}},
bidArgs{buyer, c("token2", 99)},
- types.CodeBidTooSmall,
+ types.ErrBidTooSmall,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 100),
false,
+ false,
},
{
"surplus: invalid bid (equal)",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
nil,
bidArgs{buyer, c("token2", 0)}, // min bid is technically 0 at default 5%, but it's capped at 1
- types.CodeBidTooSmall,
+ types.ErrBidTooSmall,
types.DistantFuture,
nil, // surplus auctions are created with initial bidder as a nil address
c("token2", 0),
false,
+ false,
},
{
"surplus: invalid bid (less than min increment)",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
[]bidArgs{{buyer, c("token2", 100)}},
bidArgs{buyer, c("token2", 104)}, // min bid is 105 at default 5%
- types.CodeBidTooSmall,
+ types.ErrBidTooSmall,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 100),
false,
+ false,
},
{
"debt: normal",
auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot
nil,
bidArgs{buyer, c("token1", 10)},
- sdk.CodeType(0),
+ nil,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 100),
true,
+ false,
},
{
"debt: second bidder",
auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot
[]bidArgs{{buyer, c("token1", 10)}},
bidArgs{secondBuyer, c("token1", 9)},
- sdk.CodeType(0),
+ nil,
someTime.Add(types.DefaultBidDuration),
secondBuyer,
c("token2", 100),
true,
+ false,
},
{
"debt: invalid lot denom",
auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot
nil,
bidArgs{buyer, c("badtoken", 10)},
- types.CodeInvalidLotDenom,
+ types.ErrInvalidLotDenom,
types.DistantFuture,
supply.NewModuleAddress(modName),
c("token2", 100),
false,
+ false,
},
{
"debt: invalid lot size (larger)",
auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}},
nil,
bidArgs{buyer, c("token1", 21)},
- types.CodeLotTooLarge,
+ types.ErrLotTooLarge,
types.DistantFuture,
supply.NewModuleAddress(modName),
c("token2", 100),
false,
+ false,
},
{
"debt: invalid lot size (equal)",
auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}},
nil,
bidArgs{buyer, c("token1", 20)},
- types.CodeLotTooLarge,
+ types.ErrLotTooLarge,
types.DistantFuture,
supply.NewModuleAddress(modName),
c("token2", 100),
false,
+ false,
},
{
"debt: invalid lot size (larger than min increment)",
auctionArgs{Debt, modName, c("token1", 60), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}},
nil,
bidArgs{buyer, c("token1", 58)}, // max lot at default 5% is 57
- types.CodeLotTooLarge,
+ types.ErrLotTooLarge,
types.DistantFuture,
supply.NewModuleAddress(modName),
c("token2", 100),
- false,
+ false, false,
},
{
"collateral [forward]: normal",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
nil,
bidArgs{buyer, c("token2", 10)},
- sdk.CodeType(0),
+ nil,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
true,
+ false,
},
{
"collateral [forward]: second bidder",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
[]bidArgs{{buyer, c("token2", 10)}},
bidArgs{secondBuyer, c("token2", 11)},
- sdk.CodeType(0),
+ nil,
someTime.Add(types.DefaultBidDuration),
secondBuyer,
c("token2", 11),
true,
+ false,
},
{
"collateral [forward]: invalid bid denom",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
nil,
bidArgs{buyer, c("badtoken", 10)},
- types.CodeInvalidBidDenom,
+ types.ErrInvalidBidDenom,
types.DistantFuture,
nil,
c("token2", 0),
false,
+ false,
},
{
"collateral [forward]: invalid bid size (smaller)",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
[]bidArgs{{buyer, c("token2", 10)}},
bidArgs{buyer, c("token2", 9)},
- types.CodeBidTooSmall,
+ types.ErrBidTooSmall,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
false,
+ false,
},
{
"collateral [forward]: invalid bid size (equal)",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
nil,
bidArgs{buyer, c("token2", 0)},
- types.CodeBidTooSmall,
+ types.ErrBidTooSmall,
types.DistantFuture,
nil,
c("token2", 0),
false,
+ false,
},
{
"collateral [forward]: invalid bid size (less than min increment)",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
[]bidArgs{{buyer, c("token2", 50)}},
bidArgs{buyer, c("token2", 51)},
- types.CodeBidTooSmall,
+ types.ErrBidTooSmall,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 50),
false,
+ false,
},
{
"collateral [forward]: less than min increment but equal to maxBid",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
[]bidArgs{{buyer, c("token2", 99)}},
bidArgs{buyer, c("token2", 100)}, // min bid at default 5% is 104
- sdk.CodeType(0),
+ nil,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 100),
true,
+ false,
},
{
"collateral [forward]: invalid bid size (greater than max)",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
nil,
bidArgs{buyer, c("token2", 101)},
- types.CodeBidTooLarge,
+ types.ErrBidTooLarge,
types.DistantFuture,
nil,
c("token2", 0),
false,
+ false,
},
{
"collateral [reverse]: normal",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
[]bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase
bidArgs{buyer, c("token1", 15)},
- sdk.CodeType(0),
+ nil,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 50),
true,
+ false,
},
{
"collateral [reverse]: second bidder",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
[]bidArgs{{buyer, c("token2", 50)}, {buyer, c("token1", 15)}}, // put auction into reverse phase, and add a reverse phase bid
bidArgs{secondBuyer, c("token1", 14)},
- sdk.CodeType(0),
+ nil,
someTime.Add(types.DefaultBidDuration),
secondBuyer,
c("token2", 50),
true,
+ false,
},
{
"collateral [reverse]: invalid lot denom",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
[]bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase
bidArgs{buyer, c("badtoken", 15)},
- types.CodeInvalidLotDenom,
+ types.ErrInvalidLotDenom,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 50),
false,
+ false,
},
{
"collateral [reverse]: invalid lot size (greater)",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
[]bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase
bidArgs{buyer, c("token1", 21)},
- types.CodeLotTooLarge,
+ types.ErrLotTooLarge,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 50),
false,
+ false,
},
{
"collateral [reverse]: invalid lot size (equal)",
auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
[]bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase
bidArgs{buyer, c("token1", 20)},
- types.CodeLotTooLarge,
+ types.ErrLotTooLarge,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 50),
false,
+ false,
},
{
"collateral [reverse]: invalid lot size (larger than min increment)",
auctionArgs{Collateral, modName, c("token1", 60), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
[]bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase
bidArgs{buyer, c("token1", 58)}, // max lot at default 5% is 57
- types.CodeLotTooLarge,
+ types.ErrLotTooLarge,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 50),
false,
+ false,
},
}
for _, tc := range tests {
@@ -394,17 +423,26 @@ func TestAuctionBidding(t *testing.T) {
// Start Auction
var id uint64
- var err sdk.Error
+ var err error
switch tc.auctionArgs.auctionType {
case Surplus:
- id, _ = keeper.StartSurplusAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid.Denom)
+ if tc.expectPanic {
+ require.Panics(t, func() {
+ id, err = keeper.StartSurplusAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid.Denom)
+ })
+ } else {
+ id, err = keeper.StartSurplusAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid.Denom)
+ }
case Debt:
- id, _ = keeper.StartDebtAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.bid, tc.auctionArgs.lot, tc.auctionArgs.debt)
+ id, err = keeper.StartDebtAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.bid, tc.auctionArgs.lot, tc.auctionArgs.debt)
case Collateral:
- id, _ = keeper.StartCollateralAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid, tc.auctionArgs.addresses, tc.auctionArgs.weights, tc.auctionArgs.debt) // seller, lot, maxBid, otherPerson
+ id, err = keeper.StartCollateralAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid, tc.auctionArgs.addresses, tc.auctionArgs.weights, tc.auctionArgs.debt) // seller, lot, maxBid, otherPerson
default:
t.Fail()
}
+
+ require.NoError(t, err)
+
// Place setup bids
for _, b := range tc.setupBids {
require.NoError(t, keeper.PlaceBid(ctx, id, b.bidder, b.amount))
@@ -428,7 +466,7 @@ func TestAuctionBidding(t *testing.T) {
err = keeper.PlaceBid(ctx, id, tc.bidArgs.bidder, tc.bidArgs.amount)
// Check success/failure
- if tc.expectpass {
+ if tc.expectPass {
require.Nil(t, err)
// Check auction was found
newAuction, found := keeper.GetAuction(ctx, id)
@@ -445,7 +483,8 @@ func TestAuctionBidding(t *testing.T) {
case Debt:
bidAmt = oldAuction.GetBid()
case Collateral:
- collatAuction, _ := oldAuction.(types.CollateralAuction)
+ collatAuction, ok := oldAuction.(types.CollateralAuction)
+ require.True(t, ok, tc.name)
if collatAuction.IsReversePhase() {
bidAmt = oldAuction.GetBid()
}
@@ -453,18 +492,18 @@ func TestAuctionBidding(t *testing.T) {
if oldBidder.Equals(tc.bidArgs.bidder) { // same bidder
require.Equal(t, newBidderOldCoins.Sub(cs(bidAmt.Sub(oldAuction.GetBid()))), bank.GetCoins(ctx, tc.bidArgs.bidder))
} else {
- require.Equal(t, cs(newBidderOldCoins.Sub(cs(bidAmt))...), bank.GetCoins(ctx, tc.bidArgs.bidder)) // wrapping in cs() to avoid comparing nil and empty coins
- if oldBidder.Equals(supply.NewModuleAddress(oldAuction.GetInitiator())) { // handle checking debt coins for case debt auction has had no bids placed yet TODO make this less confusing
- require.Equal(t, cs(oldBidderOldCoins.Add(cs(oldAuction.GetBid()))...).Add(cs(c("debt", oldAuction.GetBid().Amount.Int64()))), bank.GetCoins(ctx, oldBidder))
+ require.Equal(t, newBidderOldCoins.Sub(cs(bidAmt)), bank.GetCoins(ctx, tc.bidArgs.bidder)) // wrapping in cs() to avoid comparing nil and empty coins
+ if oldBidder.Equals(supply.NewModuleAddress(oldAuction.GetInitiator())) { // handle checking debt coins for case debt auction has had no bids placed yet TODO make this less confusing
+ require.Equal(t, oldBidderOldCoins.Add(oldAuction.GetBid()).Add(c("debt", oldAuction.GetBid().Amount.Int64())), bank.GetCoins(ctx, oldBidder))
} else {
- require.Equal(t, cs(oldBidderOldCoins.Add(cs(oldAuction.GetBid()))...), bank.GetCoins(ctx, oldBidder))
+ require.Equal(t, cs(oldBidderOldCoins.Add(oldAuction.GetBid())...), bank.GetCoins(ctx, oldBidder))
}
}
} else {
// Check expected error code type
require.NotNil(t, err, "PlaceBid did not return an error") // catch nil values before they cause a panic below
- require.Equal(t, tc.expectedError, err.Result().Code)
+ require.True(t, errors.Is(err, tc.expectedError))
// Check auction values
newAuction, found := keeper.GetAuction(ctx, id)
diff --git a/x/auction/keeper/invariants.go b/x/auction/keeper/invariants.go
new file mode 100644
index 00000000..05eee629
--- /dev/null
+++ b/x/auction/keeper/invariants.go
@@ -0,0 +1,143 @@
+package keeper
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/store/prefix"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/x/auction/types"
+)
+
+// RegisterInvariants registers all staking invariants
+func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) {
+
+ ir.RegisterRoute(types.ModuleName, "module-account",
+ ModuleAccountInvariants(k))
+ ir.RegisterRoute(types.ModuleName, "valid-auctions",
+ ValidAuctionInvariant(k))
+ ir.RegisterRoute(types.ModuleName, "valid-index",
+ ValidIndexInvariant(k))
+}
+
+// ModuleAccountInvariants checks that the module account's coins matches those stored in auctions
+func ModuleAccountInvariants(k Keeper) sdk.Invariant {
+ return func(ctx sdk.Context) (string, bool) {
+
+ totalAuctionCoins := sdk.NewCoins()
+ k.IterateAuctions(ctx, func(auction types.Auction) bool {
+ a, ok := auction.(types.GenesisAuction)
+ if !ok {
+ panic("stored auction type does not fulfill GenesisAuction interface")
+ }
+ totalAuctionCoins = totalAuctionCoins.Add(a.GetModuleAccountCoins()...)
+ return false
+ })
+
+ moduleAccCoins := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins()
+ broken := !moduleAccCoins.IsEqual(totalAuctionCoins)
+
+ invariantMessage := sdk.FormatInvariant(
+ types.ModuleName,
+ "module account",
+ fmt.Sprintf(
+ "\texpected ModuleAccount coins: %s\n"+
+ "\tactual ModuleAccount coins: %s\n",
+ totalAuctionCoins, moduleAccCoins),
+ )
+ return invariantMessage, broken
+ }
+}
+
+// ValidAuctionInvariant verifies that all auctions in the store are independently valid
+func ValidAuctionInvariant(k Keeper) sdk.Invariant {
+ return func(ctx sdk.Context) (string, bool) {
+ var validationErr error
+ var invalidAuction types.Auction
+ k.IterateAuctions(ctx, func(auction types.Auction) bool {
+ a, ok := auction.(types.GenesisAuction)
+ if !ok {
+ panic("stored auction type does not fulfill GenesisAuction interface")
+ }
+
+ currentTime := ctx.BlockTime()
+ if !currentTime.Equal(time.Time{}) { // this avoids a simulator bug where app.InitGenesis is called with blockTime=0 instead of the correct time
+ if a.GetEndTime().Before(currentTime) {
+ validationErr = fmt.Errorf("endTime after current block time (%s)", currentTime)
+ invalidAuction = a
+ return true
+ }
+ }
+
+ if err := a.Validate(); err != nil {
+ validationErr = err
+ invalidAuction = a
+ return true
+ }
+ return false
+ })
+
+ broken := validationErr != nil
+ invariantMessage := sdk.FormatInvariant(
+ types.ModuleName,
+ "valid auctions",
+ fmt.Sprintf(
+ "\tfound invalid auction, reason: %s\n"+
+ "\tauction:\n\t%s\n",
+ validationErr, invalidAuction),
+ )
+ return invariantMessage, broken
+ }
+}
+
+// ValidIndexInvariant checks that all auctions in the store are also in the index and vice versa.
+func ValidIndexInvariant(k Keeper) sdk.Invariant {
+ return func(ctx sdk.Context) (string, bool) {
+ /* Method:
+ - check all the auction IDs in the index have a corresponding auction in the store
+ - index is now valid but there could be extra auction in the store
+ - check for these extra auctions by checking num items in the store equals that of index (store keys are always unique)
+ - doesn't check the IDs in the auction structs match the IDs in the keys
+ */
+
+ // Check all auction IDs in the index are in the auction store
+ store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
+
+ indexIterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix)
+ defer indexIterator.Close()
+
+ var indexLength int
+ for ; indexIterator.Valid(); indexIterator.Next() {
+ indexLength++
+
+ idBytes := indexIterator.Value()
+ auctionBytes := store.Get(idBytes)
+ if auctionBytes == nil {
+ invariantMessage := sdk.FormatInvariant(
+ types.ModuleName,
+ "valid index",
+ fmt.Sprintf("\tauction with ID '%d' found in index but not in store", types.Uint64FromBytes(idBytes)))
+ return invariantMessage, true
+ }
+ }
+
+ // Check length of auction store matches the length of the index
+ storeIterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
+ defer storeIterator.Close()
+ var storeLength int
+ for ; storeIterator.Valid(); storeIterator.Next() {
+ storeLength++
+ }
+
+ if storeLength != indexLength {
+ invariantMessage := sdk.FormatInvariant(
+ types.ModuleName,
+ "valid index",
+ fmt.Sprintf("\tmismatched number of items in auction store (%d) and index (%d)", storeLength, indexLength))
+ return invariantMessage, true
+ }
+
+ return "", false
+ }
+}
diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go
index 7a51302b..da8caf89 100644
--- a/x/auction/keeper/keeper.go
+++ b/x/auction/keeper/keeper.go
@@ -18,7 +18,6 @@ type Keeper struct {
storeKey sdk.StoreKey
cdc *codec.Codec
paramSubspace subspace.Subspace
- codespace sdk.CodespaceType
}
// NewKeeper returns a new auction keeper.
@@ -26,11 +25,16 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.Suppl
if addr := supplyKeeper.GetModuleAddress(types.ModuleName); addr == nil {
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
}
+
+ if !paramstore.HasKeyTable() {
+ paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
+ }
+
return Keeper{
supplyKeeper: supplyKeeper,
storeKey: storeKey,
cdc: cdc,
- paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
+ paramSubspace: paramstore,
}
}
@@ -46,17 +50,17 @@ func (k Keeper) SetNextAuctionID(ctx sdk.Context, id uint64) {
}
// GetNextAuctionID reads the next available global ID from store
-func (k Keeper) GetNextAuctionID(ctx sdk.Context) (uint64, sdk.Error) {
+func (k Keeper) GetNextAuctionID(ctx sdk.Context) (uint64, error) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.NextAuctionIDKey)
if bz == nil {
- return 0, types.ErrInvalidInitialAuctionID(k.codespace)
+ return 0, types.ErrInvalidInitialAuctionID
}
return types.Uint64FromBytes(bz), nil
}
// IncrementNextAuctionID increments the next auction ID in the store by 1.
-func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error {
+func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) error {
id, err := k.GetNextAuctionID(ctx)
if err != nil {
return err
@@ -66,7 +70,7 @@ func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error {
}
// StoreNewAuction stores an auction, adding a new ID
-func (k Keeper) StoreNewAuction(ctx sdk.Context, auction types.Auction) (uint64, sdk.Error) {
+func (k Keeper) StoreNewAuction(ctx sdk.Context, auction types.Auction) (uint64, error) {
newAuctionID, err := k.GetNextAuctionID(ctx)
if err != nil {
return 0, err
diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go
index 08d337b3..e7d6460a 100644
--- a/x/auction/keeper/querier.go
+++ b/x/auction/keeper/querier.go
@@ -1,16 +1,18 @@
package keeper
import (
+ abci "github.com/tendermint/tendermint/abci/types"
+
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
- abci "github.com/tendermint/tendermint/abci/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/auction/types"
)
// NewQuerier is the module level router for state queries
func NewQuerier(keeper Keeper) sdk.Querier {
- return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
+ return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
switch path[0] {
case types.QueryGetAuction:
return queryAuction(ctx, req, keeper)
@@ -19,35 +21,35 @@ func NewQuerier(keeper Keeper) sdk.Querier {
case types.QueryGetParams:
return queryGetParams(ctx, req, keeper)
default:
- return nil, sdk.ErrUnknownRequest("unknown auction query endpoint")
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
}
}
}
-func queryAuction(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryAuction(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
// Decode request
var requestParams types.QueryAuctionParams
- err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
+ err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
- return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
// Lookup auction
auction, found := keeper.GetAuction(ctx, requestParams.AuctionID)
if !found {
- return nil, types.ErrAuctionNotFound(types.DefaultCodespace, requestParams.AuctionID)
+ return nil, sdkerrors.Wrapf(types.ErrAuctionNotFound, "%d", requestParams.AuctionID)
}
// Encode results
bz, err := codec.MarshalJSONIndent(keeper.cdc, auction)
if err != nil {
- return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
-func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
// Get all auctions
auctionsList := types.Auctions{}
keeper.IterateAuctions(ctx, func(a types.Auction) bool {
@@ -58,21 +60,21 @@ func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byt
// Encode Results
bz, err := codec.MarshalJSONIndent(keeper.cdc, auctionsList)
if err != nil {
- return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// query params in the auction store
-func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
// Get params
params := keeper.GetParams(ctx)
// Encode results
bz, err := codec.MarshalJSONIndent(keeper.cdc, params)
if err != nil {
- return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
diff --git a/x/auction/module.go b/x/auction/module.go
index f34b19f5..dd1134a0 100644
--- a/x/auction/module.go
+++ b/x/auction/module.go
@@ -12,6 +12,7 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
+ "github.com/cosmos/cosmos-sdk/x/auth"
sim "github.com/cosmos/cosmos-sdk/x/simulation"
abci "github.com/tendermint/tendermint/abci/types"
@@ -24,7 +25,7 @@ import (
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
- _ module.AppModuleSimulation = AppModuleSimulation{}
+ _ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic implements the sdk.AppModuleBasic interface.
@@ -72,46 +73,29 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
//____________________________________________________________________________
-// AppModuleSimulation defines the module simulation functions used by the auction module.
-type AppModuleSimulation struct{}
-
-// RegisterStoreDecoder registers a decoder for auction module's types
-func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
- sdr[StoreKey] = simulation.DecodeStore
-}
-
-// GenerateGenesisState creates a randomized GenState of the auction module
-func (AppModuleSimulation) GenerateGenesisState(simState *module.SimulationState) {
- simulation.RandomizedGenState(simState)
-}
-
-// RandomizedParams creates randomized auction param changes for the simulator.
-func (AppModuleSimulation) RandomizedParams(r *rand.Rand) []sim.ParamChange {
- return simulation.ParamChanges(r)
-}
-
-//____________________________________________________________________________
-
// AppModule implements the sdk.AppModule interface.
type AppModule struct {
AppModuleBasic
- AppModuleSimulation
- keeper Keeper
- supplyKeeper types.SupplyKeeper
+ keeper Keeper
+ accountKeeper auth.AccountKeeper
+ supplyKeeper types.SupplyKeeper
}
// NewAppModule creates a new AppModule object
-func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule {
+func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper, supplyKeeper types.SupplyKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
+ accountKeeper: accountKeeper,
supplyKeeper: supplyKeeper,
}
}
-// RegisterInvariants performs a no-op.
-func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
+// RegisterInvariants registers the module invariants.
+func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
+ RegisterInvariants(ir, am.keeper)
+}
// Route module message route name
func (AppModule) Route() string {
@@ -157,3 +141,30 @@ func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) {
func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
+
+//____________________________________________________________________________
+
+// GenerateGenesisState creates a randomized GenState of the auction module
+func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
+ simulation.RandomizedGenState(simState)
+}
+
+// ProposalContents doesn't return any content functions for governance proposals.
+func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
+ return nil
+}
+
+// RandomizedParams returns nil because auction has no params.
+func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
+ return simulation.ParamChanges(r)
+}
+
+// RegisterStoreDecoder registers a decoder for auction module's types
+func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
+ sdr[StoreKey] = simulation.DecodeStore
+}
+
+// WeightedOperations returns the all the auction module operations with their respective weights.
+func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
+ return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper)
+}
diff --git a/x/auction/simulation/decoder.go b/x/auction/simulation/decoder.go
index a64f8744..a6a58e92 100644
--- a/x/auction/simulation/decoder.go
+++ b/x/auction/simulation/decoder.go
@@ -1,12 +1,33 @@
package simulation
import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+
+ "github.com/tendermint/tendermint/libs/kv"
+
"github.com/cosmos/cosmos-sdk/codec"
- cmn "github.com/tendermint/tendermint/libs/common"
+
+ "github.com/kava-labs/kava/x/auction/types"
)
// DecodeStore unmarshals the KVPair's Value to the corresponding auction type
-func DecodeStore(cdc *codec.Codec, kvA, kvB cmn.KVPair) string {
- // TODO implement this
- return ""
+func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
+ switch {
+ case bytes.Equal(kvA.Key[:1], types.AuctionKeyPrefix):
+ var auctionA, auctionB types.Auction
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &auctionA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &auctionB)
+ return fmt.Sprintf("%v\n%v", auctionA, auctionB)
+
+ case bytes.Equal(kvA.Key[:1], types.AuctionByTimeKeyPrefix),
+ bytes.Equal(kvA.Key[:1], types.NextAuctionIDKey):
+ auctionIDA := binary.BigEndian.Uint64(kvA.Value)
+ auctionIDB := binary.BigEndian.Uint64(kvB.Value)
+ return fmt.Sprintf("%d\n%d", auctionIDA, auctionIDB)
+
+ default:
+ panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
+ }
}
diff --git a/x/auction/simulation/decoder_test.go b/x/auction/simulation/decoder_test.go
new file mode 100644
index 00000000..f1109332
--- /dev/null
+++ b/x/auction/simulation/decoder_test.go
@@ -0,0 +1,57 @@
+package simulation
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/x/auction/types"
+)
+
+func makeTestCodec() (cdc *codec.Codec) {
+ cdc = codec.New()
+ sdk.RegisterCodec(cdc)
+ types.RegisterCodec(cdc)
+ return
+}
+
+func TestDecodeDistributionStore(t *testing.T) {
+ cdc := makeTestCodec()
+
+ oneCoin := sdk.NewCoin("coin", sdk.OneInt())
+ auction := types.NewSurplusAuction("me", oneCoin, "coin", time.Now().UTC())
+
+ kvPairs := kv.Pairs{
+ kv.Pair{Key: types.AuctionKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&auction)},
+ kv.Pair{Key: types.AuctionByTimeKeyPrefix, Value: sdk.Uint64ToBigEndian(2)},
+ kv.Pair{Key: types.NextAuctionIDKey, Value: sdk.Uint64ToBigEndian(10)},
+ kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
+ }
+
+ tests := []struct {
+ name string
+ expectedLog string
+ }{
+ {"Auction", fmt.Sprintf("%v\n%v", auction, auction)},
+ {"AuctionByTime", "2\n2"},
+ {"NextAuctionI", "10\n10"},
+ {"other", ""},
+ }
+ for i, tt := range tests {
+ i, tt := i, tt
+ t.Run(tt.name, func(t *testing.T) {
+ switch i {
+ case len(tests) - 1:
+ require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
+ default:
+ require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
+ }
+ })
+ }
+}
diff --git a/x/auction/simulation/genesis.go b/x/auction/simulation/genesis.go
index 20bac731..3d4949f9 100644
--- a/x/auction/simulation/genesis.go
+++ b/x/auction/simulation/genesis.go
@@ -2,21 +2,170 @@ package simulation
import (
"fmt"
+ "math/rand"
+ "time"
"github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+ "github.com/cosmos/cosmos-sdk/x/supply"
"github.com/kava-labs/kava/x/auction/types"
+ cdptypes "github.com/kava-labs/kava/x/cdp/types"
)
+const (
+ // Block time params are un-exported constants in cosmos-sdk/x/simulation.
+ // Copy them here in lieu of importing them.
+ minTimePerBlock time.Duration = (10000 / 2) * time.Second
+ maxTimePerBlock time.Duration = 10000 * time.Second
+
+ // Calculate the average block time
+ AverageBlockTime time.Duration = (maxTimePerBlock - minTimePerBlock) / 2
+ // MaxBidDuration is a crude way of ensuring that BidDuration ≤ MaxAuctionDuration for all generated params
+ MaxBidDuration time.Duration = AverageBlockTime * 50
+)
+
+func GenBidDuration(r *rand.Rand) time.Duration {
+ d, err := RandomPositiveDuration(r, 0, MaxBidDuration)
+ if err != nil {
+ panic(err)
+ }
+ return d
+}
+func GenMaxAuctionDuration(r *rand.Rand) time.Duration {
+ d, err := RandomPositiveDuration(r, MaxBidDuration, AverageBlockTime*200)
+ if err != nil {
+ panic(err)
+ }
+ return d
+}
+
+func GenIncrementCollateral(r *rand.Rand) sdk.Dec {
+ return simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1"))
+}
+
+var GenIncrementDebt = GenIncrementCollateral
+var GenIncrementSurplus = GenIncrementCollateral
+
// RandomizedGenState generates a random GenesisState for auction
func RandomizedGenState(simState *module.SimulationState) {
- // TODO implement this fully
- // - randomly generating the genesis params
- // - overwriting with genesis provided to simulation
- auctionGenesis := types.DefaultGenesisState()
+ p := types.NewParams(
+ GenMaxAuctionDuration(simState.Rand),
+ GenBidDuration(simState.Rand),
+ GenIncrementSurplus(simState.Rand),
+ GenIncrementDebt(simState.Rand),
+ GenIncrementCollateral(simState.Rand),
+ )
+ if err := p.Validate(); err != nil {
+ panic(err)
+ }
+ auctionGenesis := types.NewGenesisState(
+ types.DefaultNextAuctionID,
+ p,
+ nil,
+ )
+ // Add auctions
+ auctions := types.GenesisAuctions{
+ types.NewDebtAuction(
+ cdptypes.LiquidatorMacc, // using cdp account rather than generic test one to avoid having to set permissions on the supply keeper
+ sdk.NewInt64Coin("usdx", 100),
+ sdk.NewInt64Coin("ukava", 1000000000000),
+ simState.GenTimestamp.Add(time.Hour*5),
+ sdk.NewInt64Coin("debt", 100), // same as usdx
+ ),
+ }
+ var startingID = auctionGenesis.NextAuctionID
+ var ok bool
+ var totalAuctionCoins sdk.Coins
+ for i, a := range auctions {
+ auctions[i], ok = a.WithID(uint64(i) + startingID).(types.GenesisAuction)
+ if !ok {
+ panic("can't convert Auction to GenesisAuction")
+ }
+ totalAuctionCoins = totalAuctionCoins.Add(a.GetModuleAccountCoins()...)
+ }
+ auctionGenesis.NextAuctionID = startingID + uint64(len(auctions))
+ auctionGenesis.Auctions = append(auctionGenesis.Auctions, auctions...)
+
+ // Also need to update the auction module account (to reflect the coins held in the auctions)
+ var authGenesis auth.GenesisState
+ simState.Cdc.MustUnmarshalJSON(simState.GenState[auth.ModuleName], &authGenesis)
+
+ auctionModAcc, found := getAccount(authGenesis.Accounts, supply.NewModuleAddress(types.ModuleName))
+ if !found {
+ auctionModAcc = supply.NewEmptyModuleAccount(types.ModuleName)
+ }
+ if err := auctionModAcc.SetCoins(totalAuctionCoins); err != nil {
+ panic(err)
+ }
+ authGenesis.Accounts = replaceOrAppendAccount(authGenesis.Accounts, auctionModAcc)
+
+ // TODO adding bidder coins as well - this should be moved elsewhere
+ bidder, found := getAccount(authGenesis.Accounts, simState.Accounts[0].Address) // 0 is the bidder // FIXME
+ if !found {
+ panic("bidder not found")
+ }
+ bidderCoins := sdk.NewCoins(sdk.NewInt64Coin("usdx", 10000000000))
+ if err := bidder.SetCoins(bidder.GetCoins().Add(bidderCoins...)); err != nil {
+ panic(err)
+ }
+ authGenesis.Accounts = replaceOrAppendAccount(authGenesis.Accounts, bidder)
+
+ simState.GenState[auth.ModuleName] = simState.Cdc.MustMarshalJSON(authGenesis)
+
+ // Update the supply genesis state to reflect the new coins
+ // TODO find some way for this to happen automatically / move it elsewhere
+ var supplyGenesis supply.GenesisState
+ simState.Cdc.MustUnmarshalJSON(simState.GenState[supply.ModuleName], &supplyGenesis)
+ supplyGenesis.Supply = supplyGenesis.Supply.Add(totalAuctionCoins...).Add(bidderCoins...)
+ simState.GenState[supply.ModuleName] = simState.Cdc.MustMarshalJSON(supplyGenesis)
+
+ // TODO liquidator mod account doesn't need to be initialized for this example
+ // - it just mints kava, doesn't need a starting balance
+ // - and supply.GetModuleAccount creates one if it doesn't exist
+
+ // Note: this line prints out the auction genesis state, not just the auction parameters. Some sdk modules print out just the parameters.
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, auctionGenesis))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(auctionGenesis)
}
+
+// Return an account from a list of accounts that matches an address.
+func getAccount(accounts []authexported.GenesisAccount, addr sdk.AccAddress) (authexported.GenesisAccount, bool) {
+ for _, a := range accounts {
+ if a.GetAddress().Equals(addr) {
+ return a, true
+ }
+ }
+ return nil, false
+}
+
+// In a list of accounts, replace the first account found with the same address. If not found, append the account.
+func replaceOrAppendAccount(accounts []authexported.GenesisAccount, acc authexported.GenesisAccount) []authexported.GenesisAccount {
+ newAccounts := accounts
+ for i, a := range accounts {
+ if a.GetAddress().Equals(acc.GetAddress()) {
+ newAccounts[i] = acc
+ return newAccounts
+ }
+ }
+ return append(newAccounts, acc)
+}
+
+func RandomPositiveDuration(r *rand.Rand, inclusiveMin, exclusiveMax time.Duration) (time.Duration, error) {
+ min := int64(inclusiveMin)
+ max := int64(exclusiveMax)
+ if min < 0 || max < 0 {
+ return 0, fmt.Errorf("min and max must be positive")
+ }
+ if min >= max {
+ return 0, fmt.Errorf("max must be < min")
+ }
+ randPositiveInt64 := r.Int63n(max-min) + min
+ return time.Duration(randPositiveInt64), nil
+}
diff --git a/x/auction/simulation/operations.go b/x/auction/simulation/operations.go
new file mode 100644
index 00000000..84f2297c
--- /dev/null
+++ b/x/auction/simulation/operations.go
@@ -0,0 +1,251 @@
+package simulation
+
+import (
+ "errors"
+ "fmt"
+ "math/big"
+ "math/rand"
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/baseapp"
+ "github.com/cosmos/cosmos-sdk/codec"
+ "github.com/cosmos/cosmos-sdk/simapp/helpers"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+
+ appparams "github.com/kava-labs/kava/app/params"
+ "github.com/kava-labs/kava/x/auction/keeper"
+ "github.com/kava-labs/kava/x/auction/types"
+)
+
+var (
+ errorNotEnoughCoins = errors.New("account doesn't have enough coins")
+ errorCantReceiveBids = errors.New("auction can't receive bids (lot = 0 in reverse auction)")
+)
+
+// Simulation operation weights constants
+const (
+ OpWeightMsgPlaceBid = "op_weight_msg_place_bid"
+)
+
+// WeightedOperations returns all the operations from the module with their respective weights
+func WeightedOperations(
+ appParams simulation.AppParams, cdc *codec.Codec, ak auth.AccountKeeper, k keeper.Keeper,
+) simulation.WeightedOperations {
+ var weightMsgPlaceBid int
+
+ appParams.GetOrGenerate(cdc, OpWeightMsgPlaceBid, &weightMsgPlaceBid, nil,
+ func(_ *rand.Rand) {
+ weightMsgPlaceBid = appparams.DefaultWeightMsgPlaceBid
+ },
+ )
+
+ return simulation.WeightedOperations{
+ simulation.NewWeightedOperation(
+ weightMsgPlaceBid,
+ SimulateMsgPlaceBid(ak, k),
+ ),
+ }
+}
+
+// SimulateMsgPlaceBid returns a function that runs a random state change on the module keeper.
+// There's two error paths
+// - return a OpMessage, but nil error - this will log a message but keep running the simulation
+// - return an error - this will stop the simulation
+func SimulateMsgPlaceBid(ak auth.AccountKeeper, keeper keeper.Keeper) simulation.Operation {
+ return func(
+ r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
+ ) (simulation.OperationMsg, []simulation.FutureOperation, error) {
+ // get open auctions
+ openAuctions := types.Auctions{}
+ keeper.IterateAuctions(ctx, func(a types.Auction) bool {
+ openAuctions = append(openAuctions, a)
+ return false
+ })
+
+ // shuffle auctions slice so that bids are evenly distributed across auctions
+ r.Shuffle(len(openAuctions), func(i, j int) {
+ openAuctions[i], openAuctions[j] = openAuctions[j], openAuctions[i]
+ })
+
+ // search through auctions and an accounts to find a pair where a bid can be placed (ie account has enough coins to place bid on auction)
+ blockTime := ctx.BlockHeader().Time
+ params := keeper.GetParams(ctx)
+ bidder, openAuction, found := findValidAccountAuctionPair(accs, openAuctions, func(acc simulation.Account, auc types.Auction) bool {
+ account := ak.GetAccount(ctx, acc.Address)
+ _, err := generateBidAmount(r, params, auc, account, blockTime)
+ if err == errorNotEnoughCoins || err == errorCantReceiveBids {
+ return false // keep searching
+ } else if err != nil {
+ panic(err) // raise errors
+ }
+ return true // found valid pair
+ })
+ if !found {
+ return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation (no valid auction and bidder)", "", false, nil), nil, nil
+ }
+
+ bidderAcc := ak.GetAccount(ctx, bidder.Address)
+ if bidderAcc == nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("couldn't find account %s", bidder.Address)
+ }
+
+ // pick a bid amount for the chosen auction and bidder
+ amount, err := generateBidAmount(r, params, openAuction, bidderAcc, blockTime)
+ if err != nil { // shouldn't happen given the checks above
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ // create and deliver a tx
+ msg := types.NewMsgPlaceBid(openAuction.GetID(), bidder.Address, amount)
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ sdk.NewCoins(), // TODO pick a random amount fees
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{bidderAcc.GetAccountNumber()},
+ []uint64{bidderAcc.GetSequence()},
+ bidder.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ // to aid debugging, add the stack trace to the comment field of the returned opMsg
+ return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
+ }
+ // to aid debugging, add the result log to the comment field
+ return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
+ }
+}
+
+func generateBidAmount(
+ r *rand.Rand, params types.Params, auc types.Auction,
+ bidder authexported.Account, blockTime time.Time) (sdk.Coin, error) {
+ bidderBalance := bidder.SpendableCoins(blockTime)
+
+ switch a := auc.(type) {
+
+ case types.DebtAuction:
+ // Check bidder has enough (stable coin) to pay in
+ if bidderBalance.AmountOf(a.Bid.Denom).LT(a.Bid.Amount) { // stable coin
+ return sdk.Coin{}, errorNotEnoughCoins
+ }
+ // Check auction can still receive new bids
+ if a.Lot.Amount.Equal(sdk.ZeroInt()) {
+ return sdk.Coin{}, errorCantReceiveBids
+ }
+ // Generate a new lot amount (gov coin)
+ maxNewLotAmt := a.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost
+ sdk.MaxInt(
+ sdk.NewInt(1),
+ sdk.NewDecFromInt(a.Lot.Amount).Mul(params.IncrementDebt).RoundInt(),
+ ),
+ )
+ amt, err := RandIntInclusive(r, sdk.ZeroInt(), maxNewLotAmt) // maxNewLotAmt shouldn't be < 0 given the check above
+ if err != nil {
+ panic(err)
+ }
+ return sdk.NewCoin(a.Lot.Denom, amt), nil // gov coin
+
+ case types.SurplusAuction:
+ // Check the bidder has enough (gov coin) to pay in
+ minNewBidAmt := a.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost
+ sdk.MaxInt(
+ sdk.NewInt(1),
+ sdk.NewDecFromInt(a.Bid.Amount).Mul(params.IncrementSurplus).RoundInt(),
+ ),
+ )
+ if bidderBalance.AmountOf(a.Bid.Denom).LT(minNewBidAmt) { // gov coin
+ return sdk.Coin{}, errorNotEnoughCoins
+ }
+ // Generate a new bid amount (gov coin)
+ amt, err := RandIntInclusive(r, minNewBidAmt, bidderBalance.AmountOf(a.Bid.Denom))
+ if err != nil {
+ panic(err)
+ }
+ return sdk.NewCoin(a.Bid.Denom, amt), nil // gov coin
+
+ case types.CollateralAuction:
+ // Check the bidder has enough (stable coin) to pay in
+ minNewBidAmt := a.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost
+ sdk.MaxInt(
+ sdk.NewInt(1),
+ sdk.NewDecFromInt(a.Bid.Amount).Mul(params.IncrementCollateral).RoundInt(),
+ ),
+ )
+ minNewBidAmt = sdk.MinInt(minNewBidAmt, a.MaxBid.Amount) // allow new bids to hit MaxBid even though it may be less than the increment %
+ if bidderBalance.AmountOf(a.Bid.Denom).LT(minNewBidAmt) {
+ return sdk.Coin{}, errorNotEnoughCoins
+ }
+ // Check auction can still receive new bids
+ if a.IsReversePhase() && a.Lot.Amount.Equal(sdk.ZeroInt()) {
+ return sdk.Coin{}, errorCantReceiveBids
+ }
+ // Generate a new bid amount (collateral coin in reverse phase)
+ if a.IsReversePhase() {
+ maxNewLotAmt := a.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost
+ sdk.MaxInt(
+ sdk.NewInt(1),
+ sdk.NewDecFromInt(a.Lot.Amount).Mul(params.IncrementCollateral).RoundInt(),
+ ),
+ )
+ amt, err := RandIntInclusive(r, sdk.ZeroInt(), maxNewLotAmt) // maxNewLotAmt shouldn't be < 0 given the check above
+ if err != nil {
+ panic(err)
+ }
+ return sdk.NewCoin(a.Lot.Denom, amt), nil // collateral coin
+
+ // Generate a new bid amount (stable coin in forward phase)
+ } else {
+ amt, err := RandIntInclusive(r, minNewBidAmt, sdk.MinInt(bidderBalance.AmountOf(a.Bid.Denom), a.MaxBid.Amount))
+ if err != nil {
+ panic(err)
+ }
+ // when the bidder has enough coins, pick the MaxBid amount more frequently to increase chance auctions phase get into reverse phase
+ if r.Intn(2) == 0 && bidderBalance.AmountOf(a.Bid.Denom).GTE(a.MaxBid.Amount) { // 50%
+ amt = a.MaxBid.Amount
+ }
+ return sdk.NewCoin(a.Bid.Denom, amt), nil // stable coin
+ }
+
+ default:
+ return sdk.Coin{}, fmt.Errorf("unknown auction type")
+ }
+}
+
+// findValidAccountAuctionPair finds an auction and account for which the callback func returns true
+func findValidAccountAuctionPair(accounts []simulation.Account, auctions types.Auctions, cb func(simulation.Account, types.Auction) bool) (simulation.Account, types.Auction, bool) {
+ for _, auc := range auctions {
+ for _, acc := range accounts {
+ if isValid := cb(acc, auc); isValid {
+ return acc, auc, true
+ }
+ }
+ }
+ return simulation.Account{}, nil, false
+}
+
+// RandIntInclusive randomly generates an sdk.Int in the range [inclusiveMin, inclusiveMax]. It works for negative and positive integers.
+func RandIntInclusive(r *rand.Rand, inclusiveMin, inclusiveMax sdk.Int) (sdk.Int, error) {
+ if inclusiveMin.GT(inclusiveMax) {
+ return sdk.Int{}, fmt.Errorf("min larger than max")
+ }
+ return RandInt(r, inclusiveMin, inclusiveMax.Add(sdk.OneInt()))
+}
+
+// RandInt randomly generates an sdk.Int in the range [inclusiveMin, exclusiveMax). It works for negative and positive integers.
+func RandInt(r *rand.Rand, inclusiveMin, exclusiveMax sdk.Int) (sdk.Int, error) {
+ // validate input
+ if inclusiveMin.GTE(exclusiveMax) {
+ return sdk.Int{}, fmt.Errorf("min larger or equal to max")
+ }
+ // shift the range to start at 0
+ shiftedRange := exclusiveMax.Sub(inclusiveMin) // should always be positive given the check above
+ // randomly pick from the shifted range
+ shiftedRandInt := sdk.NewIntFromBigInt(new(big.Int).Rand(r, shiftedRange.BigInt()))
+ // shift back to the original range
+ return shiftedRandInt.Add(inclusiveMin), nil
+}
diff --git a/x/auction/simulation/params.go b/x/auction/simulation/params.go
index 8c1f7aff..a241c17c 100644
--- a/x/auction/simulation/params.go
+++ b/x/auction/simulation/params.go
@@ -1,14 +1,46 @@
package simulation
import (
+ "fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/x/simulation"
+
+ "github.com/kava-labs/kava/x/auction/types"
)
// ParamChanges defines the parameters that can be modified by param change proposals
// on the simulation
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
- // TODO implement this
- return []simulation.ParamChange{}
+ // Note: params are encoded to JSON before being stored in the param store. These param changes
+ // update the raw values in the store so values need to be JSON. This is why values that are represented
+ // as strings in JSON (such as time.Duration) have the escaped quotes.
+ // TODO should we encode the values properly with ModuleCdc.MustMarshalJSON()?
+ return []simulation.ParamChange{
+ simulation.NewSimParamChange(types.ModuleName, string(types.KeyBidDuration),
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("%d", GenBidDuration(r))
+ },
+ ),
+ simulation.NewSimParamChange(types.ModuleName, string(types.KeyMaxAuctionDuration),
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("%d", GenMaxAuctionDuration(r))
+ },
+ ),
+ simulation.NewSimParamChange(types.ModuleName, string(types.KeyIncrementCollateral),
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("%d", GenIncrementCollateral(r))
+ },
+ ),
+ simulation.NewSimParamChange(types.ModuleName, string(types.KeyIncrementDebt),
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("%d", GenIncrementDebt(r))
+ },
+ ),
+ simulation.NewSimParamChange(types.ModuleName, string(types.KeyIncrementSurplus),
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("%d", GenIncrementSurplus(r))
+ },
+ ),
+ }
}
diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go
index 6a84f4bb..2e06b86e 100644
--- a/x/auction/types/auctions.go
+++ b/x/auction/types/auctions.go
@@ -69,6 +69,12 @@ func (a BaseAuction) Validate() error {
if a.EndTime.After(a.MaxEndTime) {
return fmt.Errorf("MaxEndTime < EndTime (%s < %s)", a.MaxEndTime, a.EndTime)
}
+ if !a.Lot.IsValid() {
+ return fmt.Errorf("invalid lot: %s", a.Lot)
+ }
+ if !a.Bid.IsValid() {
+ return fmt.Errorf("invalid bid: %s", a.Bid)
+ }
return nil
}
@@ -148,6 +154,13 @@ func (a DebtAuction) GetModuleAccountCoins() sdk.Coins {
// GetPhase returns the direction of a debt auction, which never changes.
func (a DebtAuction) GetPhase() string { return "reverse" }
+func (a DebtAuction) Validate() error {
+ if !a.CorrespondingDebt.IsValid() {
+ return fmt.Errorf("invalid corresponding debt: %s", a.CorrespondingDebt)
+ }
+ return a.BaseAuction.Validate()
+}
+
// NewDebtAuction returns a new debt auction.
func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, endTime time.Time, debt sdk.Coin) DebtAuction {
// Note: Bidder is set to the initiator's module account address instead of module name. (when the first bid is placed, it is paid out to the initiator)
@@ -191,7 +204,7 @@ func (a CollateralAuction) GetType() string { return "collateral" }
// It is used in genesis initialize the module account correctly.
func (a CollateralAuction) GetModuleAccountCoins() sdk.Coins {
// a.Bid is paid out on bids, so is never stored in the module account
- return sdk.NewCoins(a.Lot).Add(sdk.NewCoins(a.CorrespondingDebt))
+ return sdk.NewCoins(a.Lot).Add(sdk.NewCoins(a.CorrespondingDebt)...)
}
// IsReversePhase returns whether the auction has switched over to reverse phase or not.
@@ -208,6 +221,19 @@ func (a CollateralAuction) GetPhase() string {
return "forward"
}
+func (a CollateralAuction) Validate() error {
+ if !a.CorrespondingDebt.IsValid() {
+ return fmt.Errorf("invalid corresponding debt: %s", a.CorrespondingDebt)
+ }
+ if !a.MaxBid.IsValid() {
+ return fmt.Errorf("invalid max bid: %s", a.MaxBid)
+ }
+ if err := a.LotReturns.Validate(); err != nil {
+ return fmt.Errorf("invalid lot returns: %w", err)
+ }
+ return a.BaseAuction.Validate()
+}
+
func (a CollateralAuction) String() string {
return fmt.Sprintf(`Auction %d:
Initiator: %s
@@ -250,17 +276,26 @@ type WeightedAddresses struct {
}
// NewWeightedAddresses returns a new list addresses with weights.
-func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, sdk.Error) {
- if len(addrs) != len(weights) {
- return WeightedAddresses{}, sdk.ErrInternal("number of addresses doesn't match number of weights")
- }
- for _, w := range weights {
- if w.IsNegative() {
- return WeightedAddresses{}, sdk.ErrInternal("weights contain a negative amount")
- }
- }
- return WeightedAddresses{
+func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, error) {
+ wa := WeightedAddresses{
Addresses: addrs,
Weights: weights,
- }, nil
+ }
+ if err := wa.Validate(); err != nil {
+ return WeightedAddresses{}, err
+ }
+ return wa, nil
+}
+
+// Validate checks for that the weights are not negative and that lengths match.
+func (wa WeightedAddresses) Validate() error {
+ if len(wa.Addresses) != len(wa.Weights) {
+ return fmt.Errorf("number of addresses doesn't match number of weights, %d ≠ %d", len(wa.Addresses), len(wa.Weights))
+ }
+ for _, w := range wa.Weights {
+ if w.IsNegative() {
+ return fmt.Errorf("weights contain a negative amount: %s", w)
+ }
+ }
+ return nil
}
diff --git a/x/auction/types/errors.go b/x/auction/types/errors.go
index 85e6b718..6aa78673 100644
--- a/x/auction/types/errors.go
+++ b/x/auction/types/errors.go
@@ -1,98 +1,36 @@
-// DONTCOVER
package types
-import (
- "fmt"
- "time"
+import sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
- sdk "github.com/cosmos/cosmos-sdk/types"
+// DONTCOVER
+
+var (
+ // ErrInvalidInitialAuctionID error for when the initial auction ID hasn't been set
+ ErrInvalidInitialAuctionID = sdkerrors.Register(ModuleName, 2, "initial auction ID hasn't been set")
+ // ErrInvalidModulePermissions error for when module doesn't have valid permissions
+ ErrInvalidModulePermissions = sdkerrors.Register(ModuleName, 3, "module does not have required permission")
+ // ErrUnrecognizedAuctionType error for unrecognized auction type
+ ErrUnrecognizedAuctionType = sdkerrors.Register(ModuleName, 4, "unrecognized auction type")
+ // ErrAuctionNotFound error for when an auction is not found
+ ErrAuctionNotFound = sdkerrors.Register(ModuleName, 5, "auction not found")
+ // ErrAuctionHasNotExpired error for attempting to close an auction that has not passed its end time
+ ErrAuctionHasNotExpired = sdkerrors.Register(ModuleName, 6, "auction can't be closed as curent block time has not passed auction end time")
+ // ErrAuctionHasExpired error for when an auction is closed and unavailable for bidding
+ ErrAuctionHasExpired = sdkerrors.Register(ModuleName, 7, "auction has closed")
+ // ErrInvalidBidDenom error for when bid denom doesn't match auction bid denom
+ ErrInvalidBidDenom = sdkerrors.Register(ModuleName, 8, "bid denom doesn't match auction bid denom")
+ // ErrInvalidLotDenom error for when lot denom doesn't match auction lot denom
+ ErrInvalidLotDenom = sdkerrors.Register(ModuleName, 9, "lot denom doesn't match auction lot denom")
+ // ErrBidTooSmall error for when bid is not greater than auction's min bid amount
+ ErrBidTooSmall = sdkerrors.Register(ModuleName, 10, "bid is not greater than auction's min new bid amount")
+ // ErrBidTooLarge error for when bid is larger than auction's maximum allowed bid
+ ErrBidTooLarge = sdkerrors.Register(ModuleName, 11, "bid is greater than auction's max bid")
+ // ErrLotTooSmall error for when lot is less than zero
+ ErrLotTooSmall = sdkerrors.Register(ModuleName, 12, "lot is not greater than auction's min new lot amount")
+ // ErrLotTooLarge error for when lot is not smaller than auction's max new lot amount
+ ErrLotTooLarge = sdkerrors.Register(ModuleName, 13, "lot is greater than auction's max new lot amount")
+ // ErrCollateralAuctionIsInReversePhase error for when attempting to place a forward bid on a collateral auction in reverse phase
+ ErrCollateralAuctionIsInReversePhase = sdkerrors.Register(ModuleName, 14, "invalid bid: auction is in reverse phase")
+ // ErrCollateralAuctionIsInForwardPhase error for when attempting to place a reverse bid on a collateral auction in forward phase
+ ErrCollateralAuctionIsInForwardPhase = sdkerrors.Register(ModuleName, 15, "invalid bid: auction is in forward phase")
)
-
-// Error codes specific to auction module
-const (
- DefaultCodespace sdk.CodespaceType = ModuleName
- CodeInvalidInitialAuctionID sdk.CodeType = 1
- CodeInvalidModulePermissions sdk.CodeType = 2
- CodeUnrecognizedAuctionType sdk.CodeType = 3
- CodeAuctionNotFound sdk.CodeType = 4
- CodeAuctionHasNotExpired sdk.CodeType = 5
- CodeAuctionHasExpired sdk.CodeType = 6
- CodeInvalidBidDenom sdk.CodeType = 7
- CodeInvalidLotDenom sdk.CodeType = 8
- CodeBidTooSmall sdk.CodeType = 9
- CodeBidTooLarge sdk.CodeType = 10
- CodeLotTooSmall sdk.CodeType = 11
- CodeLotTooLarge sdk.CodeType = 12
- CodeCollateralAuctionIsInReversePhase sdk.CodeType = 13
- CodeCollateralAuctionIsInForwardPhase sdk.CodeType = 14
-)
-
-// ErrInvalidInitialAuctionID error for when the initial auction ID hasn't been set
-func ErrInvalidInitialAuctionID(codespace sdk.CodespaceType) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidInitialAuctionID, fmt.Sprintf("initial auction ID hasn't been set"))
-}
-
-// ErrInvalidModulePermissions error for when module doesn't have valid permissions
-func ErrInvalidModulePermissions(codespace sdk.CodespaceType, permission string) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidModulePermissions, fmt.Sprintf("module does not have required permission '%s'", permission))
-}
-
-// ErrUnrecognizedAuctionType error for unrecognized auction type
-func ErrUnrecognizedAuctionType(codespace sdk.CodespaceType) sdk.Error {
- return sdk.NewError(codespace, CodeUnrecognizedAuctionType, fmt.Sprintf("unrecognized auction type"))
-}
-
-// ErrAuctionNotFound error for when an auction is not found
-func ErrAuctionNotFound(codespace sdk.CodespaceType, id uint64) sdk.Error {
- return sdk.NewError(codespace, CodeAuctionNotFound, fmt.Sprintf("auction %d was not found", id))
-}
-
-// ErrAuctionHasNotExpired error for attempting to close an auction that has not passed its end time
-func ErrAuctionHasNotExpired(codespace sdk.CodespaceType, blockTime time.Time, endTime time.Time) sdk.Error {
- return sdk.NewError(codespace, CodeAuctionHasNotExpired, fmt.Sprintf("auction can't be closed as curent block time (%v) has not passed auction end time (%v)", blockTime, endTime))
-}
-
-// ErrAuctionHasExpired error for when an auction is closed and unavailable for bidding
-func ErrAuctionHasExpired(codespace sdk.CodespaceType, id uint64) sdk.Error {
- return sdk.NewError(codespace, CodeAuctionHasExpired, fmt.Sprintf("auction %d has closed", id))
-}
-
-// ErrInvalidBidDenom error for when bid denom doesn't match auction bid denom
-func ErrInvalidBidDenom(codespace sdk.CodespaceType, bidDenom string, auctionBidDenom string) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidBidDenom, fmt.Sprintf("bid denom %s doesn't match auction bid denom %s", bidDenom, auctionBidDenom))
-}
-
-// ErrInvalidLotDenom error for when lot denom doesn't match auction lot denom
-func ErrInvalidLotDenom(codespace sdk.CodespaceType, lotDenom string, auctionLotDenom string) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidLotDenom, fmt.Sprintf("lot denom %s doesn't match auction lot denom %s", lotDenom, auctionLotDenom))
-}
-
-// ErrBidTooSmall error for when bid is not greater than auction's min bid amount
-func ErrBidTooSmall(codespace sdk.CodespaceType, bid sdk.Coin, minBid sdk.Coin) sdk.Error {
- return sdk.NewError(codespace, CodeBidTooSmall, fmt.Sprintf("bid %s is not greater than auction's min new bid amount %s", bid.String(), minBid.String()))
-}
-
-// ErrBidTooLarge error for when bid is larger than auction's maximum allowed bid
-func ErrBidTooLarge(codespace sdk.CodespaceType, bid sdk.Coin, maxBid sdk.Coin) sdk.Error {
- return sdk.NewError(codespace, CodeBidTooLarge, fmt.Sprintf("bid %s is greater than auction's max bid %s", bid.String(), maxBid.String()))
-}
-
-// ErrLotToosmall error for when lot is less than zero
-func ErrLotTooSmall(codespace sdk.CodespaceType, lot sdk.Coin, minLot sdk.Coin) sdk.Error {
- return sdk.NewError(codespace, CodeLotTooSmall, fmt.Sprintf("lot %s is not greater than auction's min new lot amount %s", lot.String(), minLot.String()))
-}
-
-// ErrLotTooLarge error for when lot is not smaller than auction's max new lot amount
-func ErrLotTooLarge(codespace sdk.CodespaceType, lot sdk.Coin, maxLot sdk.Coin) sdk.Error {
- return sdk.NewError(codespace, CodeLotTooLarge, fmt.Sprintf("lot %s is greater than auction's max new lot amount %s", lot.String(), maxLot.String()))
-}
-
-// ErrCollateralAuctionIsInReversePhase error for when attempting to place a forward bid on a collateral auction in reverse phase
-func ErrCollateralAuctionIsInReversePhase(codespace sdk.CodespaceType, id uint64) sdk.Error {
- return sdk.NewError(codespace, CodeCollateralAuctionIsInReversePhase, fmt.Sprintf("invalid bid - auction %d is in reverse phase", id))
-}
-
-// ErrCollateralAuctionIsInForwardPhase error for when attempting to place a reverse bid on a collateral auction in forward phase
-func ErrCollateralAuctionIsInForwardPhase(codespace sdk.CodespaceType, id uint64) sdk.Error {
- return sdk.NewError(codespace, CodeCollateralAuctionIsInForwardPhase, fmt.Sprintf("invalid bid - auction %d is in forward phase", id))
-}
diff --git a/x/auction/types/expected_keepers.go b/x/auction/types/expected_keepers.go
index 83141c9e..bb58ef9b 100644
--- a/x/auction/types/expected_keepers.go
+++ b/x/auction/types/expected_keepers.go
@@ -10,10 +10,10 @@ type SupplyKeeper interface {
GetModuleAddress(name string) sdk.AccAddress
GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI
- SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) sdk.Error
- SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error
- SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error
+ SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) error
+ SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
+ SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
- BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
- MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
+ BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error
+ MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error
}
diff --git a/x/auction/types/msg.go b/x/auction/types/msg.go
index 435ad08f..4614f512 100644
--- a/x/auction/types/msg.go
+++ b/x/auction/types/msg.go
@@ -2,7 +2,9 @@ package types
import (
"fmt"
+
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// ensure Msg interface compliance at compile time
@@ -31,12 +33,12 @@ func (msg MsgPlaceBid) Route() string { return RouterKey }
func (msg MsgPlaceBid) Type() string { return "place_bid" }
// ValidateBasic does a simple validation check that doesn't require access to state.
-func (msg MsgPlaceBid) ValidateBasic() sdk.Error {
+func (msg MsgPlaceBid) ValidateBasic() error {
if msg.Bidder.Empty() {
- return sdk.ErrInvalidAddress("invalid (empty) bidder address")
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "bidder address cannot be empty")
}
if !msg.Amount.IsValid() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("invalid bid amount: %s", msg.Amount))
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "bid amount %s", msg.Amount)
}
return nil
}
@@ -51,3 +53,12 @@ func (msg MsgPlaceBid) GetSignBytes() []byte {
func (msg MsgPlaceBid) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Bidder}
}
+
+func (msg MsgPlaceBid) String() string {
+ // String implements the Stringer interface
+ return fmt.Sprintf(`Place Bid Message:
+ Auction ID: %d
+ Bidder: %s
+ Amount: %s
+`, msg.AuctionID, msg.Bidder, msg.Amount)
+}
diff --git a/x/auction/types/params.go b/x/auction/types/params.go
index 74f2037a..89b82aa5 100644
--- a/x/auction/types/params.go
+++ b/x/auction/types/params.go
@@ -2,13 +2,17 @@ package types
import (
"bytes"
+ "errors"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/params"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
)
+var emptyDec = sdk.Dec{}
+
// Defaults for auction params
const (
// DefaultMaxAuctionDuration max length of auction
@@ -69,11 +73,11 @@ func ParamKeyTable() subspace.KeyTable {
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs.
func (p *Params) ParamSetPairs() subspace.ParamSetPairs {
return subspace.ParamSetPairs{
- {Key: KeyBidDuration, Value: &p.BidDuration},
- {Key: KeyMaxAuctionDuration, Value: &p.MaxAuctionDuration},
- {Key: KeyIncrementSurplus, Value: &p.IncrementSurplus},
- {Key: KeyIncrementDebt, Value: &p.IncrementDebt},
- {Key: KeyIncrementCollateral, Value: &p.IncrementCollateral},
+ params.NewParamSetPair(KeyBidDuration, &p.BidDuration, validateBidDurationParam),
+ params.NewParamSetPair(KeyMaxAuctionDuration, &p.MaxAuctionDuration, validateMaxAuctionDurationParam),
+ params.NewParamSetPair(KeyIncrementSurplus, &p.IncrementSurplus, validateIncrementSurplusParam),
+ params.NewParamSetPair(KeyIncrementDebt, &p.IncrementDebt, validateIncrementDebtParam),
+ params.NewParamSetPair(KeyIncrementCollateral, &p.IncrementCollateral, validateIncrementCollateralParam),
}
}
@@ -97,26 +101,102 @@ func (p Params) String() string {
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
- if p.BidDuration < 0 {
- return sdk.ErrInternal("bid duration cannot be negative")
+ if err := validateBidDurationParam(p.BidDuration); err != nil {
+ return err
}
- if p.MaxAuctionDuration < 0 {
- return sdk.ErrInternal("max auction duration cannot be negative")
+
+ if err := validateMaxAuctionDurationParam(p.MaxAuctionDuration); err != nil {
+ return err
}
+
if p.BidDuration > p.MaxAuctionDuration {
- return sdk.ErrInternal("bid duration param cannot be larger than max auction duration")
+ return errors.New("bid duration param cannot be larger than max auction duration")
}
- if p.IncrementSurplus == (sdk.Dec{}) || p.IncrementDebt == (sdk.Dec{}) || p.IncrementCollateral == (sdk.Dec{}) {
- return sdk.ErrInternal("auction increment values cannot be nil")
+
+ if err := validateIncrementSurplusParam(p.IncrementSurplus); err != nil {
+ return err
}
- if p.IncrementSurplus.IsNegative() {
- return sdk.ErrInternal("surplus auction increment cannot be less than zero")
+
+ if err := validateIncrementDebtParam(p.IncrementDebt); err != nil {
+ return err
}
- if p.IncrementDebt.IsNegative() {
- return sdk.ErrInternal("debt auction increment cannot be less than zero")
+
+ return validateIncrementCollateralParam(p.IncrementCollateral)
+}
+
+func validateBidDurationParam(i interface{}) error {
+ bidDuration, ok := i.(time.Duration)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
}
- if p.IncrementCollateral.IsNegative() {
- return sdk.ErrInternal("collateral auction increment cannot be less than zero")
+
+ if bidDuration < 0 {
+ return fmt.Errorf("bid duration cannot be negative %d", bidDuration)
}
+
+ return nil
+}
+
+func validateMaxAuctionDurationParam(i interface{}) error {
+ maxAuctionDuration, ok := i.(time.Duration)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if maxAuctionDuration < 0 {
+ return fmt.Errorf("max auction duration cannot be negative %d", maxAuctionDuration)
+ }
+
+ return nil
+}
+
+func validateIncrementSurplusParam(i interface{}) error {
+ incrementSurplus, ok := i.(sdk.Dec)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if incrementSurplus == emptyDec || incrementSurplus.IsNil() {
+ return errors.New("surplus auction increment cannot be nil or empty")
+ }
+
+ if incrementSurplus.IsNegative() {
+ return fmt.Errorf("surplus auction increment cannot be less than zero %s", incrementSurplus)
+ }
+
+ return nil
+}
+
+func validateIncrementDebtParam(i interface{}) error {
+ incrementDebt, ok := i.(sdk.Dec)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if incrementDebt == emptyDec || incrementDebt.IsNil() {
+ return errors.New("debt auction increment cannot be nil or empty")
+ }
+
+ if incrementDebt.IsNegative() {
+ return fmt.Errorf("debt auction increment cannot be less than zero %s", incrementDebt)
+ }
+
+ return nil
+}
+
+func validateIncrementCollateralParam(i interface{}) error {
+ incrementCollateral, ok := i.(sdk.Dec)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if incrementCollateral == emptyDec || incrementCollateral.IsNil() {
+ return errors.New("collateral auction increment cannot be nil or empty")
+ }
+
+ if incrementCollateral.IsNegative() {
+ return fmt.Errorf("collateral auction increment cannot be less than zero %s", incrementCollateral)
+ }
+
return nil
}
diff --git a/x/bep3/abci.go b/x/bep3/abci.go
new file mode 100644
index 00000000..b5c2d424
--- /dev/null
+++ b/x/bep3/abci.go
@@ -0,0 +1,18 @@
+package bep3
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+// BeginBlocker runs at the start of every block
+func BeginBlocker(ctx sdk.Context, k Keeper) {
+ err := k.UpdateExpiredAtomicSwaps(ctx)
+ if err != nil {
+ panic(err)
+ }
+
+ err = k.DeleteClosedAtomicSwapsFromLongtermStorage(ctx)
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/x/bep3/abci_test.go b/x/bep3/abci_test.go
new file mode 100644
index 00000000..67da0c39
--- /dev/null
+++ b/x/bep3/abci_test.go
@@ -0,0 +1,244 @@
+package bep3_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+ tmtime "github.com/tendermint/tendermint/types/time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3"
+)
+
+type ABCITestSuite struct {
+ suite.Suite
+ keeper bep3.Keeper
+ app app.TestApp
+ ctx sdk.Context
+ addrs []sdk.AccAddress
+ swapIDs []tmbytes.HexBytes
+ randomNumbers []tmbytes.HexBytes
+}
+
+func (suite *ABCITestSuite) SetupTest() {
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+
+ // Set up auth GenesisState
+ _, addrs := app.GeneratePrivKeyAddressPairs(11)
+ coins := []sdk.Coins{}
+ for j := 0; j < 11; j++ {
+ coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000)))
+ }
+ authGS := app.NewAuthGenState(addrs, coins)
+ // Initialize test app
+ tApp.InitializeFromGenesisStates(authGS, NewBep3GenStateMulti(addrs[0]))
+
+ suite.ctx = ctx
+ suite.app = tApp
+ suite.addrs = addrs
+ suite.ResetKeeper()
+}
+
+func (suite *ABCITestSuite) ResetKeeper() {
+ suite.keeper = suite.app.GetBep3Keeper()
+
+ var swapIDs []tmbytes.HexBytes
+ var randomNumbers []tmbytes.HexBytes
+ for i := 0; i < 10; i++ {
+ // Set up atomic swap variables
+ expireHeight := int64(360)
+ amount := cs(c("bnb", int64(100)))
+ timestamp := ts(i)
+ randomNumber, _ := bep3.GenerateSecureRandomNumber()
+ randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ // Create atomic swap and check err to confirm creation
+ err := suite.keeper.CreateAtomicSwap(suite.ctx, randomNumberHash, timestamp, expireHeight,
+ suite.addrs[0], suite.addrs[i], TestSenderOtherChain, TestRecipientOtherChain,
+ amount, amount.String(), true)
+ suite.Nil(err)
+
+ // Store swap's calculated ID and secret random number
+ swapID := bep3.CalculateSwapID(randomNumberHash, suite.addrs[0], TestSenderOtherChain)
+ swapIDs = append(swapIDs, swapID)
+ randomNumbers = append(randomNumbers, randomNumber.Bytes())
+ }
+ suite.swapIDs = swapIDs
+ suite.randomNumbers = randomNumbers
+}
+
+func (suite *ABCITestSuite) TestBeginBlocker_UpdateExpiredAtomicSwaps() {
+ testCases := []struct {
+ name string
+ firstCtx sdk.Context
+ secondCtx sdk.Context
+ expectedStatus bep3.SwapStatus
+ expectInStorage bool
+ }{
+ {
+ name: "normal",
+ firstCtx: suite.ctx,
+ secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 10),
+ expectedStatus: bep3.Open,
+ expectInStorage: true,
+ },
+ {
+ name: "after expiration",
+ firstCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400),
+ secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 410),
+ expectedStatus: bep3.Expired,
+ expectInStorage: true,
+ },
+ {
+ name: "after completion",
+ firstCtx: suite.ctx,
+ secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 10),
+ expectedStatus: bep3.Completed,
+ expectInStorage: true,
+ },
+ {
+ name: "after deletion",
+ firstCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400),
+ secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400 + bep3.DefaultLongtermStorageDuration),
+ expectedStatus: bep3.NULL,
+ expectInStorage: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ // Reset keeper and run the initial begin blocker
+ suite.ResetKeeper()
+ suite.Run(tc.name, func() {
+ bep3.BeginBlocker(tc.firstCtx, suite.keeper)
+
+ switch tc.expectedStatus {
+ case bep3.Completed:
+ for i, swapID := range suite.swapIDs {
+ err := suite.keeper.ClaimAtomicSwap(tc.firstCtx, suite.addrs[5], swapID, suite.randomNumbers[i])
+ suite.Nil(err)
+ }
+ case bep3.NULL:
+ for _, swapID := range suite.swapIDs {
+ err := suite.keeper.RefundAtomicSwap(tc.firstCtx, suite.addrs[5], swapID)
+ suite.Nil(err)
+ }
+ }
+
+ // Run the second begin blocker
+ bep3.BeginBlocker(tc.secondCtx, suite.keeper)
+
+ // Check each swap's availibility and status
+ for _, swapID := range suite.swapIDs {
+ storedSwap, found := suite.keeper.GetAtomicSwap(tc.secondCtx, swapID)
+ if tc.expectInStorage {
+ suite.True(found)
+ } else {
+ suite.False(found)
+ }
+ suite.Equal(tc.expectedStatus, storedSwap.Status)
+ }
+ })
+ }
+}
+
+func (suite *ABCITestSuite) TestBeginBlocker_DeleteClosedAtomicSwapsFromLongtermStorage() {
+ type Action int
+ const (
+ NULL Action = 0x00
+ Refund Action = 0x01
+ Claim Action = 0x02
+ )
+
+ testCases := []struct {
+ name string
+ firstCtx sdk.Context
+ action Action
+ secondCtx sdk.Context
+ expectInStorage bool
+ }{
+ {
+ name: "no action with long storage duration",
+ firstCtx: suite.ctx,
+ action: NULL,
+ secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + bep3.DefaultLongtermStorageDuration),
+ expectInStorage: true,
+ },
+ {
+ name: "claim with short storage duration",
+ firstCtx: suite.ctx,
+ action: Claim,
+ secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 5000),
+ expectInStorage: true,
+ },
+ {
+ name: "claim with long storage duration",
+ firstCtx: suite.ctx,
+ action: Claim,
+ secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + bep3.DefaultLongtermStorageDuration),
+ expectInStorage: false,
+ },
+ {
+ name: "refund with short storage duration",
+ firstCtx: suite.ctx,
+ action: Refund,
+ secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 5000),
+ expectInStorage: true,
+ },
+ {
+ name: "refund with long storage duration",
+ firstCtx: suite.ctx,
+ action: Refund,
+ secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + bep3.DefaultLongtermStorageDuration),
+ expectInStorage: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ // Reset keeper and run the initial begin blocker
+ suite.ResetKeeper()
+ suite.Run(tc.name, func() {
+ bep3.BeginBlocker(tc.firstCtx, suite.keeper)
+
+ switch tc.action {
+ case Claim:
+ for i, swapID := range suite.swapIDs {
+ err := suite.keeper.ClaimAtomicSwap(tc.firstCtx, suite.addrs[5], swapID, suite.randomNumbers[i])
+ suite.Nil(err)
+ }
+ case Refund:
+ for _, swapID := range suite.swapIDs {
+ swap, _ := suite.keeper.GetAtomicSwap(tc.firstCtx, swapID)
+ refundCtx := suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + swap.ExpireHeight)
+ bep3.BeginBlocker(refundCtx, suite.keeper)
+ err := suite.keeper.RefundAtomicSwap(refundCtx, suite.addrs[5], swapID)
+ suite.Nil(err)
+ // Add expire height to second ctx block height
+ tc.secondCtx = tc.secondCtx.WithBlockHeight(tc.secondCtx.BlockHeight() + swap.ExpireHeight)
+ }
+ }
+
+ // Run the second begin blocker
+ bep3.BeginBlocker(tc.secondCtx, suite.keeper)
+
+ // Check each swap's availibility and status
+ for _, swapID := range suite.swapIDs {
+ _, found := suite.keeper.GetAtomicSwap(tc.secondCtx, swapID)
+ if tc.expectInStorage {
+ suite.True(found)
+ } else {
+ suite.False(found)
+ }
+ }
+ })
+ }
+}
+
+func TestABCITestSuite(t *testing.T) {
+ suite.Run(t, new(ABCITestSuite))
+}
diff --git a/x/bep3/alias.go b/x/bep3/alias.go
new file mode 100644
index 00000000..1a315ad9
--- /dev/null
+++ b/x/bep3/alias.go
@@ -0,0 +1,141 @@
+package bep3
+
+import (
+ "github.com/kava-labs/kava/x/bep3/client/rest"
+ "github.com/kava-labs/kava/x/bep3/keeper"
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+const (
+ AddrByteCount = types.AddrByteCount
+ AttributeKeyAmount = types.AttributeKeyAmount
+ AttributeKeyAtomicSwapID = types.AttributeKeyAtomicSwapID
+ AttributeKeyAtomicSwapIDs = types.AttributeKeyAtomicSwapIDs
+ AttributeKeyClaimSender = types.AttributeKeyClaimSender
+ AttributeKeyDirection = types.AttributeKeyDirection
+ AttributeKeyExpectedIncome = types.AttributeKeyExpectedIncome
+ AttributeKeyExpireHeight = types.AttributeKeyExpireHeight
+ AttributeKeyRandomNumber = types.AttributeKeyRandomNumber
+ AttributeKeyRandomNumberHash = types.AttributeKeyRandomNumberHash
+ AttributeKeyRecipient = types.AttributeKeyRecipient
+ AttributeKeyRefundSender = types.AttributeKeyRefundSender
+ AttributeKeySender = types.AttributeKeySender
+ AttributeKeySenderOtherChain = types.AttributeKeySenderOtherChain
+ AttributeKeyTimestamp = types.AttributeKeyTimestamp
+ AttributeValueCategory = types.AttributeValueCategory
+ CalcSwapID = types.CalcSwapID
+ ClaimAtomicSwap = types.ClaimAtomicSwap
+ Completed = types.Completed
+ CreateAtomicSwap = types.CreateAtomicSwap
+ DefaultLongtermStorageDuration = types.DefaultLongtermStorageDuration
+ DefaultParamspace = types.DefaultParamspace
+ DepositAtomicSwap = types.DepositAtomicSwap
+ EventTypeClaimAtomicSwap = types.EventTypeClaimAtomicSwap
+ EventTypeCreateAtomicSwap = types.EventTypeCreateAtomicSwap
+ EventTypeRefundAtomicSwap = types.EventTypeRefundAtomicSwap
+ EventTypeSwapsExpired = types.EventTypeSwapsExpired
+ Expired = types.Expired
+ INVALID = types.INVALID
+ Incoming = types.Incoming
+ Int64Size = types.Int64Size
+ MaxExpectedIncomeLength = types.MaxExpectedIncomeLength
+ MaxOtherChainAddrLength = types.MaxOtherChainAddrLength
+ ModuleName = types.ModuleName
+ NULL = types.NULL
+ Open = types.Open
+ Outgoing = types.Outgoing
+ QuerierRoute = types.QuerierRoute
+ QueryGetAssetSupply = types.QueryGetAssetSupply
+ QueryGetAtomicSwap = types.QueryGetAtomicSwap
+ QueryGetAtomicSwaps = types.QueryGetAtomicSwaps
+ QueryGetParams = types.QueryGetParams
+ RandomNumberHashLength = types.RandomNumberHashLength
+ RandomNumberLength = types.RandomNumberLength
+ RefundAtomicSwap = types.RefundAtomicSwap
+ RouterKey = types.RouterKey
+ StoreKey = types.StoreKey
+ SwapIDLength = types.SwapIDLength
+)
+
+var (
+ NewKeeper = keeper.NewKeeper
+ NewQuerier = keeper.NewQuerier
+ RegisterRoutes = rest.RegisterRoutes
+ BytesToHex = types.BytesToHex
+ CalculateRandomHash = types.CalculateRandomHash
+ CalculateSwapID = types.CalculateSwapID
+ DefaultGenesisState = types.DefaultGenesisState
+ DefaultParams = types.DefaultParams
+ ErrAssetNotActive = types.ErrAssetNotActive
+ ErrAssetNotSupported = types.ErrAssetNotSupported
+ ErrAssetSupplyNotFound = types.ErrAssetSupplyNotFound
+ ErrAtomicSwapAlreadyExists = types.ErrAtomicSwapAlreadyExists
+ ErrAtomicSwapNotFound = types.ErrAtomicSwapNotFound
+ ErrExceedsAvailableSupply = types.ErrExceedsAvailableSupply
+ ErrExceedsSupplyLimit = types.ErrExceedsSupplyLimit
+ ErrInvalidClaimSecret = types.ErrInvalidClaimSecret
+ ErrInvalidCurrentSupply = types.ErrInvalidCurrentSupply
+ ErrInvalidHeightSpan = types.ErrInvalidHeightSpan
+ ErrInvalidIncomingSupply = types.ErrInvalidIncomingSupply
+ ErrInvalidOutgoingSupply = types.ErrInvalidOutgoingSupply
+ ErrInvalidTimestamp = types.ErrInvalidTimestamp
+ ErrSwapNotClaimable = types.ErrSwapNotClaimable
+ ErrSwapNotRefundable = types.ErrSwapNotRefundable
+ GenerateSecureRandomNumber = types.GenerateSecureRandomNumber
+ GetAtomicSwapByHeightKey = types.GetAtomicSwapByHeightKey
+ HexToBytes = types.HexToBytes
+ NewAssetSupply = types.NewAssetSupply
+ NewAtomicSwap = types.NewAtomicSwap
+ NewGenesisState = types.NewGenesisState
+ NewMsgClaimAtomicSwap = types.NewMsgClaimAtomicSwap
+ NewMsgCreateAtomicSwap = types.NewMsgCreateAtomicSwap
+ NewMsgRefundAtomicSwap = types.NewMsgRefundAtomicSwap
+ NewParams = types.NewParams
+ NewQueryAssetSupply = types.NewQueryAssetSupply
+ NewQueryAtomicSwapByID = types.NewQueryAtomicSwapByID
+ NewQueryAtomicSwaps = types.NewQueryAtomicSwaps
+ NewSwapDirectionFromString = types.NewSwapDirectionFromString
+ NewSwapStatusFromString = types.NewSwapStatusFromString
+ ParamKeyTable = types.ParamKeyTable
+ RegisterCodec = types.RegisterCodec
+ Uint64FromBytes = types.Uint64FromBytes
+ Uint64ToBytes = types.Uint64ToBytes
+
+ // variable aliases
+ AbsoluteMaximumBlockLock = types.AbsoluteMaximumBlockLock
+ AbsoluteMinimumBlockLock = types.AbsoluteMinimumBlockLock
+ AssetSupplyKeyPrefix = types.AssetSupplyKeyPrefix
+ AtomicSwapByBlockPrefix = types.AtomicSwapByBlockPrefix
+ AtomicSwapCoinsAccAddr = types.AtomicSwapCoinsAccAddr
+ AtomicSwapKeyPrefix = types.AtomicSwapKeyPrefix
+ AtomicSwapLongtermStoragePrefix = types.AtomicSwapLongtermStoragePrefix
+ DefaultMaxBlockLock = types.DefaultMaxBlockLock
+ DefaultMinBlockLock = types.DefaultMinBlockLock
+ DefaultSupportedAssets = types.DefaultSupportedAssets
+ KeyBnbDeputyAddress = types.KeyBnbDeputyAddress
+ KeyMaxBlockLock = types.KeyMaxBlockLock
+ KeyMinBlockLock = types.KeyMinBlockLock
+ KeySupportedAssets = types.KeySupportedAssets
+ ModuleCdc = types.ModuleCdc
+)
+
+type (
+ Keeper = keeper.Keeper
+ AssetParam = types.AssetParam
+ AssetParams = types.AssetParams
+ AssetSupplies = types.AssetSupplies
+ AssetSupply = types.AssetSupply
+ AtomicSwap = types.AtomicSwap
+ AtomicSwaps = types.AtomicSwaps
+ GenesisState = types.GenesisState
+ MsgClaimAtomicSwap = types.MsgClaimAtomicSwap
+ MsgCreateAtomicSwap = types.MsgCreateAtomicSwap
+ MsgRefundAtomicSwap = types.MsgRefundAtomicSwap
+ Params = types.Params
+ QueryAssetSupply = types.QueryAssetSupply
+ QueryAtomicSwapByID = types.QueryAtomicSwapByID
+ QueryAtomicSwaps = types.QueryAtomicSwaps
+ SupplyKeeper = types.SupplyKeeper
+ SwapDirection = types.SwapDirection
+ SwapStatus = types.SwapStatus
+)
diff --git a/x/bep3/client/cli/query.go b/x/bep3/client/cli/query.go
new file mode 100644
index 00000000..a60e52af
--- /dev/null
+++ b/x/bep3/client/cli/query.go
@@ -0,0 +1,225 @@
+package cli
+
+import (
+ "encoding/hex"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/spf13/cobra"
+ tmtime "github.com/tendermint/tendermint/types/time"
+
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+// GetQueryCmd returns the cli query commands for this module
+func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
+ // Group bep3 queries under a subcommand
+ bep3QueryCmd := &cobra.Command{
+ Use: "bep3",
+ Short: "Querying commands for the bep3 module",
+ }
+
+ bep3QueryCmd.AddCommand(flags.GetCommands(
+ QueryCalcSwapIDCmd(queryRoute, cdc),
+ QueryCalcRandomNumberHashCmd(queryRoute, cdc),
+ QueryGetAtomicSwapCmd(queryRoute, cdc),
+ QueryGetAssetSupplyCmd(queryRoute, cdc),
+ QueryGetAtomicSwapsCmd(queryRoute, cdc),
+ QueryParamsCmd(queryRoute, cdc),
+ )...)
+
+ return bep3QueryCmd
+}
+
+// QueryCalcRandomNumberHashCmd calculates the random number hash for a number and timestamp
+func QueryCalcRandomNumberHashCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "calc-rnh [unix-timestamp]",
+ Short: "calculates an example random number hash from an optional timestamp",
+ Example: "bep3 calc-rnh now",
+ Args: cobra.MaximumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+
+ userTimestamp := "now"
+ if len(args) > 0 {
+ userTimestamp = args[0]
+ }
+
+ // Timestamp defaults to time.Now() unless it's explicitly set
+ var timestamp int64
+ if strings.Compare(userTimestamp, "now") == 0 {
+ timestamp = tmtime.Now().Unix()
+ } else {
+ userTimestamp, err := strconv.ParseInt(userTimestamp, 10, 64)
+ if err != nil {
+ return err
+ }
+ timestamp = userTimestamp
+ }
+
+ // Load hex-encoded cryptographically strong pseudo-random number
+ randomNumber, err := types.GenerateSecureRandomNumber()
+ if err != nil {
+ return err
+ }
+ randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ // Prepare random number, timestamp, and hash for output
+ randomNumberStr := fmt.Sprintf("Random number: %s\n", randomNumber)
+ timestampStr := fmt.Sprintf("Timestamp: %d\n", timestamp)
+ randomNumberHashStr := fmt.Sprintf("Random number hash: %s", hex.EncodeToString(randomNumberHash))
+ output := []string{randomNumberStr, timestampStr, randomNumberHashStr}
+ return cliCtx.PrintOutput(strings.Join(output, ""))
+ },
+ }
+}
+
+// QueryCalcSwapIDCmd calculates the swapID for a random number hash, sender, and sender other chain
+func QueryCalcSwapIDCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "calc-swapid [random-number-hash] [sender] [sender-other-chain]",
+ Short: "calculate swap ID for the given random number hash, sender, and sender other chain",
+ Example: "bep3 calc-swapid 0677bd8a303dd981810f34d8e5cc6507f13b391899b84d3c1be6c6045a17d747 kava15qdefkmwswysgg4qxgcqpqr35k3m49pkx2jdfnw bnb1ud3q90r98l3mhd87kswv3h8cgrymzeljct8qn7",
+ Args: cobra.MinimumNArgs(3),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+
+ // Parse query params
+ randomNumberHash, err := types.HexToBytes(args[0])
+ if err != nil {
+ return err
+ }
+ sender := sdk.AccAddress(args[1])
+ senderOtherChain := args[2]
+
+ // Calculate swap ID and convert to human-readable string
+ swapID := types.CalculateSwapID(randomNumberHash, sender, senderOtherChain)
+ return cliCtx.PrintOutput(hex.EncodeToString(swapID))
+ },
+ }
+}
+
+// QueryGetAssetSupplyCmd queries as asset's current in swap supply, active, supply, and supply limit
+func QueryGetAssetSupplyCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "supply [denom]",
+ Short: "get information about an asset's supply",
+ Example: "bep3 supply bnb",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+
+ // Prepare query params
+ bz, err := cdc.MarshalJSON(types.NewQueryAssetSupply([]byte(args[0])))
+ if err != nil {
+ return err
+ }
+
+ // Execute query
+ res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAssetSupply), bz)
+ if err != nil {
+ return err
+ }
+
+ // Decode and print results
+ var assetSupply types.AssetSupply
+ cdc.MustUnmarshalJSON(res, &assetSupply)
+ return cliCtx.PrintOutput(assetSupply)
+ },
+ }
+}
+
+// QueryGetAtomicSwapCmd queries an AtomicSwap by swapID
+func QueryGetAtomicSwapCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "swap [swap-id]",
+ Short: "get atomic swap information",
+ Example: "bep3 swap 6682c03cc3856879c8fb98c9733c6b0c30758299138166b6523fe94628b1d3af",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+
+ // Decode swapID's hex encoded string to []byte
+ swapID, err := types.HexToBytes(args[0])
+ if err != nil {
+ return err
+ }
+
+ // Prepare query params
+ bz, err := cdc.MarshalJSON(types.NewQueryAtomicSwapByID(swapID))
+ if err != nil {
+ return err
+ }
+
+ // Execute query
+ res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAtomicSwap), bz)
+ if err != nil {
+ return err
+ }
+
+ var atomicSwap types.AtomicSwap
+ cdc.MustUnmarshalJSON(res, &atomicSwap)
+
+ cliCtx = cliCtx.WithHeight(height)
+ return cliCtx.PrintOutput(atomicSwap.String())
+ },
+ }
+}
+
+// QueryGetAtomicSwapsCmd queries AtomicSwaps in the store
+func QueryGetAtomicSwapsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "swaps",
+ Short: "get a list of active atomic swaps",
+ Example: "bep3 swaps",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+
+ res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAtomicSwaps), nil)
+ if err != nil {
+ return err
+ }
+
+ var atomicSwaps types.AtomicSwaps
+ cdc.MustUnmarshalJSON(res, &atomicSwaps)
+
+ if len(atomicSwaps) == 0 {
+ return fmt.Errorf("There are currently no atomic swaps")
+ }
+
+ cliCtx = cliCtx.WithHeight(height)
+ return cliCtx.PrintOutput(atomicSwaps.String())
+ },
+ }
+}
+
+// QueryParamsCmd queries the bep3 module parameters
+func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "params",
+ Short: "get the bep3 module parameters",
+ Example: "bep3 params",
+ Args: cobra.NoArgs,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+
+ // Query
+ route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
+ res, _, err := cliCtx.QueryWithData(route, nil)
+ if err != nil {
+ return err
+ }
+
+ // Decode and print results
+ var out types.Params
+ cdc.MustUnmarshalJSON(res, &out)
+ return cliCtx.PrintOutput(out)
+ },
+ }
+}
diff --git a/x/bep3/client/cli/tx.go b/x/bep3/client/cli/tx.go
new file mode 100644
index 00000000..9543d5c2
--- /dev/null
+++ b/x/bep3/client/cli/tx.go
@@ -0,0 +1,185 @@
+package cli
+
+import (
+ "bufio"
+ "encoding/hex"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/version"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
+ "github.com/kava-labs/kava/x/bep3/types"
+ "github.com/spf13/cobra"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+// GetTxCmd returns the transaction commands for this module
+func GetTxCmd(cdc *codec.Codec) *cobra.Command {
+ bep3TxCmd := &cobra.Command{
+ Use: "bep3",
+ Short: "bep3 transactions subcommands",
+ }
+
+ bep3TxCmd.AddCommand(flags.PostCommands(
+ GetCmdCreateAtomicSwap(cdc),
+ GetCmdClaimAtomicSwap(cdc),
+ GetCmdRefundAtomicSwap(cdc),
+ )...)
+
+ return bep3TxCmd
+}
+
+// GetCmdCreateAtomicSwap cli command for creating atomic swaps
+func GetCmdCreateAtomicSwap(cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "create [to] [recipient-other-chain] [sender-other-chain] [timestamp] [coins] [expected-income] [height-span] [cross-chain]",
+ Short: "create a new atomic swap",
+ Example: fmt.Sprintf("%s tx %s create kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7 bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7 now 100bnb 100bnb 360 true --from validator",
+ version.ClientName, types.ModuleName),
+ Args: cobra.ExactArgs(8),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
+
+ from := cliCtx.GetFromAddress() // same as KavaExecutor.DeputyAddress (for cross-chain)
+ to, err := sdk.AccAddressFromBech32(args[0])
+ if err != nil {
+ return err
+ }
+
+ recipientOtherChain := args[1] // same as OtherExecutor.DeputyAddress
+ senderOtherChain := args[2]
+
+ // Timestamp defaults to time.Now() unless it's explicitly set
+ var timestamp int64
+ if strings.Compare(args[3], "now") == 0 {
+ timestamp = tmtime.Now().Unix()
+ } else {
+ timestamp, err = strconv.ParseInt(args[3], 10, 64)
+ if err != nil {
+ return err
+ }
+ }
+
+ // Generate cryptographically strong pseudo-random number
+ randomNumber, err := types.GenerateSecureRandomNumber()
+ if err != nil {
+ return err
+ }
+
+ randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ // Print random number, timestamp, and hash to user's console
+ fmt.Printf("\nRandom number: %s\n", hex.EncodeToString(randomNumber.Bytes()))
+ fmt.Printf("Timestamp: %d\n", timestamp)
+ fmt.Printf("Random number hash: %s\n\n", hex.EncodeToString(randomNumberHash))
+
+ coins, err := sdk.ParseCoins(args[4])
+ if err != nil {
+ return err
+ }
+
+ expectedIncome := args[5]
+
+ heightSpan, err := strconv.ParseInt(args[6], 10, 64)
+ if err != nil {
+ return err
+ }
+
+ crossChain, err := strconv.ParseBool(args[7])
+ if err != nil {
+ return err
+ }
+
+ msg := types.NewMsgCreateAtomicSwap(
+ from, to, recipientOtherChain, senderOtherChain, randomNumberHash,
+ timestamp, coins, expectedIncome, heightSpan, crossChain,
+ )
+
+ err = msg.ValidateBasic()
+ if err != nil {
+ return err
+ }
+
+ return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
+ },
+ }
+}
+
+// GetCmdClaimAtomicSwap cli command for claiming an atomic swap
+func GetCmdClaimAtomicSwap(cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "claim [swap-id] [random-number]",
+ Short: "claim coins in an atomic swap using the secret number",
+ Example: fmt.Sprintf("%s tx %s claim 6682c03cc3856879c8fb98c9733c6b0c30758299138166b6523fe94628b1d3af 56f13e6a5cd397447f8b5f8c82fdb5bbf56127db75269f5cc14e50acd8ac9a4c --from accA", version.ClientName, types.ModuleName),
+ Args: cobra.ExactArgs(2),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
+
+ from := cliCtx.GetFromAddress()
+
+ swapID, err := types.HexToBytes(args[0])
+ if err != nil {
+ return err
+ }
+
+ if len(strings.TrimSpace(args[1])) == 0 {
+ return fmt.Errorf("random-number cannot be empty")
+ }
+
+ randomNumber, err := types.HexToBytes(args[1])
+ if err != nil {
+ return err
+ }
+
+ msg := types.NewMsgClaimAtomicSwap(from, swapID, randomNumber)
+
+ err = msg.ValidateBasic()
+ if err != nil {
+ return err
+ }
+
+ return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
+ },
+ }
+}
+
+// GetCmdRefundAtomicSwap cli command for claiming an atomic swap
+func GetCmdRefundAtomicSwap(cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "refund [swap-id]",
+ Short: "refund the coins in an atomic swap",
+ Example: fmt.Sprintf("%s tx %s refund 6682c03cc3856879c8fb98c9733c6b0c30758299138166b6523fe94628b1d3af --from accA", version.ClientName, types.ModuleName),
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
+
+ from := cliCtx.GetFromAddress()
+
+ swapID, err := types.HexToBytes(args[0])
+ if err != nil {
+ return err
+ }
+
+ msg := types.NewMsgRefundAtomicSwap(from, swapID)
+
+ err = msg.ValidateBasic()
+ if err != nil {
+ return err
+ }
+
+ return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
+ },
+ }
+}
diff --git a/x/bep3/client/rest/query.go b/x/bep3/client/rest/query.go
new file mode 100644
index 00000000..559919f5
--- /dev/null
+++ b/x/bep3/client/rest/query.go
@@ -0,0 +1,163 @@
+package rest
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/gorilla/mux"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/types/rest"
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+const restSwapID = "swap-id"
+const restDenom = "denom"
+
+func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
+ r.HandleFunc(fmt.Sprintf("/%s/swap/{%s}", types.ModuleName, restSwapID), queryAtomicSwapHandlerFn(cliCtx)).Methods("GET")
+ r.HandleFunc(fmt.Sprintf("/%s/swaps", types.ModuleName), queryAtomicSwapsHandlerFn(cliCtx)).Methods("GET")
+ r.HandleFunc(fmt.Sprintf("/%s/supply/{%s}", types.ModuleName, restDenom), queryAssetSupplyHandlerFn(cliCtx)).Methods("GET")
+ r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET")
+
+}
+
+func queryAtomicSwapHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Parse the query height
+ cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
+ if !ok {
+ return
+ }
+
+ // Prepare params for querier
+ vars := mux.Vars(r)
+ if len(vars[restSwapID]) == 0 {
+ err := fmt.Errorf("%s required but not specified", restSwapID)
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ swapID, err := types.HexToBytes(vars[restSwapID])
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ bz, err := cliCtx.Codec.MarshalJSON(types.QueryAtomicSwapByID{SwapID: swapID})
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ // Query
+ res, height, err := cliCtx.QueryWithData(fmt.Sprintf("/custom/%s/%s", types.ModuleName, types.QueryGetAtomicSwap), bz)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ // Decode and return results
+ cliCtx = cliCtx.WithHeight(height)
+
+ var swap types.AtomicSwap
+ err = cliCtx.Codec.UnmarshalJSON(res, &swap)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+ rest.PostProcessResponse(w, cliCtx, cliCtx.Codec.MustMarshalJSON(swap))
+ }
+}
+
+func queryAtomicSwapsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Parse the query height
+ cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
+ if !ok {
+ return
+ }
+
+ route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetAtomicSwaps)
+
+ res, height, err := cliCtx.QueryWithData(route, nil)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ // Decode and return results
+ cliCtx = cliCtx.WithHeight(height)
+
+ var swaps types.AtomicSwaps
+ err = cliCtx.Codec.UnmarshalJSON(res, &swaps)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
+ return
+ }
+
+ // using empty slice so json returns [] instead of null when there's no swaps
+ sliceSwaps := types.AtomicSwaps{}
+ for _, s := range swaps {
+ sliceSwaps = append(sliceSwaps, s)
+ }
+ rest.PostProcessResponse(w, cliCtx, cliCtx.Codec.MustMarshalJSON(sliceSwaps))
+ }
+}
+
+func queryAssetSupplyHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Parse the query height
+ cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
+ if !ok {
+ return
+ }
+
+ // Prepare params for querier
+ vars := mux.Vars(r)
+ denom := []byte(vars[restDenom])
+ params := types.NewQueryAssetSupply(denom)
+
+ bz, err := cliCtx.Codec.MarshalJSON(params)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ // Query
+ res, height, err := cliCtx.QueryWithData(fmt.Sprintf("/custom/%s/%s", types.ModuleName, types.QueryGetAssetSupply), bz)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ // Decode and return results
+ cliCtx = cliCtx.WithHeight(height)
+
+ var assetSupply types.AssetSupply
+ err = cliCtx.Codec.UnmarshalJSON(res, &assetSupply)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+ rest.PostProcessResponse(w, cliCtx, cliCtx.Codec.MustMarshalJSON(assetSupply))
+ }
+}
+
+func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
+ if !ok {
+ return
+ }
+
+ route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryGetParams)
+
+ res, height, err := cliCtx.QueryWithData(route, nil)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ cliCtx = cliCtx.WithHeight(height)
+ rest.PostProcessResponse(w, cliCtx, res)
+ }
+}
diff --git a/x/bep3/client/rest/rest.go b/x/bep3/client/rest/rest.go
new file mode 100644
index 00000000..1b5bfb21
--- /dev/null
+++ b/x/bep3/client/rest/rest.go
@@ -0,0 +1,47 @@
+package rest
+
+import (
+ "github.com/gorilla/mux"
+
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/rest"
+)
+
+// RegisterRoutes registers bep3-related REST handlers to a router
+func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
+ registerQueryRoutes(cliCtx, r)
+ registerTxRoutes(cliCtx, r)
+}
+
+// PostCreateSwapReq defines the properties of a swap create request's body
+type PostCreateSwapReq struct {
+ BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
+ From sdk.AccAddress `json:"from" yaml:"from"`
+ To sdk.AccAddress `json:"to" yaml:"to"`
+ RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"`
+ SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"`
+ RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"`
+ Timestamp int64 `json:"timestamp" yaml:"timestamp"`
+ Amount sdk.Coins `json:"amount" yaml:"amount"`
+ ExpectedIncome string `json:"expected_income" yaml:"expected_income"`
+ HeightSpan int64 `json:"height_span" yaml:"height_span"`
+ CrossChain bool `json:"cross_chain" yaml:"cross_chain"`
+}
+
+// PostClaimSwapReq defines the properties of a swap claim request's body
+type PostClaimSwapReq struct {
+ BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
+ From sdk.AccAddress `json:"from" yaml:"from"`
+ SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"`
+ RandomNumber tmbytes.HexBytes `json:"random_number" yaml:"random_number"`
+}
+
+// PostRefundSwapReq defines the properties of swap refund request's body
+type PostRefundSwapReq struct {
+ BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
+ From sdk.AccAddress `json:"from" yaml:"from"`
+ SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"`
+}
diff --git a/x/bep3/client/rest/tx.go b/x/bep3/client/rest/tx.go
new file mode 100644
index 00000000..022993e3
--- /dev/null
+++ b/x/bep3/client/rest/tx.go
@@ -0,0 +1,110 @@
+package rest
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/rest"
+ "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
+ "github.com/gorilla/mux"
+
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
+ r.HandleFunc(fmt.Sprintf("/%s/swap/create", types.ModuleName), postCreateHandlerFn(cliCtx)).Methods("POST")
+ r.HandleFunc(fmt.Sprintf("/%s/swap/claim", types.ModuleName), postClaimHandlerFn(cliCtx)).Methods("POST")
+ r.HandleFunc(fmt.Sprintf("/%s/swap/refund", types.ModuleName), postRefundHandlerFn(cliCtx)).Methods("POST")
+}
+
+func postCreateHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Decode PUT request body
+ var req PostCreateSwapReq
+ if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
+ return
+ }
+ req.BaseReq = req.BaseReq.Sanitize()
+ if !req.BaseReq.ValidateBasic(w) {
+ return
+ }
+
+ // Create and return msg
+ msg := types.NewMsgCreateAtomicSwap(
+ req.From,
+ req.To,
+ req.RecipientOtherChain,
+ req.SenderOtherChain,
+ req.RandomNumberHash,
+ req.Timestamp,
+ req.Amount,
+ req.ExpectedIncome,
+ req.HeightSpan,
+ req.CrossChain,
+ )
+ if err := msg.ValidateBasic(); err != nil {
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
+ }
+}
+
+func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Decode PUT request body
+ var req PostClaimSwapReq
+ if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
+ return
+ }
+ req.BaseReq = req.BaseReq.Sanitize()
+ if !req.BaseReq.ValidateBasic(w) {
+ return
+ }
+
+ // Create and return msg
+ msg := types.NewMsgClaimAtomicSwap(
+ req.From,
+ req.SwapID,
+ req.RandomNumber,
+ )
+ if err := msg.ValidateBasic(); err != nil {
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
+ }
+}
+
+func postRefundHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Decode PUT request body
+ var req PostRefundSwapReq
+ if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
+ return
+ }
+ req.BaseReq = req.BaseReq.Sanitize()
+ if !req.BaseReq.ValidateBasic(w) {
+ return
+ }
+
+ senderAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ // Create and return msg
+ msg := types.NewMsgRefundAtomicSwap(
+ senderAddr,
+ req.SwapID,
+ )
+ if err := msg.ValidateBasic(); err != nil {
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
+ }
+}
diff --git a/x/bep3/genesis.go b/x/bep3/genesis.go
new file mode 100644
index 00000000..b6d01ad4
--- /dev/null
+++ b/x/bep3/genesis.go
@@ -0,0 +1,122 @@
+package bep3
+
+import (
+ "fmt"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+// InitGenesis initializes the store state from a genesis state.
+func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper SupplyKeeper, gs GenesisState) {
+ if err := gs.Validate(); err != nil {
+ panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
+ }
+
+ keeper.SetParams(ctx, gs.Params)
+
+ // Initialize supported assets
+ for _, asset := range gs.Params.SupportedAssets {
+ zeroCoin := sdk.NewCoin(asset.Denom, sdk.NewInt(0))
+ supply := NewAssetSupply(asset.Denom, zeroCoin, zeroCoin, zeroCoin, sdk.NewCoin(asset.Denom, asset.Limit))
+ keeper.SetAssetSupply(ctx, supply, []byte(asset.Denom))
+ }
+
+ // Increment an asset's incoming, current, and outgoing supply
+ // It it required that assets are supported but they do not have to be active
+ for _, supply := range gs.AssetSupplies {
+ // Asset must be supported but does not have to be active
+ coin, found := keeper.GetAssetByDenom(ctx, supply.Denom)
+ if !found {
+ panic(fmt.Sprintf("invalid asset supply: %s is not a supported asset", coin.Denom))
+ }
+ if !coin.Limit.Equal(supply.Limit.Amount) {
+ panic(fmt.Sprintf("supported asset limit %s does not equal asset supply %s", coin.Limit, supply.Limit.Amount))
+ }
+
+ // Increment current, incoming, and outgoing asset supplies
+ err := keeper.IncrementCurrentAssetSupply(ctx, supply.CurrentSupply)
+ if err != nil {
+ panic(err)
+ }
+ err = keeper.IncrementIncomingAssetSupply(ctx, supply.IncomingSupply)
+ if err != nil {
+ panic(err)
+ }
+ err = keeper.IncrementOutgoingAssetSupply(ctx, supply.OutgoingSupply)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ var incomingSupplies sdk.Coins
+ var outgoingSupplies sdk.Coins
+ for _, swap := range gs.AtomicSwaps {
+ if swap.Validate() != nil {
+ panic(fmt.Sprintf("invalid swap %s", swap.GetSwapID()))
+ }
+
+ // Atomic swap assets must be both supported and active
+ err := keeper.ValidateLiveAsset(ctx, swap.Amount[0])
+ if err != nil {
+ panic(err)
+ }
+
+ keeper.SetAtomicSwap(ctx, swap)
+
+ // Add swap to block index or longterm storage based on swap.Status
+ // Increment incoming or outgoing supply based on swap.Direction
+ switch swap.Direction {
+ case Incoming:
+ switch swap.Status {
+ case Open:
+ // This index expires unclaimed swaps
+ keeper.InsertIntoByBlockIndex(ctx, swap)
+ incomingSupplies = incomingSupplies.Add(swap.Amount...)
+ case Expired:
+ incomingSupplies = incomingSupplies.Add(swap.Amount...)
+ case Completed:
+ // This index stores swaps until deletion
+ keeper.InsertIntoLongtermStorage(ctx, swap)
+ default:
+ panic(fmt.Sprintf("swap %s has invalid status %s", swap.GetSwapID(), swap.Status.String()))
+ }
+ case Outgoing:
+ switch swap.Status {
+ case Open:
+ keeper.InsertIntoByBlockIndex(ctx, swap)
+ outgoingSupplies = outgoingSupplies.Add(swap.Amount...)
+ case Expired:
+ outgoingSupplies = outgoingSupplies.Add(swap.Amount...)
+ case Completed:
+ keeper.InsertIntoLongtermStorage(ctx, swap)
+ default:
+ panic(fmt.Sprintf("swap %s has invalid status %s", swap.GetSwapID(), swap.Status.String()))
+ }
+ default:
+ panic(fmt.Sprintf("swap %s has invalid direction %s", swap.GetSwapID(), swap.Direction.String()))
+ }
+ }
+
+ // Asset's given incoming/outgoing supply much match the amount of coins in incoming/outgoing atomic swaps
+ supplies := keeper.GetAllAssetSupplies(ctx)
+ for _, supply := range supplies {
+ incomingSupply := incomingSupplies.AmountOf(supply.Denom)
+ if !supply.IncomingSupply.Amount.Equal(incomingSupply) {
+ panic(fmt.Sprintf("asset's incoming supply %s does not match amount %s in incoming atomic swaps",
+ supply.IncomingSupply, incomingSupply))
+ }
+ outgoingSupply := outgoingSupplies.AmountOf(supply.Denom)
+ if !supply.OutgoingSupply.Amount.Equal(outgoingSupply) {
+ panic(fmt.Sprintf("asset's outgoing supply %s does not match amount %s in outgoing atomic swaps",
+ supply.OutgoingSupply, outgoingSupply))
+ }
+ }
+}
+
+// ExportGenesis writes the current store values to a genesis file, which can be imported again with InitGenesis
+func ExportGenesis(ctx sdk.Context, k Keeper) (data GenesisState) {
+ params := k.GetParams(ctx)
+ swaps := k.GetAllAtomicSwaps(ctx)
+ assets := k.GetAllAssetSupplies(ctx)
+ return NewGenesisState(params, swaps, assets)
+}
diff --git a/x/bep3/genesis_test.go b/x/bep3/genesis_test.go
new file mode 100644
index 00000000..7b76af77
--- /dev/null
+++ b/x/bep3/genesis_test.go
@@ -0,0 +1,298 @@
+package bep3_test
+
+import (
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/stretchr/testify/suite"
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3"
+)
+
+type GenesisTestSuite struct {
+ suite.Suite
+
+ app app.TestApp
+ ctx sdk.Context
+ keeper bep3.Keeper
+ addrs []sdk.AccAddress
+}
+
+func (suite *GenesisTestSuite) SetupTest() {
+ tApp := app.NewTestApp()
+ suite.ctx = tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+ suite.keeper = tApp.GetBep3Keeper()
+ suite.app = tApp
+
+ _, addrs := app.GeneratePrivKeyAddressPairs(3)
+ suite.addrs = addrs
+}
+
+func (suite *GenesisTestSuite) TestGenesisState() {
+
+ type GenState func() app.GenesisState
+
+ testCases := []struct {
+ name string
+ genState GenState
+ expectPass bool
+ }{
+ {
+ name: "default",
+ genState: func() app.GenesisState {
+ return NewBep3GenStateMulti(suite.addrs[0])
+ },
+ expectPass: true,
+ },
+ {
+ name: "import atomic swaps and asset supplies",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ _, addrs := app.GeneratePrivKeyAddressPairs(3)
+ var swaps bep3.AtomicSwaps
+ var supplies bep3.AssetSupplies
+ for i := 0; i < 3; i++ {
+ swap, supply := loadSwapAndSupply(addrs[i], i)
+ swaps = append(swaps, swap)
+ supplies = append(supplies, supply)
+ }
+ gs.AtomicSwaps = swaps
+ gs.AssetSupplies = supplies
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: true,
+ },
+ {
+ name: "incoming supply doesn't match amount in incoming atomic swaps",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ _, addrs := app.GeneratePrivKeyAddressPairs(1)
+ swap, _ := loadSwapAndSupply(addrs[0], 2)
+ gs.AtomicSwaps = bep3.AtomicSwaps{swap}
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "current supply above limit",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ assetParam, _ := suite.keeper.GetAssetByDenom(suite.ctx, "bnb")
+ gs.AssetSupplies = bep3.AssetSupplies{
+ bep3.AssetSupply{
+ Denom: "bnb",
+ IncomingSupply: c("bnb", 0),
+ OutgoingSupply: c("bnb", 0),
+ CurrentSupply: c("bnb", assetParam.Limit.Add(i(1)).Int64()),
+ Limit: c("bnb", assetParam.Limit.Int64()),
+ },
+ }
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "incoming supply above limit",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ // Set up overlimit amount
+ assetParam, _ := suite.keeper.GetAssetByDenom(suite.ctx, "bnb")
+ overLimitAmount := assetParam.Limit.Add(i(1))
+
+ // Set up an atomic swap with amount equal to the currently asset supply
+ _, addrs := app.GeneratePrivKeyAddressPairs(2)
+ timestamp := ts(0)
+ randomNumber, _ := bep3.GenerateSecureRandomNumber()
+ randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+ swap := bep3.NewAtomicSwap(cs(c("bnb", overLimitAmount.Int64())), randomNumberHash,
+ int64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain,
+ TestRecipientOtherChain, 0, bep3.Open, true, bep3.Incoming)
+ gs.AtomicSwaps = bep3.AtomicSwaps{swap}
+
+ // Set up asset supply with overlimit current supply
+ gs.AssetSupplies = bep3.AssetSupplies{
+ bep3.AssetSupply{
+ Denom: "bnb",
+ IncomingSupply: c("bnb", assetParam.Limit.Add(i(1)).Int64()),
+ OutgoingSupply: c("bnb", 0),
+ CurrentSupply: c("bnb", 0),
+ Limit: c("bnb", assetParam.Limit.Int64()),
+ },
+ }
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "incoming supply + current supply above limit",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ // Set up overlimit amount
+ assetParam, _ := suite.keeper.GetAssetByDenom(suite.ctx, "bnb")
+ halfLimit := assetParam.Limit.Int64() / 2
+ overHalfLimit := halfLimit + 1
+
+ // Set up an atomic swap with amount equal to the currently asset supply
+ _, addrs := app.GeneratePrivKeyAddressPairs(2)
+ timestamp := ts(0)
+ randomNumber, _ := bep3.GenerateSecureRandomNumber()
+ randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+ swap := bep3.NewAtomicSwap(cs(c("bnb", halfLimit)), randomNumberHash,
+ int64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain,
+ TestRecipientOtherChain, 0, bep3.Open, true, bep3.Incoming)
+ gs.AtomicSwaps = bep3.AtomicSwaps{swap}
+
+ // Set up asset supply with overlimit current supply
+ gs.AssetSupplies = bep3.AssetSupplies{
+ bep3.AssetSupply{
+ Denom: "bnb",
+ IncomingSupply: c("bnb", halfLimit),
+ OutgoingSupply: c("bnb", 0),
+ CurrentSupply: c("bnb", overHalfLimit),
+ Limit: c("bnb", assetParam.Limit.Int64()),
+ },
+ }
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "asset supply denom is not a supported asset",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ gs.AssetSupplies = bep3.AssetSupplies{
+ bep3.AssetSupply{
+ Denom: "fake",
+ IncomingSupply: c("fake", 0),
+ OutgoingSupply: c("fake", 0),
+ CurrentSupply: c("fake", 0),
+ Limit: c("fake", StandardSupplyLimit.Int64()),
+ },
+ }
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "atomic swap asset type is unsupported",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ _, addrs := app.GeneratePrivKeyAddressPairs(2)
+ timestamp := ts(0)
+ randomNumber, _ := bep3.GenerateSecureRandomNumber()
+ randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+ swap := bep3.NewAtomicSwap(cs(c("fake", 500000)), randomNumberHash,
+ int64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain,
+ TestRecipientOtherChain, 0, bep3.Open, true, bep3.Incoming)
+
+ gs.AtomicSwaps = bep3.AtomicSwaps{swap}
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "atomic swap status is invalid",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ _, addrs := app.GeneratePrivKeyAddressPairs(2)
+ timestamp := ts(0)
+ randomNumber, _ := bep3.GenerateSecureRandomNumber()
+ randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+ swap := bep3.NewAtomicSwap(cs(c("bnb", 5000)), randomNumberHash,
+ int64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain,
+ TestRecipientOtherChain, 0, bep3.NULL, true, bep3.Incoming)
+
+ gs.AtomicSwaps = bep3.AtomicSwaps{swap}
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "minimum block lock below limit",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ gs.Params.MinBlockLock = 1
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "minimum block lock above limit",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ gs.Params.MinBlockLock = 500000
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "maximum block lock below limit",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ gs.Params.MaxBlockLock = 1
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "maximum block lock above limit",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ gs.Params.MaxBlockLock = 100000000
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "empty supported asset denom",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ gs.Params.SupportedAssets[0].Denom = ""
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "negative supported asset limit",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ gs.Params.SupportedAssets[0].Limit = i(-100)
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ {
+ name: "duplicate supported asset denom",
+ genState: func() app.GenesisState {
+ gs := baseGenState(suite.addrs[0])
+ gs.Params.SupportedAssets[1].Denom = "bnb"
+ return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
+ },
+ expectPass: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ suite.SetupTest()
+ if tc.expectPass {
+ suite.Run(tc.name, func() {
+ suite.NotPanics(func() {
+ suite.app.InitializeFromGenesisStates(tc.genState())
+ })
+ })
+ } else {
+ suite.Run(tc.name, func() {
+ suite.Panics(func() {
+ suite.app.InitializeFromGenesisStates(tc.genState())
+ })
+ })
+ }
+ }
+}
+
+func TestGenesisTestSuite(t *testing.T) {
+ suite.Run(t, new(GenesisTestSuite))
+}
diff --git a/x/bep3/handler.go b/x/bep3/handler.go
new file mode 100644
index 00000000..fa92f055
--- /dev/null
+++ b/x/bep3/handler.go
@@ -0,0 +1,87 @@
+package bep3
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+)
+
+// NewHandler creates an sdk.Handler for all the bep3 type messages
+func NewHandler(k Keeper) sdk.Handler {
+ return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
+ ctx = ctx.WithEventManager(sdk.NewEventManager())
+ switch msg := msg.(type) {
+ case MsgCreateAtomicSwap:
+ return handleMsgCreateAtomicSwap(ctx, k, msg)
+ case MsgClaimAtomicSwap:
+ return handleMsgClaimAtomicSwap(ctx, k, msg)
+ case MsgRefundAtomicSwap:
+ return handleMsgRefundAtomicSwap(ctx, k, msg)
+ default:
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
+ }
+ }
+}
+
+// handleMsgCreateAtomicSwap handles requests to create a new AtomicSwap
+func handleMsgCreateAtomicSwap(ctx sdk.Context, k Keeper, msg MsgCreateAtomicSwap) (*sdk.Result, error) {
+ err := k.CreateAtomicSwap(ctx, msg.RandomNumberHash, msg.Timestamp, msg.HeightSpan, msg.From, msg.To,
+ msg.SenderOtherChain, msg.RecipientOtherChain, msg.Amount, msg.ExpectedIncome, msg.CrossChain)
+
+ if err != nil {
+ return nil, err
+ }
+
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ sdk.EventTypeMessage,
+ sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory),
+ sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()),
+ ),
+ )
+
+ return &sdk.Result{
+ Events: ctx.EventManager().Events(),
+ }, nil
+}
+
+// handleMsgClaimAtomicSwap handles requests to claim funds in an active AtomicSwap
+func handleMsgClaimAtomicSwap(ctx sdk.Context, k Keeper, msg MsgClaimAtomicSwap) (*sdk.Result, error) {
+
+ err := k.ClaimAtomicSwap(ctx, msg.From, msg.SwapID, msg.RandomNumber)
+ if err != nil {
+ return nil, err
+ }
+
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ sdk.EventTypeMessage,
+ sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory),
+ sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()),
+ ),
+ )
+
+ return &sdk.Result{
+ Events: ctx.EventManager().Events(),
+ }, nil
+}
+
+// handleMsgRefundAtomicSwap handles requests to refund an active AtomicSwap
+func handleMsgRefundAtomicSwap(ctx sdk.Context, k Keeper, msg MsgRefundAtomicSwap) (*sdk.Result, error) {
+
+ err := k.RefundAtomicSwap(ctx, msg.From, msg.SwapID)
+ if err != nil {
+ return nil, err
+ }
+
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ sdk.EventTypeMessage,
+ sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory),
+ sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()),
+ ),
+ )
+
+ return &sdk.Result{
+ Events: ctx.EventManager().Events(),
+ }, nil
+}
diff --git a/x/bep3/handler_test.go b/x/bep3/handler_test.go
new file mode 100644
index 00000000..888d819d
--- /dev/null
+++ b/x/bep3/handler_test.go
@@ -0,0 +1,134 @@
+package bep3_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+ tmtime "github.com/tendermint/tendermint/types/time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3"
+)
+
+type HandlerTestSuite struct {
+ suite.Suite
+
+ ctx sdk.Context
+ app app.TestApp
+ handler sdk.Handler
+ keeper bep3.Keeper
+ addrs []sdk.AccAddress
+}
+
+func (suite *HandlerTestSuite) SetupTest() {
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+ keeper := tApp.GetBep3Keeper()
+
+ // Set up genesis state and initialize
+ _, addrs := app.GeneratePrivKeyAddressPairs(3)
+ coins := []sdk.Coins{}
+ for j := 0; j < 3; j++ {
+ coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000)))
+ }
+ authGS := app.NewAuthGenState(addrs, coins)
+ tApp.InitializeFromGenesisStates(authGS, NewBep3GenStateMulti(addrs[0]))
+
+ suite.addrs = addrs
+ suite.handler = bep3.NewHandler(keeper)
+ suite.keeper = keeper
+ suite.app = tApp
+ suite.ctx = ctx
+}
+
+func (suite *HandlerTestSuite) AddAtomicSwap() (tmbytes.HexBytes, tmbytes.HexBytes) {
+ expireHeight := int64(360)
+ amount := cs(c("bnb", int64(50000)))
+ timestamp := ts(0)
+ randomNumber, _ := bep3.GenerateSecureRandomNumber()
+ randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ // Create atomic swap and check err to confirm creation
+ err := suite.keeper.CreateAtomicSwap(suite.ctx, randomNumberHash, timestamp, expireHeight,
+ suite.addrs[0], suite.addrs[1], TestSenderOtherChain, TestRecipientOtherChain,
+ amount, amount.String(), true)
+ suite.Nil(err)
+
+ swapID := bep3.CalculateSwapID(randomNumberHash, suite.addrs[0], TestSenderOtherChain)
+ return swapID, randomNumber.Bytes()
+}
+
+func (suite *HandlerTestSuite) TestMsgCreateAtomicSwap() {
+ amount := cs(c("bnb", int64(10000)))
+ timestamp := ts(0)
+ randomNumber, _ := bep3.GenerateSecureRandomNumber()
+ randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ msg := bep3.NewMsgCreateAtomicSwap(
+ suite.addrs[0], suite.addrs[2], TestRecipientOtherChain, TestSenderOtherChain,
+ randomNumberHash, timestamp, amount, amount.String(), int64(300), true)
+
+ res, err := suite.handler(suite.ctx, msg)
+ suite.Require().NoError(err)
+ suite.Require().NotNil(res)
+}
+
+func (suite *HandlerTestSuite) TestMsgClaimAtomicSwap() {
+ // Attempt claim msg on fake atomic swap
+ badRandomNumber, _ := bep3.GenerateSecureRandomNumber()
+ badRandomNumberHash := bep3.CalculateRandomHash(badRandomNumber.Bytes(), ts(0))
+ badSwapID := bep3.CalculateSwapID(badRandomNumberHash, suite.addrs[0], TestSenderOtherChain)
+ badMsg := bep3.NewMsgClaimAtomicSwap(suite.addrs[0], badSwapID, badRandomNumber.Bytes())
+ badRes, err := suite.handler(suite.ctx, badMsg)
+ suite.Require().Error(err)
+ suite.Require().Nil(badRes)
+
+ // Add an atomic swap before attempting new claim msg
+ swapID, randomNumber := suite.AddAtomicSwap()
+ msg := bep3.NewMsgClaimAtomicSwap(suite.addrs[0], swapID, randomNumber)
+ res, err := suite.handler(suite.ctx, msg)
+ suite.Require().NoError(err)
+ suite.Require().NotNil(res)
+}
+
+func (suite *HandlerTestSuite) TestMsgRefundAtomicSwap() {
+ // Attempt refund msg on fake atomic swap
+ badRandomNumber, _ := bep3.GenerateSecureRandomNumber()
+ badRandomNumberHash := bep3.CalculateRandomHash(badRandomNumber.Bytes(), ts(0))
+ badSwapID := bep3.CalculateSwapID(badRandomNumberHash, suite.addrs[0], TestSenderOtherChain)
+ badMsg := bep3.NewMsgRefundAtomicSwap(suite.addrs[0], badSwapID)
+ badRes, err := suite.handler(suite.ctx, badMsg)
+ suite.Require().Error(err)
+ suite.Require().Nil(badRes)
+
+ // Add an atomic swap and build refund msg
+ swapID, _ := suite.AddAtomicSwap()
+ msg := bep3.NewMsgRefundAtomicSwap(suite.addrs[0], swapID)
+
+ // Attempt to refund active atomic swap
+ res1, err := suite.handler(suite.ctx, msg)
+ suite.Require().Error(err)
+ suite.Require().Nil(res1)
+
+ // Expire the atomic swap with begin blocker and attempt refund
+ laterCtx := suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400)
+ bep3.BeginBlocker(laterCtx, suite.keeper)
+ res2, err := suite.handler(laterCtx, msg)
+ suite.Require().NoError(err)
+ suite.Require().NotNil(res2)
+}
+
+func (suite *HandlerTestSuite) TestInvalidMsg() {
+ res, err := suite.handler(suite.ctx, sdk.NewTestMsg())
+ suite.Require().Error(err)
+ suite.Require().Nil(res)
+}
+
+func TestHandlerTestSuite(t *testing.T) {
+ suite.Run(t, new(HandlerTestSuite))
+}
diff --git a/x/bep3/integration_test.go b/x/bep3/integration_test.go
new file mode 100644
index 00000000..bb9bc7c2
--- /dev/null
+++ b/x/bep3/integration_test.go
@@ -0,0 +1,87 @@
+package bep3_test
+
+import (
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3"
+)
+
+const (
+ TestSenderOtherChain = "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7"
+ TestRecipientOtherChain = "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7"
+ TestDeputy = "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj"
+ TestUser = "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p"
+)
+
+var (
+ StandardSupplyLimit = i(100000000000)
+ DenomMap = map[int]string{0: "btc", 1: "eth", 2: "bnb", 3: "xrp", 4: "dai"}
+)
+
+func i(in int64) sdk.Int { return sdk.NewInt(in) }
+func d(de int64) sdk.Dec { return sdk.NewDec(de) }
+func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
+func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
+func ts(minOffset int) int64 { return tmtime.Now().Add(time.Duration(minOffset) * time.Minute).Unix() }
+
+func NewBep3GenStateMulti(deputy sdk.AccAddress) app.GenesisState {
+ bep3Genesis := baseGenState(deputy)
+ return app.GenesisState{bep3.ModuleName: bep3.ModuleCdc.MustMarshalJSON(bep3Genesis)}
+}
+
+func baseGenState(deputy sdk.AccAddress) bep3.GenesisState {
+ bep3Genesis := bep3.GenesisState{
+ Params: bep3.Params{
+ BnbDeputyAddress: deputy,
+ MinBlockLock: bep3.DefaultMinBlockLock, // 80
+ MaxBlockLock: bep3.DefaultMaxBlockLock, // 360
+ SupportedAssets: bep3.AssetParams{
+ bep3.AssetParam{
+ Denom: "btc",
+ CoinID: 714,
+ Limit: StandardSupplyLimit,
+ Active: true,
+ },
+ bep3.AssetParam{
+ Denom: "eth",
+ CoinID: 999999,
+ Limit: StandardSupplyLimit,
+ Active: true,
+ },
+ bep3.AssetParam{
+ Denom: "bnb",
+ CoinID: 99999,
+ Limit: StandardSupplyLimit,
+ Active: true,
+ },
+ bep3.AssetParam{
+ Denom: "inc",
+ CoinID: 9999,
+ Limit: i(100),
+ Active: false,
+ },
+ },
+ },
+ }
+ return bep3Genesis
+}
+
+func loadSwapAndSupply(addr sdk.AccAddress, index int) (bep3.AtomicSwap, bep3.AssetSupply) {
+ coin := c(DenomMap[index], 50000)
+ expireOffset := int64((index * 15) + 360) // Default expire height + offet to match timestamp
+ timestamp := ts(index) // One minute apart
+ randomNumber, _ := bep3.GenerateSecureRandomNumber()
+ randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+ swap := bep3.NewAtomicSwap(cs(coin), randomNumberHash,
+ expireOffset, timestamp, addr, addr, TestSenderOtherChain,
+ TestRecipientOtherChain, 0, bep3.Open, true, bep3.Incoming)
+
+ supply := bep3.NewAssetSupply(coin.Denom, coin, c(coin.Denom, 0),
+ c(coin.Denom, 0), c(coin.Denom, StandardSupplyLimit.Int64()))
+
+ return swap, supply
+}
diff --git a/x/bep3/keeper/asset.go b/x/bep3/keeper/asset.go
new file mode 100644
index 00000000..cf8f1ef7
--- /dev/null
+++ b/x/bep3/keeper/asset.go
@@ -0,0 +1,115 @@
+package keeper
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+// IncrementCurrentAssetSupply increments an asset's supply by the coin
+func (k Keeper) IncrementCurrentAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
+ supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom))
+ if !found {
+ return sdkerrors.Wrap(types.ErrAssetNotSupported, coin.Denom)
+ }
+
+ // Resulting current supply must be under asset's limit
+ if !supply.Limit.IsGTE(supply.CurrentSupply.Add(coin)) {
+ return sdkerrors.Wrapf(types.ErrExceedsSupplyLimit, "increase %s, asset supply %s, limit %s", coin, supply.CurrentSupply, supply.Limit)
+ }
+
+ supply.CurrentSupply = supply.CurrentSupply.Add(coin)
+ k.SetAssetSupply(ctx, supply, []byte(coin.Denom))
+ return nil
+}
+
+// DecrementCurrentAssetSupply decrement an asset's supply by the coin
+func (k Keeper) DecrementCurrentAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
+ supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom))
+ if !found {
+ return sdkerrors.Wrap(types.ErrAssetNotSupported, coin.Denom)
+ }
+
+ // Resulting current supply must be greater than or equal to 0
+ // Use sdk.Int instead of sdk.Coin to prevent panic if true
+ if supply.CurrentSupply.Amount.Sub(coin.Amount).IsNegative() {
+ return sdkerrors.Wrapf(types.ErrInvalidCurrentSupply, "decrease %s, asset supply %s", coin, supply.CurrentSupply)
+ }
+
+ supply.CurrentSupply = supply.CurrentSupply.Sub(coin)
+ k.SetAssetSupply(ctx, supply, []byte(coin.Denom))
+ return nil
+}
+
+// IncrementIncomingAssetSupply increments an asset's incoming supply
+func (k Keeper) IncrementIncomingAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
+ supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom))
+ if !found {
+ return sdkerrors.Wrap(types.ErrAssetNotSupported, coin.Denom)
+ }
+
+ // Result of (current + incoming + amount) must be under asset's limit
+ totalSupply := supply.CurrentSupply.Add(supply.IncomingSupply)
+ if !supply.Limit.IsGTE(totalSupply.Add(coin)) {
+ return sdkerrors.Wrapf(types.ErrExceedsSupplyLimit, "increase %s, asset supply %s, limit %s", coin, totalSupply, supply.Limit)
+ }
+
+ supply.IncomingSupply = supply.IncomingSupply.Add(coin)
+ k.SetAssetSupply(ctx, supply, []byte(coin.Denom))
+ return nil
+}
+
+// DecrementIncomingAssetSupply decrements an asset's incoming supply
+func (k Keeper) DecrementIncomingAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
+ supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom))
+ if !found {
+ return sdkerrors.Wrap(types.ErrAssetNotSupported, coin.Denom)
+ }
+
+ // Resulting incoming supply must be greater than or equal to 0
+ // Use sdk.Int instead of sdk.Coin to prevent panic if true
+ if supply.IncomingSupply.Amount.Sub(coin.Amount).IsNegative() {
+ return sdkerrors.Wrapf(types.ErrInvalidIncomingSupply, "decrease %s, incoming supply %s", coin, supply.IncomingSupply)
+ }
+
+ supply.IncomingSupply = supply.IncomingSupply.Sub(coin)
+ k.SetAssetSupply(ctx, supply, []byte(coin.Denom))
+ return nil
+}
+
+// IncrementOutgoingAssetSupply increments an asset's outoing supply
+func (k Keeper) IncrementOutgoingAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
+ supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom))
+ if !found {
+ return sdkerrors.Wrap(types.ErrAssetNotSupported, coin.Denom)
+ }
+
+ // Result of (outgoing + amount) must be less than current supply
+ if !supply.CurrentSupply.IsGTE(supply.OutgoingSupply.Add(coin)) {
+ return sdkerrors.Wrapf(types.ErrExceedsAvailableSupply, "swap amount %s, available supply %s", coin,
+ supply.CurrentSupply.Amount.Sub(supply.OutgoingSupply.Amount))
+ }
+
+ supply.OutgoingSupply = supply.OutgoingSupply.Add(coin)
+ k.SetAssetSupply(ctx, supply, []byte(coin.Denom))
+ return nil
+}
+
+// DecrementOutgoingAssetSupply decrements an asset's outoing supply
+func (k Keeper) DecrementOutgoingAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
+ supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom))
+ if !found {
+ return sdkerrors.Wrap(types.ErrAssetNotSupported, coin.Denom)
+ }
+
+ // Resulting outgoing supply must be greater than or equal to 0
+ // Use sdk.Int instead of sdk.Coin to prevent panic if true
+ if supply.OutgoingSupply.Amount.Sub(coin.Amount).IsNegative() {
+ return sdkerrors.Wrapf(types.ErrInvalidOutgoingSupply, "decrease %s, outgoing supply %s", coin, supply.OutgoingSupply)
+ }
+
+ supply.OutgoingSupply = supply.OutgoingSupply.Sub(coin)
+ k.SetAssetSupply(ctx, supply, []byte(coin.Denom))
+ return nil
+}
diff --git a/x/bep3/keeper/asset_test.go b/x/bep3/keeper/asset_test.go
new file mode 100644
index 00000000..9c167289
--- /dev/null
+++ b/x/bep3/keeper/asset_test.go
@@ -0,0 +1,415 @@
+package keeper_test
+
+import (
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3/keeper"
+ "github.com/kava-labs/kava/x/bep3/types"
+ "github.com/stretchr/testify/suite"
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+type AssetTestSuite struct {
+ suite.Suite
+
+ keeper keeper.Keeper
+ app app.TestApp
+ ctx sdk.Context
+}
+
+func (suite *AssetTestSuite) SetupTest() {
+ config := sdk.GetConfig()
+ app.SetBech32AddressPrefixes(config)
+
+ // Initialize test app and set context
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+
+ // Initialize genesis state
+ deputy, _ := sdk.AccAddressFromBech32(TestDeputy)
+ tApp.InitializeFromGenesisStates(NewBep3GenStateMulti(deputy))
+
+ keeper := tApp.GetBep3Keeper()
+
+ // Set asset supply with standard value for testing
+ supply := types.AssetSupply{
+ Denom: "bnb",
+ IncomingSupply: c("bnb", 5),
+ OutgoingSupply: c("bnb", 5),
+ CurrentSupply: c("bnb", 40),
+ Limit: c("bnb", 50),
+ }
+ keeper.SetAssetSupply(ctx, supply, []byte(supply.Denom))
+
+ suite.app = tApp
+ suite.ctx = ctx
+ suite.keeper = keeper
+ return
+}
+
+func (suite *AssetTestSuite) TestIncrementCurrentAssetSupply() {
+ type args struct {
+ coin sdk.Coin
+ }
+ testCases := []struct {
+ name string
+ args args
+ expectPass bool
+ }{
+ {
+ "normal",
+ args{
+ coin: c("bnb", 5),
+ },
+ true,
+ },
+ {
+ "equal limit",
+ args{
+ coin: c("bnb", 10),
+ },
+ true,
+ },
+ {
+ "exceeds limit",
+ args{
+ coin: c("bnb", 11),
+ },
+ false,
+ },
+ {
+ "unsupported asset",
+ args{
+ coin: c("xyz", 5),
+ },
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ suite.SetupTest()
+ suite.Run(tc.name, func() {
+ supplyKeyPrefix := []byte(tc.args.coin.Denom)
+
+ preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+ err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coin)
+ postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+
+ if tc.expectPass {
+ suite.True(found)
+ suite.NoError(err)
+ suite.Equal(preSupply.CurrentSupply.Add(tc.args.coin), postSupply.CurrentSupply)
+ } else {
+ suite.Error(err)
+ suite.Equal(preSupply.CurrentSupply, postSupply.CurrentSupply)
+ }
+ })
+ }
+}
+
+func (suite *AssetTestSuite) TestDecrementCurrentAssetSupply() {
+ type args struct {
+ coin sdk.Coin
+ }
+ testCases := []struct {
+ name string
+ args args
+ expectPass bool
+ }{
+ {
+ "normal",
+ args{
+ coin: c("bnb", 30),
+ },
+ true,
+ },
+ {
+ "equal current",
+ args{
+ coin: c("bnb", 40),
+ },
+ true,
+ },
+ {
+ "exceeds current",
+ args{
+ coin: c("bnb", 41),
+ },
+ false,
+ },
+ {
+ "unsupported asset",
+ args{
+ coin: c("xyz", 30),
+ },
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ suite.SetupTest()
+ suite.Run(tc.name, func() {
+ supplyKeyPrefix := []byte(tc.args.coin.Denom)
+
+ preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+ err := suite.keeper.DecrementCurrentAssetSupply(suite.ctx, tc.args.coin)
+ postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+
+ if tc.expectPass {
+ suite.True(found)
+ suite.NoError(err)
+ suite.True(preSupply.CurrentSupply.Sub(tc.args.coin).IsEqual(postSupply.CurrentSupply))
+ } else {
+ suite.Error(err)
+ suite.Equal(preSupply.CurrentSupply, postSupply.CurrentSupply)
+ }
+ })
+ }
+}
+
+func (suite *AssetTestSuite) TestIncrementIncomingAssetSupply() {
+ type args struct {
+ coin sdk.Coin
+ }
+ testCases := []struct {
+ name string
+ args args
+ expectPass bool
+ }{
+ {
+ "normal",
+ args{
+ coin: c("bnb", 2),
+ },
+ true,
+ },
+ {
+ "incoming + current = limit",
+ args{
+ coin: c("bnb", 5),
+ },
+ true,
+ },
+ {
+ "incoming + current > limit",
+ args{
+ coin: c("bnb", 6),
+ },
+ false,
+ },
+ {
+ "unsupported asset",
+ args{
+ coin: c("xyz", 2),
+ },
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ suite.SetupTest()
+ suite.Run(tc.name, func() {
+ supplyKeyPrefix := []byte(tc.args.coin.Denom)
+
+ preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+ err := suite.keeper.IncrementIncomingAssetSupply(suite.ctx, tc.args.coin)
+ postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+
+ if tc.expectPass {
+ suite.True(found)
+ suite.NoError(err)
+ suite.Equal(preSupply.IncomingSupply.Add(tc.args.coin), postSupply.IncomingSupply)
+ } else {
+ suite.Error(err)
+ suite.Equal(preSupply.IncomingSupply, postSupply.IncomingSupply)
+ }
+ })
+ }
+}
+
+func (suite *AssetTestSuite) TestDecrementIncomingAssetSupply() {
+ type args struct {
+ coin sdk.Coin
+ }
+ testCases := []struct {
+ name string
+ args args
+ expectPass bool
+ }{
+ {
+ "normal",
+ args{
+ coin: c("bnb", 4),
+ },
+ true,
+ },
+ {
+ "equal incoming",
+ args{
+ coin: c("bnb", 5),
+ },
+ true,
+ },
+ {
+ "exceeds incoming",
+ args{
+ coin: c("bnb", 6),
+ },
+ false,
+ },
+ {
+ "unsupported asset",
+ args{
+ coin: c("xyz", 4),
+ },
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ suite.SetupTest()
+ suite.Run(tc.name, func() {
+ supplyKeyPrefix := []byte(tc.args.coin.Denom)
+
+ preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+ err := suite.keeper.DecrementIncomingAssetSupply(suite.ctx, tc.args.coin)
+ postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+
+ if tc.expectPass {
+ suite.True(found)
+ suite.NoError(err)
+ suite.True(preSupply.IncomingSupply.Sub(tc.args.coin).IsEqual(postSupply.IncomingSupply))
+ } else {
+ suite.Error(err)
+ suite.Equal(preSupply.IncomingSupply, postSupply.IncomingSupply)
+ }
+ })
+ }
+}
+
+func (suite *AssetTestSuite) TestIncrementOutgoingAssetSupply() {
+ type args struct {
+ coin sdk.Coin
+ }
+ testCases := []struct {
+ name string
+ args args
+ expectPass bool
+ }{
+ {
+ "normal",
+ args{
+ coin: c("bnb", 30),
+ },
+ true,
+ },
+ {
+ "outgoing + amount = current",
+ args{
+ coin: c("bnb", 35),
+ },
+ true,
+ },
+ {
+ "outoing + amount > current",
+ args{
+ coin: c("bnb", 36),
+ },
+ false,
+ },
+ {
+ "unsupported asset",
+ args{
+ coin: c("xyz", 30),
+ },
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ suite.SetupTest()
+ suite.Run(tc.name, func() {
+ supplyKeyPrefix := []byte(tc.args.coin.Denom)
+
+ preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+ err := suite.keeper.IncrementOutgoingAssetSupply(suite.ctx, tc.args.coin)
+ postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+
+ if tc.expectPass {
+ suite.True(found)
+ suite.NoError(err)
+ suite.Equal(preSupply.OutgoingSupply.Add(tc.args.coin), postSupply.OutgoingSupply)
+ } else {
+ suite.Error(err)
+ suite.Equal(preSupply.OutgoingSupply, postSupply.OutgoingSupply)
+ }
+ })
+ }
+}
+
+func (suite *AssetTestSuite) TestDecrementOutgoingAssetSupply() {
+ type args struct {
+ coin sdk.Coin
+ }
+ testCases := []struct {
+ name string
+ args args
+ expectPass bool
+ }{
+ {
+ "normal",
+ args{
+ coin: c("bnb", 4),
+ },
+ true,
+ },
+ {
+ "equal outgoing",
+ args{
+ coin: c("bnb", 5),
+ },
+ true,
+ },
+ {
+ "exceeds outgoing",
+ args{
+ coin: c("bnb", 6),
+ },
+ false,
+ },
+ {
+ "unsupported asset",
+ args{
+ coin: c("xyz", 4),
+ },
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ suite.SetupTest()
+ suite.Run(tc.name, func() {
+ supplyKeyPrefix := []byte(tc.args.coin.Denom)
+
+ preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+ err := suite.keeper.DecrementOutgoingAssetSupply(suite.ctx, tc.args.coin)
+ postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix)
+
+ if tc.expectPass {
+ suite.True(found)
+ suite.NoError(err)
+ suite.True(preSupply.OutgoingSupply.Sub(tc.args.coin).IsEqual(postSupply.OutgoingSupply))
+ } else {
+ suite.Error(err)
+ suite.Equal(preSupply.OutgoingSupply, postSupply.OutgoingSupply)
+ }
+ })
+ }
+}
+
+func TestAssetTestSuite(t *testing.T) {
+ suite.Run(t, new(AssetTestSuite))
+}
diff --git a/x/bep3/keeper/integration_test.go b/x/bep3/keeper/integration_test.go
new file mode 100644
index 00000000..5e4d4a37
--- /dev/null
+++ b/x/bep3/keeper/integration_test.go
@@ -0,0 +1,94 @@
+package keeper_test
+
+import (
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3"
+ "github.com/kava-labs/kava/x/bep3/types"
+ "github.com/tendermint/tendermint/crypto"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+const (
+ TestSenderOtherChain = "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7"
+ TestRecipientOtherChain = "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7"
+ TestDeputy = "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj"
+)
+
+var (
+ StandardSupplyLimit = i(100000000000)
+ DenomMap = map[int]string{0: "btc", 1: "eth", 2: "bnb", 3: "xrp", 4: "dai"}
+ TestUser1 = sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1")))
+ TestUser2 = sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser2")))
+)
+
+func i(in int64) sdk.Int { return sdk.NewInt(in) }
+func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
+func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
+func ts(minOffset int) int64 { return tmtime.Now().Add(time.Duration(minOffset) * time.Minute).Unix() }
+
+func NewBep3GenStateMulti(deputy sdk.AccAddress) app.GenesisState {
+ bep3Genesis := types.GenesisState{
+ Params: bep3.Params{
+ BnbDeputyAddress: deputy,
+ MinBlockLock: types.DefaultMinBlockLock, // 80
+ MaxBlockLock: types.DefaultMaxBlockLock, // 360
+ SupportedAssets: types.AssetParams{
+ types.AssetParam{
+ Denom: "bnb",
+ CoinID: 714,
+ Limit: StandardSupplyLimit,
+ Active: true,
+ },
+ types.AssetParam{
+ Denom: "inc",
+ CoinID: 9999,
+ Limit: i(100),
+ Active: false,
+ },
+ },
+ },
+ }
+ return app.GenesisState{bep3.ModuleName: bep3.ModuleCdc.MustMarshalJSON(bep3Genesis)}
+}
+
+func atomicSwaps(ctx sdk.Context, count int) types.AtomicSwaps {
+ var swaps types.AtomicSwaps
+ for i := 0; i < count; i++ {
+ swap := atomicSwap(ctx, i)
+ swaps = append(swaps, swap)
+ }
+ return swaps
+}
+
+func atomicSwap(ctx sdk.Context, index int) types.AtomicSwap {
+ expireOffset := int64((index * 15) + 360) // Default expire height + offet to match timestamp
+ timestamp := ts(index) // One minute apart
+ randomNumber, _ := types.GenerateSecureRandomNumber()
+ randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ return types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash,
+ ctx.BlockHeight()+expireOffset, timestamp, TestUser1, TestUser2,
+ TestSenderOtherChain, TestRecipientOtherChain, 0, types.Open, true,
+ types.Incoming)
+}
+
+func assetSupplies(count int) types.AssetSupplies {
+ if count > 5 { // Max 5 asset supplies
+ return types.AssetSupplies{}
+ }
+
+ var supplies types.AssetSupplies
+
+ for i := 0; i < count; i++ {
+ supply := assetSupply(DenomMap[i])
+ supplies = append(supplies, supply)
+ }
+ return supplies
+}
+
+func assetSupply(denom string) types.AssetSupply {
+ return types.NewAssetSupply(denom, c(denom, 0), c(denom, 0), c(denom, 0), c(denom, 10000))
+}
diff --git a/x/bep3/keeper/keeper.go b/x/bep3/keeper/keeper.go
new file mode 100644
index 00000000..3a800422
--- /dev/null
+++ b/x/bep3/keeper/keeper.go
@@ -0,0 +1,226 @@
+package keeper
+
+import (
+ "fmt"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ "github.com/cosmos/cosmos-sdk/store/prefix"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/params/subspace"
+ "github.com/kava-labs/kava/x/bep3/types"
+ "github.com/tendermint/tendermint/libs/log"
+)
+
+// Keeper of the bep3 store
+type Keeper struct {
+ key sdk.StoreKey
+ cdc *codec.Codec
+ paramSubspace subspace.Subspace
+ supplyKeeper types.SupplyKeeper
+}
+
+// NewKeeper creates a bep3 keeper
+func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, sk types.SupplyKeeper, paramstore subspace.Subspace) Keeper {
+ if addr := sk.GetModuleAddress(types.ModuleName); addr == nil {
+ panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
+ }
+
+ if !paramstore.HasKeyTable() {
+ paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
+ }
+
+ keeper := Keeper{
+ key: key,
+ cdc: cdc,
+ paramSubspace: paramstore,
+ supplyKeeper: sk,
+ }
+ return keeper
+}
+
+// Logger returns a module-specific logger.
+func (k Keeper) Logger(ctx sdk.Context) log.Logger {
+ return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
+}
+
+// ------------------------------------------
+// Atomic Swaps
+// ------------------------------------------
+
+// SetAtomicSwap puts the AtomicSwap into the store, and updates any indexes.
+func (k Keeper) SetAtomicSwap(ctx sdk.Context, atomicSwap types.AtomicSwap) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix)
+ bz := k.cdc.MustMarshalBinaryLengthPrefixed(atomicSwap)
+ store.Set(atomicSwap.GetSwapID(), bz)
+}
+
+// GetAtomicSwap gets an AtomicSwap from the store.
+func (k Keeper) GetAtomicSwap(ctx sdk.Context, swapID []byte) (types.AtomicSwap, bool) {
+ var atomicSwap types.AtomicSwap
+
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix)
+ bz := store.Get(swapID)
+ if bz == nil {
+ return atomicSwap, false
+ }
+
+ k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &atomicSwap)
+ return atomicSwap, true
+}
+
+// RemoveAtomicSwap removes an AtomicSwap from the AtomicSwapKeyPrefix.
+func (k Keeper) RemoveAtomicSwap(ctx sdk.Context, swapID []byte) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix)
+ store.Delete(swapID)
+}
+
+// IterateAtomicSwaps provides an iterator over all stored AtomicSwaps.
+// For each AtomicSwap, cb will be called. If cb returns true, the iterator will close and stop.
+func (k Keeper) IterateAtomicSwaps(ctx sdk.Context, cb func(atomicSwap types.AtomicSwap) (stop bool)) {
+ iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix)
+
+ defer iterator.Close()
+ for ; iterator.Valid(); iterator.Next() {
+ var atomicSwap types.AtomicSwap
+ k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &atomicSwap)
+
+ if cb(atomicSwap) {
+ break
+ }
+ }
+}
+
+// GetAllAtomicSwaps returns all AtomicSwaps from the store
+func (k Keeper) GetAllAtomicSwaps(ctx sdk.Context) (atomicSwaps types.AtomicSwaps) {
+ k.IterateAtomicSwaps(ctx, func(atomicSwap types.AtomicSwap) bool {
+ atomicSwaps = append(atomicSwaps, atomicSwap)
+ return false
+ })
+ return
+}
+
+// ------------------------------------------
+// Atomic Swap Block Index
+// ------------------------------------------
+
+// InsertIntoByBlockIndex adds a swap ID and expiration time into the byBlock index.
+func (k Keeper) InsertIntoByBlockIndex(ctx sdk.Context, atomicSwap types.AtomicSwap) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapByBlockPrefix)
+ store.Set(types.GetAtomicSwapByHeightKey(atomicSwap.ExpireHeight, atomicSwap.GetSwapID()), atomicSwap.GetSwapID())
+}
+
+// RemoveFromByBlockIndex removes an AtomicSwap from the byBlock index.
+func (k Keeper) RemoveFromByBlockIndex(ctx sdk.Context, atomicSwap types.AtomicSwap) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapByBlockPrefix)
+ store.Delete(types.GetAtomicSwapByHeightKey(atomicSwap.ExpireHeight, atomicSwap.GetSwapID()))
+}
+
+// IterateAtomicSwapsByBlock provides an iterator over AtomicSwaps ordered by AtomicSwap expiration block
+// For each AtomicSwap cb will be called. If cb returns true the iterator will close and stop.
+func (k Keeper) IterateAtomicSwapsByBlock(ctx sdk.Context, inclusiveCutoffTime uint64, cb func(swapID []byte) (stop bool)) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapByBlockPrefix)
+ iterator := store.Iterator(
+ nil, // start at the very start of the prefix store
+ sdk.PrefixEndBytes(types.Uint64ToBytes(inclusiveCutoffTime)), // end of range
+ )
+
+ defer iterator.Close()
+ for ; iterator.Valid(); iterator.Next() {
+
+ id := iterator.Value()
+
+ if cb(id) {
+ break
+ }
+ }
+}
+
+// ------------------------------------------
+// Atomic Swap Longterm Storage Index
+// ------------------------------------------
+
+// InsertIntoLongtermStorage adds a swap ID and deletion time into the longterm storage index.
+// Completed swaps are stored for 1 week.
+func (k Keeper) InsertIntoLongtermStorage(ctx sdk.Context, atomicSwap types.AtomicSwap) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapLongtermStoragePrefix)
+ store.Set(types.GetAtomicSwapByHeightKey(atomicSwap.ClosedBlock+types.DefaultLongtermStorageDuration,
+ atomicSwap.GetSwapID()), atomicSwap.GetSwapID())
+}
+
+// RemoveFromLongtermStorage removes a swap from the into the longterm storage index
+func (k Keeper) RemoveFromLongtermStorage(ctx sdk.Context, atomicSwap types.AtomicSwap) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapLongtermStoragePrefix)
+ store.Delete(types.GetAtomicSwapByHeightKey(atomicSwap.ClosedBlock+types.DefaultLongtermStorageDuration,
+ atomicSwap.GetSwapID()))
+}
+
+// IterateAtomicSwapsLongtermStorage provides an iterator over AtomicSwaps ordered by deletion height.
+// For each AtomicSwap cb will be called. If cb returns true the iterator will close and stop.
+func (k Keeper) IterateAtomicSwapsLongtermStorage(ctx sdk.Context, inclusiveCutoffTime uint64,
+ cb func(swapID []byte) (stop bool)) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapLongtermStoragePrefix)
+ iterator := store.Iterator(
+ nil, // start at the very start of the prefix store
+ sdk.PrefixEndBytes(types.Uint64ToBytes(inclusiveCutoffTime)), // end of range
+ )
+
+ defer iterator.Close()
+ for ; iterator.Valid(); iterator.Next() {
+
+ id := iterator.Value()
+
+ if cb(id) {
+ break
+ }
+ }
+}
+
+// ------------------------------------------
+// Asset Supplies
+// ------------------------------------------
+
+// GetAssetSupply gets an asset's current supply from the store.
+func (k Keeper) GetAssetSupply(ctx sdk.Context, denom []byte) (types.AssetSupply, bool) {
+ var supply types.AssetSupply
+
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AssetSupplyKeyPrefix)
+ bz := store.Get(denom)
+ if bz == nil {
+ return types.AssetSupply{}, false
+ }
+
+ k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &supply)
+ return supply, true
+}
+
+// SetAssetSupply updates an asset's current active supply
+func (k Keeper) SetAssetSupply(ctx sdk.Context, supply types.AssetSupply, denom []byte) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.AssetSupplyKeyPrefix)
+ bz := k.cdc.MustMarshalBinaryLengthPrefixed(supply)
+ store.Set(denom, bz)
+}
+
+// IterateAssetSupplies provides an iterator over current asset supplies.
+// For each asset supply, cb will be called. If cb returns true, the iterator will close and stop.
+func (k Keeper) IterateAssetSupplies(ctx sdk.Context, cb func(supply types.AssetSupply) (stop bool)) {
+ iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.AssetSupplyKeyPrefix)
+
+ defer iterator.Close()
+ for ; iterator.Valid(); iterator.Next() {
+ var supply types.AssetSupply
+ k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &supply)
+
+ if cb(supply) {
+ break
+ }
+ }
+}
+
+// GetAllAssetSupplies returns current asset supplies from the store as an array of sdk.Coin
+func (k Keeper) GetAllAssetSupplies(ctx sdk.Context) (supplies types.AssetSupplies) {
+ k.IterateAssetSupplies(ctx, func(supply types.AssetSupply) bool {
+ supplies = append(supplies, supply)
+ return false
+ })
+ return
+}
diff --git a/x/bep3/keeper/keeper_test.go b/x/bep3/keeper/keeper_test.go
new file mode 100644
index 00000000..9837a747
--- /dev/null
+++ b/x/bep3/keeper/keeper_test.go
@@ -0,0 +1,370 @@
+package keeper_test
+
+import (
+ "testing"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3/keeper"
+ "github.com/kava-labs/kava/x/bep3/types"
+ "github.com/stretchr/testify/suite"
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+const LongtermStorageDuration = 86400
+
+type KeeperTestSuite struct {
+ suite.Suite
+
+ keeper keeper.Keeper
+ app app.TestApp
+ ctx sdk.Context
+}
+
+func (suite *KeeperTestSuite) SetupTest() {
+ config := sdk.GetConfig()
+ app.SetBech32AddressPrefixes(config)
+ suite.ResetChain()
+ return
+}
+
+func (suite *KeeperTestSuite) ResetChain() {
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+ keeper := tApp.GetBep3Keeper()
+
+ suite.app = tApp
+ suite.ctx = ctx
+ suite.keeper = keeper
+}
+
+func (suite *KeeperTestSuite) TestGetSetAtomicSwap() {
+ suite.ResetChain()
+
+ // Set new atomic swap
+ atomicSwap := atomicSwap(suite.ctx, 1)
+ suite.keeper.SetAtomicSwap(suite.ctx, atomicSwap)
+
+ // Check atomic swap in store
+ s, found := suite.keeper.GetAtomicSwap(suite.ctx, atomicSwap.GetSwapID())
+ suite.True(found)
+ suite.Equal(atomicSwap, s)
+
+ // Check fake atomic swap not in store
+ fakeSwapID := types.CalculateSwapID(atomicSwap.RandomNumberHash, TestUser2, "otheraddress")
+ _, found = suite.keeper.GetAtomicSwap(suite.ctx, fakeSwapID)
+ suite.False(found)
+}
+
+func (suite *KeeperTestSuite) TestRemoveAtomicSwap() {
+ suite.ResetChain()
+
+ // Set new atomic swap
+ atomicSwap := atomicSwap(suite.ctx, 1)
+ suite.keeper.SetAtomicSwap(suite.ctx, atomicSwap)
+
+ // Check atomic swap in store
+ s, found := suite.keeper.GetAtomicSwap(suite.ctx, atomicSwap.GetSwapID())
+ suite.True(found)
+ suite.Equal(atomicSwap, s)
+
+ suite.keeper.RemoveAtomicSwap(suite.ctx, atomicSwap.GetSwapID())
+
+ // Check atomic swap not in store
+ _, found = suite.keeper.GetAtomicSwap(suite.ctx, atomicSwap.GetSwapID())
+ suite.False(found)
+}
+func (suite *KeeperTestSuite) TestIterateAtomicSwaps() {
+ suite.ResetChain()
+
+ // Set atomic swaps
+ atomicSwaps := atomicSwaps(suite.ctx, 4)
+ for _, s := range atomicSwaps {
+ suite.keeper.SetAtomicSwap(suite.ctx, s)
+ }
+
+ // Read each atomic swap from the store
+ var readAtomicSwaps types.AtomicSwaps
+ suite.keeper.IterateAtomicSwaps(suite.ctx, func(a types.AtomicSwap) bool {
+ readAtomicSwaps = append(readAtomicSwaps, a)
+ return false
+ })
+
+ // Check expected values
+ suite.Equal(len(atomicSwaps), len(readAtomicSwaps))
+}
+
+func (suite *KeeperTestSuite) TestGetAllAtomicSwaps() {
+ suite.ResetChain()
+
+ // Set atomic swaps
+ atomicSwaps := atomicSwaps(suite.ctx, 4)
+ for _, s := range atomicSwaps {
+ suite.keeper.SetAtomicSwap(suite.ctx, s)
+ }
+
+ // Get and check atomic swaps
+ res := suite.keeper.GetAllAtomicSwaps(suite.ctx)
+ suite.Equal(4, len(res))
+}
+
+func (suite *KeeperTestSuite) TestInsertIntoByBlockIndex() {
+ suite.ResetChain()
+
+ // Set new atomic swap in by block index
+ atomicSwap := atomicSwap(suite.ctx, 1)
+ suite.keeper.InsertIntoByBlockIndex(suite.ctx, atomicSwap)
+
+ // Block index lacks getter methods, must use iteration to get count of swaps in store
+ var swapIDs [][]byte
+ suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, uint64(atomicSwap.ExpireHeight+1), func(id []byte) bool {
+ swapIDs = append(swapIDs, id)
+ return false
+ })
+ suite.Equal(len(swapIDs), 1)
+
+ // Marshal the expected swapID
+ cdc := suite.app.Codec()
+ res, _ := cdc.MarshalBinaryBare(atomicSwap.GetSwapID())
+ expectedSwapID := res[1:]
+
+ suite.Equal(expectedSwapID, swapIDs[0])
+}
+
+func (suite *KeeperTestSuite) TestRemoveFromByBlockIndex() {
+ suite.ResetChain()
+
+ // Set new atomic swap in by block index
+ atomicSwap := atomicSwap(suite.ctx, 1)
+ suite.keeper.InsertIntoByBlockIndex(suite.ctx, atomicSwap)
+
+ // Check stored data in block index
+ var swapIDsPre [][]byte
+ suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, uint64(atomicSwap.ExpireHeight+1), func(id []byte) bool {
+ swapIDsPre = append(swapIDsPre, id)
+ return false
+ })
+ suite.Equal(len(swapIDsPre), 1)
+
+ suite.keeper.RemoveFromByBlockIndex(suite.ctx, atomicSwap)
+
+ // Check stored data not in block index
+ var swapIDsPost [][]byte
+ suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, uint64(atomicSwap.ExpireHeight+1), func(id []byte) bool {
+ swapIDsPost = append(swapIDsPost, id)
+ return false
+ })
+ suite.Equal(len(swapIDsPost), 0)
+}
+
+func (suite *KeeperTestSuite) TestIterateAtomicSwapsByBlock() {
+ suite.ResetChain()
+
+ type args struct {
+ blockCtx sdk.Context
+ swap types.AtomicSwap
+ }
+
+ var testCases []args
+ for i := 0; i < 8; i++ {
+ // Set up context 100 blocks apart
+ blockCtx := suite.ctx.WithBlockHeight(int64(i) * 100)
+
+ // Initialize a new atomic swap (different randomNumberHash = different swap IDs)
+ timestamp := tmtime.Now().Add(time.Duration(i) * time.Minute).Unix()
+ randomNumber, _ := types.GenerateSecureRandomNumber()
+ randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ atomicSwap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash,
+ blockCtx.BlockHeight(), timestamp, TestUser1, TestUser2,
+ TestSenderOtherChain, TestRecipientOtherChain, 0, types.Open,
+ true, types.Incoming)
+
+ // Insert into block index
+ suite.keeper.InsertIntoByBlockIndex(blockCtx, atomicSwap)
+ // Add to local block index
+ testCases = append(testCases, args{blockCtx, atomicSwap})
+ }
+
+ // Set up the expected swap IDs for a given cutoff block
+ cutoffBlock := int64(450)
+ var expectedSwapIDs [][]byte
+ for _, tc := range testCases {
+ if tc.blockCtx.BlockHeight() < cutoffBlock || tc.blockCtx.BlockHeight() == cutoffBlock {
+ expectedSwapIDs = append(expectedSwapIDs, tc.swap.GetSwapID())
+ }
+ }
+
+ // Read the swap IDs from store for a given cutoff block
+ var readSwapIDs [][]byte
+ suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, uint64(cutoffBlock), func(id []byte) bool {
+ readSwapIDs = append(readSwapIDs, id)
+ return false
+ })
+
+ suite.Equal(expectedSwapIDs, readSwapIDs)
+}
+
+func (suite *KeeperTestSuite) TestInsertIntoLongtermStorage() {
+ suite.ResetChain()
+
+ // Set atomic swap in longterm storage
+ atomicSwap := atomicSwap(suite.ctx, 1)
+ atomicSwap.ClosedBlock = suite.ctx.BlockHeight()
+ suite.keeper.InsertIntoLongtermStorage(suite.ctx, atomicSwap)
+
+ // Longterm storage lacks getter methods, must use iteration to get count of swaps in store
+ var swapIDs [][]byte
+ suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(atomicSwap.ClosedBlock+LongtermStorageDuration), func(id []byte) bool {
+ swapIDs = append(swapIDs, id)
+ return false
+ })
+ suite.Equal(len(swapIDs), 1)
+
+ // Marshal the expected swapID
+ cdc := suite.app.Codec()
+ res, _ := cdc.MarshalBinaryBare(atomicSwap.GetSwapID())
+ expectedSwapID := res[1:]
+
+ suite.Equal(expectedSwapID, swapIDs[0])
+}
+
+func (suite *KeeperTestSuite) TestRemoveFromLongtermStorage() {
+ suite.ResetChain()
+
+ // Set atomic swap in longterm storage
+ atomicSwap := atomicSwap(suite.ctx, 1)
+ atomicSwap.ClosedBlock = suite.ctx.BlockHeight()
+ suite.keeper.InsertIntoLongtermStorage(suite.ctx, atomicSwap)
+
+ // Longterm storage lacks getter methods, must use iteration to get count of swaps in store
+ var swapIDs [][]byte
+ suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(atomicSwap.ClosedBlock+LongtermStorageDuration), func(id []byte) bool {
+ swapIDs = append(swapIDs, id)
+ return false
+ })
+ suite.Equal(len(swapIDs), 1)
+
+ suite.keeper.RemoveFromLongtermStorage(suite.ctx, atomicSwap)
+
+ // Check stored data not in block index
+ var swapIDsPost [][]byte
+ suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(atomicSwap.ClosedBlock+LongtermStorageDuration), func(id []byte) bool {
+ swapIDsPost = append(swapIDsPost, id)
+ return false
+ })
+ suite.Equal(len(swapIDsPost), 0)
+}
+
+func (suite *KeeperTestSuite) TestIterateAtomicSwapsLongtermStorage() {
+ suite.ResetChain()
+
+ // Set up atomic swaps with stagged closed blocks
+ var swaps types.AtomicSwaps
+ for i := 0; i < 8; i++ {
+ timestamp := tmtime.Now().Unix()
+ randomNumber, _ := types.GenerateSecureRandomNumber()
+ randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ atomicSwap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash,
+ suite.ctx.BlockHeight(), timestamp, TestUser1, TestUser2,
+ TestSenderOtherChain, TestRecipientOtherChain, 100, types.Open,
+ true, types.Incoming)
+
+ // Set closed block staggered by 100 blocks and insert into longterm storage
+ atomicSwap.ClosedBlock = int64(i) * 100
+ suite.keeper.InsertIntoLongtermStorage(suite.ctx, atomicSwap)
+ // Add to local longterm storage
+ swaps = append(swaps, atomicSwap)
+ }
+
+ // Set up the expected swap IDs for a given cutoff block.
+ cutoffBlock := int64(LongtermStorageDuration + 350)
+ var expectedSwapIDs [][]byte
+ for _, swap := range swaps {
+ if swap.ClosedBlock+LongtermStorageDuration < cutoffBlock ||
+ swap.ClosedBlock+LongtermStorageDuration == cutoffBlock {
+ expectedSwapIDs = append(expectedSwapIDs, swap.GetSwapID())
+ }
+ }
+
+ // Read the swap IDs from store for a given cutoff block
+ var readSwapIDs [][]byte
+ suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(cutoffBlock), func(id []byte) bool {
+ readSwapIDs = append(readSwapIDs, id)
+ return false
+ })
+
+ // At the cutoff block, iteration should return half of the swap IDs
+ suite.Equal(len(swaps)/2, len(expectedSwapIDs))
+ suite.Equal(len(swaps)/2, len(readSwapIDs))
+ // Should be the same IDs
+ suite.Equal(expectedSwapIDs, readSwapIDs)
+}
+
+func (suite *KeeperTestSuite) TestGetSetAssetSupply() {
+ suite.ResetChain()
+
+ denom := "bnb"
+ // Put asset supply in store
+ assetSupply := types.NewAssetSupply(denom, c(denom, 0), c(denom, 0), c(denom, 50000), c(denom, 100000))
+ suite.keeper.SetAssetSupply(suite.ctx, assetSupply, []byte(denom))
+
+ // Check asset in store
+ storedAssetSupply, found := suite.keeper.GetAssetSupply(suite.ctx, []byte(denom))
+ suite.True(found)
+ suite.Equal(assetSupply, storedAssetSupply)
+
+ // Check fake asset supply not in store
+ fakeDenom := "xyz"
+ _, found = suite.keeper.GetAssetSupply(suite.ctx, []byte(fakeDenom))
+ suite.False(found)
+}
+
+func (suite *KeeperTestSuite) TestIterateAssetSupplies() {
+ suite.ResetChain()
+
+ // Set asset supplies
+ supplies := assetSupplies(5)
+ for _, supply := range supplies {
+ suite.keeper.SetAssetSupply(suite.ctx, supply, []byte(supply.Denom))
+ }
+
+ // Read each asset supply from the store
+ var readSupplies types.AssetSupplies
+ suite.keeper.IterateAssetSupplies(suite.ctx, func(a types.AssetSupply) bool {
+ readSupplies = append(readSupplies, a)
+ return false
+ })
+
+ // Check expected values
+ for i := 0; i < len(supplies); i++ {
+ suite.Contains(readSupplies, supplies[i])
+ }
+}
+
+func (suite *KeeperTestSuite) TestGetAllAssetSupplies() {
+ suite.ResetChain()
+
+ // Set asset supplies
+ count := 3
+ supplies := assetSupplies(count)
+ for _, supply := range supplies {
+ suite.keeper.SetAssetSupply(suite.ctx, supply, []byte(supply.Denom))
+ }
+
+ // Get all asset supplies
+ readSupplies := suite.keeper.GetAllAssetSupplies(suite.ctx)
+ suite.Equal(count, len(readSupplies))
+
+ // Check expected values
+ for i := 0; i < count; i++ {
+ suite.Contains(readSupplies, supplies[i])
+ }
+}
+
+func TestKeeperTestSuite(t *testing.T) {
+ suite.Run(t, new(KeeperTestSuite))
+}
diff --git a/x/bep3/keeper/params.go b/x/bep3/keeper/params.go
new file mode 100644
index 00000000..f3dc1abf
--- /dev/null
+++ b/x/bep3/keeper/params.go
@@ -0,0 +1,77 @@
+package keeper
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+// GetParams returns the total set of bep3 parameters.
+func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) {
+ k.paramSubspace.GetParamSet(ctx, ¶ms)
+ return params
+}
+
+// SetParams sets the bep3 parameters to the param space.
+func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
+ k.paramSubspace.SetParamSet(ctx, ¶ms)
+}
+
+// GetBnbDeputyAddress returns the Bnbchain's deputy address
+func (k Keeper) GetBnbDeputyAddress(ctx sdk.Context) sdk.AccAddress {
+ params := k.GetParams(ctx)
+ return params.BnbDeputyAddress
+}
+
+// GetMaxBlockLock returns the maximum block lock
+func (k Keeper) GetMaxBlockLock(ctx sdk.Context) int64 {
+ params := k.GetParams(ctx)
+ return params.MaxBlockLock
+}
+
+// GetMinBlockLock returns the minimum block lock
+func (k Keeper) GetMinBlockLock(ctx sdk.Context) int64 {
+ params := k.GetParams(ctx)
+ return params.MinBlockLock
+}
+
+// GetAssets returns a list containing all supported assets
+func (k Keeper) GetAssets(ctx sdk.Context) (types.AssetParams, bool) {
+ params := k.GetParams(ctx)
+ return params.SupportedAssets, len(params.SupportedAssets) > 0
+}
+
+// GetAssetByDenom returns an asset by its denom
+func (k Keeper) GetAssetByDenom(ctx sdk.Context, denom string) (types.AssetParam, bool) {
+ params := k.GetParams(ctx)
+ for _, asset := range params.SupportedAssets {
+ if asset.Denom == denom {
+ return asset, true
+ }
+ }
+ return types.AssetParam{}, false
+}
+
+// GetAssetByCoinID returns an asset by its denom
+func (k Keeper) GetAssetByCoinID(ctx sdk.Context, coinID int) (types.AssetParam, bool) {
+ params := k.GetParams(ctx)
+ for _, asset := range params.SupportedAssets {
+ if asset.CoinID == coinID {
+ return asset, true
+ }
+ }
+ return types.AssetParam{}, false
+}
+
+// ValidateLiveAsset checks if an asset is both supported and active
+func (k Keeper) ValidateLiveAsset(ctx sdk.Context, coin sdk.Coin) error {
+ asset, found := k.GetAssetByDenom(ctx, coin.Denom)
+ if !found {
+ return sdkerrors.Wrap(types.ErrAssetNotSupported, coin.Denom)
+ }
+ if !asset.Active {
+ return sdkerrors.Wrap(types.ErrAssetNotActive, asset.Denom)
+ }
+ return nil
+}
diff --git a/x/bep3/keeper/params_test.go b/x/bep3/keeper/params_test.go
new file mode 100644
index 00000000..3c4bae7d
--- /dev/null
+++ b/x/bep3/keeper/params_test.go
@@ -0,0 +1,142 @@
+package keeper_test
+
+import (
+ "errors"
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3/keeper"
+ "github.com/kava-labs/kava/x/bep3/types"
+ "github.com/stretchr/testify/suite"
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+type ParamsTestSuite struct {
+ suite.Suite
+
+ keeper keeper.Keeper
+ addrs []sdk.AccAddress
+ app app.TestApp
+ ctx sdk.Context
+}
+
+func (suite *ParamsTestSuite) SetupTest() {
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+ _, addrs := app.GeneratePrivKeyAddressPairs(10)
+ tApp.InitializeFromGenesisStates(NewBep3GenStateMulti(addrs[0]))
+ suite.keeper = tApp.GetBep3Keeper()
+ suite.ctx = ctx
+ suite.addrs = addrs
+}
+
+func (suite *ParamsTestSuite) TestGetSetBnbDeputyAddress() {
+ params := suite.keeper.GetParams(suite.ctx)
+ params.BnbDeputyAddress = suite.addrs[1]
+ suite.NotPanics(func() { suite.keeper.SetParams(suite.ctx, params) })
+
+ params = suite.keeper.GetParams(suite.ctx)
+ suite.Equal(suite.addrs[1], params.BnbDeputyAddress)
+ addr := suite.keeper.GetBnbDeputyAddress(suite.ctx)
+ suite.Equal(suite.addrs[1], addr)
+}
+
+func (suite *ParamsTestSuite) TestGetMaxBlockLock() {
+ params := suite.keeper.GetParams(suite.ctx)
+ maxBlockLock := params.MaxBlockLock
+
+ res := suite.keeper.GetMaxBlockLock(suite.ctx)
+ suite.Equal(maxBlockLock, res)
+}
+
+func (suite *ParamsTestSuite) TestGetMinBlockLock() {
+ params := suite.keeper.GetParams(suite.ctx)
+ minBlockLock := params.MinBlockLock
+
+ res := suite.keeper.GetMinBlockLock(suite.ctx)
+ suite.Equal(minBlockLock, res)
+}
+
+func (suite *ParamsTestSuite) TestGetAssets() {
+ params := suite.keeper.GetParams(suite.ctx)
+ assets := params.SupportedAssets
+
+ res, found := suite.keeper.GetAssets(suite.ctx)
+ suite.True(found)
+ suite.Equal(assets, res)
+}
+
+func (suite *ParamsTestSuite) TestGetAssetByDenom() {
+ params := suite.keeper.GetParams(suite.ctx)
+ asset := params.SupportedAssets[0]
+
+ res, found := suite.keeper.GetAssetByDenom(suite.ctx, asset.Denom)
+ suite.True(found)
+ suite.Equal(asset, res)
+}
+
+func (suite *ParamsTestSuite) TestGetAssetByCoinID() {
+ params := suite.keeper.GetParams(suite.ctx)
+ asset := params.SupportedAssets[0]
+
+ res, found := suite.keeper.GetAssetByCoinID(suite.ctx, asset.CoinID)
+ suite.True(found)
+ suite.Equal(asset, res)
+}
+
+func (suite *AssetTestSuite) TestValidateLiveAsset() {
+ type args struct {
+ coin sdk.Coin
+ }
+ testCases := []struct {
+ name string
+ args args
+ expectedError error
+ expectPass bool
+ }{
+ {
+ "normal",
+ args{
+ coin: c("bnb", 1),
+ },
+ nil,
+ true,
+ },
+ {
+ "asset not supported",
+ args{
+ coin: c("bad", 1),
+ },
+ types.ErrAssetNotSupported,
+ false,
+ },
+ {
+ "asset not active",
+ args{
+ coin: c("inc", 1),
+ },
+ types.ErrAssetNotActive,
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ suite.SetupTest()
+ suite.Run(tc.name, func() {
+ err := suite.keeper.ValidateLiveAsset(suite.ctx, tc.args.coin)
+
+ if tc.expectPass {
+ suite.Require().NoError(err)
+ } else {
+ suite.Require().Error(err)
+ suite.Require().True(errors.Is(err, tc.expectedError))
+ }
+ })
+ }
+}
+
+func TestParamsTestSuite(t *testing.T) {
+ suite.Run(t, new(ParamsTestSuite))
+}
diff --git a/x/bep3/keeper/querier.go b/x/bep3/keeper/querier.go
new file mode 100644
index 00000000..8cc573b6
--- /dev/null
+++ b/x/bep3/keeper/querier.go
@@ -0,0 +1,103 @@
+package keeper
+
+import (
+ abci "github.com/tendermint/tendermint/abci/types"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+// NewQuerier is the module level router for state queries
+func NewQuerier(keeper Keeper) sdk.Querier {
+ return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
+ switch path[0] {
+ case types.QueryGetAssetSupply:
+ return queryAssetSupply(ctx, req, keeper)
+ case types.QueryGetAtomicSwap:
+ return queryAtomicSwap(ctx, req, keeper)
+ case types.QueryGetAtomicSwaps:
+ return queryAtomicSwaps(ctx, req, keeper)
+ case types.QueryGetParams:
+ return queryGetParams(ctx, req, keeper)
+ default:
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
+ }
+ }
+}
+
+func queryAssetSupply(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
+ // Decode request
+ var requestParams types.QueryAssetSupply
+ err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams)
+ if err != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
+ }
+
+ assetSupply, found := keeper.GetAssetSupply(ctx, []byte(requestParams.Denom))
+ if !found {
+ return nil, sdkerrors.Wrap(types.ErrAssetSupplyNotFound, string(requestParams.Denom))
+ }
+
+ // Encode results
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, assetSupply)
+ if err != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
+ }
+
+ return bz, nil
+}
+
+func queryAtomicSwap(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
+ // Decode request
+ var requestParams types.QueryAtomicSwapByID
+ err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams)
+ if err != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
+ }
+
+ // Lookup atomic swap
+ atomicSwap, found := keeper.GetAtomicSwap(ctx, requestParams.SwapID)
+ if !found {
+ return nil, sdkerrors.Wrapf(types.ErrAtomicSwapNotFound, "%d", requestParams.SwapID)
+ }
+
+ // Encode results
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, atomicSwap)
+ if err != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
+ }
+
+ return bz, nil
+}
+
+func queryAtomicSwaps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err error) {
+ var swaps types.AtomicSwaps
+
+ keeper.IterateAtomicSwaps(ctx, func(s types.AtomicSwap) bool {
+ swaps = append(swaps, s)
+ return false
+ })
+
+ bz, err2 := codec.MarshalJSONIndent(types.ModuleCdc, swaps)
+ if err2 != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
+ }
+
+ return bz, nil
+}
+
+// query params in the bep3 store
+func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
+ // Get params
+ params := keeper.GetParams(ctx)
+
+ // Encode results
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, params)
+ if err != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
+ }
+ return bz, nil
+}
diff --git a/x/bep3/keeper/querier_test.go b/x/bep3/keeper/querier_test.go
new file mode 100644
index 00000000..ddf705e5
--- /dev/null
+++ b/x/bep3/keeper/querier_test.go
@@ -0,0 +1,170 @@
+package keeper_test
+
+import (
+ "encoding/hex"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+ tmtime "github.com/tendermint/tendermint/types/time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3/keeper"
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+const (
+ custom = "custom"
+)
+
+type QuerierTestSuite struct {
+ suite.Suite
+ keeper keeper.Keeper
+ app app.TestApp
+ ctx sdk.Context
+ querier sdk.Querier
+ addrs []sdk.AccAddress
+ isSupplyDenom map[string]bool
+ swapIDs []tmbytes.HexBytes
+ isSwapID map[string]bool
+}
+
+func (suite *QuerierTestSuite) SetupTest() {
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+
+ // Set up auth GenesisState
+ _, addrs := app.GeneratePrivKeyAddressPairs(10)
+ coins := []sdk.Coins{}
+ for j := 0; j < 10; j++ {
+ coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000)))
+ }
+ authGS := app.NewAuthGenState(addrs, coins)
+
+ tApp.InitializeFromGenesisStates(
+ authGS,
+ NewBep3GenStateMulti(addrs[0]),
+ )
+
+ suite.ctx = ctx
+ suite.app = tApp
+ suite.keeper = tApp.GetBep3Keeper()
+ suite.querier = keeper.NewQuerier(suite.keeper)
+ suite.addrs = addrs
+
+ // Create atomic swaps and save IDs
+ var swapIDs []tmbytes.HexBytes
+ isSwapID := make(map[string]bool)
+ for i := 0; i < 10; i++ {
+ // Set up atomic swap variables
+ expireHeight := int64(360)
+ amount := cs(c("bnb", 100))
+ timestamp := ts(0)
+ randomNumber, _ := types.GenerateSecureRandomNumber()
+ randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ // Create atomic swap and check err
+ err := suite.keeper.CreateAtomicSwap(suite.ctx, randomNumberHash, timestamp, expireHeight,
+ addrs[0], suite.addrs[i], TestSenderOtherChain, TestRecipientOtherChain, amount,
+ amount.String(), true)
+ suite.Nil(err)
+
+ // Calculate swap ID and save
+ swapID := types.CalculateSwapID(randomNumberHash, addrs[0], TestSenderOtherChain)
+ swapIDs = append(swapIDs, swapID)
+ isSwapID[hex.EncodeToString(swapID)] = true
+ }
+ suite.swapIDs = swapIDs
+ suite.isSwapID = isSwapID
+}
+
+func (suite *QuerierTestSuite) TestQueryAssetSupply() {
+ ctx := suite.ctx.WithIsCheckTx(false)
+
+ // Set up request query
+ denom := "bnb"
+ query := abci.RequestQuery{
+ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAssetSupply}, "/"),
+ Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAssetSupply(tmbytes.HexBytes(denom))),
+ }
+
+ // Execute query and check the []byte result
+ bz, err := suite.querier(ctx, []string{types.QueryGetAssetSupply}, query)
+ suite.Nil(err)
+ suite.NotNil(bz)
+
+ // Unmarshal the bytes into type asset supply
+ var supply types.AssetSupply
+ suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &supply))
+
+ expectedSupply := types.NewAssetSupply(denom, c(denom, 1000),
+ c(denom, 0), c(denom, 0), c(denom, StandardSupplyLimit.Int64()))
+ suite.Equal(supply, expectedSupply)
+}
+
+func (suite *QuerierTestSuite) TestQueryAtomicSwap() {
+ ctx := suite.ctx.WithIsCheckTx(false)
+
+ // Set up request query
+ query := abci.RequestQuery{
+ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAtomicSwap}, "/"),
+ Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAtomicSwapByID(suite.swapIDs[0])),
+ }
+
+ // Execute query and check the []byte result
+ bz, err := suite.querier(ctx, []string{types.QueryGetAtomicSwap}, query)
+ suite.Nil(err)
+ suite.NotNil(bz)
+
+ // Unmarshal the bytes into type atomic swap
+ var swap types.AtomicSwap
+ suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &swap))
+
+ // Check the returned atomic swap's ID
+ suite.True(suite.isSwapID[hex.EncodeToString(swap.GetSwapID())])
+}
+
+func (suite *QuerierTestSuite) TestQueryAtomicSwaps() {
+ ctx := suite.ctx.WithIsCheckTx(false)
+ // Set up request query
+ query := abci.RequestQuery{
+ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAtomicSwaps}, "/"),
+ Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAtomicSwaps(1, 100)),
+ }
+
+ bz, err := suite.querier(ctx, []string{types.QueryGetAtomicSwaps}, query)
+ suite.Nil(err)
+ suite.NotNil(bz)
+
+ var swaps types.AtomicSwaps
+ suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &swaps))
+
+ suite.Equal(len(suite.swapIDs), len(swaps))
+ for _, swap := range swaps {
+ suite.True(suite.isSwapID[hex.EncodeToString(swap.GetSwapID())])
+ }
+}
+
+func (suite *QuerierTestSuite) TestQueryParams() {
+ ctx := suite.ctx.WithIsCheckTx(false)
+ bz, err := suite.querier(ctx, []string{types.QueryGetParams}, abci.RequestQuery{})
+ suite.Nil(err)
+ suite.NotNil(bz)
+
+ var p types.Params
+ suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p))
+
+ bep3GenesisState := NewBep3GenStateMulti(suite.addrs[0])
+ gs := types.GenesisState{}
+ types.ModuleCdc.UnmarshalJSON(bep3GenesisState["bep3"], &gs)
+ suite.Equal(gs.Params, p)
+}
+
+func TestQuerierTestSuite(t *testing.T) {
+ suite.Run(t, new(QuerierTestSuite))
+}
diff --git a/x/bep3/keeper/swap.go b/x/bep3/keeper/swap.go
new file mode 100644
index 00000000..58d53121
--- /dev/null
+++ b/x/bep3/keeper/swap.go
@@ -0,0 +1,274 @@
+package keeper
+
+import (
+ "bytes"
+ "encoding/hex"
+ "fmt"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+// CreateAtomicSwap creates a new AtomicSwap
+func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, timestamp int64, heightSpan int64,
+ sender sdk.AccAddress, recipient sdk.AccAddress, senderOtherChain, recipientOtherChain string,
+ amount sdk.Coins, expectedIncome string, crossChain bool) error {
+ // Confirm that this is not a duplicate swap
+ swapID := types.CalculateSwapID(randomNumberHash, sender, senderOtherChain)
+ _, found := k.GetAtomicSwap(ctx, swapID)
+ if found {
+ return sdkerrors.Wrap(types.ErrAtomicSwapAlreadyExists, hex.EncodeToString(swapID))
+ }
+
+ // The heightSpan period should be more than 10 minutes and less than one week
+ // Assume average block time interval is 10 second. 10 mins = 60 blocks, 1 week = 60480 blocks
+ if heightSpan < k.GetMinBlockLock(ctx) || heightSpan > k.GetMaxBlockLock(ctx) {
+ return sdkerrors.Wrapf(types.ErrInvalidHeightSpan, "height span %d, range %d - %d", heightSpan, k.GetMinBlockLock(ctx), k.GetMaxBlockLock(ctx))
+ }
+
+ // Unix timestamp must be in range [-15 mins, 30 mins] of the current time
+ pastTimestampLimit := ctx.BlockTime().Add(time.Duration(-15) * time.Minute).Unix()
+ futureTimestampLimit := ctx.BlockTime().Add(time.Duration(30) * time.Minute).Unix()
+ if timestamp < pastTimestampLimit || timestamp >= futureTimestampLimit {
+ return sdkerrors.Wrap(types.ErrInvalidTimestamp, time.Unix(timestamp, 0).UTC().String())
+ }
+
+ if len(amount) != 1 {
+ return fmt.Errorf("amount must contain exactly one coin")
+ }
+
+ err := k.ValidateLiveAsset(ctx, amount[0])
+ if err != nil {
+ return err
+ }
+
+ var direction types.SwapDirection
+ deputy := k.GetBnbDeputyAddress(ctx)
+ if sender.Equals(deputy) {
+ direction = types.Incoming
+ } else {
+ direction = types.Outgoing
+ }
+
+ switch direction {
+ case types.Incoming:
+ err = k.IncrementIncomingAssetSupply(ctx, amount[0])
+ case types.Outgoing:
+ err = k.IncrementOutgoingAssetSupply(ctx, amount[0])
+ default:
+ err = fmt.Errorf("invalid swap direction: %s", direction.String())
+ }
+
+ if err != nil {
+ return err
+ }
+
+ // Transfer coins to module
+ err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amount)
+ if err != nil {
+ return err
+ }
+
+ // Store the details of the swap
+ atomicSwap := types.NewAtomicSwap(amount, randomNumberHash, ctx.BlockHeight()+heightSpan,
+ timestamp, sender, recipient, senderOtherChain, recipientOtherChain, 0, types.Open,
+ crossChain, direction)
+
+ k.SetAtomicSwap(ctx, atomicSwap)
+ k.InsertIntoByBlockIndex(ctx, atomicSwap)
+
+ // Emit 'create_atomic_swap' event
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ types.EventTypeCreateAtomicSwap,
+ sdk.NewAttribute(types.AttributeKeySender, atomicSwap.Sender.String()),
+ sdk.NewAttribute(types.AttributeKeyRecipient, atomicSwap.Recipient.String()),
+ sdk.NewAttribute(types.AttributeKeyAtomicSwapID, hex.EncodeToString(atomicSwap.GetSwapID())),
+ sdk.NewAttribute(types.AttributeKeyRandomNumberHash, hex.EncodeToString(atomicSwap.RandomNumberHash)),
+ sdk.NewAttribute(types.AttributeKeyTimestamp, fmt.Sprintf("%d", atomicSwap.Timestamp)),
+ sdk.NewAttribute(types.AttributeKeySenderOtherChain, atomicSwap.SenderOtherChain),
+ sdk.NewAttribute(types.AttributeKeyExpireHeight, fmt.Sprintf("%d", atomicSwap.ExpireHeight)),
+ sdk.NewAttribute(types.AttributeKeyAmount, atomicSwap.Amount[0].String()),
+ sdk.NewAttribute(types.AttributeKeyExpectedIncome, expectedIncome),
+ sdk.NewAttribute(types.AttributeKeyDirection, atomicSwap.Direction.String()),
+ ),
+ )
+
+ return nil
+}
+
+// ClaimAtomicSwap validates a claim attempt, and if successful, sends the escrowed amount and closes the AtomicSwap
+func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []byte, randomNumber []byte) error {
+ atomicSwap, found := k.GetAtomicSwap(ctx, swapID)
+ if !found {
+ return sdkerrors.Wrapf(types.ErrAtomicSwapNotFound, "%s", swapID)
+ }
+
+ // Only open atomic swaps can be claimed
+ if atomicSwap.Status != types.Open {
+ return types.ErrSwapNotClaimable
+ }
+
+ // Calculate hashed secret using submitted number
+ hashedSubmittedNumber := types.CalculateRandomHash(randomNumber, atomicSwap.Timestamp)
+ hashedSecret := types.CalculateSwapID(hashedSubmittedNumber, atomicSwap.Sender, atomicSwap.SenderOtherChain)
+
+ // Confirm that secret unlocks the atomic swap
+ if !bytes.Equal(hashedSecret, atomicSwap.GetSwapID()) {
+ return sdkerrors.Wrapf(types.ErrInvalidClaimSecret, "%s ≠ %s", hex.EncodeToString(hashedSecret), hex.EncodeToString(atomicSwap.GetSwapID()))
+ }
+
+ var err error
+ switch atomicSwap.Direction {
+ case types.Incoming:
+ err := k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0])
+ if err != nil {
+ break
+ }
+ err = k.IncrementCurrentAssetSupply(ctx, atomicSwap.Amount[0])
+ case types.Outgoing:
+ err = k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0])
+ if err != nil {
+ break
+ }
+ err = k.DecrementCurrentAssetSupply(ctx, atomicSwap.Amount[0])
+ default:
+ err = fmt.Errorf("invalid swap direction: %s", atomicSwap.Direction.String())
+ }
+
+ if err != nil {
+ return err
+ }
+
+ // Send intended recipient coins
+ err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Recipient, atomicSwap.Amount)
+ if err != nil {
+ return err
+ }
+
+ // Complete swap
+ atomicSwap.Status = types.Completed
+ atomicSwap.ClosedBlock = ctx.BlockHeight()
+ k.SetAtomicSwap(ctx, atomicSwap)
+
+ // Remove from byBlock index and transition to longterm storage
+ k.RemoveFromByBlockIndex(ctx, atomicSwap)
+ k.InsertIntoLongtermStorage(ctx, atomicSwap)
+
+ // Emit 'claim_atomic_swap' event
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ types.EventTypeClaimAtomicSwap,
+ sdk.NewAttribute(types.AttributeKeyClaimSender, from.String()),
+ sdk.NewAttribute(types.AttributeKeyRecipient, atomicSwap.Recipient.String()),
+ sdk.NewAttribute(types.AttributeKeyAtomicSwapID, hex.EncodeToString(atomicSwap.GetSwapID())),
+ sdk.NewAttribute(types.AttributeKeyRandomNumberHash, hex.EncodeToString(atomicSwap.RandomNumberHash)),
+ sdk.NewAttribute(types.AttributeKeyRandomNumber, hex.EncodeToString(randomNumber)),
+ ),
+ )
+
+ return nil
+}
+
+// RefundAtomicSwap refunds an AtomicSwap, sending assets to the original sender and closing the AtomicSwap
+func (k Keeper) RefundAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []byte) error {
+ atomicSwap, found := k.GetAtomicSwap(ctx, swapID)
+ if !found {
+ return sdkerrors.Wrapf(types.ErrAtomicSwapNotFound, "%s", swapID)
+ }
+ // Only expired swaps may be refunded
+ if atomicSwap.Status != types.Expired {
+ return types.ErrSwapNotRefundable
+ }
+
+ var err error
+ switch atomicSwap.Direction {
+ case types.Incoming:
+ err = k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0])
+ case types.Outgoing:
+ err = k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0])
+ default:
+ err = fmt.Errorf("invalid swap direction: %s", atomicSwap.Direction.String())
+ }
+
+ if err != nil {
+ return err
+ }
+
+ // Refund coins to original swap sender
+ err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Sender, atomicSwap.Amount)
+ if err != nil {
+ return err
+ }
+
+ // Complete swap
+ atomicSwap.Status = types.Completed
+ atomicSwap.ClosedBlock = ctx.BlockHeight()
+ k.SetAtomicSwap(ctx, atomicSwap)
+
+ // Transition to longterm storage
+ k.InsertIntoLongtermStorage(ctx, atomicSwap)
+
+ // Emit 'refund_atomic_swap' event
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ types.EventTypeRefundAtomicSwap,
+ sdk.NewAttribute(types.AttributeKeyRefundSender, fmt.Sprintf("%s", from)),
+ sdk.NewAttribute(types.AttributeKeySender, fmt.Sprintf("%s", atomicSwap.Sender)),
+ sdk.NewAttribute(types.AttributeKeyAtomicSwapID, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.GetSwapID()))),
+ sdk.NewAttribute(types.AttributeKeyRandomNumberHash, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.RandomNumberHash))),
+ ),
+ )
+
+ return nil
+}
+
+// UpdateExpiredAtomicSwaps finds all AtomicSwaps that are past (or at) their ending times and expires them.
+func (k Keeper) UpdateExpiredAtomicSwaps(ctx sdk.Context) error {
+ var expiredSwaps [][]byte
+ k.IterateAtomicSwapsByBlock(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool {
+ expiredSwaps = append(expiredSwaps, id)
+ return false
+ })
+
+ // Expire incomplete swaps (claimed swaps have already been removed from byBlock index)
+ var expiredSwapIDs []string
+ for _, id := range expiredSwaps {
+ atomicSwap, _ := k.GetAtomicSwap(ctx, id)
+ atomicSwap.Status = types.Expired
+ k.SetAtomicSwap(ctx, atomicSwap)
+ k.RemoveFromByBlockIndex(ctx, atomicSwap)
+ expiredSwapIDs = append(expiredSwapIDs, hex.EncodeToString(atomicSwap.GetSwapID()))
+ }
+
+ // Emit 'swaps_expired' event
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ types.EventTypeSwapsExpired,
+ sdk.NewAttribute(types.AttributeKeyAtomicSwapIDs, fmt.Sprintf("%s", expiredSwapIDs)),
+ sdk.NewAttribute(types.AttributeExpirationBlock, fmt.Sprintf("%d", ctx.BlockHeight())),
+ ),
+ )
+
+ return nil
+}
+
+// DeleteClosedAtomicSwapsFromLongtermStorage removes swaps one week after completion
+func (k Keeper) DeleteClosedAtomicSwapsFromLongtermStorage(ctx sdk.Context) error {
+ var swapsToDelete [][]byte
+ k.IterateAtomicSwapsLongtermStorage(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool {
+ swapsToDelete = append(swapsToDelete, id)
+ return false
+ })
+
+ // Delete closed atomic swaps
+ for _, id := range swapsToDelete {
+ swap, _ := k.GetAtomicSwap(ctx, id)
+ k.RemoveAtomicSwap(ctx, swap.GetSwapID())
+ k.RemoveFromLongtermStorage(ctx, swap)
+ }
+ return nil
+}
diff --git a/x/bep3/keeper/swap_test.go b/x/bep3/keeper/swap_test.go
new file mode 100644
index 00000000..3389cf58
--- /dev/null
+++ b/x/bep3/keeper/swap_test.go
@@ -0,0 +1,699 @@
+package keeper_test
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+ tmtime "github.com/tendermint/tendermint/types/time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3"
+ "github.com/kava-labs/kava/x/bep3/keeper"
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+type AtomicSwapTestSuite struct {
+ suite.Suite
+
+ keeper keeper.Keeper
+ app app.TestApp
+ ctx sdk.Context
+ deputy sdk.AccAddress
+ addrs []sdk.AccAddress
+ timestamps []int64
+ randomNumberHashes []tmbytes.HexBytes
+ swapIDs []tmbytes.HexBytes
+ randomNumbers []tmbytes.HexBytes
+}
+
+const (
+ STARING_BNB_BALANCE = int64(1000000000)
+ BNB_DENOM = "bnb"
+)
+
+func (suite *AtomicSwapTestSuite) SetupTest() {
+ config := sdk.GetConfig()
+ app.SetBech32AddressPrefixes(config)
+
+ // Initialize test app and set context
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+
+ // Create and load 20 accounts with bnb tokens
+ coins := []sdk.Coins{}
+ for i := 0; i < 20; i++ {
+ coins = append(coins, cs(c(BNB_DENOM, STARING_BNB_BALANCE)))
+ }
+ _, addrs := app.GeneratePrivKeyAddressPairs(20)
+ deputy := addrs[0]
+ authGS := app.NewAuthGenState(addrs, coins)
+
+ // Initialize genesis state
+ tApp.InitializeFromGenesisStates(authGS, NewBep3GenStateMulti(deputy))
+
+ suite.app = tApp
+ suite.ctx = ctx
+ suite.deputy = deputy
+ suite.addrs = addrs
+ suite.keeper = suite.app.GetBep3Keeper()
+ suite.GenerateSwapDetails()
+}
+
+func (suite *AtomicSwapTestSuite) GenerateSwapDetails() {
+ var timestamps []int64
+ var randomNumberHashes []tmbytes.HexBytes
+ var randomNumbers []tmbytes.HexBytes
+ for i := 0; i < 10; i++ {
+ // Set up atomic swap details
+ timestamp := ts(i)
+ randomNumber, _ := types.GenerateSecureRandomNumber()
+ randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ timestamps = append(timestamps, timestamp)
+ randomNumberHashes = append(randomNumberHashes, randomNumberHash)
+ randomNumbers = append(randomNumbers, randomNumber.Bytes())
+ }
+ suite.timestamps = timestamps
+ suite.randomNumberHashes = randomNumberHashes
+ suite.randomNumbers = randomNumbers
+}
+
+func (suite *AtomicSwapTestSuite) TestCreateAtomicSwap() {
+ currentTmTime := tmtime.Now()
+ type args struct {
+ randomNumberHash []byte
+ timestamp int64
+ heightSpan int64
+ sender sdk.AccAddress
+ recipient sdk.AccAddress
+ senderOtherChain string
+ recipientOtherChain string
+ coins sdk.Coins
+ expectedIncome string
+ crossChain bool
+ direction types.SwapDirection
+ }
+ testCases := []struct {
+ name string
+ blockTime time.Time
+ args args
+ expectPass bool
+ shouldBeFound bool
+ }{
+ {
+ "incoming swap",
+ currentTmTime,
+ args{
+ randomNumberHash: suite.randomNumberHashes[0],
+ timestamp: suite.timestamps[0],
+ heightSpan: int64(360),
+ sender: suite.deputy,
+ recipient: suite.addrs[1],
+ senderOtherChain: TestSenderOtherChain,
+ recipientOtherChain: TestRecipientOtherChain,
+ coins: cs(c(BNB_DENOM, 50000)),
+ expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM),
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ true,
+ true,
+ },
+ {
+ "outgoing swap",
+ currentTmTime,
+ args{
+ randomNumberHash: suite.randomNumberHashes[0],
+ timestamp: suite.timestamps[0],
+ heightSpan: int64(360),
+ sender: suite.addrs[1],
+ recipient: suite.addrs[2],
+ senderOtherChain: TestSenderOtherChain,
+ recipientOtherChain: TestRecipientOtherChain,
+ coins: cs(c(BNB_DENOM, 50000)),
+ expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM),
+ crossChain: true,
+ direction: types.Outgoing,
+ },
+ true,
+ true,
+ },
+ {
+ "unsupported asset",
+ currentTmTime,
+ args{
+ randomNumberHash: suite.randomNumberHashes[1],
+ timestamp: suite.timestamps[1],
+ heightSpan: int64(360),
+ sender: suite.deputy,
+ recipient: suite.addrs[2],
+ senderOtherChain: TestSenderOtherChain,
+ recipientOtherChain: TestRecipientOtherChain,
+ coins: cs(c("xyz", 50000)),
+ expectedIncome: "50000xyz",
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ false,
+ false,
+ },
+ {
+ "past timestamp",
+ currentTmTime,
+ args{
+ randomNumberHash: suite.randomNumberHashes[2],
+ timestamp: suite.timestamps[2] - 2000,
+ heightSpan: int64(360),
+ sender: suite.deputy,
+ recipient: suite.addrs[3],
+ senderOtherChain: TestSenderOtherChain,
+ recipientOtherChain: TestRecipientOtherChain,
+ coins: cs(c(BNB_DENOM, 50000)),
+ expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM),
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ false,
+ false,
+ },
+ {
+ "future timestamp",
+ currentTmTime,
+ args{
+ randomNumberHash: suite.randomNumberHashes[3],
+ timestamp: suite.timestamps[3] + 5000,
+ heightSpan: int64(360),
+ sender: suite.deputy,
+ recipient: suite.addrs[4],
+ senderOtherChain: TestSenderOtherChain,
+ recipientOtherChain: TestRecipientOtherChain,
+ coins: cs(c(BNB_DENOM, 50000)),
+ expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM),
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ false,
+ false,
+ },
+ {
+ "small height span",
+ currentTmTime,
+ args{
+ randomNumberHash: suite.randomNumberHashes[4],
+ timestamp: suite.timestamps[4],
+ heightSpan: int64(5),
+ sender: suite.deputy,
+ recipient: suite.addrs[5],
+ senderOtherChain: TestSenderOtherChain,
+ recipientOtherChain: TestRecipientOtherChain,
+ coins: cs(c(BNB_DENOM, 50000)),
+ expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM),
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ false,
+ false,
+ },
+ {
+ "big height span",
+ currentTmTime,
+ args{
+ randomNumberHash: suite.randomNumberHashes[5],
+ timestamp: suite.timestamps[5],
+ heightSpan: int64(1000000),
+ sender: suite.deputy,
+ recipient: suite.addrs[6],
+ senderOtherChain: TestSenderOtherChain,
+ recipientOtherChain: TestRecipientOtherChain,
+ coins: cs(c(BNB_DENOM, 50000)),
+ expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM),
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ false,
+ false,
+ },
+ {
+ "zero amount",
+ currentTmTime,
+ args{
+ randomNumberHash: suite.randomNumberHashes[6],
+ timestamp: suite.timestamps[6],
+ heightSpan: int64(360),
+ sender: suite.deputy,
+ recipient: suite.addrs[7],
+ senderOtherChain: TestSenderOtherChain,
+ recipientOtherChain: TestRecipientOtherChain,
+ coins: cs(c(BNB_DENOM, 0)),
+ expectedIncome: fmt.Sprintf("0%s", BNB_DENOM),
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ false,
+ false,
+ },
+ {
+ "duplicate swap",
+ currentTmTime,
+ args{
+ randomNumberHash: suite.randomNumberHashes[0],
+ timestamp: suite.timestamps[0],
+ heightSpan: int64(360),
+ sender: suite.deputy,
+ recipient: suite.addrs[1],
+ senderOtherChain: TestSenderOtherChain,
+ recipientOtherChain: TestRecipientOtherChain,
+ coins: cs(c(BNB_DENOM, 50000)),
+ expectedIncome: "50000bnb",
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ false,
+ true,
+ },
+ }
+
+ for _, tc := range testCases {
+ suite.Run(tc.name, func() {
+ // Increment current asset supply to support outgoing swaps
+ if tc.args.direction == types.Outgoing {
+ err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coins[0])
+ suite.Nil(err)
+ }
+
+ // Load asset denom (required for zero coins test case)
+ var swapAssetDenom string
+ if len(tc.args.coins) == 1 {
+ swapAssetDenom = tc.args.coins[0].Denom
+ } else {
+ swapAssetDenom = BNB_DENOM
+ }
+
+ // Load sender's account prior to swap creation
+ ak := suite.app.GetAccountKeeper()
+ senderAccPre := ak.GetAccount(suite.ctx, tc.args.sender)
+ senderBalancePre := senderAccPre.GetCoins().AmountOf(swapAssetDenom)
+ assetSupplyPre, _ := suite.keeper.GetAssetSupply(suite.ctx, []byte(swapAssetDenom))
+
+ // Create atomic swap
+ err := suite.keeper.CreateAtomicSwap(suite.ctx, tc.args.randomNumberHash, tc.args.timestamp,
+ tc.args.heightSpan, tc.args.sender, tc.args.recipient, tc.args.senderOtherChain,
+ tc.args.recipientOtherChain, tc.args.coins, tc.args.expectedIncome, tc.args.crossChain)
+
+ // Load sender's account after swap creation
+ senderAccPost := ak.GetAccount(suite.ctx, tc.args.sender)
+ senderBalancePost := senderAccPost.GetCoins().AmountOf(swapAssetDenom)
+ assetSupplyPost, _ := suite.keeper.GetAssetSupply(suite.ctx, []byte(swapAssetDenom))
+
+ // Load expected swap ID
+ expectedSwapID := types.CalculateSwapID(tc.args.randomNumberHash, tc.args.sender, tc.args.senderOtherChain)
+
+ if tc.expectPass {
+ suite.NoError(err)
+
+ // Check coins moved
+ suite.Equal(senderBalancePre.Sub(tc.args.coins[0].Amount), senderBalancePost)
+
+ // Check incoming/outgoing asset supply increased
+ switch tc.args.direction {
+ case types.Incoming:
+ suite.Equal(assetSupplyPre.IncomingSupply.Add(tc.args.coins[0]), assetSupplyPost.IncomingSupply)
+ case types.Outgoing:
+ suite.Equal(assetSupplyPre.OutgoingSupply.Add(tc.args.coins[0]), assetSupplyPost.OutgoingSupply)
+ default:
+ suite.Fail("should not have invalid direction")
+ }
+
+ // Check swap in store
+ actualSwap, found := suite.keeper.GetAtomicSwap(suite.ctx, expectedSwapID)
+ suite.True(found)
+ suite.NotNil(actualSwap)
+
+ // Confirm swap contents
+ expectedSwap :=
+ types.AtomicSwap{
+ Amount: tc.args.coins,
+ RandomNumberHash: tc.args.randomNumberHash,
+ ExpireHeight: suite.ctx.BlockHeight() + tc.args.heightSpan,
+ Timestamp: tc.args.timestamp,
+ Sender: tc.args.sender,
+ Recipient: tc.args.recipient,
+ SenderOtherChain: tc.args.senderOtherChain,
+ RecipientOtherChain: tc.args.recipientOtherChain,
+ ClosedBlock: 0,
+ Status: types.Open,
+ CrossChain: tc.args.crossChain,
+ Direction: tc.args.direction,
+ }
+ suite.Equal(expectedSwap, actualSwap)
+ } else {
+ suite.Error(err)
+ // Check coins not moved
+ suite.Equal(senderBalancePre, senderBalancePost)
+
+ // Check incoming/outgoing asset supply not increased
+ switch tc.args.direction {
+ case types.Incoming:
+ suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
+ case types.Outgoing:
+ suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
+ default:
+ suite.Fail("should not have invalid direction")
+ }
+
+ // Check if swap found in store
+ _, found := suite.keeper.GetAtomicSwap(suite.ctx, expectedSwapID)
+ if !tc.shouldBeFound {
+ suite.False(found)
+ } else {
+ suite.True(found)
+ }
+ }
+ })
+ }
+}
+
+func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() {
+ suite.SetupTest()
+ invalidRandomNumber, _ := types.GenerateSecureRandomNumber()
+ type args struct {
+ swapID []byte
+ randomNumber []byte
+ direction types.SwapDirection
+ }
+ testCases := []struct {
+ name string
+ claimCtx sdk.Context
+ args args
+ expectPass bool
+ }{
+ {
+ "normal incoming swap",
+ suite.ctx,
+ args{
+ swapID: []byte{},
+ randomNumber: []byte{},
+ direction: types.Incoming,
+ },
+ true,
+ },
+ {
+ "normal outgoing swap",
+ suite.ctx,
+ args{
+ swapID: []byte{},
+ randomNumber: []byte{},
+ direction: types.Outgoing,
+ },
+ true,
+ },
+ {
+ "invalid random number",
+ suite.ctx,
+ args{
+ swapID: []byte{},
+ randomNumber: invalidRandomNumber.Bytes(),
+ direction: types.Incoming,
+ },
+ false,
+ },
+ {
+ "wrong swap ID",
+ suite.ctx,
+ args{
+ swapID: types.CalculateSwapID(suite.randomNumberHashes[3], suite.addrs[6], TestRecipientOtherChain),
+ randomNumber: []byte{},
+ direction: types.Outgoing,
+ },
+ false,
+ },
+ {
+ "past expiration",
+ suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 2000),
+ args{
+ swapID: []byte{},
+ randomNumber: []byte{},
+ direction: types.Incoming,
+ },
+ false,
+ },
+ }
+
+ for i, tc := range testCases {
+ suite.GenerateSwapDetails()
+ suite.Run(tc.name, func() {
+ expectedRecipient := suite.addrs[5]
+ expectedClaimAmount := cs(c(BNB_DENOM, 50000))
+ sender := suite.deputy
+
+ // Set sender to other and increment current asset supply for outgoing swap
+ if tc.args.direction == types.Outgoing {
+ sender = suite.addrs[6]
+ err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, expectedClaimAmount[0])
+ suite.Nil(err)
+ }
+
+ // Create atomic swap
+ err := suite.keeper.CreateAtomicSwap(suite.ctx, suite.randomNumberHashes[i], suite.timestamps[i],
+ int64(360), sender, expectedRecipient, TestSenderOtherChain, TestRecipientOtherChain,
+ expectedClaimAmount, expectedClaimAmount.String(), true)
+ suite.NoError(err)
+
+ realSwapID := types.CalculateSwapID(suite.randomNumberHashes[i], sender, TestSenderOtherChain)
+
+ // If args contains an invalid swap ID claim attempt will use it instead of the real swap ID
+ var claimSwapID []byte
+ if len(tc.args.swapID) == 0 {
+ claimSwapID = realSwapID
+ } else {
+ claimSwapID = tc.args.swapID
+ }
+
+ // If args contains an invalid random number claim attempt will use it instead of the real random number
+ var claimRandomNumber []byte
+ if len(tc.args.randomNumber) == 0 {
+ claimRandomNumber = suite.randomNumbers[i]
+ } else {
+ claimRandomNumber = tc.args.randomNumber
+ }
+
+ // Run the beginblocker before attempting claim
+ bep3.BeginBlocker(tc.claimCtx, suite.keeper)
+
+ // Load expected recipient's account prior to claim attempt
+ ak := suite.app.GetAccountKeeper()
+ expectedRecipientAccPre := ak.GetAccount(tc.claimCtx, expectedRecipient)
+ expectedRecipientBalancePre := expectedRecipientAccPre.GetCoins().AmountOf(expectedClaimAmount[0].Denom)
+ // Load asset supplies prior to claim attempt
+ assetSupplyPre, _ := suite.keeper.GetAssetSupply(tc.claimCtx, []byte(expectedClaimAmount[0].Denom))
+
+ // Attempt to claim atomic swap
+ err = suite.keeper.ClaimAtomicSwap(tc.claimCtx, expectedRecipient, claimSwapID, claimRandomNumber)
+
+ // Load expected recipient's account after the claim attempt
+ expectedRecipientAccPost := ak.GetAccount(tc.claimCtx, expectedRecipient)
+ expectedRecipientBalancePost := expectedRecipientAccPost.GetCoins().AmountOf(expectedClaimAmount[0].Denom)
+ // Load asset supplies after the claim attempt
+ assetSupplyPost, _ := suite.keeper.GetAssetSupply(tc.claimCtx, []byte(expectedClaimAmount[0].Denom))
+
+ if tc.expectPass {
+ suite.NoError(err)
+ // Check coins moved
+ suite.Equal(expectedRecipientBalancePre.Add(expectedClaimAmount[0].Amount), expectedRecipientBalancePost)
+
+ // Check asset supply changes
+ switch tc.args.direction {
+ case types.Incoming:
+ // Check incoming supply decreased
+ suite.True(assetSupplyPre.IncomingSupply.Amount.Sub(expectedClaimAmount[0].Amount).Equal(assetSupplyPost.IncomingSupply.Amount))
+ // Check current supply increased
+ suite.Equal(assetSupplyPre.CurrentSupply.Add(expectedClaimAmount[0]), assetSupplyPost.CurrentSupply)
+ // Check outgoing supply not changed
+ suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
+ case types.Outgoing:
+ // Check incoming supply not changed
+ suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
+ // Check current supply decreased
+ suite.Equal(assetSupplyPre.CurrentSupply.Sub(expectedClaimAmount[0]), assetSupplyPost.CurrentSupply)
+ // Check outgoing supply decreased
+ suite.True(assetSupplyPre.OutgoingSupply.Sub(expectedClaimAmount[0]).IsEqual(assetSupplyPost.OutgoingSupply))
+ default:
+ suite.Fail("should not have invalid direction")
+ }
+ } else {
+ suite.Error(err)
+ // Check coins not moved
+ suite.Equal(expectedRecipientBalancePre, expectedRecipientBalancePost)
+
+ // Check asset supply has not changed
+ switch tc.args.direction {
+ case types.Incoming:
+ suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
+ suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
+ suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
+ case types.Outgoing:
+ suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
+ suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
+ suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
+ default:
+ suite.Fail("should not have invalid direction")
+ }
+ }
+ })
+ }
+}
+
+func (suite *AtomicSwapTestSuite) TestRefundAtomicSwap() {
+ suite.SetupTest()
+
+ type args struct {
+ swapID []byte
+ direction types.SwapDirection
+ }
+ testCases := []struct {
+ name string
+ refundCtx sdk.Context
+ args args
+ expectPass bool
+ }{
+ {
+ "normal incoming swap",
+ suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400),
+ args{
+ swapID: []byte{},
+ direction: types.Incoming,
+ },
+ true,
+ },
+ {
+ "normal outgoing swap",
+ suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400),
+ args{
+ swapID: []byte{},
+ direction: types.Outgoing,
+ },
+ true,
+ },
+ {
+ "before expiration",
+ suite.ctx,
+ args{
+ swapID: []byte{},
+ direction: types.Incoming,
+ },
+ false,
+ },
+ {
+ "wrong swapID",
+ suite.ctx,
+ args{
+ swapID: types.CalculateSwapID(suite.randomNumberHashes[6], suite.addrs[1], TestRecipientOtherChain),
+ direction: types.Incoming,
+ },
+ false,
+ },
+ }
+
+ for i, tc := range testCases {
+ suite.GenerateSwapDetails()
+ suite.Run(tc.name, func() {
+ // Create atomic swap
+ expectedRefundAmount := cs(c(BNB_DENOM, 50000))
+ sender := suite.deputy
+
+ // Set sender to other and increment current asset supply for outgoing swap
+ if tc.args.direction == types.Outgoing {
+ sender = suite.addrs[6]
+ err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, expectedRefundAmount[0])
+ suite.Nil(err)
+ }
+
+ err := suite.keeper.CreateAtomicSwap(suite.ctx, suite.randomNumberHashes[i], suite.timestamps[i],
+ int64(360), sender, suite.addrs[8], TestSenderOtherChain, TestRecipientOtherChain,
+ expectedRefundAmount, expectedRefundAmount.String(), true)
+ suite.NoError(err)
+
+ realSwapID := types.CalculateSwapID(suite.randomNumberHashes[i], sender, TestSenderOtherChain)
+
+ // If args contains an invalid swap ID refund attempt will use it instead of the real swap ID
+ var refundSwapID []byte
+ if len(tc.args.swapID) == 0 {
+ refundSwapID = realSwapID
+ } else {
+ refundSwapID = tc.args.swapID
+ }
+
+ // Run the beginblocker before attempting refund
+ bep3.BeginBlocker(tc.refundCtx, suite.keeper)
+
+ // Load sender's account prior to swap refund
+ ak := suite.app.GetAccountKeeper()
+ originalSenderAccPre := ak.GetAccount(tc.refundCtx, sender)
+ originalSenderBalancePre := originalSenderAccPre.GetCoins().AmountOf(expectedRefundAmount[0].Denom)
+ // Load asset supply prior to swap refund
+ assetSupplyPre, _ := suite.keeper.GetAssetSupply(tc.refundCtx, []byte(expectedRefundAmount[0].Denom))
+
+ // Attempt to refund atomic swap
+ err = suite.keeper.RefundAtomicSwap(tc.refundCtx, sender, refundSwapID)
+
+ // Load sender's account after refund
+ originalSenderAccPost := ak.GetAccount(tc.refundCtx, sender)
+ originalSenderBalancePost := originalSenderAccPost.GetCoins().AmountOf(expectedRefundAmount[0].Denom)
+ // Load asset supply after to swap refund
+ assetSupplyPost, _ := suite.keeper.GetAssetSupply(tc.refundCtx, []byte(expectedRefundAmount[0].Denom))
+
+ if tc.expectPass {
+ suite.NoError(err)
+ // Check coins moved
+ suite.Equal(originalSenderBalancePre.Add(expectedRefundAmount[0].Amount), originalSenderBalancePost)
+
+ // Check asset supply changes
+ switch tc.args.direction {
+ case types.Incoming:
+ // Check incoming supply decreased
+ suite.True(assetSupplyPre.IncomingSupply.Sub(expectedRefundAmount[0]).IsEqual(assetSupplyPost.IncomingSupply))
+ // Check current, outgoing supply not changed
+ suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
+ suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
+ case types.Outgoing:
+ // Check incoming, current supply not changed
+ suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
+ suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
+ // Check outgoing supply decreased
+ suite.True(assetSupplyPre.OutgoingSupply.Sub(expectedRefundAmount[0]).IsEqual(assetSupplyPost.OutgoingSupply))
+ default:
+ suite.Fail("should not have invalid direction")
+ }
+ } else {
+ suite.Error(err)
+ // Check coins not moved
+ suite.Equal(originalSenderBalancePre, originalSenderBalancePost)
+
+ // Check asset supply has not changed
+ switch tc.args.direction {
+ case types.Incoming:
+ suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
+ suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
+ suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
+ case types.Outgoing:
+ suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
+ suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
+ suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
+ default:
+ suite.Fail("should not have invalid direction")
+ }
+ }
+ })
+ }
+}
+
+func TestAtomicSwapTestSuite(t *testing.T) {
+ suite.Run(t, new(AtomicSwapTestSuite))
+}
diff --git a/x/bep3/module.go b/x/bep3/module.go
new file mode 100644
index 00000000..bf675bff
--- /dev/null
+++ b/x/bep3/module.go
@@ -0,0 +1,172 @@
+package bep3
+
+import (
+ "encoding/json"
+ "math/rand"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/module"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ sim "github.com/cosmos/cosmos-sdk/x/simulation"
+ "github.com/gorilla/mux"
+ "github.com/spf13/cobra"
+ abci "github.com/tendermint/tendermint/abci/types"
+
+ "github.com/kava-labs/kava/x/bep3/client/cli"
+ "github.com/kava-labs/kava/x/bep3/client/rest"
+ "github.com/kava-labs/kava/x/bep3/simulation"
+)
+
+var (
+ _ module.AppModule = AppModule{}
+ _ module.AppModuleBasic = AppModuleBasic{}
+ _ module.AppModuleSimulation = AppModule{}
+)
+
+// AppModuleBasic defines the basic application module used by the bep3 module.
+type AppModuleBasic struct{}
+
+// Name returns the bep3 module's name.
+func (AppModuleBasic) Name() string {
+ return ModuleName
+}
+
+// RegisterCodec registers the bep3 module's types for the given codec.
+func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
+ RegisterCodec(cdc)
+}
+
+// DefaultGenesis returns default genesis state as raw bytes for the bep3
+// module.
+func (AppModuleBasic) DefaultGenesis() json.RawMessage {
+ return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
+}
+
+// ValidateGenesis performs genesis state validation for the bep3 module.
+func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
+ var gs GenesisState
+ err := ModuleCdc.UnmarshalJSON(bz, &gs)
+ if err != nil {
+ return err
+ }
+ return gs.Validate()
+}
+
+// RegisterRESTRoutes registers the REST routes for the bep3 module.
+func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
+ rest.RegisterRoutes(ctx, rtr)
+}
+
+// GetTxCmd returns the root tx command for the bep3 module.
+func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
+ return cli.GetTxCmd(cdc)
+}
+
+// GetQueryCmd returns no root query command for the bep3 module.
+func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
+ return cli.GetQueryCmd(StoreKey, cdc)
+}
+
+//____________________________________________________________________________
+
+// AppModule implements the sdk.AppModule interface.
+type AppModule struct {
+ AppModuleBasic
+
+ keeper Keeper
+ accountKeeper auth.AccountKeeper
+ supplyKeeper SupplyKeeper
+}
+
+// NewAppModule creates a new AppModule object
+func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper, supplyKeeper SupplyKeeper) AppModule {
+ return AppModule{
+ AppModuleBasic: AppModuleBasic{},
+ keeper: keeper,
+ accountKeeper: accountKeeper,
+ supplyKeeper: supplyKeeper,
+ }
+}
+
+// Name returns the bep3 module's name.
+func (AppModule) Name() string {
+ return ModuleName
+}
+
+// RegisterInvariants registers the bep3 module invariants.
+func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
+
+// Route returns the message routing key for the bep3 module.
+func (AppModule) Route() string {
+ return RouterKey
+}
+
+// NewHandler returns an sdk.Handler for the bep3 module.
+func (am AppModule) NewHandler() sdk.Handler {
+ return NewHandler(am.keeper)
+}
+
+// QuerierRoute returns the bep3 module's querier route name.
+func (AppModule) QuerierRoute() string {
+ return QuerierRoute
+}
+
+// NewQuerierHandler returns the bep3 module sdk.Querier.
+func (am AppModule) NewQuerierHandler() sdk.Querier {
+ return NewQuerier(am.keeper)
+}
+
+// InitGenesis performs genesis initialization for the bep3 module. It returns
+// no validator updates.
+func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
+ var genesisState GenesisState
+ ModuleCdc.MustUnmarshalJSON(data, &genesisState)
+ InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState)
+ return []abci.ValidatorUpdate{}
+}
+
+// ExportGenesis returns the exported genesis state as raw bytes for the bep3
+// module.
+func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
+ gs := ExportGenesis(ctx, am.keeper)
+ return ModuleCdc.MustMarshalJSON(gs)
+}
+
+// BeginBlock returns the begin blocker for the bep3 module.
+func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) {
+ BeginBlocker(ctx, am.keeper)
+}
+
+// EndBlock returns the end blocker for the bep3 module. It returns no validator updates.
+func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
+ return []abci.ValidatorUpdate{}
+}
+
+//____________________________________________________________________________
+
+// GenerateGenesisState creates a randomized GenState of the bep3 module
+func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
+ simulation.RandomizedGenState(simState)
+}
+
+// ProposalContents doesn't return any content functions for governance proposals.
+func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
+ return nil
+}
+
+// RandomizedParams returns nil because bep3 has no params.
+func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
+ return simulation.ParamChanges(r)
+}
+
+// RegisterStoreDecoder registers a decoder for bep3 module's types
+func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
+ sdr[StoreKey] = simulation.DecodeStore
+}
+
+// WeightedOperations returns the all the bep3 module operations with their respective weights.
+func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
+ return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper)
+}
diff --git a/x/bep3/simulation/decoder.go b/x/bep3/simulation/decoder.go
new file mode 100644
index 00000000..8fc8dd61
--- /dev/null
+++ b/x/bep3/simulation/decoder.go
@@ -0,0 +1,39 @@
+package simulation
+
+import (
+ "bytes"
+ "fmt"
+
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+// DecodeStore unmarshals the KVPair's Value to the module's corresponding type
+func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
+ switch {
+ case bytes.Equal(kvA.Key[:1], types.AtomicSwapKeyPrefix):
+ var swapA, swapB types.AtomicSwap
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &swapA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &swapB)
+ return fmt.Sprintf("%v\n%v", swapA, swapB)
+
+ case bytes.Equal(kvA.Key[:1], types.AssetSupplyKeyPrefix):
+ var supplyA, supplyB types.AssetSupply
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &supplyA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &supplyB)
+ return fmt.Sprintf("%s\n%s", supplyA, supplyB)
+
+ case bytes.Equal(kvA.Key[:1], types.AtomicSwapByBlockPrefix),
+ bytes.Equal(kvA.Key[:1], types.AtomicSwapLongtermStoragePrefix):
+ var bytesA tmbytes.HexBytes = kvA.Value
+ var bytesB tmbytes.HexBytes = kvA.Value
+ return fmt.Sprintf("%s\n%s", bytesA.String(), bytesB.String())
+
+ default:
+ panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
+ }
+}
diff --git a/x/bep3/simulation/decoder_test.go b/x/bep3/simulation/decoder_test.go
new file mode 100644
index 00000000..a5b6ce08
--- /dev/null
+++ b/x/bep3/simulation/decoder_test.go
@@ -0,0 +1,62 @@
+package simulation
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+func makeTestCodec() (cdc *codec.Codec) {
+ cdc = codec.New()
+ sdk.RegisterCodec(cdc)
+ types.RegisterCodec(cdc)
+ return
+}
+
+func TestDecodeDistributionStore(t *testing.T) {
+ cdc := makeTestCodec()
+
+ oneCoin := sdk.NewCoin("coin", sdk.OneInt())
+ swap := types.NewAtomicSwap(sdk.Coins{oneCoin}, nil, 10, 100, nil, nil, "otherChainSender", "otherChainRec", 200, types.Completed, true, types.Outgoing)
+ supply := types.AssetSupply{Denom: "coin", IncomingSupply: oneCoin, OutgoingSupply: oneCoin, CurrentSupply: oneCoin, Limit: oneCoin}
+ bz := tmbytes.HexBytes([]byte{1, 2})
+
+ kvPairs := kv.Pairs{
+ kv.Pair{Key: types.AtomicSwapKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(swap)},
+ kv.Pair{Key: types.AssetSupplyKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(supply)},
+ kv.Pair{Key: types.AtomicSwapByBlockPrefix, Value: bz},
+ kv.Pair{Key: types.AtomicSwapByBlockPrefix, Value: bz},
+ kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
+ }
+
+ tests := []struct {
+ name string
+ expectedLog string
+ }{
+ {"AtomicSwap", fmt.Sprintf("%v\n%v", swap, swap)},
+ {"AssetSupply", fmt.Sprintf("%v\n%v", supply, supply)},
+ {"AtomicSwapByBlock", fmt.Sprintf("%s\n%s", bz, bz)},
+ {"AtomicSwapLongtermStorage", fmt.Sprintf("%s\n%s", bz, bz)},
+ {"other", ""},
+ }
+ for i, tt := range tests {
+ i, tt := i, tt
+ t.Run(tt.name, func(t *testing.T) {
+ switch i {
+ case len(tests) - 1:
+ require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
+ default:
+ require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
+ }
+ })
+ }
+}
diff --git a/x/bep3/simulation/genesis.go b/x/bep3/simulation/genesis.go
new file mode 100644
index 00000000..bfac2d88
--- /dev/null
+++ b/x/bep3/simulation/genesis.go
@@ -0,0 +1,168 @@
+package simulation
+
+import (
+ "fmt"
+ "math/rand"
+ "strings"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/module"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+ "github.com/cosmos/cosmos-sdk/x/supply"
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+// Simulation parameter constants
+const (
+ BnbDeputyAddress = "bnb_deputy_address"
+ MinBlockLock = "min_block_lock"
+ MaxBlockLock = "max_block_lock"
+ SupportedAssets = "supported_assets"
+)
+
+var (
+ MaxSupplyLimit = sdk.NewInt(1000000000000)
+ accs []simulation.Account
+ ConsistentDenoms = [3]string{"bnb", "xrp", "btc"}
+)
+
+// GenRandBnbDeputy randomized BnbDeputyAddress
+func GenRandBnbDeputy(r *rand.Rand) simulation.Account {
+ acc, _ := simulation.RandomAcc(r, accs)
+ return acc
+}
+
+// GenMinBlockLock randomized MinBlockLock
+func GenMinBlockLock(r *rand.Rand) int64 {
+ min := int(types.AbsoluteMinimumBlockLock)
+ max := int(types.AbsoluteMaximumBlockLock)
+ return int64(r.Intn(max-min) + min)
+}
+
+// GenMaxBlockLock randomized MaxBlockLock
+func GenMaxBlockLock(r *rand.Rand, minBlockLock int64) int64 {
+ min := int(minBlockLock)
+ max := int(types.AbsoluteMaximumBlockLock)
+ return int64(r.Intn(max-min) + min)
+}
+
+// GenSupportedAssets gets randomized SupportedAssets
+func GenSupportedAssets(r *rand.Rand) types.AssetParams {
+ var assets types.AssetParams
+ numAssets := (r.Intn(10) + 1)
+ for i := 0; i < numAssets; i++ {
+ denom := strings.ToLower(simulation.RandStringOfLength(r, (r.Intn(3) + 3)))
+ asset := genSupportedAsset(r, denom)
+ assets = append(assets, asset)
+ }
+ // Add bnb, btc, or xrp as a supported asset for interactions with other modules
+ stableAsset := genSupportedAsset(r, ConsistentDenoms[r.Intn(3)])
+ assets = append(assets, stableAsset)
+
+ return assets
+}
+
+func genSupportedAsset(r *rand.Rand, denom string) types.AssetParam {
+ coinID, _ := simulation.RandPositiveInt(r, sdk.NewInt(100000))
+ limit, _ := simulation.RandPositiveInt(r, MaxSupplyLimit)
+ return types.AssetParam{
+ Denom: denom,
+ CoinID: int(coinID.Int64()),
+ Limit: limit,
+ Active: true,
+ }
+}
+
+// RandomizedGenState generates a random GenesisState
+func RandomizedGenState(simState *module.SimulationState) {
+ accs = simState.Accounts
+
+ bep3Genesis := loadRandomBep3GenState(simState)
+ fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, bep3Genesis))
+ simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(bep3Genesis)
+
+ authGenesis, totalCoins := loadAuthGenState(simState, bep3Genesis)
+ simState.GenState[auth.ModuleName] = simState.Cdc.MustMarshalJSON(authGenesis)
+
+ // Update supply to match amount of coins in auth
+ var supplyGenesis supply.GenesisState
+ simState.Cdc.MustUnmarshalJSON(simState.GenState[supply.ModuleName], &supplyGenesis)
+ for _, deputyCoin := range totalCoins {
+ supplyGenesis.Supply = supplyGenesis.Supply.Add(deputyCoin...)
+ }
+ simState.GenState[supply.ModuleName] = simState.Cdc.MustMarshalJSON(supplyGenesis)
+}
+
+func loadRandomBep3GenState(simState *module.SimulationState) types.GenesisState {
+ bnbDeputy := GenRandBnbDeputy(simState.Rand)
+
+ // min/max block lock are hardcoded to 50/100 for expected -NumBlocks=100
+ minBlockLock := int64(types.AbsoluteMinimumBlockLock)
+ maxBlockLock := minBlockLock * 2
+
+ var supportedAssets types.AssetParams
+ simState.AppParams.GetOrGenerate(
+ simState.Cdc, SupportedAssets, &supportedAssets, simState.Rand,
+ func(r *rand.Rand) { supportedAssets = GenSupportedAssets(r) },
+ )
+
+ bep3Genesis := types.GenesisState{
+ Params: types.Params{
+ BnbDeputyAddress: bnbDeputy.Address,
+ MinBlockLock: minBlockLock,
+ MaxBlockLock: maxBlockLock,
+ SupportedAssets: supportedAssets,
+ },
+ }
+
+ return bep3Genesis
+}
+
+func loadAuthGenState(simState *module.SimulationState, bep3Genesis types.GenesisState) (
+ auth.GenesisState, []sdk.Coins) {
+ var authGenesis auth.GenesisState
+ simState.Cdc.MustUnmarshalJSON(simState.GenState[auth.ModuleName], &authGenesis)
+
+ deputy, found := getAccount(authGenesis.Accounts, bep3Genesis.Params.BnbDeputyAddress)
+ if !found {
+ panic("deputy address not found in available accounts")
+ }
+
+ // Load total limit of each supported asset to deputy's account
+ var totalCoins []sdk.Coins
+ for _, asset := range bep3Genesis.Params.SupportedAssets {
+ assetCoin := sdk.NewCoins(sdk.NewCoin(asset.Denom, asset.Limit))
+ if err := deputy.SetCoins(deputy.GetCoins().Add(assetCoin...)); err != nil {
+ panic(err)
+ }
+ totalCoins = append(totalCoins, assetCoin)
+ }
+ authGenesis.Accounts = replaceOrAppendAccount(authGenesis.Accounts, deputy)
+
+ return authGenesis, totalCoins
+}
+
+// Return an account from a list of accounts that matches an address.
+func getAccount(accounts []authexported.GenesisAccount, addr sdk.AccAddress) (authexported.GenesisAccount, bool) {
+ for _, a := range accounts {
+ if a.GetAddress().Equals(addr) {
+ return a, true
+ }
+ }
+ return nil, false
+}
+
+// In a list of accounts, replace the first account found with the same address. If not found, append the account.
+func replaceOrAppendAccount(accounts []authexported.GenesisAccount, acc authexported.GenesisAccount) []authexported.GenesisAccount {
+ newAccounts := accounts
+ for i, a := range accounts {
+ if a.GetAddress().Equals(acc.GetAddress()) {
+ newAccounts[i] = acc
+ return newAccounts
+ }
+ }
+ return append(newAccounts, acc)
+}
diff --git a/x/bep3/simulation/operations.go b/x/bep3/simulation/operations.go
new file mode 100644
index 00000000..0f3592f5
--- /dev/null
+++ b/x/bep3/simulation/operations.go
@@ -0,0 +1,210 @@
+package simulation
+
+import (
+ "fmt"
+ "math"
+ "math/rand"
+
+ "github.com/cosmos/cosmos-sdk/baseapp"
+ "github.com/cosmos/cosmos-sdk/codec"
+ "github.com/cosmos/cosmos-sdk/simapp/helpers"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+
+ appparams "github.com/kava-labs/kava/app/params"
+ "github.com/kava-labs/kava/x/bep3/keeper"
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+var (
+ noOpMsg = simulation.NoOpMsg(types.ModuleName)
+)
+
+// Simulation operation weights constants
+const (
+ OpWeightMsgCreateAtomicSwap = "op_weight_msg_create_atomic_swap"
+)
+
+// WeightedOperations returns all the operations from the module with their respective weights
+func WeightedOperations(
+ appParams simulation.AppParams, cdc *codec.Codec, ak auth.AccountKeeper, k keeper.Keeper,
+) simulation.WeightedOperations {
+ var weightCreateAtomicSwap int
+
+ appParams.GetOrGenerate(cdc, OpWeightMsgCreateAtomicSwap, &weightCreateAtomicSwap, nil,
+ func(_ *rand.Rand) {
+ weightCreateAtomicSwap = appparams.DefaultWeightMsgCreateAtomicSwap
+ },
+ )
+
+ return simulation.WeightedOperations{
+ simulation.NewWeightedOperation(
+ weightCreateAtomicSwap,
+ SimulateMsgCreateAtomicSwap(ak, k),
+ ),
+ }
+}
+
+// SimulateMsgCreateAtomicSwap generates a MsgCreateAtomicSwap with random values
+func SimulateMsgCreateAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper) simulation.Operation {
+ return func(
+ r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
+ ) (simulation.OperationMsg, []simulation.FutureOperation, error) {
+
+ senderAddr := k.GetBnbDeputyAddress(ctx)
+
+ sender, found := simulation.FindAccount(accs, senderAddr)
+ if !found {
+ return noOpMsg, nil, nil
+ }
+
+ recipient, _ := simulation.RandomAcc(r, accs)
+
+ recipientOtherChain := simulation.RandStringOfLength(r, 43)
+ senderOtherChain := simulation.RandStringOfLength(r, 43)
+
+ // Generate cryptographically strong pseudo-random number
+ randomNumber, err := simulation.RandPositiveInt(r, sdk.NewInt(math.MaxInt64))
+ if err != nil {
+ return noOpMsg, nil, err
+ }
+ // Must use current blocktime instead of 'now' since initial blocktime was randomly generated
+ timestamp := ctx.BlockTime().Unix()
+ randomNumberHash := types.CalculateRandomHash(randomNumber.BigInt().Bytes(), timestamp)
+
+ // Randomly select an asset from supported assets
+ assets, found := k.GetAssets(ctx)
+ if !found {
+ return noOpMsg, nil, fmt.Errorf("no supported assets found")
+ }
+ asset := assets[r.Intn(len(assets))]
+
+ // Check that the sender has coins of this type
+ senderAcc := ak.GetAccount(ctx, senderAddr)
+ fees, err := simulation.RandomFees(r, ctx, senderAcc.SpendableCoins(ctx.BlockTime()))
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+ availableAmount := senderAcc.SpendableCoins(ctx.BlockTime()).Sub(fees).AmountOf(asset.Denom)
+ // Get an amount of coins between 0.1 and 2% of total coins
+ amount := availableAmount.Quo(sdk.NewInt(int64(simulation.RandIntBetween(r, 50, 1000))))
+ if amount.IsZero() {
+ return simulation.NewOperationMsgBasic(types.ModuleName, fmt.Sprintf("no-operation (all funds exhausted for asset %s)", asset.Denom), "", false, nil), nil, nil
+ }
+ coin := sdk.NewCoin(asset.Denom, amount)
+ coins := sdk.NewCoins(coin)
+ expectedIncome := coin.String()
+
+ // We're assuming that sims are run with -NumBlocks=100
+ heightSpan := int64(55)
+ crossChain := true
+
+ msg := types.NewMsgCreateAtomicSwap(
+ senderAddr, recipient.Address, recipientOtherChain, senderOtherChain, randomNumberHash,
+ timestamp, coins, expectedIncome, heightSpan, crossChain,
+ )
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ fees,
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{senderAcc.GetAccountNumber()},
+ []uint64{senderAcc.GetSequence()},
+ sender.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ // If created, construct a MsgClaimAtomicSwap or MsgRefundAtomicSwap future operation
+ var futureOp simulation.FutureOperation
+ swapID := types.CalculateSwapID(msg.RandomNumberHash, msg.From, msg.SenderOtherChain)
+ if r.Intn(100) < 50 {
+ // Claim future operation
+ executionBlock := ctx.BlockHeight() + (msg.HeightSpan / 2)
+ futureOp = simulation.FutureOperation{
+ BlockHeight: int(executionBlock),
+ Op: operationClaimAtomicSwap(ak, k, swapID, randomNumber.BigInt().Bytes()),
+ }
+ } else {
+ // Refund future operation
+ executionBlock := ctx.BlockHeight() + msg.HeightSpan
+ futureOp = simulation.FutureOperation{
+ BlockHeight: int(executionBlock),
+ Op: operationRefundAtomicSwap(ak, k, swapID),
+ }
+ }
+
+ return simulation.NewOperationMsg(msg, true, result.Log), []simulation.FutureOperation{futureOp}, nil
+ }
+}
+
+func operationClaimAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper, swapID []byte, randomNumber []byte) simulation.Operation {
+ return func(
+ r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
+ ) (simulation.OperationMsg, []simulation.FutureOperation, error) {
+ simAccount, _ := simulation.RandomAcc(r, accs)
+ acc := ak.GetAccount(ctx, simAccount.Address)
+
+ msg := types.NewMsgClaimAtomicSwap(acc.GetAddress(), swapID, randomNumber)
+
+ fees, err := simulation.RandomFees(r, ctx, acc.SpendableCoins(ctx.BlockTime()))
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ fees,
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{acc.GetAccountNumber()},
+ []uint64{acc.GetSequence()},
+ simAccount.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
+ }
+}
+
+func operationRefundAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper, swapID []byte) simulation.Operation {
+ return func(
+ r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
+ ) (simulation.OperationMsg, []simulation.FutureOperation, error) {
+ simAccount, _ := simulation.RandomAcc(r, accs)
+ acc := ak.GetAccount(ctx, simAccount.Address)
+
+ msg := types.NewMsgRefundAtomicSwap(acc.GetAddress(), swapID)
+
+ fees, err := simulation.RandomFees(r, ctx, acc.SpendableCoins(ctx.BlockTime()))
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ fees,
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{acc.GetAccountNumber()},
+ []uint64{acc.GetSequence()},
+ simAccount.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
+ }
+}
diff --git a/x/bep3/simulation/params.go b/x/bep3/simulation/params.go
new file mode 100644
index 00000000..76656750
--- /dev/null
+++ b/x/bep3/simulation/params.go
@@ -0,0 +1,46 @@
+package simulation
+
+import (
+ "fmt"
+ "math/rand"
+
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+const (
+ keyBnbDeputyAddress = "BnbDeputyAddress"
+ keyMinBlockLock = "MinBlockLock"
+ keyMaxBlockLock = "MaxBlockLock"
+ keySupportedAssets = "SupportedAssets"
+)
+
+// ParamChanges defines the parameters that can be modified by param change proposals
+func ParamChanges(r *rand.Rand) []simulation.ParamChange {
+ // We generate MinBlockLock first because the result is required by GenMaxBlockLock()
+ minBlockLockVal := GenMinBlockLock(r)
+
+ return []simulation.ParamChange{
+ simulation.NewSimParamChange(types.ModuleName, keyBnbDeputyAddress,
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("\"%s\"", GenRandBnbDeputy(r).Address)
+ },
+ ),
+ simulation.NewSimParamChange(types.ModuleName, keyMinBlockLock,
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("\"%d\"", minBlockLockVal)
+ },
+ ),
+ simulation.NewSimParamChange(types.ModuleName, keyMaxBlockLock,
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("\"%d\"", GenMaxBlockLock(r, minBlockLockVal))
+ },
+ ),
+ simulation.NewSimParamChange(types.ModuleName, keySupportedAssets,
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("\"%v\"", GenSupportedAssets(r))
+ },
+ ),
+ }
+}
diff --git a/x/bep3/spec/01_concepts.md b/x/bep3/spec/01_concepts.md
new file mode 100644
index 00000000..f897237c
--- /dev/null
+++ b/x/bep3/spec/01_concepts.md
@@ -0,0 +1,36 @@
+# Concepts
+
+ The BEP3 module implements the [BEP3 protocol](https://github.com/binance-chain/BEPs/blob/master/BEP3.md) for secure cross-chain asset transfers between Kava and other BEP3 compatible chains, such as Binance Chain. Tranactions are witnessed and relayed between the two blockchains by Binance's BEP3 deputy process. The deputy maintains an address on both chains and is responsible for delivering tokens upon the successful completion of an Atomic Swap. Learn more about the BEP3 deputy process [here](https://github.com/binance-chain/bep3-deputy).
+
+## Requirements
+Kava
+- The deputy’s Kava testnet-5000 address is **kava1aphsdnz5hu2t5ty2au6znprug5kx3zpy6zwq29**.
+- We recommend using http://kava-testnet-5000.kava.io:1317 as Kava’s API endpoint.
+
+Binance Chain
+- The deputy’s Binance Chain testnet address is **tbnb1et8vmd0dgvswjnyaf73ez8ye0jehc8a7t7fljv**.
+- We recommend using https://testnet-dex.binance.org/ as Binance Chain’s API endpoint.
+
+Kava's [JavaScript SDK](https://github.com/Kava-Labs/javascript-sdk) and Binance Chain’s [JavaScript SDK](https://github.com/binance-chain/javascript-sdk) can be used to create, claim, and refund swaps.
+
+## Binance Chain to Kava
+
+When a user wants to transfer tokens from Binance Chain to Kava, the following steps are taken:
+1. User’s tokens are locked on Binance Chain along with the hash of a secret only known to the user. If the secret is not revealed before the deadline, the tokens are refundable.
+2. The deputy sends a message to Kava saying “a user has locked X tokens, if their secret is revealed before the deadline issue them an equivalent amount of pegged tokens”.
+3. The user reveals the secret on Kava and receives the pegged tokens.
+4. The deputy relays the secret to Binance Chain and the original tokens are locked permanently.
+
+
+![Binance Chain to Kava Diagram](./diagrams/BEP3_binance_chain_to_kava.jpg)
+
+## Kava to Binance Chain
+1. When a user wants to transfer tokens from Kava to Binance Chain by redeeming pegged tokens, the following steps are taken:
+User’s pegged tokens are locked on Kava along with the hash of a secret only known to the user. If the secret is not revealed before the deadline, the tokens are refundable.
+2. The deputy sends a message to Binance Chain saying “a user has locked X pegged tokens, if their secret is revealed before the deadline issue them an equivalent amount of tokens”.
+3. The user reveals the secret on Binance Chain and receives the tokens.
+4. The deputy relays the secret to Kava and the pegged tokens are locked permanently.
+
+
+![Kava to Binance Chain Diagram](./diagrams/BEP3_kava_to_binance_chain.jpg)
+
diff --git a/x/bep3/spec/02_state.md b/x/bep3/spec/02_state.md
new file mode 100644
index 00000000..7ce2df13
--- /dev/null
+++ b/x/bep3/spec/02_state.md
@@ -0,0 +1,82 @@
+# State
+
+## Parameters and genesis state
+
+`Paramaters` define the rules according to which swaps are executed. Parameter updates can be made via on-chain parameter update proposals.
+
+```go
+// Params governance parameters for bep3 module
+type Params struct {
+ BnbDeputyAddress sdk.AccAddress `json:"bnb_deputy_address" yaml:"bnb_deputy_address"` // deputy's address on Kava
+ MinBlockLock int64 `json:"min_block_lock" yaml:"min_block_lock"` // minimum swap expire height
+ MaxBlockLock int64 `json:"max_block_lock" yaml:"max_block_lock"` // maximum swap expire height
+ SupportedAssets AssetParams `json:"supported_assets" yaml:"supported_assets"` // array of supported asset
+}
+
+// AssetParam governance parameters for each asset within a supported chain
+type AssetParam struct {
+ Denom string `json:"denom" yaml:"denom"` // name of the asset
+ CoinID int `json:"coin_id" yaml:"coin_id"` // internationally recognized coin ID
+ Limit sdk.Int `json:"limit" yaml:"limit"` // asset supply limit
+ Active bool `json:"active" yaml:"active"` // denotes if asset is active or paused
+}
+```
+
+`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the bep3 module to resume.
+
+```go
+// GenesisState - all bep3 state that must be provided at genesis
+type GenesisState struct {
+ Params Params `json:"params" yaml:"params"`
+ AtomicSwaps AtomicSwaps `json:"atomic_swaps" yaml:"atomic_swaps"`
+ AssetSupplies AssetSupplies `json:"assets_supplies" yaml:"assets_supplies"`
+}
+```
+
+## Types
+
+```go
+// AtomicSwap contains the information for an atomic swap
+type AtomicSwap struct {
+ Amount sdk.Coins `json:"amount" yaml:"amount"`
+ RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"`
+ ExpireHeight int64 `json:"expire_height" yaml:"expire_height"`
+ Timestamp int64 `json:"timestamp" yaml:"timestamp"`
+ Sender sdk.AccAddress `json:"sender" yaml:"sender"`
+ Recipient sdk.AccAddress `json:"recipient" yaml:"recipient"`
+ SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"`
+ RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"`
+ ClosedBlock int64 `json:"closed_block" yaml:"closed_block"`
+ Status SwapStatus `json:"status" yaml:"status"`
+ CrossChain bool `json:"cross_chain" yaml:"cross_chain"`
+ Direction SwapDirection `json:"direction" yaml:"direction"`
+}
+
+// SwapStatus is the status of an AtomicSwap
+type SwapStatus byte
+
+const (
+ NULL SwapStatus = 0x00
+ Open SwapStatus = 0x01
+ Completed SwapStatus = 0x02
+ Expired SwapStatus = 0x03
+)
+
+// SwapDirection is the direction of an AtomicSwap
+type SwapDirection byte
+
+const (
+ INVALID SwapDirection = 0x00
+ Incoming SwapDirection = 0x01
+ Outgoing SwapDirection = 0x02
+)
+
+// AssetSupply contains information about an asset's supply
+type AssetSupply struct {
+ Denom string `json:"denom" yaml:"denom"`
+ IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"`
+ OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"`
+ CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"`
+ Limit sdk.Coin `json:"limit" yaml:"limit"`
+}
+```
\ No newline at end of file
diff --git a/x/bep3/spec/03_messages.md b/x/bep3/spec/03_messages.md
new file mode 100644
index 00000000..97ef3912
--- /dev/null
+++ b/x/bep3/spec/03_messages.md
@@ -0,0 +1,48 @@
+# Messages
+
+## Create swap
+
+Swaps are created using the `MsgCreateAtomicSwap` message type.
+
+```go
+// MsgCreateAtomicSwap contains an AtomicSwap struct
+type MsgCreateAtomicSwap struct {
+ From sdk.AccAddress `json:"from" yaml:"from"`
+ To sdk.AccAddress `json:"to" yaml:"to"`
+ RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"`
+ SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"`
+ RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"`
+ Timestamp int64 `json:"timestamp" yaml:"timestamp"`
+ Amount sdk.Coins `json:"amount" yaml:"amount"`
+ ExpectedIncome string `json:"expected_income" yaml:"expected_income"`
+ HeightSpan int64 `json:"height_span" yaml:"height_span"`
+ CrossChain bool `json:"cross_chain" yaml:"cross_chain"`
+}
+```
+
+## Claim swap
+
+Active swaps are claimed using the `MsgClaimAtomicSwap` message type.
+
+```go
+// MsgClaimAtomicSwap defines a AtomicSwap claim
+type MsgClaimAtomicSwap struct {
+ From sdk.AccAddress `json:"from" yaml:"from"`
+ SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"`
+ RandomNumber tmbytes.HexBytes `json:"random_number" yaml:"random_number"`
+}
+```
+
+
+
+## Refund swap
+
+Expired swaps are refunded using the `MsgRefundAtomicSwap` message type.
+
+```go
+// MsgRefundAtomicSwap defines a refund msg
+type MsgRefundAtomicSwap struct {
+ From sdk.AccAddress `json:"from" yaml:"from"`
+ SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"`
+}
+```
\ No newline at end of file
diff --git a/x/bep3/spec/04_events.md b/x/bep3/spec/04_events.md
new file mode 100644
index 00000000..059a2055
--- /dev/null
+++ b/x/bep3/spec/04_events.md
@@ -0,0 +1,52 @@
+# Events
+
+The `x/bep3` module emits the following events:
+
+## Handlers
+
+### MsgCreateAtomicSwap
+
+| Type | Attribute Key | Attribute Value |
+|--------------------|--------------------|--------------------------|
+| create_atomic_swap | sender | {sender address} |
+| create_atomic_swap | recipient | {recipient address} |
+| create_atomic_swap | atomic_swap_id | {swap ID} |
+| create_atomic_swap | random_number_hash | {random number hash} |
+| create_atomic_swap | timestamp | {timestamp} |
+| create_atomic_swap | sender_other_chain | {sender other chain} |
+| create_atomic_swap | expire_height | {swap expiration block} |
+| create_atomic_swap | amount | {coin amount} |
+| create_atomic_swap | expected_income | {expected value received}|
+| create_atomic_swap | direction | {incoming or outgoing} |
+| message | module | bep3 |
+| message | sender | {sender address} |
+
+### MsgClaimAtomicSwap
+
+| Type | Attribute Key | Attribute Value |
+|--------------------|--------------------|--------------------------|
+| claim_atomic_swap | claim_sender | {sender address} |
+| claim_atomic_swap | recipient | {recipient address} |
+| claim_atomic_swap | atomic_swap_id | {swap ID} |
+| claim_atomic_swap | random_number_hash | {random number hash} |
+| claim_atomic_swap | random_number | {secret random number} |
+| message | module | bep3 |
+| message | sender | {sender address} |
+
+## MsgRefundAtomicSwap
+
+| Type | Attribute Key | Attribute Value |
+|--------------------|--------------------|--------------------------|
+| refund_atomic_swap | refund_sender | {sender address} |
+| refund_atomic_swap | sender | {swap creator address} |
+| refund_atomic_swap | atomic_swap_id | {swap ID} |
+| refund_atomic_swap | random_number_hash | {random number hash} |
+| message | module | bep3 |
+| message | sender | {sender address} |
+
+## BeginBlock
+
+| Type | Attribute Key | Attribute Value |
+|---------------|------------------|------------------------------|
+| swaps_expired | atomic_swap_ids | {array of swap IDs} |
+| swaps_expired | expiration_block | {block height at expiration} |
diff --git a/x/bep3/spec/05_params.md b/x/bep3/spec/05_params.md
new file mode 100644
index 00000000..bcb0b51f
--- /dev/null
+++ b/x/bep3/spec/05_params.md
@@ -0,0 +1,16 @@
+# Parameters
+
+The bep3 module contains the following parameters:
+
+| Key | Type | Example | Description |
+|-------------------|-------------------------|-----------------------------------------------|-------------------------------|
+| BnbDeputyAddress | string (sdk.AccAddress) | "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj" | deputy's Kava address |
+| MinBlockLock | int64 | 80 | minimum swap expire height |
+| MaxBlockLock | int64 | 600 | maximum swap expire height |
+| SupportedAssets | AssetParams | []AssetParam | array of supported assets |
+|-------------------|-------------------------|-----------------------------------------------|-------------------------------|
+| AssetParam | AssetParam | AssetParam{"bnb", 714, sdk.NewInt(100), true} | a supported asset |
+| AssetParam.Denom | string | "bnb" | asset's name |
+| AssetParam.CoinID | int64 | 714 | asset's international coin ID |
+| AssetParam.Limit | sdk.Int | sdk.NewInt(100) | asset's supply limit |
+| AssetParam.Active | boolean | true | asset's state: live or paused |
diff --git a/x/bep3/spec/06_begin_block.md b/x/bep3/spec/06_begin_block.md
new file mode 100644
index 00000000..44a8f328
--- /dev/null
+++ b/x/bep3/spec/06_begin_block.md
@@ -0,0 +1,19 @@
+# Begin Block
+
+At the start of each block, atomic swaps that have reached `ExpireHeight` are expired. The logic to expire atomic swaps is as follows:
+
+```go
+ var expiredSwaps [][]byte
+ k.IterateAtomicSwapsByBlock(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool {
+ expiredSwaps = append(expiredSwaps, id)
+ return false
+ })
+
+ // Expire incomplete swaps (claimed swaps have already been removed from byBlock index)
+ for _, id := range expiredSwaps {
+ atomicSwap, _ := k.GetAtomicSwap(ctx, id)
+ atomicSwap.Status = types.Expired
+ k.SetAtomicSwap(ctx, atomicSwap)
+ k.RemoveFromByBlockIndex(ctx, atomicSwap)
+ }
+```
diff --git a/x/bep3/spec/README.md b/x/bep3/spec/README.md
new file mode 100644
index 00000000..226e2c66
--- /dev/null
+++ b/x/bep3/spec/README.md
@@ -0,0 +1,13 @@
+# `bep3` module specification
+
+
+1. **[Concepts](01_concepts.md)**
+2. **[State](02_state.md)**
+3. **[Messages](03_messages.md)**
+4. **[Events](04_events.md)**
+5. **[Params](05_params.md)**
+6. **[BeginBlock](06_begin_block.md)**
+
+## Abstract
+
+`x/bep3` is an implementation of a Cosmos SDK Module that handles cross-chain Atomic Swaps between Kava and blockchains that implement the BEP3 protocol. Atomic Swaps are created, then either claimed before their expiration block or refunded after they've expired.
diff --git a/x/bep3/spec/diagrams/BEP3_binance_chain_to_kava.jpg b/x/bep3/spec/diagrams/BEP3_binance_chain_to_kava.jpg
new file mode 100644
index 00000000..350d659a
Binary files /dev/null and b/x/bep3/spec/diagrams/BEP3_binance_chain_to_kava.jpg differ
diff --git a/x/bep3/spec/diagrams/BEP3_kava_to_binance_chain.jpg b/x/bep3/spec/diagrams/BEP3_kava_to_binance_chain.jpg
new file mode 100644
index 00000000..a0d0650e
Binary files /dev/null and b/x/bep3/spec/diagrams/BEP3_kava_to_binance_chain.jpg differ
diff --git a/x/bep3/types/asset.go b/x/bep3/types/asset.go
new file mode 100644
index 00000000..51ef07e5
--- /dev/null
+++ b/x/bep3/types/asset.go
@@ -0,0 +1,50 @@
+package types
+
+import (
+ "fmt"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+// AssetSupply contains information about an asset's supply
+type AssetSupply struct {
+ Denom string `json:"denom" yaml:"denom"`
+ IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"`
+ OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"`
+ CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"`
+ Limit sdk.Coin `json:"limit" yaml:"limit"`
+}
+
+// NewAssetSupply initializes a new AssetSupply
+func NewAssetSupply(denom string, incomingSupply, outgoingSupply, currentSupply, limit sdk.Coin) AssetSupply {
+ return AssetSupply{
+ Denom: denom,
+ IncomingSupply: incomingSupply,
+ OutgoingSupply: outgoingSupply,
+ CurrentSupply: currentSupply,
+ Limit: limit,
+ }
+}
+
+// String implements stringer
+func (a AssetSupply) String() string {
+ return fmt.Sprintf("Asset Supply"+
+ "\n Denom: %s"+
+ "\n Incoming supply: %s"+
+ "\n Outgoing supply: %s"+
+ "\n Current supply: %s"+
+ "\n Limit: %s"+
+ a.Denom, a.IncomingSupply, a.OutgoingSupply, a.CurrentSupply, a.Limit)
+}
+
+// AssetSupplies is a slice of AssetSupply
+type AssetSupplies []AssetSupply
+
+// String implements stringer
+func (supplies AssetSupplies) String() string {
+ out := ""
+ for _, supply := range supplies {
+ out += supply.String() + "\n"
+ }
+ return out
+}
diff --git a/x/bep3/types/codec.go b/x/bep3/types/codec.go
new file mode 100644
index 00000000..8ee32588
--- /dev/null
+++ b/x/bep3/types/codec.go
@@ -0,0 +1,18 @@
+package types
+
+import (
+ "github.com/cosmos/cosmos-sdk/codec"
+)
+
+var ModuleCdc = codec.New()
+
+func init() {
+ RegisterCodec(ModuleCdc)
+}
+
+// RegisterCodec registers concrete types on amino
+func RegisterCodec(cdc *codec.Codec) {
+ cdc.RegisterConcrete(MsgCreateAtomicSwap{}, "bep3/MsgCreateAtomicSwap", nil)
+ cdc.RegisterConcrete(MsgRefundAtomicSwap{}, "bep3/MsgRefundAtomicSwap", nil)
+ cdc.RegisterConcrete(MsgClaimAtomicSwap{}, "bep3/MsgClaimAtomicSwap", nil)
+}
diff --git a/x/bep3/types/common_test.go b/x/bep3/types/common_test.go
new file mode 100644
index 00000000..57601d1e
--- /dev/null
+++ b/x/bep3/types/common_test.go
@@ -0,0 +1,35 @@
+package types_test
+
+import (
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/x/bep3/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+func i(in int64) sdk.Int { return sdk.NewInt(in) }
+func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
+func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
+func ts(minOffset int) int64 { return tmtime.Now().Add(time.Duration(minOffset) * time.Minute).Unix() }
+
+func atomicSwaps(count int) types.AtomicSwaps {
+ var swaps types.AtomicSwaps
+ for i := 0; i < count; i++ {
+ swap := atomicSwap(i)
+ swaps = append(swaps, swap)
+ }
+ return swaps
+}
+
+func atomicSwap(index int) types.AtomicSwap {
+ expireOffset := int64((index * 15) + 360) // Default expire height + offet to match timestamp
+ timestamp := ts(index) // One minute apart
+ randomNumber, _ := types.GenerateSecureRandomNumber()
+ randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+
+ swap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash, expireOffset, timestamp, kavaAddrs[0],
+ kavaAddrs[1], binanceAddrs[0].String(), binanceAddrs[1].String(), 0, types.Open, true, types.Incoming)
+
+ return swap
+}
diff --git a/x/bep3/types/errors.go b/x/bep3/types/errors.go
new file mode 100644
index 00000000..71939c46
--- /dev/null
+++ b/x/bep3/types/errors.go
@@ -0,0 +1,40 @@
+package types
+
+import (
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+)
+
+// DONTCOVER
+
+var (
+ // ErrInvalidTimestamp error for when an timestamp is outside of bounds. Assumes block time of 10 seconds.
+ ErrInvalidTimestamp = sdkerrors.Register(ModuleName, 2, "timestamp can neither be 15 minutes ahead of the current time, nor 30 minutes later")
+ // ErrInvalidHeightSpan error a proposed height span is outside of lock time range
+ ErrInvalidHeightSpan = sdkerrors.Register(ModuleName, 3, "height span is outside acceptable range")
+ // ErrAssetNotSupported error for when an asset is not supported
+ ErrAssetNotSupported = sdkerrors.Register(ModuleName, 4, "asset not on the list of supported assets")
+ // ErrAssetNotActive error for when an asset is currently inactive
+ ErrAssetNotActive = sdkerrors.Register(ModuleName, 5, "asset is currently inactive")
+ // ErrAssetSupplyNotFound error for when an asset's supply is not found in the store
+ ErrAssetSupplyNotFound = sdkerrors.Register(ModuleName, 6, "asset supply not found in store")
+ // ErrExceedsSupplyLimit error for when the proposed supply increase would put the supply above limit
+ ErrExceedsSupplyLimit = sdkerrors.Register(ModuleName, 7, "asset supply over limit")
+ // ErrExceedsAvailableSupply error for when the proposed outgoing amount exceeds the total available supply
+ ErrExceedsAvailableSupply = sdkerrors.Register(ModuleName, 8, "outgoing swap exceeds total available supply")
+ // ErrInvalidCurrentSupply error for when the proposed decrease would result in a negative current supplyx
+ ErrInvalidCurrentSupply = sdkerrors.Register(ModuleName, 9, "supply decrease puts current asset supply below 0")
+ // ErrInvalidIncomingSupply error for when the proposed decrease would result in a negative incoming supply
+ ErrInvalidIncomingSupply = sdkerrors.Register(ModuleName, 10, "supply decrease puts incoming asset supply below 0")
+ // ErrInvalidOutgoingSupply error for when the proposed decrease would result in a negative outgoing supply
+ ErrInvalidOutgoingSupply = sdkerrors.Register(ModuleName, 11, "supply decrease puts outgoing asset supply below 0")
+ // ErrInvalidClaimSecret error when a submitted secret doesn't match an AtomicSwap's swapID
+ ErrInvalidClaimSecret = sdkerrors.Register(ModuleName, 12, "hashed claim attempt does not match")
+ // ErrAtomicSwapAlreadyExists error for when an AtomicSwap with this swapID already exists
+ ErrAtomicSwapAlreadyExists = sdkerrors.Register(ModuleName, 13, "atomic swap already exists")
+ // ErrAtomicSwapNotFound error for when an atomic swap is not found
+ ErrAtomicSwapNotFound = sdkerrors.Register(ModuleName, 14, "atomic swap not found")
+ // ErrSwapNotRefundable error for when an AtomicSwap has not expired and cannot be refunded
+ ErrSwapNotRefundable = sdkerrors.Register(ModuleName, 15, "atomic swap is still active and cannot be refunded")
+ // ErrSwapNotClaimable error for when an atomic swap is not open and cannot be claimed
+ ErrSwapNotClaimable = sdkerrors.Register(ModuleName, 16, "atomic swap is not claimable")
+)
diff --git a/x/bep3/types/events.go b/x/bep3/types/events.go
new file mode 100644
index 00000000..298f3be6
--- /dev/null
+++ b/x/bep3/types/events.go
@@ -0,0 +1,26 @@
+package types
+
+// Events for bep3 module
+const (
+ EventTypeCreateAtomicSwap = "create_atomic_swap"
+ EventTypeClaimAtomicSwap = "claim_atomic_swap"
+ EventTypeRefundAtomicSwap = "refund_atomic_swap"
+ EventTypeSwapsExpired = "swaps_expired"
+
+ AttributeValueCategory = ModuleName
+ AttributeKeySender = "sender"
+ AttributeKeyRecipient = "recipient"
+ AttributeKeyAtomicSwapID = "atomic_swap_id"
+ AttributeKeyRandomNumberHash = "random_number_hash"
+ AttributeKeyTimestamp = "timestamp"
+ AttributeKeySenderOtherChain = "sender_other_chain"
+ AttributeKeyExpireHeight = "expire_height"
+ AttributeKeyAmount = "amount"
+ AttributeKeyExpectedIncome = "expected_income"
+ AttributeKeyDirection = "direction"
+ AttributeKeyClaimSender = "claim_sender"
+ AttributeKeyRandomNumber = "random_number"
+ AttributeKeyRefundSender = "refund_sender"
+ AttributeKeyAtomicSwapIDs = "atomic_swap_ids"
+ AttributeExpirationBlock = "expiration_block"
+)
diff --git a/x/bep3/types/expected_keepers.go b/x/bep3/types/expected_keepers.go
new file mode 100644
index 00000000..bb58ef9b
--- /dev/null
+++ b/x/bep3/types/expected_keepers.go
@@ -0,0 +1,19 @@
+package types
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
+)
+
+// SupplyKeeper defines the expected supply Keeper
+type SupplyKeeper interface {
+ GetModuleAddress(name string) sdk.AccAddress
+ GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI
+
+ SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) error
+ SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
+ SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
+
+ BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error
+ MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error
+}
diff --git a/x/bep3/types/genesis.go b/x/bep3/types/genesis.go
new file mode 100644
index 00000000..a2159e13
--- /dev/null
+++ b/x/bep3/types/genesis.go
@@ -0,0 +1,66 @@
+package types
+
+import (
+ "bytes"
+ "encoding/hex"
+ "fmt"
+)
+
+// GenesisState - all bep3 state that must be provided at genesis
+type GenesisState struct {
+ Params Params `json:"params" yaml:"params"`
+ AtomicSwaps AtomicSwaps `json:"atomic_swaps" yaml:"atomic_swaps"`
+ AssetSupplies AssetSupplies `json:"assets_supplies" yaml:"assets_supplies"`
+}
+
+// NewGenesisState creates a new GenesisState object
+func NewGenesisState(params Params, swaps AtomicSwaps, supplies AssetSupplies) GenesisState {
+ return GenesisState{
+ Params: params,
+ AtomicSwaps: swaps,
+ AssetSupplies: supplies,
+ }
+}
+
+// DefaultGenesisState - default GenesisState used by Cosmos Hub
+func DefaultGenesisState() GenesisState {
+ return NewGenesisState(
+ DefaultParams(),
+ AtomicSwaps{},
+ AssetSupplies{},
+ )
+}
+
+// Equal checks whether two GenesisState structs are equivalent.
+func (gs GenesisState) Equal(gs2 GenesisState) bool {
+ b1 := ModuleCdc.MustMarshalBinaryBare(gs)
+ b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
+ return bytes.Equal(b1, b2)
+}
+
+// IsEmpty returns true if a GenesisState is empty.
+func (gs GenesisState) IsEmpty() bool {
+ return gs.Equal(GenesisState{})
+}
+
+// Validate validates genesis inputs. It returns error if validation of any input fails.
+func (gs GenesisState) Validate() error {
+ if err := gs.Params.Validate(); err != nil {
+ return err
+ }
+ denoms := map[string]bool{}
+ for _, a := range gs.AssetSupplies {
+ if denoms[a.Denom] {
+ return fmt.Errorf("found duplicate asset denom %s", a.Denom)
+ }
+ denoms[a.Denom] = true
+ }
+ ids := map[string]bool{}
+ for _, a := range gs.AtomicSwaps {
+ if ids[hex.EncodeToString(a.GetSwapID())] {
+ return fmt.Errorf("found duplicate atomic swap ID %s", hex.EncodeToString(a.GetSwapID()))
+ }
+ ids[hex.EncodeToString(a.GetSwapID())] = true
+ }
+ return nil
+}
diff --git a/x/bep3/types/genesis_test.go b/x/bep3/types/genesis_test.go
new file mode 100644
index 00000000..b72f9be8
--- /dev/null
+++ b/x/bep3/types/genesis_test.go
@@ -0,0 +1,104 @@
+package types_test
+
+import (
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3/types"
+ "github.com/stretchr/testify/suite"
+)
+
+type GenesisTestSuite struct {
+ suite.Suite
+ swaps types.AtomicSwaps
+ supplies types.AssetSupplies
+}
+
+func (suite *GenesisTestSuite) SetupTest() {
+ config := sdk.GetConfig()
+ app.SetBech32AddressPrefixes(config)
+
+ count := 10
+ suite.swaps = atomicSwaps(count)
+
+ incomingSupply := int64(count * 50000)
+ supply := types.NewAssetSupply("bnb", c("bnb", incomingSupply),
+ c("bnb", 0), c("bnb", 0), c("bnb", 100000000000))
+ suite.supplies = types.AssetSupplies{supply}
+}
+
+func (suite *GenesisTestSuite) TestValidate() {
+ type args struct {
+ swaps types.AtomicSwaps
+ supplies types.AssetSupplies
+ }
+ testCases := []struct {
+ name string
+ args args
+ expectPass bool
+ }{
+ {
+ "default",
+ args{
+ swaps: types.AtomicSwaps{},
+ supplies: types.AssetSupplies{},
+ },
+ true,
+ },
+ {
+ "with swaps",
+ args{
+ swaps: suite.swaps,
+ supplies: types.AssetSupplies{},
+ },
+ true,
+ },
+ {
+ "with supplies",
+ args{
+ swaps: types.AtomicSwaps{},
+ supplies: suite.supplies,
+ },
+ true,
+ },
+ {
+ "duplicate swaps",
+ args{
+ swaps: types.AtomicSwaps{suite.swaps[2], suite.swaps[2]},
+ supplies: types.AssetSupplies{},
+ },
+ false,
+ },
+ {
+ "duplicate supplies",
+ args{
+ swaps: types.AtomicSwaps{},
+ supplies: types.AssetSupplies{suite.supplies[0], suite.supplies[0]},
+ },
+ false,
+ }}
+
+ for _, tc := range testCases {
+ suite.SetupTest()
+ suite.Run(tc.name, func() {
+ var gs types.GenesisState
+ if tc.name == "default" {
+ gs = types.DefaultGenesisState()
+ } else {
+ gs = types.NewGenesisState(types.DefaultParams(), tc.args.swaps, tc.args.supplies)
+ }
+
+ err := gs.Validate()
+ if tc.expectPass {
+ suite.Nil(err)
+ } else {
+ suite.Error(err)
+ }
+ })
+ }
+}
+
+func TestGenesisTestSuite(t *testing.T) {
+ suite.Run(t, new(GenesisTestSuite))
+}
diff --git a/x/bep3/types/hash.go b/x/bep3/types/hash.go
new file mode 100644
index 00000000..bcb23fcf
--- /dev/null
+++ b/x/bep3/types/hash.go
@@ -0,0 +1,48 @@
+package types
+
+import (
+ "crypto/rand"
+ "encoding/binary"
+ "errors"
+ "math/big"
+ "strings"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/tendermint/tendermint/crypto/tmhash"
+)
+
+// GenerateSecureRandomNumber generates cryptographically strong pseudo-random number
+func GenerateSecureRandomNumber() (*big.Int, error) {
+ max := new(big.Int)
+ max.Exp(big.NewInt(2), big.NewInt(256), nil) // 256-bits integer i.e. 2^256
+
+ // Generate number between 0 - max
+ randomNumber, err := rand.Int(rand.Reader, max)
+ if err != nil {
+ return big.NewInt(0), errors.New("random number generation error")
+ }
+
+ // Catch random numbers that encode to hexadecimal poorly
+ if len(randomNumber.Text(16)) != 64 {
+ return GenerateSecureRandomNumber()
+ }
+
+ return randomNumber, nil
+}
+
+// CalculateRandomHash calculates the hash of a number and timestamp
+func CalculateRandomHash(randomNumber []byte, timestamp int64) []byte {
+ data := make([]byte, RandomNumberLength+Int64Size)
+ copy(data[:RandomNumberLength], randomNumber)
+ binary.BigEndian.PutUint64(data[RandomNumberLength:], uint64(timestamp))
+ return tmhash.Sum(data)
+}
+
+// CalculateSwapID calculates the hash of a RandomNumberHash, sdk.AccAddress, and string
+func CalculateSwapID(randomNumberHash []byte, sender sdk.AccAddress, senderOtherChain string) []byte {
+ senderOtherChain = strings.ToLower(senderOtherChain)
+ data := randomNumberHash
+ data = append(data, []byte(sender)...)
+ data = append(data, []byte(senderOtherChain)...)
+ return tmhash.Sum(data)
+}
diff --git a/x/bep3/types/hash_test.go b/x/bep3/types/hash_test.go
new file mode 100644
index 00000000..0161a059
--- /dev/null
+++ b/x/bep3/types/hash_test.go
@@ -0,0 +1,61 @@
+package types_test
+
+import (
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3/types"
+ "github.com/stretchr/testify/suite"
+)
+
+type HashTestSuite struct {
+ suite.Suite
+ addrs []sdk.AccAddress
+ timestamps []int64
+}
+
+func (suite *HashTestSuite) SetupTest() {
+ // Generate 10 addresses
+ _, addrs := app.GeneratePrivKeyAddressPairs(10)
+
+ // Generate 10 timestamps
+ var timestamps []int64
+ for i := 0; i < 10; i++ {
+ timestamps = append(timestamps, ts(i))
+ }
+
+ suite.addrs = addrs
+ suite.timestamps = timestamps
+ return
+}
+
+func (suite *HashTestSuite) TestGenerateSecureRandomNumber() {
+ secureRandomNumber, err := types.GenerateSecureRandomNumber()
+ suite.Nil(err)
+ suite.NotNil(secureRandomNumber)
+ suite.Equal(64, len(secureRandomNumber.Text(16)))
+}
+
+func (suite *HashTestSuite) TestCalculateRandomHash() {
+ randomNumber, _ := types.GenerateSecureRandomNumber()
+ hash := types.CalculateRandomHash(randomNumber.Bytes(), suite.timestamps[0])
+ suite.NotNil(hash)
+ suite.Equal(32, len(hash))
+}
+
+func (suite *HashTestSuite) TestCalculateSwapID() {
+ randomNumber, _ := types.GenerateSecureRandomNumber()
+ hash := types.CalculateRandomHash(randomNumber.Bytes(), suite.timestamps[3])
+ swapID := types.CalculateSwapID(hash, suite.addrs[3], suite.addrs[5].String())
+ suite.NotNil(swapID)
+ suite.Equal(32, len(swapID))
+
+ diffHash := types.CalculateRandomHash(randomNumber.Bytes(), suite.timestamps[2])
+ diffSwapID := types.CalculateSwapID(diffHash, suite.addrs[3], suite.addrs[5].String())
+ suite.NotEqual(swapID, diffSwapID)
+}
+
+func TestHashTestSuite(t *testing.T) {
+ suite.Run(t, new(HashTestSuite))
+}
\ No newline at end of file
diff --git a/x/bep3/types/keys.go b/x/bep3/types/keys.go
new file mode 100644
index 00000000..fa5316dc
--- /dev/null
+++ b/x/bep3/types/keys.go
@@ -0,0 +1,67 @@
+package types
+
+import (
+ "encoding/binary"
+ "encoding/hex"
+)
+
+const (
+ // ModuleName is the name of the module
+ ModuleName = "bep3"
+
+ // StoreKey to be used when creating the KVStore
+ StoreKey = ModuleName
+
+ // RouterKey to be used for routing msgs
+ RouterKey = ModuleName
+
+ // QuerierRoute is the querier route for bep3
+ QuerierRoute = ModuleName
+
+ // DefaultParamspace default namestore
+ DefaultParamspace = ModuleName
+)
+
+// DefaultLongtermStorageDuration is 1 week (assuming a block time of 7 seconds)
+const DefaultLongtermStorageDuration int64 = 86400
+
+// Key prefixes
+var (
+ AtomicSwapKeyPrefix = []byte{0x00} // prefix for keys that store AtomicSwaps
+ AtomicSwapByBlockPrefix = []byte{0x01} // prefix for keys of the AtomicSwapsByBlock index
+ AssetSupplyKeyPrefix = []byte{0x02} // prefix for keys that store global asset supply counts
+ AtomicSwapLongtermStoragePrefix = []byte{0x03} // prefix for keys of the AtomicSwapLongtermStorage index
+)
+
+// GetAtomicSwapByHeightKey is used by the AtomicSwapByBlock index and AtomicSwapLongtermStorage index
+func GetAtomicSwapByHeightKey(height int64, swapID []byte) []byte {
+ return append(Uint64ToBytes(uint64(height)), swapID...)
+}
+
+// Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys.
+func Uint64ToBytes(id uint64) []byte {
+ bz := make([]byte, 8)
+ binary.BigEndian.PutUint64(bz, uint64(id))
+ return bz
+}
+
+// Uint64FromBytes converts some fixed length bytes back into a uint64.
+func Uint64FromBytes(bz []byte) uint64 {
+ return binary.BigEndian.Uint64(bz)
+}
+
+// BytesToHex converts data from []byte to a hex-encoded string
+func BytesToHex(data []byte) string {
+ encodedData := make([]byte, hex.EncodedLen(len(data)))
+ hex.Encode(encodedData, data)
+ return string(encodedData)
+}
+
+// HexToBytes converts data from a hex-encoded string to []bytes
+func HexToBytes(data string) ([]byte, error) {
+ decodedData, err := hex.DecodeString(data)
+ if err != nil {
+ return []byte{}, err
+ }
+ return decodedData, nil
+}
diff --git a/x/bep3/types/msg.go b/x/bep3/types/msg.go
new file mode 100644
index 00000000..92be18da
--- /dev/null
+++ b/x/bep3/types/msg.go
@@ -0,0 +1,273 @@
+package types
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/tendermint/tendermint/crypto"
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+)
+
+const (
+ CreateAtomicSwap = "createAtomicSwap"
+ DepositAtomicSwap = "depositAtomicSwap"
+ ClaimAtomicSwap = "claimAtomicSwap"
+ RefundAtomicSwap = "refundAtomicSwap"
+ CalcSwapID = "calcSwapID"
+
+ Int64Size = 8
+ RandomNumberHashLength = 32
+ RandomNumberLength = 32
+ AddrByteCount = 20
+ MaxOtherChainAddrLength = 64
+ SwapIDLength = 32
+ MaxExpectedIncomeLength = 64
+)
+
+// ensure Msg interface compliance at compile time
+var (
+ _ sdk.Msg = &MsgCreateAtomicSwap{}
+ _ sdk.Msg = &MsgClaimAtomicSwap{}
+ _ sdk.Msg = &MsgRefundAtomicSwap{}
+ AtomicSwapCoinsAccAddr = sdk.AccAddress(crypto.AddressHash([]byte("KavaAtomicSwapCoins")))
+ // kava prefix address: [INSERT BEP3-DEPUTY ADDRESS]
+ // tkava prefix address: [INSERT BEP3-DEPUTY ADDRESS]
+)
+
+// MsgCreateAtomicSwap contains an AtomicSwap struct
+type MsgCreateAtomicSwap struct {
+ From sdk.AccAddress `json:"from" yaml:"from"`
+ To sdk.AccAddress `json:"to" yaml:"to"`
+ RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"`
+ SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"`
+ RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"`
+ Timestamp int64 `json:"timestamp" yaml:"timestamp"`
+ Amount sdk.Coins `json:"amount" yaml:"amount"`
+ ExpectedIncome string `json:"expected_income" yaml:"expected_income"`
+ HeightSpan int64 `json:"height_span" yaml:"height_span"`
+ CrossChain bool `json:"cross_chain" yaml:"cross_chain"`
+}
+
+// NewMsgCreateAtomicSwap initializes a new MsgCreateAtomicSwap
+func NewMsgCreateAtomicSwap(from sdk.AccAddress, to sdk.AccAddress, recipientOtherChain,
+ senderOtherChain string, randomNumberHash tmbytes.HexBytes, timestamp int64,
+ amount sdk.Coins, expectedIncome string, heightSpan int64, crossChain bool) MsgCreateAtomicSwap {
+ return MsgCreateAtomicSwap{
+ From: from,
+ To: to,
+ RecipientOtherChain: recipientOtherChain,
+ SenderOtherChain: senderOtherChain,
+ RandomNumberHash: randomNumberHash,
+ Timestamp: timestamp,
+ Amount: amount,
+ ExpectedIncome: expectedIncome,
+ HeightSpan: heightSpan,
+ CrossChain: crossChain,
+ }
+}
+
+// Route establishes the route for the MsgCreateAtomicSwap
+func (msg MsgCreateAtomicSwap) Route() string { return RouterKey }
+
+// Type is the name of MsgCreateAtomicSwap
+func (msg MsgCreateAtomicSwap) Type() string { return CreateAtomicSwap }
+
+// String prints the MsgCreateAtomicSwap
+func (msg MsgCreateAtomicSwap) String() string {
+ return fmt.Sprintf("AtomicSwap{%v#%v#%v#%v#%v#%v#%v#%v#%v#%v}",
+ msg.From, msg.To, msg.RecipientOtherChain, msg.SenderOtherChain,
+ msg.RandomNumberHash, msg.Timestamp, msg.Amount, msg.ExpectedIncome,
+ msg.HeightSpan, msg.CrossChain)
+}
+
+// GetInvolvedAddresses gets the addresses involved in a MsgCreateAtomicSwap
+func (msg MsgCreateAtomicSwap) GetInvolvedAddresses() []sdk.AccAddress {
+ return append(msg.GetSigners(), AtomicSwapCoinsAccAddr)
+}
+
+// GetSigners gets the signers of a MsgCreateAtomicSwap
+func (msg MsgCreateAtomicSwap) GetSigners() []sdk.AccAddress {
+ return []sdk.AccAddress{msg.From}
+}
+
+// ValidateBasic validates the MsgCreateAtomicSwap
+func (msg MsgCreateAtomicSwap) ValidateBasic() error {
+ if msg.From.Empty() {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
+ }
+ if len(msg.From) != AddrByteCount {
+ return fmt.Errorf("the expected address length is %d, actual length is %d", AddrByteCount, len(msg.From))
+ }
+ if msg.To.Empty() {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "recipient address cannot be empty")
+ }
+ if len(msg.To) != AddrByteCount {
+ return fmt.Errorf("the expected address length is %d, actual length is %d", AddrByteCount, len(msg.To))
+ }
+ if !msg.CrossChain && msg.RecipientOtherChain != "" {
+ return errors.New("must leave recipient address on other chain to empty for single chain swap")
+ }
+ if !msg.CrossChain && msg.SenderOtherChain != "" {
+ return errors.New("must leave sender address on other chain to empty for single chain swap")
+ }
+ if msg.CrossChain && strings.TrimSpace(msg.RecipientOtherChain) == "" {
+ return errors.New("missing recipient address on other chain for cross chain swap")
+ }
+ if len(msg.RecipientOtherChain) > MaxOtherChainAddrLength {
+ return fmt.Errorf("the length of recipient address on other chain should be less than %d", MaxOtherChainAddrLength)
+ }
+ if len(msg.SenderOtherChain) > MaxOtherChainAddrLength {
+ return fmt.Errorf("the length of sender address on other chain should be less than %d", MaxOtherChainAddrLength)
+ }
+ if len(msg.RandomNumberHash) != RandomNumberHashLength {
+ return fmt.Errorf("the length of random number hash should be %d", RandomNumberHashLength)
+ }
+ if msg.Timestamp <= 0 {
+ return errors.New("timestamp must be positive")
+ }
+ if len(msg.Amount) == 0 {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "amount cannot be empty")
+ }
+ if !msg.Amount.IsValid() {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String())
+ }
+ if len(msg.ExpectedIncome) > MaxExpectedIncomeLength {
+ return fmt.Errorf("the length of expected income should be less than %d", MaxExpectedIncomeLength)
+ }
+ expectedIncomeCoins, err := sdk.ParseCoins(msg.ExpectedIncome)
+ if err != nil || expectedIncomeCoins == nil {
+ return fmt.Errorf("expected income %s must be in valid format e.g. 10000ukava", msg.ExpectedIncome)
+ }
+ if expectedIncomeCoins.IsAnyGT(msg.Amount) {
+ return fmt.Errorf("expected income %s cannot be greater than amount %s", msg.ExpectedIncome, msg.Amount.String())
+ }
+ if msg.HeightSpan <= 0 {
+ return errors.New("height span must be positive")
+ }
+ return nil
+}
+
+// GetSignBytes gets the sign bytes of a MsgCreateAtomicSwap
+func (msg MsgCreateAtomicSwap) GetSignBytes() []byte {
+ bz := ModuleCdc.MustMarshalJSON(msg)
+ return sdk.MustSortJSON(bz)
+}
+
+// MsgClaimAtomicSwap defines a AtomicSwap claim
+type MsgClaimAtomicSwap struct {
+ From sdk.AccAddress `json:"from" yaml:"from"`
+ SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"`
+ RandomNumber tmbytes.HexBytes `json:"random_number" yaml:"random_number"`
+}
+
+// NewMsgClaimAtomicSwap initializes a new MsgClaimAtomicSwap
+func NewMsgClaimAtomicSwap(from sdk.AccAddress, swapID, randomNumber []byte) MsgClaimAtomicSwap {
+ return MsgClaimAtomicSwap{
+ From: from,
+ SwapID: swapID,
+ RandomNumber: randomNumber,
+ }
+}
+
+// Route establishes the route for the MsgClaimAtomicSwap
+func (msg MsgClaimAtomicSwap) Route() string { return RouterKey }
+
+// Type is the name of MsgClaimAtomicSwap
+func (msg MsgClaimAtomicSwap) Type() string { return ClaimAtomicSwap }
+
+// String prints the MsgClaimAtomicSwap
+func (msg MsgClaimAtomicSwap) String() string {
+ return fmt.Sprintf("claimAtomicSwap{%v#%v#%v}", msg.From, msg.SwapID, msg.RandomNumber)
+}
+
+// GetInvolvedAddresses gets the addresses involved in a MsgClaimAtomicSwap
+func (msg MsgClaimAtomicSwap) GetInvolvedAddresses() []sdk.AccAddress {
+ return append(msg.GetSigners(), AtomicSwapCoinsAccAddr)
+}
+
+// GetSigners gets the signers of a MsgClaimAtomicSwap
+func (msg MsgClaimAtomicSwap) GetSigners() []sdk.AccAddress {
+ return []sdk.AccAddress{msg.From}
+}
+
+// ValidateBasic validates the MsgClaimAtomicSwap
+func (msg MsgClaimAtomicSwap) ValidateBasic() error {
+ if msg.From.Empty() {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
+ }
+ if len(msg.From) != AddrByteCount {
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "actual address length ≠ expected length (%d ≠ %d)", len(msg.From), AddrByteCount)
+ }
+ if len(msg.SwapID) != SwapIDLength {
+ return fmt.Errorf("the length of swapID should be %d", SwapIDLength)
+ }
+ if len(msg.RandomNumber) == 0 {
+ return errors.New("the length of random number cannot be 0")
+ }
+ return nil
+}
+
+// GetSignBytes gets the sign bytes of a MsgClaimAtomicSwap
+func (msg MsgClaimAtomicSwap) GetSignBytes() []byte {
+ bz := ModuleCdc.MustMarshalJSON(msg)
+ return sdk.MustSortJSON(bz)
+}
+
+// MsgRefundAtomicSwap defines a refund msg
+type MsgRefundAtomicSwap struct {
+ From sdk.AccAddress `json:"from" yaml:"from"`
+ SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"`
+}
+
+// NewMsgRefundAtomicSwap initializes a new MsgRefundAtomicSwap
+func NewMsgRefundAtomicSwap(from sdk.AccAddress, swapID []byte) MsgRefundAtomicSwap {
+ return MsgRefundAtomicSwap{
+ From: from,
+ SwapID: swapID,
+ }
+}
+
+// Route establishes the route for the MsgRefundAtomicSwap
+func (msg MsgRefundAtomicSwap) Route() string { return RouterKey }
+
+// Type is the name of MsgRefundAtomicSwap
+func (msg MsgRefundAtomicSwap) Type() string { return RefundAtomicSwap }
+
+// String prints the MsgRefundAtomicSwap
+func (msg MsgRefundAtomicSwap) String() string {
+ return fmt.Sprintf("refundAtomicSwap{%v#%v}", msg.From, msg.SwapID)
+}
+
+// GetInvolvedAddresses gets the addresses involved in a MsgRefundAtomicSwap
+func (msg MsgRefundAtomicSwap) GetInvolvedAddresses() []sdk.AccAddress {
+ return append(msg.GetSigners(), AtomicSwapCoinsAccAddr)
+}
+
+// GetSigners gets the signers of a MsgRefundAtomicSwap
+func (msg MsgRefundAtomicSwap) GetSigners() []sdk.AccAddress {
+ return []sdk.AccAddress{msg.From}
+}
+
+// ValidateBasic validates the MsgRefundAtomicSwap
+func (msg MsgRefundAtomicSwap) ValidateBasic() error {
+ if msg.From.Empty() {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
+ }
+ if len(msg.From) != AddrByteCount {
+ return fmt.Errorf("the expected address length is %d, actual length is %d", AddrByteCount, len(msg.From))
+ }
+ if len(msg.SwapID) != SwapIDLength {
+ return fmt.Errorf("the length of swapID should be %d", SwapIDLength)
+ }
+ return nil
+}
+
+// GetSignBytes gets the sign bytes of a MsgRefundAtomicSwap
+func (msg MsgRefundAtomicSwap) GetSignBytes() []byte {
+ bz := ModuleCdc.MustMarshalJSON(msg)
+ return sdk.MustSortJSON(bz)
+}
diff --git a/x/bep3/types/msg_test.go b/x/bep3/types/msg_test.go
new file mode 100644
index 00000000..3940e0b6
--- /dev/null
+++ b/x/bep3/types/msg_test.go
@@ -0,0 +1,125 @@
+package types_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/tendermint/tendermint/crypto"
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+var (
+ coinsSingle = sdk.NewCoins(sdk.NewInt64Coin("bnb", int64(50000)))
+ coinsZero = sdk.Coins{sdk.Coin{}}
+ binanceAddrs = []sdk.AccAddress{
+ sdk.AccAddress(crypto.AddressHash([]byte("BinanceTest1"))),
+ sdk.AccAddress(crypto.AddressHash([]byte("BinanceTest2"))),
+ }
+ kavaAddrs = []sdk.AccAddress{
+ sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
+ sdk.AccAddress(crypto.AddressHash([]byte("KavaTest2"))),
+ }
+ randomNumberBytes = []byte{15}
+ timestampInt64 = int64(100)
+ randomNumberHash = types.CalculateRandomHash(randomNumberBytes, timestampInt64)
+)
+
+func TestMsgCreateAtomicSwap(t *testing.T) {
+ tests := []struct {
+ description string
+ from sdk.AccAddress
+ to sdk.AccAddress
+ recipientOtherChain string
+ senderOtherChain string
+ randomNumberHash tmbytes.HexBytes
+ timestamp int64
+ amount sdk.Coins
+ expectedIncome string
+ heightSpan int64
+ crossChain bool
+ expectPass bool
+ }{
+ {"normal", binanceAddrs[0], kavaAddrs[0], "", "", randomNumberHash, timestampInt64, coinsSingle, "50000bnb", 500, false, true},
+ {"cross-chain", binanceAddrs[0], kavaAddrs[0], kavaAddrs[0].String(), binanceAddrs[0].String(), randomNumberHash, timestampInt64, coinsSingle, "50000bnb", 80000, true, true},
+ {"with other chain fields", binanceAddrs[0], kavaAddrs[0], kavaAddrs[0].String(), binanceAddrs[0].String(), randomNumberHash, timestampInt64, coinsSingle, "50000bnb", 500, false, false},
+ {"cross-cross no other chain fields", binanceAddrs[0], kavaAddrs[0], "", "", randomNumberHash, timestampInt64, coinsSingle, "50000bnb", 500, true, false},
+ {"zero coins", binanceAddrs[0], kavaAddrs[0], "", "", randomNumberHash, timestampInt64, coinsZero, "50000bnb", 500, true, false},
+ }
+
+ for i, tc := range tests {
+ msg := types.NewMsgCreateAtomicSwap(
+ tc.from,
+ tc.to,
+ tc.recipientOtherChain,
+ tc.senderOtherChain,
+ tc.randomNumberHash,
+ tc.timestamp,
+ tc.amount,
+ tc.expectedIncome,
+ tc.heightSpan,
+ tc.crossChain,
+ )
+ if tc.expectPass {
+ require.NoError(t, msg.ValidateBasic(), "test: %v", i)
+ } else {
+ require.Error(t, msg.ValidateBasic(), "test: %v", i)
+ }
+ }
+}
+
+func TestMsgClaimAtomicSwap(t *testing.T) {
+ swapID := types.CalculateSwapID(randomNumberHash, binanceAddrs[0], "")
+
+ tests := []struct {
+ description string
+ from sdk.AccAddress
+ swapID tmbytes.HexBytes
+ randomNumber tmbytes.HexBytes
+ expectPass bool
+ }{
+ {"normal", binanceAddrs[0], swapID, randomNumberHash, true},
+ }
+
+ for i, tc := range tests {
+ msg := types.NewMsgClaimAtomicSwap(
+ tc.from,
+ tc.swapID,
+ tc.randomNumber,
+ )
+ if tc.expectPass {
+ require.NoError(t, msg.ValidateBasic(), "test: %v", i)
+ } else {
+ require.Error(t, msg.ValidateBasic(), "test: %v", i)
+ }
+ }
+}
+
+func TestMsgRefundAtomicSwap(t *testing.T) {
+ swapID := types.CalculateSwapID(randomNumberHash, binanceAddrs[0], "")
+
+ tests := []struct {
+ description string
+ from sdk.AccAddress
+ swapID tmbytes.HexBytes
+ expectPass bool
+ }{
+ {"normal", binanceAddrs[0], swapID, true},
+ }
+
+ for i, tc := range tests {
+ msg := types.NewMsgRefundAtomicSwap(
+ tc.from,
+ tc.swapID,
+ )
+ if tc.expectPass {
+ require.NoError(t, msg.ValidateBasic(), "test: %v", i)
+ } else {
+ require.Error(t, msg.ValidateBasic(), "test: %v", i)
+ }
+ }
+}
diff --git a/x/bep3/types/params.go b/x/bep3/types/params.go
new file mode 100644
index 00000000..b7d04493
--- /dev/null
+++ b/x/bep3/types/params.go
@@ -0,0 +1,224 @@
+package types
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/params"
+)
+
+const (
+ bech32MainPrefix = "kava"
+)
+
+// Parameter keys
+var (
+ KeyBnbDeputyAddress = []byte("BnbDeputyAddress")
+ KeyMinBlockLock = []byte("MinBlockLock")
+ KeyMaxBlockLock = []byte("MaxBlockLock")
+ KeySupportedAssets = []byte("SupportedAssets")
+
+ AbsoluteMaximumBlockLock int64 = 10000
+ AbsoluteMinimumBlockLock int64 = 50
+ DefaultMinBlockLock int64 = 80
+ DefaultMaxBlockLock int64 = 600
+ DefaultSupportedAssets = AssetParams{
+ AssetParam{
+ Denom: "bnb",
+ CoinID: 714,
+ Limit: sdk.NewInt(100000000000),
+ Active: true,
+ },
+ }
+)
+
+// Params governance parameters for bep3 module
+type Params struct {
+ BnbDeputyAddress sdk.AccAddress `json:"bnb_deputy_address" yaml:"bnb_deputy_address"` // Bnbchain deputy address
+ MinBlockLock int64 `json:"min_block_lock" yaml:"min_block_lock"` // AtomicSwap minimum block lock
+ MaxBlockLock int64 `json:"max_block_lock" yaml:"max_block_lock"` // AtomicSwap maximum block lock
+ SupportedAssets AssetParams `json:"supported_assets" yaml:"supported_assets"` // Supported assets
+}
+
+// String implements fmt.Stringer
+func (p Params) String() string {
+ return fmt.Sprintf(`Params:
+ Bnbchain deputy address: %s,
+ Min block lock: %d,
+ Max block lock: %d,
+ Supported assets: %s`,
+ p.BnbDeputyAddress.String(), p.MinBlockLock, p.MaxBlockLock, p.SupportedAssets)
+}
+
+// NewParams returns a new params object
+func NewParams(bnbDeputyAddress sdk.AccAddress, minBlockLock, maxBlockLock int64, supportedAssets AssetParams,
+) Params {
+ return Params{
+ BnbDeputyAddress: bnbDeputyAddress,
+ MinBlockLock: minBlockLock,
+ MaxBlockLock: maxBlockLock,
+ SupportedAssets: supportedAssets,
+ }
+}
+
+// DefaultParams returns default params for bep3 module
+func DefaultParams() Params {
+ defaultBnbDeputyAddress, err := sdk.AccAddressFromBech32("kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj")
+ if err != nil {
+ panic(err)
+ }
+
+ return NewParams(defaultBnbDeputyAddress, DefaultMinBlockLock, DefaultMaxBlockLock, DefaultSupportedAssets)
+}
+
+// AssetParam governance parameters for each asset within a supported chain
+type AssetParam struct {
+ Denom string `json:"denom" yaml:"denom"` // name of the asset
+ CoinID int `json:"coin_id" yaml:"coin_id"` // internationally recognized coin ID
+ Limit sdk.Int `json:"limit" yaml:"limit"` // asset supply limit
+ Active bool `json:"active" yaml:"active"` // denotes if asset is available or paused
+}
+
+// String implements fmt.Stringer
+func (ap AssetParam) String() string {
+ return fmt.Sprintf(`Asset:
+ Denom: %s
+ Coin ID: %d
+ Limit: %s
+ Active: %t`,
+ ap.Denom, ap.CoinID, ap.Limit.String(), ap.Active)
+}
+
+// AssetParams array of AssetParam
+type AssetParams []AssetParam
+
+// String implements fmt.Stringer
+func (aps AssetParams) String() string {
+ out := "Asset Params\n"
+ for _, ap := range aps {
+ out += fmt.Sprintf("%s\n", ap)
+ }
+ return out
+}
+
+// ParamKeyTable Key declaration for parameters
+func ParamKeyTable() params.KeyTable {
+ return params.NewKeyTable().RegisterParamSet(&Params{})
+}
+
+// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
+// pairs of bep3 module's parameters.
+// nolint
+func (p *Params) ParamSetPairs() params.ParamSetPairs {
+ return params.ParamSetPairs{
+ params.NewParamSetPair(KeyBnbDeputyAddress, &p.BnbDeputyAddress, validateBnbDeputyAddressParam),
+ params.NewParamSetPair(KeyMinBlockLock, &p.MinBlockLock, validateMinBlockLockParam),
+ params.NewParamSetPair(KeyMaxBlockLock, &p.MaxBlockLock, validateMaxBlockLockParam),
+ params.NewParamSetPair(KeySupportedAssets, &p.SupportedAssets, validateSupportedAssetsParams),
+ }
+}
+
+// Validate ensure that params have valid values
+func (p Params) Validate() error {
+ if err := validateBnbDeputyAddressParam(p.BnbDeputyAddress); err != nil {
+ return err
+ }
+
+ if err := validateMinBlockLockParam(p.MinBlockLock); err != nil {
+ return err
+ }
+
+ if err := validateMaxBlockLockParam(p.MaxBlockLock); err != nil {
+ return err
+ }
+
+ if p.MinBlockLock >= p.MaxBlockLock {
+ return fmt.Errorf("minimum block lock cannot be ≥ maximum block lock, got %d ≥ %d", p.MinBlockLock, p.MaxBlockLock)
+ }
+
+ return validateSupportedAssetsParams(p.SupportedAssets)
+}
+
+func validateBnbDeputyAddressParam(i interface{}) error {
+ addr, ok := i.(sdk.AccAddress)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if addr.Empty() {
+ return errors.New("bnb deputy address cannot be empty")
+ }
+
+ if len(addr.Bytes()) != sdk.AddrLen {
+ return fmt.Errorf("bnb deputy address invalid bytes length got %d, want %d", len(addr.Bytes()), sdk.AddrLen)
+ }
+
+ return nil
+}
+
+func validateMinBlockLockParam(i interface{}) error {
+ minBlockLock, ok := i.(int64)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if minBlockLock < AbsoluteMinimumBlockLock {
+ return fmt.Errorf("minimum block lock cannot be less than %d, got %d", AbsoluteMinimumBlockLock, minBlockLock)
+ }
+
+ return nil
+}
+
+func validateMaxBlockLockParam(i interface{}) error {
+ maxBlockLock, ok := i.(int64)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if maxBlockLock > AbsoluteMaximumBlockLock {
+ return fmt.Errorf("maximum block lock cannot be greater than %d, got %d", AbsoluteMaximumBlockLock, maxBlockLock)
+ }
+
+ return nil
+}
+
+func validateSupportedAssetsParams(i interface{}) error {
+ assetParams, ok := i.(AssetParams)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ coinIDs := make(map[int]bool)
+ coinDenoms := make(map[string]bool)
+ for _, asset := range assetParams {
+ if strings.TrimSpace(asset.Denom) == "" {
+ return errors.New("asset denom cannot be empty")
+ }
+
+ if asset.CoinID < 0 {
+ return fmt.Errorf(fmt.Sprintf("asset %s must be a non negative integer", asset.Denom))
+ }
+
+ if !asset.Limit.IsPositive() {
+ return fmt.Errorf(fmt.Sprintf("asset %s must have a positive supply limit", asset.Denom))
+ }
+
+ _, found := coinDenoms[asset.Denom]
+ if found {
+ return fmt.Errorf(fmt.Sprintf("asset %s cannot have duplicate denom", asset.Denom))
+ }
+
+ coinDenoms[asset.Denom] = true
+
+ _, found = coinIDs[asset.CoinID]
+ if found {
+ return fmt.Errorf(fmt.Sprintf("asset %s cannot have duplicate coin id %d", asset.Denom, asset.CoinID))
+ }
+
+ coinIDs[asset.CoinID] = true
+ }
+
+ return nil
+}
diff --git a/x/bep3/types/params_test.go b/x/bep3/types/params_test.go
new file mode 100644
index 00000000..65e49cfd
--- /dev/null
+++ b/x/bep3/types/params_test.go
@@ -0,0 +1,215 @@
+package types_test
+
+import (
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3/types"
+ "github.com/stretchr/testify/suite"
+)
+
+type ParamsTestSuite struct {
+ suite.Suite
+ addr sdk.AccAddress
+}
+
+func (suite *ParamsTestSuite) SetupTest() {
+ config := sdk.GetConfig()
+ app.SetBech32AddressPrefixes(config)
+ _, addrs := app.GeneratePrivKeyAddressPairs(1)
+ suite.addr = addrs[0]
+ return
+}
+
+func (suite *ParamsTestSuite) TestParamValidation() {
+ type LoadParams func() types.Params
+
+ type args struct {
+ bnbDeputyAddress sdk.AccAddress
+ minBlockLock int64
+ maxBlockLock int64
+ supportedAssets types.AssetParams
+ }
+
+ testCases := []struct {
+ name string
+ args args
+ expectPass bool
+ expectedErr string
+ }{
+ {
+ name: "default",
+ args: args{
+ bnbDeputyAddress: suite.addr,
+ minBlockLock: types.DefaultMinBlockLock,
+ maxBlockLock: types.DefaultMaxBlockLock,
+ supportedAssets: types.DefaultSupportedAssets,
+ },
+ expectPass: true,
+ expectedErr: "",
+ },
+ {
+ name: "minimum block lock below limit",
+ args: args{
+ bnbDeputyAddress: suite.addr,
+ minBlockLock: 1,
+ maxBlockLock: types.DefaultMaxBlockLock,
+ supportedAssets: types.DefaultSupportedAssets,
+ },
+ expectPass: false,
+ expectedErr: "minimum block lock cannot be less than",
+ },
+ {
+ name: "minimum block lock above limit",
+ args: args{
+ bnbDeputyAddress: suite.addr,
+ minBlockLock: 500000,
+ maxBlockLock: types.DefaultMaxBlockLock,
+ supportedAssets: types.DefaultSupportedAssets,
+ },
+ expectPass: false,
+ expectedErr: "maximum block lock must be greater than minimum block lock",
+ },
+ {
+ name: "maximum block lock below limit",
+ args: args{
+ bnbDeputyAddress: suite.addr,
+ minBlockLock: types.DefaultMinBlockLock,
+ maxBlockLock: 1,
+ supportedAssets: types.DefaultSupportedAssets,
+ },
+ expectPass: false,
+ expectedErr: "maximum block lock must be greater than minimum block lock",
+ },
+ {
+ name: "maximum block lock above limit",
+ args: args{
+ bnbDeputyAddress: suite.addr,
+ minBlockLock: types.DefaultMinBlockLock,
+ maxBlockLock: 100000000,
+ supportedAssets: types.DefaultSupportedAssets,
+ },
+ expectPass: false,
+ expectedErr: "maximum block lock cannot be greater than",
+ },
+ {
+ name: "empty asset denom",
+ args: args{
+ bnbDeputyAddress: suite.addr,
+ minBlockLock: types.DefaultMinBlockLock,
+ maxBlockLock: types.DefaultMaxBlockLock,
+ supportedAssets: types.AssetParams{
+ types.AssetParam{
+ Denom: "",
+ CoinID: 714,
+ Limit: sdk.NewInt(100000000000),
+ Active: true,
+ },
+ },
+ },
+ expectPass: false,
+ expectedErr: "asset denom cannot be empty",
+ },
+ {
+ name: "negative asset coin ID",
+ args: args{
+ bnbDeputyAddress: suite.addr,
+ minBlockLock: types.DefaultMinBlockLock,
+ maxBlockLock: types.DefaultMaxBlockLock,
+ supportedAssets: types.AssetParams{
+ types.AssetParam{
+ Denom: "bnb",
+ CoinID: -1,
+ Limit: sdk.NewInt(100000000000),
+ Active: true,
+ },
+ },
+ },
+ expectPass: false,
+ expectedErr: "must be a positive integer",
+ },
+ {
+ name: "negative asset limit",
+ args: args{
+ bnbDeputyAddress: suite.addr,
+ minBlockLock: types.DefaultMinBlockLock,
+ maxBlockLock: types.DefaultMaxBlockLock,
+ supportedAssets: types.AssetParams{
+ types.AssetParam{
+ Denom: "bnb",
+ CoinID: 714,
+ Limit: sdk.NewInt(-10000),
+ Active: true,
+ },
+ },
+ },
+ expectPass: false,
+ expectedErr: "must have a positive supply limit",
+ },
+ {
+ name: "duplicate asset denom",
+ args: args{
+ bnbDeputyAddress: suite.addr,
+ minBlockLock: types.DefaultMinBlockLock,
+ maxBlockLock: types.DefaultMaxBlockLock,
+ supportedAssets: types.AssetParams{
+ types.AssetParam{
+ Denom: "bnb",
+ CoinID: 714,
+ Limit: sdk.NewInt(100000000000),
+ Active: true,
+ },
+ types.AssetParam{
+ Denom: "bnb",
+ CoinID: 114,
+ Limit: sdk.NewInt(500000000),
+ Active: false,
+ },
+ },
+ },
+ expectPass: false,
+ expectedErr: "cannot have duplicate denom",
+ },
+ {
+ name: "duplicate asset coin ID",
+ args: args{
+ bnbDeputyAddress: suite.addr,
+ minBlockLock: types.DefaultMinBlockLock,
+ maxBlockLock: types.DefaultMaxBlockLock,
+ supportedAssets: types.AssetParams{
+ types.AssetParam{
+ Denom: "bnb",
+ CoinID: 714,
+ Limit: sdk.NewInt(100000000000),
+ Active: true,
+ },
+ types.AssetParam{
+ Denom: "fake",
+ CoinID: 714,
+ Limit: sdk.NewInt(500000000),
+ Active: false,
+ },
+ },
+ },
+ expectPass: false,
+ expectedErr: "cannot have duplicate coin id",
+ },
+ }
+
+ for _, tc := range testCases {
+ params := types.NewParams(tc.args.bnbDeputyAddress, tc.args.minBlockLock,
+ tc.args.maxBlockLock, tc.args.supportedAssets)
+
+ err := params.Validate()
+ if tc.expectPass {
+ suite.Require().NoError(err, tc.name)
+ } else {
+ suite.Require().Error(err, tc.name)
+ }
+ }
+}
+
+func TestParamsTestSuite(t *testing.T) {
+ suite.Run(t, new(ParamsTestSuite))
+}
diff --git a/x/bep3/types/querier.go b/x/bep3/types/querier.go
new file mode 100644
index 00000000..034a851b
--- /dev/null
+++ b/x/bep3/types/querier.go
@@ -0,0 +1,52 @@
+package types
+
+import tmbytes "github.com/tendermint/tendermint/libs/bytes"
+
+const (
+ // QueryGetAssetSupply command for getting info about an asset's supply
+ QueryGetAssetSupply = "supply"
+ // QueryGetAtomicSwap command for getting info about an atomic swap
+ QueryGetAtomicSwap = "swap"
+ // QueryGetAtomicSwaps command for getting a list of atomic swaps
+ QueryGetAtomicSwaps = "swaps"
+ // QueryGetParams command for getting module params
+ QueryGetParams = "parameters"
+)
+
+// QueryAssetSupply contains the params for query 'custom/bep3/supply'
+type QueryAssetSupply struct {
+ Denom tmbytes.HexBytes `json:"denom" yaml:"denom"`
+}
+
+// NewQueryAssetSupply creates a new QueryAssetSupply
+func NewQueryAssetSupply(denom tmbytes.HexBytes) QueryAssetSupply {
+ return QueryAssetSupply{
+ Denom: denom,
+ }
+}
+
+// QueryAtomicSwapByID contains the params for query 'custom/bep3/swap'
+type QueryAtomicSwapByID struct {
+ SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"`
+}
+
+// NewQueryAtomicSwapByID creates a new QueryAtomicSwapByID
+func NewQueryAtomicSwapByID(swapBytes tmbytes.HexBytes) QueryAtomicSwapByID {
+ return QueryAtomicSwapByID{
+ SwapID: swapBytes,
+ }
+}
+
+// QueryAtomicSwaps contains the params for an AtomicSwaps query
+type QueryAtomicSwaps struct {
+ Page int `json:"page" yaml:"page"`
+ Limit int `json:"limit" yaml:"limit"`
+}
+
+// NewQueryAtomicSwaps creates a new QueryAtomicSwaps
+func NewQueryAtomicSwaps(page int, limit int) QueryAtomicSwaps {
+ return QueryAtomicSwaps{
+ Page: page,
+ Limit: limit,
+ }
+}
diff --git a/x/bep3/types/swap.go b/x/bep3/types/swap.go
new file mode 100644
index 00000000..52a75f6c
--- /dev/null
+++ b/x/bep3/types/swap.go
@@ -0,0 +1,212 @@
+package types
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+// AtomicSwap contains the information for an atomic swap
+type AtomicSwap struct {
+ Amount sdk.Coins `json:"amount" yaml:"amount"`
+ RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"`
+ ExpireHeight int64 `json:"expire_height" yaml:"expire_height"`
+ Timestamp int64 `json:"timestamp" yaml:"timestamp"`
+ Sender sdk.AccAddress `json:"sender" yaml:"sender"`
+ Recipient sdk.AccAddress `json:"recipient" yaml:"recipient"`
+ SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"`
+ RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"`
+ ClosedBlock int64 `json:"closed_block" yaml:"closed_block"`
+ Status SwapStatus `json:"status" yaml:"status"`
+ CrossChain bool `json:"cross_chain" yaml:"cross_chain"`
+ Direction SwapDirection `json:"direction" yaml:"direction"`
+}
+
+// NewAtomicSwap returns a new AtomicSwap
+func NewAtomicSwap(amount sdk.Coins, randomNumberHash tmbytes.HexBytes, expireHeight, timestamp int64, sender,
+ recipient sdk.AccAddress, senderOtherChain string, recipientOtherChain string, closedBlock int64,
+ status SwapStatus, crossChain bool, direction SwapDirection) AtomicSwap {
+ return AtomicSwap{
+ Amount: amount,
+ RandomNumberHash: randomNumberHash,
+ ExpireHeight: expireHeight,
+ Timestamp: timestamp,
+ Sender: sender,
+ Recipient: recipient,
+ SenderOtherChain: senderOtherChain,
+ RecipientOtherChain: recipientOtherChain,
+ ClosedBlock: closedBlock,
+ Status: status,
+ CrossChain: crossChain,
+ Direction: direction,
+ }
+}
+
+// GetSwapID calculates the ID of an atomic swap
+func (a AtomicSwap) GetSwapID() tmbytes.HexBytes {
+ return CalculateSwapID(a.RandomNumberHash, a.Sender, a.SenderOtherChain)
+}
+
+// GetCoins returns the swap's amount as sdk.Coins
+func (a AtomicSwap) GetCoins() sdk.Coins {
+ return sdk.NewCoins(a.Amount...)
+}
+
+// Validate verifies that recipient is not empty
+func (a AtomicSwap) Validate() error {
+ if len(a.Sender) != AddrByteCount {
+ return fmt.Errorf(fmt.Sprintf("the expected address length is %d, actual length is %d", AddrByteCount, len(a.Sender)))
+ }
+ if len(a.Recipient) != AddrByteCount {
+ return fmt.Errorf(fmt.Sprintf("the expected address length is %d, actual length is %d", AddrByteCount, len(a.Recipient)))
+ }
+ if len(a.RandomNumberHash) != RandomNumberHashLength {
+ return fmt.Errorf(fmt.Sprintf("the length of random number hash should be %d", RandomNumberHashLength))
+ }
+ if !a.Amount.IsAllPositive() {
+ return fmt.Errorf(fmt.Sprintf("the swapped out coin must be positive"))
+ }
+ return nil
+}
+
+// String implements stringer
+func (a AtomicSwap) String() string {
+ return fmt.Sprintf("Atomic Swap"+
+ "\n ID: %s"+
+ "\n Status: %s"+
+ "\n Amount: %s"+
+ "\n Random number hash: %s"+
+ "\n Expire height: %d"+
+ "\n Timestamp: %d"+
+ "\n Sender: %s"+
+ "\n Recipient: %s"+
+ "\n Sender other chain: %s"+
+ "\n Recipient other chain: %s"+
+ "\n Closed block: %d"+
+ "\n Cross chain: %t"+
+ "\n Direction: %s",
+ a.GetSwapID(), a.Status.String(), a.Amount.String(),
+ hex.EncodeToString(a.RandomNumberHash), a.ExpireHeight,
+ a.Timestamp, a.Sender.String(), a.Recipient.String(),
+ a.SenderOtherChain, a.RecipientOtherChain, a.ClosedBlock,
+ a.CrossChain, a.Direction)
+}
+
+// AtomicSwaps is a slice of AtomicSwap
+type AtomicSwaps []AtomicSwap
+
+// String implements stringer
+func (swaps AtomicSwaps) String() string {
+ out := ""
+ for _, swap := range swaps {
+ out += swap.String() + "\n"
+ }
+ return out
+}
+
+// SwapStatus is the status of an AtomicSwap
+type SwapStatus byte
+
+const (
+ NULL SwapStatus = 0x00
+ Open SwapStatus = 0x01
+ Completed SwapStatus = 0x02
+ Expired SwapStatus = 0x03
+)
+
+// NewSwapStatusFromString converts string to SwapStatus type
+func NewSwapStatusFromString(str string) SwapStatus {
+ switch str {
+ case "Open", "open":
+ return Open
+ case "Completed", "completed":
+ return Completed
+ case "Expired", "expired":
+ return Expired
+ default:
+ return NULL
+ }
+}
+
+// String returns the string representation of a SwapStatus
+func (status SwapStatus) String() string {
+ switch status {
+ case Open:
+ return "Open"
+ case Completed:
+ return "Completed"
+ case Expired:
+ return "Expired"
+ default:
+ return "NULL"
+ }
+}
+
+// MarshalJSON marshals the SwapStatus
+func (status SwapStatus) MarshalJSON() ([]byte, error) {
+ return json.Marshal(status.String())
+}
+
+// UnmarshalJSON unmarshals the SwapStatus
+func (status *SwapStatus) UnmarshalJSON(data []byte) error {
+ var s string
+ err := json.Unmarshal(data, &s)
+ if err != nil {
+ return err
+ }
+ *status = NewSwapStatusFromString(s)
+ return nil
+}
+
+// SwapDirection is the direction of an AtomicSwap
+type SwapDirection byte
+
+const (
+ INVALID SwapDirection = 0x00
+ Incoming SwapDirection = 0x01
+ Outgoing SwapDirection = 0x02
+)
+
+// NewSwapDirectionFromString converts string to SwapDirection type
+func NewSwapDirectionFromString(str string) SwapDirection {
+ switch str {
+ case "Incoming", "incoming", "inc", "I", "i":
+ return Incoming
+ case "Outgoing", "outgoing", "out", "O", "o":
+ return Outgoing
+ default:
+ return INVALID
+ }
+}
+
+// String returns the string representation of a SwapDirection
+func (direction SwapDirection) String() string {
+ switch direction {
+ case Incoming:
+ return "Incoming"
+ case Outgoing:
+ return "Outgoing"
+ default:
+ return "INVALID"
+ }
+}
+
+// MarshalJSON marshals the SwapDirection
+func (direction SwapDirection) MarshalJSON() ([]byte, error) {
+ return json.Marshal(direction.String())
+}
+
+// UnmarshalJSON unmarshals the SwapDirection
+func (direction *SwapDirection) UnmarshalJSON(data []byte) error {
+ var s string
+ err := json.Unmarshal(data, &s)
+ if err != nil {
+ return err
+ }
+ *direction = NewSwapDirectionFromString(s)
+ return nil
+}
diff --git a/x/bep3/types/swap_test.go b/x/bep3/types/swap_test.go
new file mode 100644
index 00000000..c9eae7f5
--- /dev/null
+++ b/x/bep3/types/swap_test.go
@@ -0,0 +1,143 @@
+package types_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ tmbytes "github.com/tendermint/tendermint/libs/bytes"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/bep3/types"
+)
+
+type AtomicSwapTestSuite struct {
+ suite.Suite
+ addrs []sdk.AccAddress
+ timestamps []int64
+ randomNumberHashes []tmbytes.HexBytes
+}
+
+func (suite *AtomicSwapTestSuite) SetupTest() {
+ // Generate 10 addresses
+ config := sdk.GetConfig()
+ app.SetBech32AddressPrefixes(config)
+ _, addrs := app.GeneratePrivKeyAddressPairs(10)
+
+ // Generate 10 timestamps and random number hashes
+ var timestamps []int64
+ var randomNumberHashes []tmbytes.HexBytes
+ for i := 0; i < 10; i++ {
+ timestamp := ts(i)
+ randomNumber, _ := types.GenerateSecureRandomNumber()
+ randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp)
+ timestamps = append(timestamps, timestamp)
+ randomNumberHashes = append(randomNumberHashes, randomNumberHash)
+ }
+
+ suite.addrs = addrs
+ suite.timestamps = timestamps
+ suite.randomNumberHashes = randomNumberHashes
+ return
+}
+
+func (suite *AtomicSwapTestSuite) TestNewAtomicSwap() {
+ type args struct {
+ amount sdk.Coins
+ randomNumberHash tmbytes.HexBytes
+ expireHeight int64
+ timestamp int64
+ sender sdk.AccAddress
+ recipient sdk.AccAddress
+ recipientOtherChain string
+ senderOtherChain string
+ closedBlock int64
+ status types.SwapStatus
+ crossChain bool
+ direction types.SwapDirection
+ }
+ testCases := []struct {
+ description string
+ args args
+ expectPass bool
+ }{
+ {
+ "normal",
+ args{
+ amount: cs(c("bnb", 50000)),
+ randomNumberHash: suite.randomNumberHashes[0],
+ expireHeight: int64(360),
+ timestamp: suite.timestamps[0],
+ sender: suite.addrs[0],
+ recipient: suite.addrs[5],
+ recipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7",
+ senderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7",
+ closedBlock: 0,
+ status: types.Open,
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ true,
+ },
+ {
+ "invalid random number hash length",
+ args{
+ amount: cs(c("bnb", 50000)),
+ randomNumberHash: suite.randomNumberHashes[1][0:20],
+ expireHeight: int64(360),
+ timestamp: suite.timestamps[1],
+ sender: suite.addrs[1],
+ recipient: suite.addrs[5],
+ recipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7",
+ senderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7",
+ closedBlock: 0,
+ status: types.Open,
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ false,
+ },
+ {
+ "invalid amount",
+ args{
+ amount: cs(c("bnb", 0)),
+ randomNumberHash: suite.randomNumberHashes[2],
+ expireHeight: int64(360),
+ timestamp: suite.timestamps[2],
+ sender: suite.addrs[2],
+ recipient: suite.addrs[5],
+ recipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7",
+ senderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7",
+ closedBlock: 0,
+ status: types.Open,
+ crossChain: true,
+ direction: types.Incoming,
+ },
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ // Create atomic swap
+ swap := types.NewAtomicSwap(tc.args.amount, tc.args.randomNumberHash, tc.args.expireHeight,
+ tc.args.timestamp, tc.args.sender, tc.args.recipient, tc.args.senderOtherChain,
+ tc.args.recipientOtherChain, tc.args.closedBlock, tc.args.status, tc.args.crossChain,
+ tc.args.direction)
+
+ if tc.expectPass {
+ suite.Nil(swap.Validate())
+ suite.Equal(tc.args.amount, swap.GetCoins())
+ expectedSwapID := types.CalculateSwapID(tc.args.randomNumberHash, tc.args.sender, tc.args.senderOtherChain)
+ suite.Equal(tmbytes.HexBytes(expectedSwapID), swap.GetSwapID())
+ } else {
+ suite.Error(swap.Validate())
+ }
+
+ }
+}
+
+func TestAtomicSwapTestSuite(t *testing.T) {
+ suite.Run(t, new(AtomicSwapTestSuite))
+}
diff --git a/x/cdp/abci.go b/x/cdp/abci.go
index cc36bd74..8c48cbdf 100644
--- a/x/cdp/abci.go
+++ b/x/cdp/abci.go
@@ -11,17 +11,29 @@ import (
// BeginBlocker compounds the debt in outstanding cdps and liquidates cdps that are below the required collateralization ratio
func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
params := k.GetParams(ctx)
- previousBlockTime, found := k.GetPreviousBlockTime(ctx)
+
+ previousDistTime, found := k.GetPreviousSavingsDistribution(ctx)
if !found {
- previousBlockTime = ctx.BlockTime()
+ previousDistTime = ctx.BlockTime()
+ k.SetPreviousSavingsDistribution(ctx, previousDistTime)
}
- timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix())
+
for _, cp := range params.CollateralParams {
- for _, dp := range params.DebtParams {
- k.HandleNewDebt(ctx, cp.Denom, dp.Denom, timeElapsed)
+
+ err := k.UpdateFeesForAllCdps(ctx, cp.Denom)
+
+ // handle if an error is returned then propagate up
+ if err != nil {
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ EventTypeBeginBlockerFatal,
+ sdk.NewAttribute(sdk.AttributeKeyModule, fmt.Sprintf("%s", ModuleName)),
+ sdk.NewAttribute(types.AttributeKeyError, fmt.Sprintf("%s", err)),
+ ),
+ )
}
- err := k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio)
+ err = k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio)
if err != nil {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
@@ -42,6 +54,21 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
),
)
}
- k.SetPreviousBlockTime(ctx, ctx.BlockTime())
+ distTimeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousDistTime.Unix())
+ if distTimeElapsed.GTE(sdk.NewInt(int64(params.SavingsDistributionFrequency.Seconds()))) {
+ for _, dp := range params.DebtParams {
+ err := k.DistributeSavingsRate(ctx, dp.Denom)
+ if err != nil {
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ EventTypeBeginBlockerFatal,
+ sdk.NewAttribute(sdk.AttributeKeyModule, fmt.Sprintf("%s", ModuleName)),
+ sdk.NewAttribute(types.AttributeKeyError, fmt.Sprintf("%s", err)),
+ ),
+ )
+ }
+ }
+ k.SetPreviousSavingsDistribution(ctx, ctx.BlockTime())
+ }
return
}
diff --git a/x/cdp/abci_test.go b/x/cdp/abci_test.go
index b16ab7e5..b090f672 100644
--- a/x/cdp/abci_test.go
+++ b/x/cdp/abci_test.go
@@ -152,8 +152,6 @@ func (suite *ModuleTestSuite) TestBeginBlock() {
func (suite *ModuleTestSuite) TestSeizeSingleCdpWithFees() {
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[0], cs(c("xrp", 10000000000)), cs(c("usdx", 1000000000)))
suite.NoError(err)
- suite.keeper.SetPreviousBlockTime(suite.ctx, suite.ctx.BlockTime())
- previousBlockTime, _ := suite.keeper.GetPreviousBlockTime(suite.ctx)
suite.Equal(i(1000000000), suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx"))
sk := suite.app.GetSupplyKeeper()
cdpMacc := sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
@@ -167,13 +165,10 @@ func (suite *ModuleTestSuite) TestSeizeSingleCdpWithFees() {
suite.Equal(i(1000000900), (cdpMacc.GetCoins().AmountOf("debt")))
cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", 1)
- timeElapsed := sdk.NewInt(suite.ctx.BlockTime().Unix() - previousBlockTime.Unix())
-
- fees := suite.keeper.CalculateFees(suite.ctx, cdp.Principal, timeElapsed, "xrp")
- suite.Equal(i(928), fees.AmountOf("usdx"))
-
err = suite.keeper.SeizeCollateral(suite.ctx, cdp)
suite.NoError(err)
+ _, found := suite.keeper.GetCDP(suite.ctx, "xrp", 1)
+ suite.False(found)
}
func TestModuleTestSuite(t *testing.T) {
diff --git a/x/cdp/alias.go b/x/cdp/alias.go
index d542be32..eeeb2c81 100644
--- a/x/cdp/alias.go
+++ b/x/cdp/alias.go
@@ -1,8 +1,3 @@
-// nolint
-// autogenerated code using github.com/rigelrozanski/multitool
-// aliases generated for the following subdirectories:
-// ALIASGEN: github.com/kava-labs/kava/x/cdp/types/
-// ALIASGEN: github.com/kava-labs/kava/x/cdp/keeper/
package cdp
import (
@@ -10,25 +5,14 @@ import (
"github.com/kava-labs/kava/x/cdp/types"
)
+// nolint
+// autogenerated code using github.com/rigelrozanski/multitool
+// aliases generated for the following subdirectories:
+// ALIASGEN: github.com/kava-labs/kava/x/cdp/keeper
+// ALIASGEN: github.com/kava-labs/kava/x/cdp/types
+
const (
- DefaultCodespace = types.DefaultCodespace
- CodeCdpAlreadyExists = types.CodeCdpAlreadyExists
- CodeCollateralLengthInvalid = types.CodeCollateralLengthInvalid
- CodeCollateralNotSupported = types.CodeCollateralNotSupported
- CodeDebtNotSupported = types.CodeDebtNotSupported
- CodeExceedsDebtLimit = types.CodeExceedsDebtLimit
- CodeInvalidCollateralRatio = types.CodeInvalidCollateralRatio
- CodeCdpNotFound = types.CodeCdpNotFound
- CodeDepositNotFound = types.CodeDepositNotFound
- CodeInvalidDepositDenom = types.CodeInvalidDepositDenom
- CodeInvalidPaymentDenom = types.CodeInvalidPaymentDenom
- CodeDepositNotAvailable = types.CodeDepositNotAvailable
- CodeInvalidCollateralDenom = types.CodeInvalidCollateralDenom
- CodeInvalidWithdrawAmount = types.CodeInvalidWithdrawAmount
- CodeCdpNotAvailable = types.CodeCdpNotAvailable
- CodeBelowDebtFloor = types.CodeBelowDebtFloor
- CodePaymentExceedsDebt = types.CodePaymentExceedsDebt
- CodeLoadingAugmentedCDP = types.CodeLoadingAugmentedCDP
+ BaseDigitFactor = keeper.BaseDigitFactor
EventTypeCreateCdp = types.EventTypeCreateCdp
EventTypeCdpDeposit = types.EventTypeCdpDeposit
EventTypeCdpDraw = types.EventTypeCdpDraw
@@ -47,7 +31,9 @@ const (
QuerierRoute = types.QuerierRoute
DefaultParamspace = types.DefaultParamspace
LiquidatorMacc = types.LiquidatorMacc
+ SavingsRateMacc = types.SavingsRateMacc
QueryGetCdp = types.QueryGetCdp
+ QueryGetCdpDeposits = types.QueryGetCdpDeposits
QueryGetCdps = types.QueryGetCdps
QueryGetCdpsByCollateralization = types.QueryGetCdpsByCollateralization
QueryGetParams = types.QueryGetParams
@@ -58,7 +44,10 @@ const (
var (
// functions aliases
+ NewKeeper = keeper.NewKeeper
+ NewQuerier = keeper.NewQuerier
NewCDP = types.NewCDP
+ NewAugmentedCDP = types.NewAugmentedCDP
RegisterCodec = types.RegisterCodec
NewDeposit = types.NewDeposit
ErrCdpAlreadyExists = types.ErrCdpAlreadyExists
@@ -69,14 +58,13 @@ var (
ErrInvalidCollateralRatio = types.ErrInvalidCollateralRatio
ErrCdpNotFound = types.ErrCdpNotFound
ErrDepositNotFound = types.ErrDepositNotFound
- ErrInvalidDepositDenom = types.ErrInvalidDepositDenom
- ErrInvalidPaymentDenom = types.ErrInvalidPaymentDenom
+ ErrInvalidDeposit = types.ErrInvalidDeposit
+ ErrInvalidPayment = types.ErrInvalidPayment
ErrDepositNotAvailable = types.ErrDepositNotAvailable
- ErrInvalidCollateralDenom = types.ErrInvalidCollateralDenom
+ ErrInvalidCollateral = types.ErrInvalidCollateral
ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount
ErrCdpNotAvailable = types.ErrCdpNotAvailable
ErrBelowDebtFloor = types.ErrBelowDebtFloor
- ErrPaymentExceedsDebt = types.ErrPaymentExceedsDebt
ErrLoadingAugmentedCDP = types.ErrLoadingAugmentedCDP
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
@@ -105,45 +93,47 @@ var (
ParamKeyTable = types.ParamKeyTable
NewQueryCdpsParams = types.NewQueryCdpsParams
NewQueryCdpParams = types.NewQueryCdpParams
+ NewQueryCdpDeposits = types.NewQueryCdpDeposits
NewQueryCdpsByRatioParams = types.NewQueryCdpsByRatioParams
ValidSortableDec = types.ValidSortableDec
SortableDecBytes = types.SortableDecBytes
ParseDecBytes = types.ParseDecBytes
RelativePow = types.RelativePow
- NewKeeper = keeper.NewKeeper
- NewQuerier = keeper.NewQuerier
// variable aliases
- ModuleCdc = types.ModuleCdc
- CdpIDKeyPrefix = types.CdpIDKeyPrefix
- CdpKeyPrefix = types.CdpKeyPrefix
- CollateralRatioIndexPrefix = types.CollateralRatioIndexPrefix
- CdpIDKey = types.CdpIDKey
- DebtDenomKey = types.DebtDenomKey
- GovDenomKey = types.GovDenomKey
- DepositKeyPrefix = types.DepositKeyPrefix
- PrincipalKeyPrefix = types.PrincipalKeyPrefix
- PreviousBlockTimeKey = types.PreviousBlockTimeKey
- KeyGlobalDebtLimit = types.KeyGlobalDebtLimit
- KeyCollateralParams = types.KeyCollateralParams
- KeyDebtParams = types.KeyDebtParams
- KeyCircuitBreaker = types.KeyCircuitBreaker
- KeyDebtThreshold = types.KeyDebtThreshold
- KeySurplusThreshold = types.KeySurplusThreshold
- DefaultGlobalDebt = types.DefaultGlobalDebt
- DefaultCircuitBreaker = types.DefaultCircuitBreaker
- DefaultCollateralParams = types.DefaultCollateralParams
- DefaultDebtParams = types.DefaultDebtParams
- DefaultCdpStartingID = types.DefaultCdpStartingID
- DefaultDebtDenom = types.DefaultDebtDenom
- DefaultGovDenom = types.DefaultGovDenom
- DefaultSurplusThreshold = types.DefaultSurplusThreshold
- DefaultDebtThreshold = types.DefaultDebtThreshold
- DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
- MaxSortableDec = types.MaxSortableDec
+ ModuleCdc = types.ModuleCdc
+ CdpIDKeyPrefix = types.CdpIDKeyPrefix
+ CdpKeyPrefix = types.CdpKeyPrefix
+ CollateralRatioIndexPrefix = types.CollateralRatioIndexPrefix
+ CdpIDKey = types.CdpIDKey
+ DebtDenomKey = types.DebtDenomKey
+ GovDenomKey = types.GovDenomKey
+ DepositKeyPrefix = types.DepositKeyPrefix
+ PrincipalKeyPrefix = types.PrincipalKeyPrefix
+ PreviousDistributionTimeKey = types.PreviousDistributionTimeKey
+ KeyGlobalDebtLimit = types.KeyGlobalDebtLimit
+ KeyCollateralParams = types.KeyCollateralParams
+ KeyDebtParams = types.KeyDebtParams
+ KeyDistributionFrequency = types.KeyDistributionFrequency
+ KeyCircuitBreaker = types.KeyCircuitBreaker
+ KeyDebtThreshold = types.KeyDebtThreshold
+ KeySurplusThreshold = types.KeySurplusThreshold
+ DefaultGlobalDebt = types.DefaultGlobalDebt
+ DefaultCircuitBreaker = types.DefaultCircuitBreaker
+ DefaultCollateralParams = types.DefaultCollateralParams
+ DefaultDebtParams = types.DefaultDebtParams
+ DefaultCdpStartingID = types.DefaultCdpStartingID
+ DefaultDebtDenom = types.DefaultDebtDenom
+ DefaultGovDenom = types.DefaultGovDenom
+ DefaultSurplusThreshold = types.DefaultSurplusThreshold
+ DefaultDebtThreshold = types.DefaultDebtThreshold
+ DefaultPreviousDistributionTime = types.DefaultPreviousDistributionTime
+ DefaultSavingsDistributionFrequency = types.DefaultSavingsDistributionFrequency
+ MaxSortableDec = types.MaxSortableDec
)
type (
+ Keeper = keeper.Keeper
CDP = types.CDP
CDPs = types.CDPs
AugmentedCDP = types.AugmentedCDP
@@ -165,6 +155,6 @@ type (
DebtParams = types.DebtParams
QueryCdpsParams = types.QueryCdpsParams
QueryCdpParams = types.QueryCdpParams
+ QueryCdpDeposits = types.QueryCdpDeposits
QueryCdpsByRatioParams = types.QueryCdpsByRatioParams
- Keeper = keeper.Keeper
)
diff --git a/x/cdp/client/cli/query.go b/x/cdp/client/cli/query.go
index 095b3fcb..12bb542a 100644
--- a/x/cdp/client/cli/query.go
+++ b/x/cdp/client/cli/query.go
@@ -6,8 +6,8 @@ import (
"github.com/spf13/cobra"
- "github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
@@ -23,7 +23,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
Short: "Querying commands for the cdp module",
}
- cdpQueryCmd.AddCommand(client.GetCommands(
+ cdpQueryCmd.AddCommand(flags.GetCommands(
QueryCdpCmd(queryRoute, cdc),
QueryCdpsByDenomCmd(queryRoute, cdc),
QueryCdpsByDenomAndRatioCmd(queryRoute, cdc),
@@ -131,9 +131,9 @@ $ %s query %s cdps-by-ratio uatom 1.5
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
- ratio, errSdk := sdk.NewDecFromStr(args[1])
- if errSdk != nil {
- return fmt.Errorf(errSdk.Error())
+ ratio, err := sdk.NewDecFromStr(args[1])
+ if err != nil {
+ return err
}
bz, err := cdc.MarshalJSON(types.QueryCdpsByRatioParams{
CollateralDenom: args[0],
diff --git a/x/cdp/client/cli/tx.go b/x/cdp/client/cli/tx.go
index 9214aa1b..8f15888e 100644
--- a/x/cdp/client/cli/tx.go
+++ b/x/cdp/client/cli/tx.go
@@ -1,13 +1,14 @@
package cli
import (
+ "bufio"
"fmt"
"strings"
"github.com/spf13/cobra"
- "github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
@@ -24,7 +25,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
Short: "cdp transactions subcommands",
}
- cdpTxCmd.AddCommand(client.PostCommands(
+ cdpTxCmd.AddCommand(flags.PostCommands(
GetCmdCreateCdp(cdc),
GetCmdDeposit(cdc),
GetCmdWithdraw(cdc),
@@ -48,8 +49,9 @@ $ %s tx %s create 10000000uatom 1000usdx --from myKeyName
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
- txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[0])
if err != nil {
@@ -82,8 +84,9 @@ $ %s tx %s deposit kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw 10000000uatom --f
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
- txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[1])
if err != nil {
@@ -116,8 +119,9 @@ $ %s tx %s withdraw kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw 10000000uatom --
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
- txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[1])
if err != nil {
@@ -150,8 +154,9 @@ $ %s tx %s draw uatom 1000usdx --from myKeyName
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
- txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
debt, err := sdk.ParseCoins(args[1])
if err != nil {
@@ -180,8 +185,9 @@ $ %s tx %s repay uatom 1000usdx --from myKeyName
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
- txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
payment, err := sdk.ParseCoins(args[1])
if err != nil {
diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go
index 45e34b46..9346ac9f 100644
--- a/x/cdp/genesis.go
+++ b/x/cdp/genesis.go
@@ -7,11 +7,26 @@ import (
)
// InitGenesis sets initial genesis state for cdp module
-func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, gs GenesisState) {
+func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, sk SupplyKeeper, gs GenesisState) {
+
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
}
+ // check if the module accounts exists
+ cdpModuleAcc := sk.GetModuleAccount(ctx, ModuleName)
+ if cdpModuleAcc == nil {
+ panic(fmt.Sprintf("%s module account has not been set", ModuleName))
+ }
+ liqModuleAcc := sk.GetModuleAccount(ctx, LiquidatorMacc)
+ if liqModuleAcc == nil {
+ panic(fmt.Sprintf("%s module account has not been set", LiquidatorMacc))
+ }
+ savingsRateMacc := sk.GetModuleAccount(ctx, SavingsRateMacc)
+ if savingsRateMacc == nil {
+ panic(fmt.Sprintf("%s module account has not been set", SavingsRateMacc))
+ }
+
// validate denoms - check that any collaterals in the params are in the pricefeed,
// pricefeed MUST call InitGenesis before cdp
collateralMap := make(map[string]int)
@@ -43,7 +58,7 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, gs GenesisState)
}
k.SetCDP(ctx, cdp)
k.IndexCdpByOwner(ctx, cdp)
- ratio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
+ ratio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.IndexCdpByCollateralRatio(ctx, cdp.Collateral[0].Denom, cdp.ID, ratio)
k.IncrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, cdp.Principal)
}
@@ -55,10 +70,6 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, gs GenesisState)
for _, d := range gs.Deposits {
k.SetDeposit(ctx, d)
}
- // only set the previous block time if it's different than default
- if !gs.PreviousBlockTime.Equal(DefaultPreviousBlockTime) {
- k.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
- }
}
// ExportGenesis export genesis state for cdp module
@@ -80,10 +91,10 @@ func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
debtDenom := k.GetDebtDenom(ctx)
govDenom := k.GetGovDenom(ctx)
- previousBlockTime, found := k.GetPreviousBlockTime(ctx)
+ previousDistributionTime, found := k.GetPreviousSavingsDistribution(ctx)
if !found {
- previousBlockTime = DefaultPreviousBlockTime
+ previousDistributionTime = DefaultPreviousDistributionTime
}
- return NewGenesisState(params, cdps, deposits, cdpID, debtDenom, govDenom, previousBlockTime)
+ return NewGenesisState(params, cdps, deposits, cdpID, debtDenom, govDenom, previousDistributionTime)
}
diff --git a/x/cdp/handler.go b/x/cdp/handler.go
index 13e1ef17..e1adfc4b 100644
--- a/x/cdp/handler.go
+++ b/x/cdp/handler.go
@@ -1,14 +1,13 @@
package cdp
import (
- "fmt"
-
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// NewHandler creates an sdk.Handler for cdp messages
func NewHandler(k Keeper) sdk.Handler {
- return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
+ return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
switch msg := msg.(type) {
case MsgCreateCDP:
return handleMsgCreateCDP(ctx, k, msg)
@@ -21,16 +20,15 @@ func NewHandler(k Keeper) sdk.Handler {
case MsgRepayDebt:
return handleMsgRepayDebt(ctx, k, msg)
default:
- errMsg := fmt.Sprintf("unrecognized cdp msg type: %T", msg)
- return sdk.ErrUnknownRequest(errMsg).Result()
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
}
}
}
-func handleMsgCreateCDP(ctx sdk.Context, k Keeper, msg MsgCreateCDP) sdk.Result {
+func handleMsgCreateCDP(ctx sdk.Context, k Keeper, msg MsgCreateCDP) (*sdk.Result, error) {
err := k.AddCdp(ctx, msg.Sender, msg.Collateral, msg.Principal)
if err != nil {
- return err.Result()
+ return nil, err
}
ctx.EventManager().EmitEvent(
@@ -42,16 +40,16 @@ func handleMsgCreateCDP(ctx sdk.Context, k Keeper, msg MsgCreateCDP) sdk.Result
)
id, _ := k.GetCdpID(ctx, msg.Sender, msg.Collateral[0].Denom)
- return sdk.Result{
+ return &sdk.Result{
Data: GetCdpIDBytes(id),
Events: ctx.EventManager().Events(),
- }
+ }, nil
}
-func handleMsgDeposit(ctx sdk.Context, k Keeper, msg MsgDeposit) sdk.Result {
+func handleMsgDeposit(ctx sdk.Context, k Keeper, msg MsgDeposit) (*sdk.Result, error) {
err := k.DepositCollateral(ctx, msg.Owner, msg.Depositor, msg.Collateral)
if err != nil {
- return err.Result()
+ return nil, err
}
ctx.EventManager().EmitEvent(
@@ -61,13 +59,13 @@ func handleMsgDeposit(ctx sdk.Context, k Keeper, msg MsgDeposit) sdk.Result {
sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()),
),
)
- return sdk.Result{Events: ctx.EventManager().Events()}
+ return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}
-func handleMsgWithdraw(ctx sdk.Context, k Keeper, msg MsgWithdraw) sdk.Result {
+func handleMsgWithdraw(ctx sdk.Context, k Keeper, msg MsgWithdraw) (*sdk.Result, error) {
err := k.WithdrawCollateral(ctx, msg.Owner, msg.Depositor, msg.Collateral)
if err != nil {
- return err.Result()
+ return nil, err
}
ctx.EventManager().EmitEvent(
@@ -77,13 +75,13 @@ func handleMsgWithdraw(ctx sdk.Context, k Keeper, msg MsgWithdraw) sdk.Result {
sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()),
),
)
- return sdk.Result{Events: ctx.EventManager().Events()}
+ return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}
-func handleMsgDrawDebt(ctx sdk.Context, k Keeper, msg MsgDrawDebt) sdk.Result {
+func handleMsgDrawDebt(ctx sdk.Context, k Keeper, msg MsgDrawDebt) (*sdk.Result, error) {
err := k.AddPrincipal(ctx, msg.Sender, msg.CdpDenom, msg.Principal)
if err != nil {
- return err.Result()
+ return nil, err
}
ctx.EventManager().EmitEvent(
@@ -93,13 +91,13 @@ func handleMsgDrawDebt(ctx sdk.Context, k Keeper, msg MsgDrawDebt) sdk.Result {
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
- return sdk.Result{Events: ctx.EventManager().Events()}
+ return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}
-func handleMsgRepayDebt(ctx sdk.Context, k Keeper, msg MsgRepayDebt) sdk.Result {
+func handleMsgRepayDebt(ctx sdk.Context, k Keeper, msg MsgRepayDebt) (*sdk.Result, error) {
err := k.RepayPrincipal(ctx, msg.Sender, msg.CdpDenom, msg.Payment)
if err != nil {
- return err.Result()
+ return nil, err
}
ctx.EventManager().EmitEvent(
@@ -109,5 +107,5 @@ func handleMsgRepayDebt(ctx sdk.Context, k Keeper, msg MsgRepayDebt) sdk.Result
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
- return sdk.Result{Events: ctx.EventManager().Events()}
+ return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}
diff --git a/x/cdp/handler_test.go b/x/cdp/handler_test.go
index 0652c040..ed3cc576 100644
--- a/x/cdp/handler_test.go
+++ b/x/cdp/handler_test.go
@@ -1,7 +1,6 @@
package cdp_test
import (
- "strings"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
@@ -47,16 +46,16 @@ func (suite *HandlerTestSuite) TestMsgCreateCdp() {
cs(c("xrp", 200000000)),
cs(c("usdx", 10000000)),
)
- res := suite.handler(suite.ctx, msg)
- suite.True(res.IsOK())
- suite.Equal(cdp.GetCdpIDBytes(uint64(1)), res.Data)
+ res, err := suite.handler(suite.ctx, msg)
+ suite.Require().NoError(err)
+ suite.Require().Equal(cdp.GetCdpIDBytes(uint64(1)), res.Data)
}
func (suite *HandlerTestSuite) TestInvalidMsg() {
- res := suite.handler(suite.ctx, sdk.NewTestMsg())
- suite.False(res.IsOK())
- suite.True(strings.Contains(res.Log, "unrecognized cdp msg type"))
+ res, err := suite.handler(suite.ctx, sdk.NewTestMsg())
+ suite.Require().Error(err)
+ suite.Require().Nil(res)
}
func TestHandlerTestSuite(t *testing.T) {
diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go
index 8ea36252..ee47c829 100644
--- a/x/cdp/integration_test.go
+++ b/x/cdp/integration_test.go
@@ -39,9 +39,10 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState {
func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
- GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
- SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
- DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
+ SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
+ DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: asset,
@@ -61,14 +62,15 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
+ SavingsRate: d("0.95"),
},
},
},
- StartingCdpID: cdp.DefaultCdpStartingID,
- DebtDenom: cdp.DefaultDebtDenom,
- GovDenom: cdp.DefaultGovDenom,
- CDPs: cdp.CDPs{},
- PreviousBlockTime: cdp.DefaultPreviousBlockTime,
+ StartingCdpID: cdp.DefaultCdpStartingID,
+ DebtDenom: cdp.DefaultDebtDenom,
+ GovDenom: cdp.DefaultGovDenom,
+ CDPs: cdp.CDPs{},
+ PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
@@ -101,9 +103,10 @@ func NewPricefeedGenStateMulti() app.GenesisState {
func NewCDPGenStateMulti() app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
- GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
- SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
- DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
+ SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
+ DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: "xrp",
@@ -134,20 +137,22 @@ func NewCDPGenStateMulti() app.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
+ SavingsRate: d("0.95"),
},
{
Denom: "susd",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
+ SavingsRate: d("0.95"),
},
},
},
- StartingCdpID: cdp.DefaultCdpStartingID,
- DebtDenom: cdp.DefaultDebtDenom,
- GovDenom: cdp.DefaultGovDenom,
- CDPs: cdp.CDPs{},
- PreviousBlockTime: cdp.DefaultPreviousBlockTime,
+ StartingCdpID: cdp.DefaultCdpStartingID,
+ DebtDenom: cdp.DefaultDebtDenom,
+ GovDenom: cdp.DefaultGovDenom,
+ CDPs: cdp.CDPs{},
+ PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
@@ -196,16 +201,16 @@ func badGenStates() []badGenState {
g9.DebtDenom = ""
g10 := baseGenState()
- g10.PreviousBlockTime = time.Time{}
+ g10.Params.CollateralParams[0].AuctionSize = i(-10)
g11 := baseGenState()
- g11.Params.CollateralParams[0].AuctionSize = i(-10)
+ g11.Params.CollateralParams[0].LiquidationPenalty = d("5.0")
g12 := baseGenState()
- g12.Params.CollateralParams[0].LiquidationPenalty = d("5.0")
+ g12.GovDenom = ""
g13 := baseGenState()
- g13.GovDenom = ""
+ g13.Params.DebtParams[0].SavingsRate = d("4.0")
return []badGenState{
badGenState{Genesis: g1, Reason: "duplicate collateral denom"},
@@ -216,19 +221,20 @@ func badGenStates() []badGenState {
badGenState{Genesis: g6, Reason: "duplicate debt denom"},
badGenState{Genesis: g8, Reason: "debt param not found in global debt limit"},
badGenState{Genesis: g9, Reason: "debt denom not set"},
- badGenState{Genesis: g10, Reason: "previous block time not set"},
- badGenState{Genesis: g11, Reason: "negative auction size"},
- badGenState{Genesis: g12, Reason: "invalid liquidation penalty"},
- badGenState{Genesis: g13, Reason: "gov denom not set"},
+ badGenState{Genesis: g10, Reason: "negative auction size"},
+ badGenState{Genesis: g11, Reason: "invalid liquidation penalty"},
+ badGenState{Genesis: g12, Reason: "gov denom not set"},
+ badGenState{Genesis: g13, Reason: "invalid savings rate"},
}
}
func baseGenState() cdp.GenesisState {
return cdp.GenesisState{
Params: cdp.Params{
- GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
- SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
- DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
+ SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
+ DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: "xrp",
@@ -264,10 +270,10 @@ func baseGenState() cdp.GenesisState {
},
},
},
- StartingCdpID: cdp.DefaultCdpStartingID,
- DebtDenom: cdp.DefaultDebtDenom,
- GovDenom: cdp.DefaultGovDenom,
- CDPs: cdp.CDPs{},
- PreviousBlockTime: cdp.DefaultPreviousBlockTime,
+ StartingCdpID: cdp.DefaultCdpStartingID,
+ DebtDenom: cdp.DefaultDebtDenom,
+ GovDenom: cdp.DefaultGovDenom,
+ CDPs: cdp.CDPs{},
+ PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
}
}
diff --git a/x/cdp/keeper/auctions.go b/x/cdp/keeper/auctions.go
index 6e39bbbb..3042ad78 100644
--- a/x/cdp/keeper/auctions.go
+++ b/x/cdp/keeper/auctions.go
@@ -43,7 +43,7 @@ func (pd partialDeposits) SumDebt() (sum sdk.Int) {
}
// AuctionCollateral creates auctions from the input deposits which attempt to raise the corresponding amount of debt
-func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt sdk.Int, bidDenom string) sdk.Error {
+func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt sdk.Int, bidDenom string) error {
auctionSize := k.getAuctionSize(ctx, deposits[0].Amount[0].Denom)
partialAuctionDeposits := partialDeposits{}
totalCollateral := deposits.SumCollateral()
@@ -110,7 +110,7 @@ func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt
}
// CreateAuctionsFromDeposit creates auctions from the input deposit until there is less than auctionSize left on the deposit
-func (k Keeper) CreateAuctionsFromDeposit(ctx sdk.Context, dep types.Deposit, debt sdk.Int, totalCollateral sdk.Int, auctionSize sdk.Int, principalDenom string) (debtChange sdk.Int, collateralChange sdk.Int, err sdk.Error) {
+func (k Keeper) CreateAuctionsFromDeposit(ctx sdk.Context, dep types.Deposit, debt sdk.Int, totalCollateral sdk.Int, auctionSize sdk.Int, principalDenom string) (debtChange sdk.Int, collateralChange sdk.Int, err error) {
debtChange = sdk.ZeroInt()
collateralChange = sdk.ZeroInt()
depositAmount := dep.Amount[0].Amount
@@ -138,7 +138,7 @@ func (k Keeper) CreateAuctionsFromDeposit(ctx sdk.Context, dep types.Deposit, de
}
// CreateAuctionFromPartialDeposits creates an auction from the input partial deposits
-func (k Keeper) CreateAuctionFromPartialDeposits(ctx sdk.Context, partialDeps partialDeposits, debt sdk.Int, collateral sdk.Int, auctionSize sdk.Int, bidDenom string) (debtChange, collateralChange sdk.Int, err sdk.Error) {
+func (k Keeper) CreateAuctionFromPartialDeposits(ctx sdk.Context, partialDeps partialDeposits, debt sdk.Int, collateral sdk.Int, auctionSize sdk.Int, bidDenom string) (debtChange, collateralChange sdk.Int, err error) {
returnAddrs := []sdk.AccAddress{}
returnWeights := []sdk.Int{}
@@ -159,7 +159,7 @@ func (k Keeper) CreateAuctionFromPartialDeposits(ctx sdk.Context, partialDeps pa
// NetSurplusAndDebt burns surplus and debt coins equal to the minimum of surplus and debt balances held by the liquidator module account
// for example, if there is 1000 debt and 100 surplus, 100 surplus and 100 debt are burned, netting to 900 debt
-func (k Keeper) NetSurplusAndDebt(ctx sdk.Context) sdk.Error {
+func (k Keeper) NetSurplusAndDebt(ctx sdk.Context) error {
totalSurplus := k.GetTotalSurplus(ctx, types.LiquidatorMacc)
debt := k.GetTotalDebt(ctx, types.LiquidatorMacc)
netAmount := sdk.MinInt(totalSurplus, debt)
@@ -210,7 +210,7 @@ func (k Keeper) GetTotalDebt(ctx sdk.Context, accountName string) sdk.Int {
}
// RunSurplusAndDebtAuctions nets the surplus and debt balances and then creates surplus or debt auctions if the remaining balance is above the auction threshold parameter
-func (k Keeper) RunSurplusAndDebtAuctions(ctx sdk.Context) sdk.Error {
+func (k Keeper) RunSurplusAndDebtAuctions(ctx sdk.Context) error {
k.NetSurplusAndDebt(ctx)
remainingDebt := k.GetTotalDebt(ctx, types.LiquidatorMacc)
params := k.GetParams(ctx)
@@ -220,11 +220,12 @@ func (k Keeper) RunSurplusAndDebtAuctions(ctx sdk.Context) sdk.Error {
return err
}
}
- remainingSurplus := k.GetTotalSurplus(ctx, types.LiquidatorMacc)
- if remainingSurplus.GTE(params.SurplusAuctionThreshold) {
- for _, dp := range params.DebtParams {
- surplusLot := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(dp.Denom)
- _, err := k.auctionKeeper.StartSurplusAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(dp.Denom, surplusLot), k.GetGovDenom(ctx))
+
+ for _, dp := range params.DebtParams {
+ surplus := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(dp.Denom)
+ if surplus.GTE(params.SurplusAuctionThreshold) {
+ surplusLot := sdk.NewCoin(dp.Denom, surplus)
+ _, err := k.auctionKeeper.StartSurplusAuction(ctx, types.LiquidatorMacc, surplusLot, k.GetGovDenom(ctx))
if err != nil {
return err
}
diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go
index 5d729263..beff0c2e 100644
--- a/x/cdp/keeper/cdp.go
+++ b/x/cdp/keeper/cdp.go
@@ -6,6 +6,7 @@ import (
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/cdp/types"
)
@@ -13,7 +14,7 @@ import (
const BaseDigitFactor = 1000000000000000000
// AddCdp adds a cdp for a specific owner and collateral type
-func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) sdk.Error {
+func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) error {
// validation
err := k.ValidateCollateral(ctx, collateral)
if err != nil {
@@ -21,12 +22,17 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
}
_, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom)
if found {
- return types.ErrCdpAlreadyExists(k.codespace, owner, collateral[0].Denom)
+ return sdkerrors.Wrapf(types.ErrCdpAlreadyExists, "owner %s, denom %s", owner, collateral[0].Denom)
}
err = k.ValidatePrincipalAdd(ctx, principal)
if err != nil {
return err
}
+
+ err = k.ValidateDebtLimit(ctx, collateral[0].Denom, principal)
+ if err != nil {
+ return err
+ }
err = k.ValidateCollateralizationRatio(ctx, collateral, principal, sdk.NewCoins())
if err != nil {
return err
@@ -98,10 +104,10 @@ func (k Keeper) SetCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP, ra
}
// MintDebtCoins mints debt coins in the cdp module account
-func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom string, principalCoins sdk.Coins) sdk.Error {
+func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom string, principalCoins sdk.Coins) error {
coinsToMint := sdk.NewCoins()
for _, sc := range principalCoins {
- coinsToMint = coinsToMint.Add(sdk.NewCoins(sdk.NewCoin(denom, sc.Amount)))
+ coinsToMint = coinsToMint.Add(sdk.NewCoin(denom, sc.Amount))
}
err := k.supplyKeeper.MintCoins(ctx, moduleAccount, coinsToMint)
if err != nil {
@@ -111,10 +117,10 @@ func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom strin
}
// BurnDebtCoins burns debt coins from the cdp module account
-func (k Keeper) BurnDebtCoins(ctx sdk.Context, moduleAccount string, denom string, paymentCoins sdk.Coins) sdk.Error {
+func (k Keeper) BurnDebtCoins(ctx sdk.Context, moduleAccount string, denom string, paymentCoins sdk.Coins) error {
coinsToBurn := sdk.NewCoins()
for _, pc := range paymentCoins {
- coinsToBurn = coinsToBurn.Add(sdk.NewCoins(sdk.NewCoin(denom, pc.Amount)))
+ coinsToBurn = coinsToBurn.Add(sdk.NewCoin(denom, pc.Amount))
}
err := k.supplyKeeper.BurnCoins(ctx, moduleAccount, coinsToBurn)
if err != nil {
@@ -335,56 +341,65 @@ func (k Keeper) SetGovDenom(ctx sdk.Context, denom string) {
}
// ValidateCollateral validates that a collateral is valid for use in cdps
-func (k Keeper) ValidateCollateral(ctx sdk.Context, collateral sdk.Coins) sdk.Error {
+func (k Keeper) ValidateCollateral(ctx sdk.Context, collateral sdk.Coins) error {
if len(collateral) != 1 {
- return types.ErrInvalidCollateralLength(k.codespace, len(collateral))
+ return sdkerrors.Wrapf(types.ErrInvalidCollateralLength, "%d", len(collateral))
}
_, found := k.GetCollateral(ctx, collateral[0].Denom)
if !found {
- return types.ErrCollateralNotSupported(k.codespace, collateral[0].Denom)
+ return sdkerrors.Wrap(types.ErrCollateralNotSupported, collateral[0].Denom)
}
return nil
}
// ValidatePrincipalAdd validates that an asset is valid for use as debt when creating a new cdp
-func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) sdk.Error {
+func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) error {
for _, dc := range principal {
dp, found := k.GetDebtParam(ctx, dc.Denom)
if !found {
- return types.ErrDebtNotSupported(k.codespace, dc.Denom)
+ return sdkerrors.Wrap(types.ErrDebtNotSupported, dc.Denom)
}
if dc.Amount.LT(dp.DebtFloor) {
- return types.ErrBelowDebtFloor(k.codespace, sdk.NewCoins(dc), dp.DebtFloor)
+ return sdkerrors.Wrapf(types.ErrBelowDebtFloor, "proposed %s < minimum %s", dc, dp.DebtFloor)
}
}
return nil
}
// ValidatePrincipalDraw validates that an asset is valid for use as debt when drawing debt off an existing cdp
-func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coins) sdk.Error {
+func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coins) error {
for _, dc := range principal {
_, found := k.GetDebtParam(ctx, dc.Denom)
if !found {
- return types.ErrDebtNotSupported(k.codespace, dc.Denom)
+ return sdkerrors.Wrap(types.ErrDebtNotSupported, dc.Denom)
}
}
return nil
}
-// ValidateDebtLimit validates that the input debt amount does not exceed the global debt limit
-func (k Keeper) ValidateDebtLimit(ctx sdk.Context, collateralDenom string, principal sdk.Coins) sdk.Error {
+// ValidateDebtLimit validates that the input debt amount does not exceed the global debt limit or the debt limit for that collateral
+func (k Keeper) ValidateDebtLimit(ctx sdk.Context, collateralDenom string, principal sdk.Coins) error {
+ cp, found := k.GetCollateral(ctx, collateralDenom)
+ if !found {
+ return sdkerrors.Wrap(types.ErrCollateralNotSupported, collateralDenom)
+ }
+
for _, dc := range principal {
totalPrincipal := k.GetTotalPrincipal(ctx, collateralDenom, dc.Denom).Add(dc.Amount)
+ collateralLimit := cp.DebtLimit.AmountOf(dc.Denom)
+ if totalPrincipal.GT(collateralLimit) {
+ return sdkerrors.Wrapf(types.ErrExceedsDebtLimit, "debt increase %s > collateral debt limit %s", sdk.NewCoins(sdk.NewCoin(dc.Denom, totalPrincipal)), sdk.NewCoins(sdk.NewCoin(dc.Denom, collateralLimit)))
+ }
globalLimit := k.GetParams(ctx).GlobalDebtLimit.AmountOf(dc.Denom)
if totalPrincipal.GT(globalLimit) {
- return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(sdk.NewCoin(dc.Denom, totalPrincipal)), sdk.NewCoins(sdk.NewCoin(dc.Denom, globalLimit)))
+ return sdkerrors.Wrapf(types.ErrExceedsDebtLimit, "debt increase %s > global debt limit %s", sdk.NewCoin(dc.Denom, totalPrincipal), sdk.NewCoin(dc.Denom, globalLimit))
}
}
return nil
}
// ValidateCollateralizationRatio validate that adding the input principal doesn't put the cdp below the liquidation ratio
-func (k Keeper) ValidateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) sdk.Error {
+func (k Keeper) ValidateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) error {
//
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, collateral, principal, fees)
if err != nil {
@@ -392,7 +407,7 @@ func (k Keeper) ValidateCollateralizationRatio(ctx sdk.Context, collateral sdk.C
}
liquidationRatio := k.getLiquidationRatio(ctx, collateral[0].Denom)
if collateralizationRatio.LT(liquidationRatio) {
- return types.ErrInvalidCollateralRatio(k.codespace, collateral[0].Denom, collateralizationRatio, liquidationRatio)
+ return sdkerrors.Wrapf(types.ErrInvalidCollateralRatio, "collateral %s, collateral ratio %s, liquidation ratio %s", collateral[0].Denom, collateralizationRatio, liquidationRatio)
}
return nil
}
@@ -414,12 +429,11 @@ func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.C
}
// LoadAugmentedCDP creates a new augmented CDP from an existing CDP
-func (k Keeper) LoadAugmentedCDP(ctx sdk.Context, cdp types.CDP) (types.AugmentedCDP, sdk.Error) {
+func (k Keeper) LoadAugmentedCDP(ctx sdk.Context, cdp types.CDP) (types.AugmentedCDP, error) {
// calculate additional fees
periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix()))
- fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom)
- totalFees := cdp.AccumulatedFees.Add(fees)
-
+ fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees...), periods, cdp.Collateral[0].Denom)
+ totalFees := cdp.AccumulatedFees.Add(fees...)
// calculate collateralization ratio
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal, totalFees)
if err != nil {
@@ -431,7 +445,7 @@ func (k Keeper) LoadAugmentedCDP(ctx sdk.Context, cdp types.CDP) (types.Augmente
for _, principalCoin := range cdp.Principal {
totalDebt += principalCoin.Amount.Int64()
}
- for _, feeCoin := range cdp.AccumulatedFees.Add(fees) {
+ for _, feeCoin := range cdp.AccumulatedFees.Add(fees...) {
totalDebt += feeCoin.Amount.Int64()
}
@@ -446,7 +460,7 @@ func (k Keeper) LoadAugmentedCDP(ctx sdk.Context, cdp types.CDP) (types.Augmente
}
// CalculateCollateralizationRatio returns the collateralization ratio of the input collateral to the input debt plus fees
-func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) (sdk.Dec, sdk.Error) {
+func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) (sdk.Dec, error) {
if collateral.IsZero() {
return sdk.ZeroDec(), nil
}
@@ -472,7 +486,7 @@ func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk.
}
// CalculateCollateralizationRatioFromAbsoluteRatio takes a coin's denom and an absolute ratio and returns the respective collateralization ratio
-func (k Keeper) CalculateCollateralizationRatioFromAbsoluteRatio(ctx sdk.Context, collateralDenom string, absoluteRatio sdk.Dec) (sdk.Dec, sdk.Error) {
+func (k Keeper) CalculateCollateralizationRatioFromAbsoluteRatio(ctx sdk.Context, collateralDenom string, absoluteRatio sdk.Dec) (sdk.Dec, error) {
// get price collateral
marketID := k.getMarketID(ctx, collateralDenom)
price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID)
diff --git a/x/cdp/keeper/cdp_test.go b/x/cdp/keeper/cdp_test.go
index 89e35136..0ca288d6 100644
--- a/x/cdp/keeper/cdp_test.go
+++ b/x/cdp/keeper/cdp_test.go
@@ -1,6 +1,7 @@
package keeper_test
import (
+ "errors"
"testing"
"time"
@@ -38,22 +39,29 @@ func (suite *CdpTestSuite) SetupTest() {
}
func (suite *CdpTestSuite) TestAddCdp() {
- _, addrs := app.GeneratePrivKeyAddressPairs(1)
+ _, addrs := app.GeneratePrivKeyAddressPairs(2)
ak := suite.app.GetAccountKeeper()
acc := ak.NewAccountWithAddress(suite.ctx, addrs[0])
acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000)))
ak.SetAccount(suite.ctx, acc)
err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("usdx", 26000000)))
- suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 500000000)), cs(c("usdx", 26000000)))
- suite.Error(err)
+ suite.Error(err) // insufficient balance
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("xusd", 10000000)))
- suite.Equal(types.CodeDebtNotSupported, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrDebtNotSupported))
+
+ acc2 := ak.NewAccountWithAddress(suite.ctx, addrs[1])
+ acc2.SetCoins(cs(c("btc", 500000000000)))
+ ak.SetAccount(suite.ctx, acc2)
+ err = suite.keeper.AddCdp(suite.ctx, addrs[1], cs(c("btc", 500000000000)), cs(c("usdx", 500000000001)))
+ suite.Require().True(errors.Is(err, types.ErrExceedsDebtLimit))
+
ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2))
pk := suite.app.GetPriceFeedKeeper()
_ = pk.SetCurrentPrices(ctx, "xrp:usd")
err = suite.keeper.AddCdp(ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000)))
- suite.Error(err)
+ suite.Error(err) // no prices in pricefeed
_ = pk.SetCurrentPrices(suite.ctx, "xrp:usd")
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000)))
@@ -69,7 +77,7 @@ func (suite *CdpTestSuite) TestAddCdp() {
suite.Equal(cs(c("usdx", 10000000), c("xrp", 100000000), c("btc", 500000000)), acc.GetCoins())
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("btc", 500000000)), cs(c("usdx", 26667000000)))
- suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("btc", 500000000)), cs(c("usdx", 100000000)))
suite.NoError(err)
@@ -83,9 +91,9 @@ func (suite *CdpTestSuite) TestAddCdp() {
suite.Equal(cs(c("usdx", 110000000), c("xrp", 100000000)), acc.GetCoins())
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("lol", 100)), cs(c("usdx", 10)))
- suite.Equal(types.CodeCollateralNotSupported, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrCollateralNotSupported))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100)), cs(c("usdx", 10)))
- suite.Equal(types.CodeCdpAlreadyExists, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrCdpAlreadyExists))
}
func (suite *CdpTestSuite) TestGetSetDenomByte() {
@@ -114,6 +122,7 @@ func (suite *CdpTestSuite) TestGetSetCdp() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 1)), cs(c("usdx", 1)), tmtime.Canonical(time.Now()))
suite.keeper.SetCDP(suite.ctx, cdp)
+
t, found := suite.keeper.GetCDP(suite.ctx, "xrp", types.DefaultCdpStartingID)
suite.True(found)
suite.Equal(cdp, t)
@@ -239,10 +248,10 @@ func (suite *CdpTestSuite) TestValidateCollateral() {
suite.NoError(err)
c = sdk.NewCoins(sdk.NewCoin("lol", sdk.NewInt(1)))
err = suite.keeper.ValidateCollateral(suite.ctx, c)
- suite.Equal(types.CodeCollateralNotSupported, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrCollateralNotSupported))
c = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1)), sdk.NewCoin("xrp", sdk.NewInt(1)))
err = suite.keeper.ValidateCollateral(suite.ctx, c)
- suite.Equal(types.CodeCollateralLengthInvalid, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrInvalidCollateralLength))
}
func (suite *CdpTestSuite) TestValidatePrincipal() {
@@ -254,10 +263,10 @@ func (suite *CdpTestSuite) TestValidatePrincipal() {
suite.NoError(err)
d = sdk.NewCoins(sdk.NewCoin("xusd", sdk.NewInt(1)))
err = suite.keeper.ValidatePrincipalAdd(suite.ctx, d)
- suite.Equal(types.CodeDebtNotSupported, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrDebtNotSupported))
d = sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(1000000000001)))
err = suite.keeper.ValidateDebtLimit(suite.ctx, "xrp", d)
- suite.Equal(types.CodeExceedsDebtLimit, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrExceedsDebtLimit))
d = sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(100000000)))
err = suite.keeper.ValidateDebtLimit(suite.ctx, "xrp", d)
suite.NoError(err)
@@ -282,16 +291,19 @@ func (suite *CdpTestSuite) TestMintBurnDebtCoins() {
cd := cdps()[1]
err := suite.keeper.MintDebtCoins(suite.ctx, types.ModuleName, suite.keeper.GetDebtDenom(suite.ctx), cd.Principal)
suite.NoError(err)
- err = suite.keeper.MintDebtCoins(suite.ctx, "notamodule", suite.keeper.GetDebtDenom(suite.ctx), cd.Principal)
- suite.Error(err)
+ suite.Require().Panics(func() {
+ _ = suite.keeper.MintDebtCoins(suite.ctx, "notamodule", suite.keeper.GetDebtDenom(suite.ctx), cd.Principal)
+ })
+
sk := suite.app.GetSupplyKeeper()
acc := sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(cs(c("debt", 10000000)), acc.GetCoins())
err = suite.keeper.BurnDebtCoins(suite.ctx, types.ModuleName, suite.keeper.GetDebtDenom(suite.ctx), cd.Principal)
suite.NoError(err)
- err = suite.keeper.BurnDebtCoins(suite.ctx, "notamodule", suite.keeper.GetDebtDenom(suite.ctx), cd.Principal)
- suite.Error(err)
+ suite.Require().Panics(func() {
+ _ = suite.keeper.BurnDebtCoins(suite.ctx, "notamodule", suite.keeper.GetDebtDenom(suite.ctx), cd.Principal)
+ })
sk = suite.app.GetSupplyKeeper()
acc = sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(sdk.Coins(nil), acc.GetCoins())
diff --git a/x/cdp/keeper/deposit.go b/x/cdp/keeper/deposit.go
index 38b8dd85..5dbee127 100644
--- a/x/cdp/keeper/deposit.go
+++ b/x/cdp/keeper/deposit.go
@@ -5,23 +5,24 @@ import (
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/cdp/types"
)
// DepositCollateral adds collateral to a cdp
-func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) sdk.Error {
+func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) error {
err := k.ValidateCollateral(ctx, collateral)
if err != nil {
return err
}
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom)
if !found {
- return types.ErrCdpNotFound(k.codespace, owner, collateral[0].Denom)
+ return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, collateral %s", owner, collateral[0].Denom)
}
deposit, found := k.GetDeposit(ctx, cdp.ID, depositor)
if found {
- deposit.Amount = deposit.Amount.Add(collateral)
+ deposit.Amount = deposit.Amount.Add(collateral...)
} else {
deposit = types.NewDeposit(cdp.ID, depositor, collateral)
}
@@ -39,46 +40,41 @@ func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, deposit
k.SetDeposit(ctx, deposit)
- periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix()))
- fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom)
- oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
+ oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio)
- cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees)
cdp.FeesUpdated = ctx.BlockTime()
- cdp.Collateral = cdp.Collateral.Add(collateral)
- collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
+ cdp.Collateral = cdp.Collateral.Add(collateral...)
+ collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
return nil
}
// WithdrawCollateral removes collateral from a cdp if it does not put the cdp below the liquidation ratio
-func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) sdk.Error {
+func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) error {
err := k.ValidateCollateral(ctx, collateral)
if err != nil {
return err
}
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom)
if !found {
- return types.ErrCdpNotFound(k.codespace, owner, collateral[0].Denom)
+ return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, collateral %s", owner, collateral[0].Denom)
}
deposit, found := k.GetDeposit(ctx, cdp.ID, depositor)
if !found {
- return types.ErrDepositNotFound(k.codespace, depositor, cdp.ID)
+ return sdkerrors.Wrapf(types.ErrDepositNotFound, "depositor %s, collateral %s", depositor, collateral[0].Denom)
}
if collateral.IsAnyGT(deposit.Amount) {
- return types.ErrInvalidWithdrawAmount(k.codespace, collateral, deposit.Amount)
+ return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "collateral %s, deposit %s", collateral, deposit.Amount)
}
- periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix()))
- fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom)
- collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral.Sub(collateral), cdp.Principal, cdp.AccumulatedFees.Add(fees))
+ collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral.Sub(collateral), cdp.Principal, cdp.AccumulatedFees)
if err != nil {
return err
}
liquidationRatio := k.getLiquidationRatio(ctx, collateral[0].Denom)
if collateralizationRatio.LT(liquidationRatio) {
- return types.ErrInvalidCollateralRatio(k.codespace, collateral[0].Denom, collateralizationRatio, liquidationRatio)
+ return sdkerrors.Wrapf(types.ErrInvalidCollateralRatio, "collateral %s, collateral ratio %s, liquidation ration %s", collateral[0].Denom, collateralizationRatio, liquidationRatio)
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
@@ -92,13 +88,12 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, deposi
if err != nil {
panic(err)
}
- oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
+ oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio)
- cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees)
cdp.FeesUpdated = ctx.BlockTime()
cdp.Collateral = cdp.Collateral.Sub(collateral)
- collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
+ collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
deposit.Amount = deposit.Amount.Sub(collateral)
diff --git a/x/cdp/keeper/deposit_test.go b/x/cdp/keeper/deposit_test.go
index d9a5e6b0..b543ce3d 100644
--- a/x/cdp/keeper/deposit_test.go
+++ b/x/cdp/keeper/deposit_test.go
@@ -1,6 +1,7 @@
package keeper_test
import (
+ "errors"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
@@ -76,10 +77,10 @@ func (suite *DepositTestSuite) TestDepositCollateral() {
suite.Equal(i(90000000), acc.GetCoins().AmountOf("xrp"))
err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("btc", 1)))
- suite.Equal(types.CodeCdpNotFound, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 1)))
- suite.Equal(types.CodeCdpNotFound, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], cs(c("xrp", 10000000)))
suite.NoError(err)
@@ -94,17 +95,17 @@ func (suite *DepositTestSuite) TestDepositCollateral() {
func (suite *DepositTestSuite) TestWithdrawCollateral() {
err := suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 400000000)))
- suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 321000000)))
- suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 10000000)))
- suite.Equal(types.CodeCdpNotFound, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
cd, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
cd.AccumulatedFees = cs(c("usdx", 1))
suite.keeper.SetCDP(suite.ctx, cd)
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 320000000)))
- suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000)))
suite.NoError(err)
@@ -116,7 +117,7 @@ func (suite *DepositTestSuite) TestWithdrawCollateral() {
suite.Equal(i(110000000), acc.GetCoins().AmountOf("xrp"))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], cs(c("xrp", 10000000)))
- suite.Equal(types.CodeDepositNotFound, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrDepositNotFound))
}
func TestDepositTestSuite(t *testing.T) {
diff --git a/x/cdp/keeper/draw.go b/x/cdp/keeper/draw.go
index 94665f5b..4c3383fa 100644
--- a/x/cdp/keeper/draw.go
+++ b/x/cdp/keeper/draw.go
@@ -4,15 +4,16 @@ import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/cdp/types"
)
// AddPrincipal adds debt to a cdp if the additional debt does not put the cdp below the liquidation ratio
-func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, principal sdk.Coins) sdk.Error {
+func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, principal sdk.Coins) error {
// validation
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, denom)
if !found {
- return types.ErrCdpNotFound(k.codespace, owner, denom)
+ return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", owner, denom)
}
err := k.ValidatePrincipalDraw(ctx, principal)
if err != nil {
@@ -24,11 +25,7 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string
return err
}
- // fee calculation
- periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix()))
- fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom)
-
- err = k.ValidateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal.Add(principal), cdp.AccumulatedFees.Add(fees))
+ err = k.ValidateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal.Add(principal...), cdp.AccumulatedFees)
if err != nil {
return err
}
@@ -59,19 +56,18 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string
)
// remove old collateral:debt index
- oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
+ oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.RemoveCdpCollateralRatioIndex(ctx, denom, cdp.ID, oldCollateralToDebtRatio)
// update cdp state
- cdp.Principal = cdp.Principal.Add(principal)
- cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees)
+ cdp.Principal = cdp.Principal.Add(principal...)
cdp.FeesUpdated = ctx.BlockTime()
// increment total principal for the input collateral type
k.IncrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, principal)
// set cdp state and indexes in the store
- collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
+ collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
return nil
@@ -79,32 +75,29 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string
// RepayPrincipal removes debt from the cdp
// If all debt is repaid, the collateral is returned to depositors and the cdp is removed from the store
-func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, payment sdk.Coins) sdk.Error {
+func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, payment sdk.Coins) error {
// validation
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, denom)
if !found {
- return types.ErrCdpNotFound(k.codespace, owner, denom)
+ return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", owner, denom)
}
- // calculate fees
- periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix()))
- fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom)
- err := k.ValidatePaymentCoins(ctx, cdp, payment, cdp.Principal.Add(cdp.AccumulatedFees).Add(fees))
+ err := k.ValidatePaymentCoins(ctx, cdp, payment, cdp.Principal.Add(cdp.AccumulatedFees...))
if err != nil {
return err
}
// calculate fee and principal payment
- feePayment, principalPayment := k.calculatePayment(ctx, cdp.Principal.Add(cdp.AccumulatedFees).Add(fees), cdp.AccumulatedFees.Add(fees), payment)
+ feePayment, principalPayment := k.calculatePayment(ctx, cdp.Principal.Add(cdp.AccumulatedFees...), cdp.AccumulatedFees, payment)
// send the payment from the sender to the cpd module
- err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, feePayment.Add(principalPayment))
+ err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, feePayment.Add(principalPayment...))
if err != nil {
return err
}
// burn the payment coins
- err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, feePayment.Add(principalPayment))
+ err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, feePayment.Add(principalPayment...))
if err != nil {
panic(err)
}
@@ -112,7 +105,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
// burn the corresponding amount of debt coins
cdpDebt := k.getModAccountDebt(ctx, types.ModuleName)
paymentAmount := sdk.ZeroInt()
- for _, c := range feePayment.Add(principalPayment) {
+ for _, c := range feePayment.Add(principalPayment...) {
paymentAmount = paymentAmount.Add(c.Amount)
}
coinsToBurn := sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), paymentAmount))
@@ -128,24 +121,24 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpRepay,
- sdk.NewAttribute(sdk.AttributeKeyAmount, feePayment.Add(principalPayment).String()),
+ sdk.NewAttribute(sdk.AttributeKeyAmount, feePayment.Add(principalPayment...).String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
// remove the old collateral:debt ratio index
- oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
+ oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.RemoveCdpCollateralRatioIndex(ctx, denom, cdp.ID, oldCollateralToDebtRatio)
// update cdp state
if !principalPayment.IsZero() {
cdp.Principal = cdp.Principal.Sub(principalPayment)
}
- cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees).Sub(feePayment)
+ cdp.AccumulatedFees = cdp.AccumulatedFees.Sub(feePayment)
cdp.FeesUpdated = ctx.BlockTime()
// decrement the total principal for the input collateral type
- k.DecrementTotalPrincipal(ctx, denom, feePayment.Add(principalPayment))
+ k.DecrementTotalPrincipal(ctx, denom, feePayment.Add(principalPayment...))
// if the debt is fully paid, return collateral to depositors,
// and remove the cdp and indexes from the store
@@ -165,13 +158,13 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
}
// set cdp state and update indexes
- collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
+ collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
return nil
}
// ValidatePaymentCoins validates that the input coins are valid for repaying debt
-func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk.Coins, debt sdk.Coins) sdk.Error {
+func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk.Coins, debt sdk.Coins) error {
subset := payment.DenomsSubsetOf(cdp.Principal)
if !subset {
var paymentDenoms []string
@@ -182,13 +175,13 @@ func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk
for _, pc := range payment {
paymentDenoms = append(paymentDenoms, pc.Denom)
}
- return types.ErrInvalidPaymentDenom(k.codespace, cdp.ID, principalDenoms, paymentDenoms)
+ return sdkerrors.Wrapf(types.ErrInvalidPayment, "cdp %d: expected %s, got %s", cdp.ID, principalDenoms, paymentDenoms)
}
for _, dc := range payment {
dp, _ := k.GetDebtParam(ctx, dc.Denom)
proposedBalance := cdp.Principal.AmountOf(dc.Denom).Sub(dc.Amount)
if proposedBalance.GT(sdk.ZeroInt()) && proposedBalance.LT(dp.DebtFloor) {
- return types.ErrBelowDebtFloor(k.codespace, sdk.NewCoins(sdk.NewCoin(dc.Denom, proposedBalance)), dp.DebtFloor)
+ return sdkerrors.Wrapf(types.ErrBelowDebtFloor, "proposed %s < minimum %s", sdk.NewCoins(sdk.NewCoin(dc.Denom, proposedBalance)), dp.DebtFloor)
}
}
return nil
@@ -223,12 +216,12 @@ func (k Keeper) calculatePayment(ctx sdk.Context, owed sdk.Coins, fees sdk.Coins
for _, fc := range fees {
if payment.AmountOf(fc.Denom).IsPositive() {
if payment.AmountOf(fc.Denom).GT(fc.Amount) {
- feePayment = feePayment.Add(sdk.NewCoins(fc))
+ feePayment = feePayment.Add(fc)
pc := sdk.NewCoin(fc.Denom, payment.AmountOf(fc.Denom).Sub(fc.Amount))
- principalPayment = principalPayment.Add(sdk.NewCoins(pc))
+ principalPayment = principalPayment.Add(pc)
} else {
fc := sdk.NewCoin(fc.Denom, payment.AmountOf(fc.Denom))
- feePayment = feePayment.Add(sdk.NewCoins(fc))
+ feePayment = feePayment.Add(fc)
}
}
}
diff --git a/x/cdp/keeper/draw_test.go b/x/cdp/keeper/draw_test.go
index 9239ffe8..e5b0993f 100644
--- a/x/cdp/keeper/draw_test.go
+++ b/x/cdp/keeper/draw_test.go
@@ -1,6 +1,7 @@
package keeper_test
import (
+ "errors"
"testing"
"time"
@@ -53,7 +54,7 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() {
t, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.Equal(cs(c("usdx", 20000000)), t.Principal)
- ctd := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees))
+ ctd := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees...))
suite.Equal(d("20.0"), ctd)
ts := suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("20.0"))
suite.Equal(0, len(ts))
@@ -69,7 +70,7 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() {
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.Equal(cs(c("usdx", 20000000), c("susd", 10000000)), t.Principal)
- ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees))
+ ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees...))
suite.Equal(d("400000000").Quo(d("30000000")), ctd)
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("400").Quo(d("30")))
suite.Equal(0, len(ts))
@@ -82,18 +83,18 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() {
suite.Equal(cs(c("xrp", 400000000), c("debt", 30000000)), acc.GetCoins())
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("usdx", 10000000)))
- suite.Equal(types.CodeCdpNotFound, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("xusd", 10000000)))
- suite.Equal(types.CodeDebtNotSupported, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrDebtNotSupported))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 311000000)))
- suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.Equal(cs(c("usdx", 10000000), c("susd", 10000000)), t.Principal)
- ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees))
+ ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees...))
suite.Equal(d("20.0"), ctd)
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("20.0"))
suite.Equal(0, len(ts))
@@ -110,7 +111,7 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() {
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.Equal(cs(c("usdx", 10000000)), t.Principal)
- ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees))
+ ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees...))
suite.Equal(d("40.0"), ctd)
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("40.0"))
suite.Equal(0, len(ts))
@@ -123,12 +124,12 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() {
suite.Equal(cs(c("xrp", 400000000), c("debt", 10000000)), acc.GetCoins())
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("xusd", 10000000)))
- suite.Equal(types.CodeInvalidPaymentDenom, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrInvalidPayment))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("xusd", 10000000)))
- suite.Equal(types.CodeCdpNotFound, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 9000000)))
- suite.Equal(types.CodeBelowDebtFloor, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrBelowDebtFloor))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
suite.NoError(err)
@@ -158,11 +159,12 @@ func (suite *DrawTestSuite) TestAddRepayPrincipalFees() {
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000000)))
suite.NoError(err)
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Minute * 10))
+ err = suite.keeper.UpdateFeesForAllCdps(suite.ctx, "xrp")
+ suite.NoError(err)
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 10000000)))
suite.NoError(err)
t, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
suite.Equal(cs(c("usdx", 92827)), t.AccumulatedFees)
- _ = suite.keeper.MintDebtCoins(suite.ctx, types.ModuleName, "debt", cs(c("usdx", 92827)))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 100)))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
@@ -175,7 +177,9 @@ func (suite *DrawTestSuite) TestAddRepayPrincipalFees() {
err = suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000)))
suite.NoError(err)
- suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 31536000))
+ suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 31536000)) // move forward one year in time
+ err = suite.keeper.UpdateFeesForAllCdps(suite.ctx, "xrp")
+ suite.NoError(err)
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 100000000)))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(3))
diff --git a/x/cdp/keeper/fees.go b/x/cdp/keeper/fees.go
index 0c5671f6..7140d4f3 100644
--- a/x/cdp/keeper/fees.go
+++ b/x/cdp/keeper/fees.go
@@ -1,8 +1,6 @@
package keeper
import (
- "time"
-
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
@@ -22,11 +20,69 @@ func (k Keeper) CalculateFees(ctx sdk.Context, principal sdk.Coins, periods sdk.
accumulator := sdk.NewDecFromInt(types.RelativePow(feeRateInt, periods, scalar)).Mul(sdk.SmallestDec())
feesAccumulated := (sdk.NewDecFromInt(pc.Amount).Mul(accumulator)).Sub(sdk.NewDecFromInt(pc.Amount))
// TODO this will always round down, causing precision loss between the sum of all fees in CDPs and surplus coins in liquidator account
- newFees = newFees.Add(sdk.NewCoins(sdk.NewCoin(pc.Denom, feesAccumulated.TruncateInt())))
+ newFees = newFees.Add(sdk.NewCoin(pc.Denom, feesAccumulated.TruncateInt()))
}
return newFees
}
+// UpdateFeesForAllCdps updates the fees for each of the CDPs
+func (k Keeper) UpdateFeesForAllCdps(ctx sdk.Context, collateralDenom string) error {
+
+ k.IterateCdpsByDenom(ctx, collateralDenom, func(cdp types.CDP) bool {
+
+ oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
+ periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix()))
+
+ newFees := k.CalculateFees(ctx, cdp.Principal, periods, collateralDenom)
+
+ // exit without updating fees if amount has rounded down to zero
+ // cdp will get updated next block when newFees, newFeesSavings, newFeesSurplus >0
+ if newFees.IsZero() {
+ return false
+ }
+
+ // note - only works if principal length is one
+ for _, dc := range cdp.Principal {
+ dp, found := k.GetDebtParam(ctx, dc.Denom)
+ if !found {
+ return false
+ }
+ savingsRate := dp.SavingsRate
+
+ newFeesSavings := sdk.NewDecFromInt(newFees.AmountOf(dp.Denom)).Mul(savingsRate).RoundInt()
+ newFeesSurplus := newFees.AmountOf(dp.Denom).Sub(newFeesSavings)
+
+ // similar to checking for rounding to zero of all fees, but in this case we
+ // need to handle cases where we expect surplus or savings fees to be zero, namely
+ // if newFeesSavings = 0, check if savings rate is not zero
+ // if newFeesSurplus = 0, check if savings rate is not one
+ if (newFeesSavings.IsZero() && !savingsRate.IsZero()) || (newFeesSurplus.IsZero() && !savingsRate.Equal(sdk.OneDec())) {
+ return false
+ }
+ // mint debt coins to the cdp account
+ k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), newFees)
+ previousDebt := k.GetTotalPrincipal(ctx, collateralDenom, dp.Denom)
+ feeCoins := sdk.NewCoins(sdk.NewCoin(dp.Denom, previousDebt))
+ k.SetTotalPrincipal(ctx, collateralDenom, dp.Denom, feeCoins.Add(newFees...).AmountOf(dp.Denom))
+
+ // mint surplus coins divided between the liquidator and savings module accounts.
+ k.supplyKeeper.MintCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, newFeesSurplus)))
+ k.supplyKeeper.MintCoins(ctx, types.SavingsRateMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, newFeesSavings)))
+ }
+
+ // now add the new fees fees to the accumulated fees for the cdp
+ cdp.AccumulatedFees = cdp.AccumulatedFees.Add(newFees...)
+
+ // and set the fees updated time to the current block time since we just updated it
+ cdp.FeesUpdated = ctx.BlockTime()
+ collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
+ k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio)
+ k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
+ return false // this returns true when you want to stop iterating. Since we want to iterate through all we return false
+ })
+ return nil
+}
+
// IncrementTotalPrincipal increments the total amount of debt that has been drawn with that collateral type
func (k Keeper) IncrementTotalPrincipal(ctx sdk.Context, collateralDenom string, principal sdk.Coins) {
for _, pc := range principal {
@@ -66,20 +122,3 @@ func (k Keeper) SetTotalPrincipal(ctx sdk.Context, collateralDenom string, princ
store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix)
store.Set([]byte(collateralDenom+principalDenom), k.cdc.MustMarshalBinaryLengthPrefixed(total))
}
-
-// GetPreviousBlockTime get the blocktime for the previous block
-func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) {
- store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
- b := store.Get([]byte{})
- if b == nil {
- return time.Time{}, false
- }
- k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &blockTime)
- return blockTime, true
-}
-
-// SetPreviousBlockTime set the time of the previous block
-func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) {
- store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
- store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(blockTime))
-}
diff --git a/x/cdp/keeper/fees_test.go b/x/cdp/keeper/fees_test.go
index 48a5fd2d..1b17554a 100644
--- a/x/cdp/keeper/fees_test.go
+++ b/x/cdp/keeper/fees_test.go
@@ -3,6 +3,7 @@ package keeper_test
import (
"math/rand"
"testing"
+ "time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/simulation"
@@ -47,7 +48,7 @@ func (suite *FeeTestSuite) TestCalculateFeesPrecisionLoss() {
suite.NoError(err)
c := sdk.NewCoins(sdk.NewCoin("usdx", ri))
coins = append(coins, c)
- total = total.Add(cs(sdk.NewCoin("usdx", ri)))
+ total = total.Add(sdk.NewCoin("usdx", ri))
}
numBlocks := []int{100, 1000, 10000, 100000}
@@ -56,40 +57,78 @@ func (suite *FeeTestSuite) TestCalculateFeesPrecisionLoss() {
bulkFees := sdk.NewCoins()
individualFees := sdk.NewCoins()
for x := 0; x < nb; x++ {
- fee := suite.keeper.CalculateFees(suite.ctx, total.Add(bulkFees), i(7), "xrp")
- bulkFees = bulkFees.Add(fee)
+ fee := suite.keeper.CalculateFees(suite.ctx, total.Add(bulkFees...), i(7), "xrp")
+ bulkFees = bulkFees.Add(fee...)
}
for _, cns := range coins {
fee := suite.keeper.CalculateFees(suite.ctx, cns, i(int64(nb*7)), "xrp")
- individualFees = individualFees.Add(fee)
+ individualFees = individualFees.Add(fee...)
}
absError := (sdk.OneDec().Sub(sdk.NewDecFromInt(bulkFees[0].Amount).Quo(sdk.NewDecFromInt(individualFees[0].Amount)))).Abs()
- suite.T().Log(bulkFees)
- suite.T().Log(individualFees)
- suite.T().Log(absError)
-
suite.True(d("0.00001").GTE(absError))
}
}
-func (suite *FeeTestSuite) TestGetSetPreviousBlockTime() {
- now := tmtime.Now()
+// createCdps is a helper function to create two CDPs each with zero fees
+func (suite *FeeTestSuite) createCdps() {
+ // create 2 accounts in the state and give them some coins
+ // create two private key pair addresses
+ _, addrs := app.GeneratePrivKeyAddressPairs(2)
+ ak := suite.app.GetAccountKeeper()
+ // setup the first account
+ acc := ak.NewAccountWithAddress(suite.ctx, addrs[0])
+ acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000)))
- _, f := suite.keeper.GetPreviousBlockTime(suite.ctx)
- suite.False(f)
+ ak.SetAccount(suite.ctx, acc)
+ // now setup the second account
+ acc2 := ak.NewAccountWithAddress(suite.ctx, addrs[1])
+ acc2.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000)))
+ ak.SetAccount(suite.ctx, acc2)
- suite.NotPanics(func() { suite.keeper.SetPreviousBlockTime(suite.ctx, now) })
+ // now create two cdps with the addresses we just created
+ // use the created account to create a cdp that SHOULD have fees updated
+ err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("usdx", 24000000)))
+ suite.NoError(err) // check that no error was thrown
- bpt, f := suite.keeper.GetPreviousBlockTime(suite.ctx)
- suite.True(f)
- suite.Equal(now, bpt)
+ // use the other account to create a cdp that SHOULD NOT have fees updated
+ err = suite.keeper.AddCdp(suite.ctx, addrs[1], cs(c("xrp", 200000000)), cs(c("usdx", 10000000)))
+ suite.NoError(err) // check that no error was thrown
}
+// TestUpdateFees tests the functionality for updating the fees for CDPs
+func (suite *FeeTestSuite) TestUpdateFees() {
+ // this helper function creates two CDPs with id 1 and 2 respectively, each with zero fees
+ suite.createCdps()
+
+ // move the context forward in time so that cdps will have fees accumulate if CalculateFees is called
+ // note - time must be moved forward by a sufficient amount in order for additional
+ // fees to accumulate, in this example 600 seconds
+ oldtime := suite.ctx.BlockTime()
+ suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 600))
+ err := suite.keeper.UpdateFeesForAllCdps(suite.ctx, "xrp")
+ suite.NoError(err) // check that we don't have any error
+
+ // cdp we expect fees to accumulate for
+ cdp1, _ := suite.keeper.GetCDP(suite.ctx, "xrp", 1)
+ // check fees are not zero
+ // check that the fees have been updated
+ suite.False(cdp1.AccumulatedFees.Empty())
+ // now check that we have the correct amount of fees overall (22 USDX for this scenario)
+ suite.Equal(sdk.NewInt(22), cdp1.AccumulatedFees.AmountOf("usdx"))
+ suite.Equal(suite.ctx.BlockTime(), cdp1.FeesUpdated)
+ // cdp we expect fees to not accumulate for because of rounding to zero
+ cdp2, _ := suite.keeper.GetCDP(suite.ctx, "xrp", 2)
+
+ // check fees are zero
+ suite.True(cdp2.AccumulatedFees.Empty())
+ suite.Equal(oldtime, cdp2.FeesUpdated)
+}
+
func TestFeeTestSuite(t *testing.T) {
suite.Run(t, new(FeeTestSuite))
}
diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go
index 6905882a..66bb4dc2 100644
--- a/x/cdp/keeper/integration_test.go
+++ b/x/cdp/keeper/integration_test.go
@@ -39,9 +39,10 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState {
func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
- GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
- SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
- DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
+ SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
+ DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: asset,
@@ -61,14 +62,15 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
+ SavingsRate: d("0.9"),
},
},
},
- StartingCdpID: cdp.DefaultCdpStartingID,
- DebtDenom: cdp.DefaultDebtDenom,
- GovDenom: cdp.DefaultGovDenom,
- CDPs: cdp.CDPs{},
- PreviousBlockTime: cdp.DefaultPreviousBlockTime,
+ StartingCdpID: cdp.DefaultCdpStartingID,
+ DebtDenom: cdp.DefaultDebtDenom,
+ GovDenom: cdp.DefaultGovDenom,
+ CDPs: cdp.CDPs{},
+ PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
@@ -101,9 +103,10 @@ func NewPricefeedGenStateMulti() app.GenesisState {
func NewCDPGenStateMulti() app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
- GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
- SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
- DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
+ SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
+ DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: "xrp",
@@ -134,20 +137,79 @@ func NewCDPGenStateMulti() app.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
+ SavingsRate: d("0.95"),
},
{
Denom: "susd",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
+ SavingsRate: d("0.95"),
},
},
},
- StartingCdpID: cdp.DefaultCdpStartingID,
- DebtDenom: cdp.DefaultDebtDenom,
- GovDenom: cdp.DefaultGovDenom,
- CDPs: cdp.CDPs{},
- PreviousBlockTime: cdp.DefaultPreviousBlockTime,
+ StartingCdpID: cdp.DefaultCdpStartingID,
+ DebtDenom: cdp.DefaultDebtDenom,
+ GovDenom: cdp.DefaultGovDenom,
+ CDPs: cdp.CDPs{},
+ PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
+ }
+ return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
+}
+
+func NewCDPGenStateHighDebtLimit() app.GenesisState {
+ cdpGenesis := cdp.GenesisState{
+ Params: cdp.Params{
+ GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 100000000000000), sdk.NewInt64Coin("susd", 100000000000000)),
+ SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
+ DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
+ CollateralParams: cdp.CollateralParams{
+ {
+ Denom: "xrp",
+ LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
+ DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000), sdk.NewInt64Coin("susd", 50000000000000)),
+ StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
+ LiquidationPenalty: d("0.05"),
+ AuctionSize: i(7000000000),
+ Prefix: 0x20,
+ MarketID: "xrp:usd",
+ ConversionFactor: i(6),
+ },
+ {
+ Denom: "btc",
+ LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
+ DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000), sdk.NewInt64Coin("susd", 50000000000000)),
+ StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr
+ LiquidationPenalty: d("0.025"),
+ AuctionSize: i(10000000),
+ Prefix: 0x21,
+ MarketID: "btc:usd",
+ ConversionFactor: i(8),
+ },
+ },
+ DebtParams: cdp.DebtParams{
+ {
+ Denom: "usdx",
+ ReferenceAsset: "usd",
+ ConversionFactor: i(6),
+ DebtFloor: i(10000000),
+ SavingsRate: d("0.95"),
+ },
+ {
+ Denom: "susd",
+ ReferenceAsset: "usd",
+ ConversionFactor: i(6),
+ DebtFloor: i(10000000),
+ SavingsRate: d("0.95"),
+ },
+ },
+ },
+ StartingCdpID: cdp.DefaultCdpStartingID,
+ DebtDenom: cdp.DefaultDebtDenom,
+ GovDenom: cdp.DefaultGovDenom,
+ CDPs: cdp.CDPs{},
+ PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
diff --git a/x/cdp/keeper/keeper.go b/x/cdp/keeper/keeper.go
index 94570d2b..f7d3e585 100644
--- a/x/cdp/keeper/keeper.go
+++ b/x/cdp/keeper/keeper.go
@@ -18,30 +18,23 @@ type Keeper struct {
pricefeedKeeper types.PricefeedKeeper
supplyKeeper types.SupplyKeeper
auctionKeeper types.AuctionKeeper
- codespace sdk.CodespaceType
+ accountKeeper types.AccountKeeper
}
// NewKeeper creates a new keeper
-func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, ak types.AuctionKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) Keeper {
-
- // ensure cdp module account is set
- if addr := sk.GetModuleAddress(types.ModuleName); addr == nil {
- panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
- }
-
- // ensure liquidator module account is set
- if addr := sk.GetModuleAddress(types.LiquidatorMacc); addr == nil {
- panic(fmt.Sprintf("%s module account has not been set", types.LiquidatorMacc))
+func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, ak types.AuctionKeeper, sk types.SupplyKeeper, ack types.AccountKeeper) Keeper {
+ if !paramstore.HasKeyTable() {
+ paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
}
return Keeper{
key: key,
cdc: cdc,
- paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
+ paramSubspace: paramstore,
pricefeedKeeper: pfk,
auctionKeeper: ak,
supplyKeeper: sk,
- codespace: codespace,
+ accountKeeper: ack,
}
}
diff --git a/x/cdp/keeper/keeper_bench_test.go b/x/cdp/keeper/keeper_bench_test.go
index 20b88c82..bee4d37b 100644
--- a/x/cdp/keeper/keeper_bench_test.go
+++ b/x/cdp/keeper/keeper_bench_test.go
@@ -115,7 +115,7 @@ func BenchmarkCdpIteration(b *testing.B) {
}
-var errResult sdk.Error
+var errResult error
func BenchmarkCdpCreation(b *testing.B) {
tApp := app.NewTestApp()
diff --git a/x/cdp/keeper/querier.go b/x/cdp/keeper/querier.go
index 440b4e1a..ba69c490 100644
--- a/x/cdp/keeper/querier.go
+++ b/x/cdp/keeper/querier.go
@@ -1,16 +1,20 @@
package keeper
import (
+ "fmt"
+
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+
"github.com/kava-labs/kava/x/cdp/types"
)
// NewQuerier returns a new querier function
func NewQuerier(keeper Keeper) sdk.Querier {
- return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
+ return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
switch path[0] {
case types.QueryGetCdp:
return queryGetCdp(ctx, req, keeper)
@@ -23,85 +27,85 @@ func NewQuerier(keeper Keeper) sdk.Querier {
case types.QueryGetCdpDeposits:
return queryGetDeposits(ctx, req, keeper)
default:
- return nil, sdk.ErrUnknownRequest("unknown cdp query endpoint")
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint %s", types.ModuleName, path[0])
}
}
}
// query a specific cdp
-func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var requestParams types.QueryCdpParams
- err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
+ err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
- return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
if !valid {
- return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom)
+ return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralDenom)
}
cdp, found := keeper.GetCdpByOwnerAndDenom(ctx, requestParams.Owner, requestParams.CollateralDenom)
if !found {
- return nil, types.ErrCdpNotFound(keeper.codespace, requestParams.Owner, requestParams.CollateralDenom)
+ return nil, sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", requestParams.Owner, requestParams.CollateralDenom)
}
augmentedCDP, err := keeper.LoadAugmentedCDP(ctx, cdp)
if err != nil {
- return nil, types.ErrLoadingAugmentedCDP(keeper.codespace, cdp.ID)
+ return nil, sdkerrors.Wrap(types.ErrLoadingAugmentedCDP, fmt.Sprintf("%v: %d", err.Error(), cdp.ID))
}
- bz, err := codec.MarshalJSONIndent(keeper.cdc, augmentedCDP)
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, augmentedCDP)
if err != nil {
- return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// query deposits on a particular cdp
-func queryGetDeposits(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryGetDeposits(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var requestParams types.QueryCdpDeposits
- err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
+ err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
- return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
if !valid {
- return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom)
+ return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralDenom)
}
cdp, found := keeper.GetCdpByOwnerAndDenom(ctx, requestParams.Owner, requestParams.CollateralDenom)
if !found {
- return nil, types.ErrCdpNotFound(keeper.codespace, requestParams.Owner, requestParams.CollateralDenom)
+ return nil, sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", requestParams.Owner, requestParams.CollateralDenom)
}
deposits := keeper.GetDeposits(ctx, cdp.ID)
- bz, err := codec.MarshalJSONIndent(keeper.cdc, deposits)
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, deposits)
if err != nil {
- return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// query cdps with matching denom and ratio LESS THAN the input ratio
-func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var requestParams types.QueryCdpsByRatioParams
- err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
+ err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
- return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
if !valid {
- return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom)
+ return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralDenom)
}
ratio, err := keeper.CalculateCollateralizationRatioFromAbsoluteRatio(ctx, requestParams.CollateralDenom, requestParams.Ratio)
if err != nil {
- return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could get collateralization ratio from absolute ratio", err.Error()))
+ return nil, sdkerrors.Wrap(err, "couldn't get collateralization ratio from absolute ratio")
}
cdps := keeper.GetAllCdpsByDenomAndRatio(ctx, requestParams.CollateralDenom, ratio)
@@ -113,23 +117,23 @@ func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper)
augmentedCDPs = append(augmentedCDPs, augmentedCDP)
}
}
- bz, err := codec.MarshalJSONIndent(keeper.cdc, augmentedCDPs)
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, augmentedCDPs)
if err != nil {
- return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// query all cdps with matching collateral denom
-func queryGetCdpsByDenom(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryGetCdpsByDenom(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var requestParams types.QueryCdpsParams
- err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
+ err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
- return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
if !valid {
- return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom)
+ return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralDenom)
}
cdps := keeper.GetAllCdpsByDenom(ctx, requestParams.CollateralDenom)
@@ -141,22 +145,22 @@ func queryGetCdpsByDenom(ctx sdk.Context, req abci.RequestQuery, keeper Keeper)
augmentedCDPs = append(augmentedCDPs, augmentedCDP)
}
}
- bz, err := codec.MarshalJSONIndent(keeper.cdc, augmentedCDPs)
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, augmentedCDPs)
if err != nil {
- return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// query params in the cdp store
-func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
// Get params
params := keeper.GetParams(ctx)
// Encode results
- bz, err := codec.MarshalJSONIndent(keeper.cdc, params)
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, params)
if err != nil {
- return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
diff --git a/x/cdp/keeper/querier_test.go b/x/cdp/keeper/querier_test.go
index 41b65467..e68afb77 100644
--- a/x/cdp/keeper/querier_test.go
+++ b/x/cdp/keeper/querier_test.go
@@ -53,7 +53,7 @@ func (suite *QuerierTestSuite) SetupTest() {
tApp.InitializeFromGenesisStates(
authGS,
NewPricefeedGenStateMulti(),
- NewCDPGenStateMulti(),
+ NewCDPGenStateHighDebtLimit(),
)
suite.ctx = ctx
@@ -93,11 +93,13 @@ func (suite *QuerierTestSuite) SetupTest() {
amount = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 500000000, 5000000000)
debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 1000000000, 25000000000)
}
- suite.Nil(suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt)))))
+ err = suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt))))
+ suite.NoError(err)
c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1))
suite.True(f)
cdps[j] = c
- aCDP, _ := suite.keeper.LoadAugmentedCDP(suite.ctx, c)
+ aCDP, err := suite.keeper.LoadAugmentedCDP(suite.ctx, c)
+ suite.NoError(err)
augmentedCDPs[j] = aCDP
}
@@ -250,7 +252,7 @@ func (suite *QuerierTestSuite) TestQueryParams() {
var p types.Params
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p))
- cdpGS := NewCDPGenStateMulti()
+ cdpGS := NewCDPGenStateHighDebtLimit()
gs := types.GenesisState{}
types.ModuleCdc.UnmarshalJSON(cdpGS["cdp"], &gs)
suite.Equal(gs.Params, p)
diff --git a/x/cdp/keeper/savings.go b/x/cdp/keeper/savings.go
new file mode 100644
index 00000000..ec9c5944
--- /dev/null
+++ b/x/cdp/keeper/savings.go
@@ -0,0 +1,96 @@
+package keeper
+
+import (
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/store/prefix"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
+ supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
+ auctiontypes "github.com/kava-labs/kava/x/auction/types"
+ "github.com/kava-labs/kava/x/cdp/types"
+)
+
+// DistributeSavingsRate distributes surplus that has accumulated in the liquidator account to address holding stable coins according the the savings rate
+func (k Keeper) DistributeSavingsRate(ctx sdk.Context, debtDenom string) error {
+ dp, found := k.GetDebtParam(ctx, debtDenom)
+ if !found {
+ return sdkerrors.Wrap(types.ErrDebtNotSupported, debtDenom)
+ }
+ savingsRateMacc := k.supplyKeeper.GetModuleAccount(ctx, types.SavingsRateMacc)
+ surplusToDistribute := savingsRateMacc.GetCoins().AmountOf(dp.Denom)
+ if surplusToDistribute.IsZero() {
+ return nil
+ }
+
+ modAccountCoins := k.getModuleAccountCoins(ctx, dp.Denom)
+ totalSupplyLessModAccounts := k.supplyKeeper.GetSupply(ctx).GetTotal().Sub(modAccountCoins)
+ surplusDistributed := sdk.ZeroInt()
+ var iterationErr error
+ k.accountKeeper.IterateAccounts(ctx, func(acc authexported.Account) (stop bool) {
+ _, ok := acc.(supplyexported.ModuleAccountI)
+ if ok {
+ // don't distribute savings rate to module accounts
+ return false
+ }
+ debtAmount := acc.GetCoins().AmountOf(debtDenom)
+ if !debtAmount.IsPositive() {
+ return false
+ }
+ // (balance * rewardToDisribute) / totalSupply
+ // interest is the ratable fraction of savings rate owed to that account, rounded using bankers rounding
+ interest := (sdk.NewDecFromInt(debtAmount).Mul(sdk.NewDecFromInt(surplusToDistribute))).Quo(sdk.NewDecFromInt(totalSupplyLessModAccounts.AmountOf(debtDenom))).RoundInt()
+ // sanity check, if we are going to over-distribute due to rounding, distribute only the remaining savings rate that hasn't been distributed.
+ if interest.GT(surplusToDistribute.Sub(surplusDistributed)) {
+ interest = surplusToDistribute.Sub(surplusDistributed)
+ }
+ // sanity check - don't send saving rate if the rounded amount is zero
+ if !interest.IsPositive() {
+ return false
+ }
+ interestCoins := sdk.NewCoins(sdk.NewCoin(debtDenom, interest))
+ err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.SavingsRateMacc, acc.GetAddress(), interestCoins)
+ if err != nil {
+ iterationErr = err
+ return true
+ }
+ surplusDistributed = surplusDistributed.Add(interest)
+ return false
+ })
+ if iterationErr != nil {
+ return iterationErr
+ }
+ return nil
+}
+
+// GetPreviousSavingsDistribution get the time of the previous savings rate distribution
+func (k Keeper) GetPreviousSavingsDistribution(ctx sdk.Context) (distTime time.Time, found bool) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousDistributionTimeKey)
+ b := store.Get([]byte{})
+ if b == nil {
+ return time.Time{}, false
+ }
+ k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &distTime)
+ return distTime, true
+}
+
+// SetPreviousSavingsDistribution set the time of the previous block
+func (k Keeper) SetPreviousSavingsDistribution(ctx sdk.Context, distTime time.Time) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousDistributionTimeKey)
+ store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(distTime))
+}
+
+func (k Keeper) getModuleAccountCoins(ctx sdk.Context, denom string) sdk.Coins {
+ // NOTE: these are the module accounts that could end up holding stable denoms at some point.
+ // Since there are currently no api methods to 'GetAllModuleAccounts', this function will need to be updated if a
+ // new module account is added which can hold stable denoms.
+ savingsRateMaccCoinAmount := k.supplyKeeper.GetModuleAccount(ctx, types.SavingsRateMacc).GetCoins().AmountOf(denom)
+ cdpMaccCoinAmount := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins().AmountOf(denom)
+ auctionMaccCoinAmount := k.supplyKeeper.GetModuleAccount(ctx, auctiontypes.ModuleName).GetCoins().AmountOf(denom)
+ liquidatorMaccCoinAmount := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(denom)
+ feeMaccCoinAmount := k.supplyKeeper.GetModuleAccount(ctx, authtypes.FeeCollectorName).GetCoins().AmountOf(denom)
+ totalModAccountAmount := savingsRateMaccCoinAmount.Add(cdpMaccCoinAmount).Add(auctionMaccCoinAmount).Add(liquidatorMaccCoinAmount).Add(feeMaccCoinAmount)
+ return sdk.NewCoins(sdk.NewCoin(denom, totalModAccountAmount))
+}
diff --git a/x/cdp/keeper/savings_test.go b/x/cdp/keeper/savings_test.go
new file mode 100644
index 00000000..5e5a910e
--- /dev/null
+++ b/x/cdp/keeper/savings_test.go
@@ -0,0 +1,81 @@
+package keeper_test
+
+import (
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/cdp/keeper"
+ "github.com/kava-labs/kava/x/cdp/types"
+ "github.com/stretchr/testify/suite"
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+type SavingsTestSuite struct {
+ suite.Suite
+
+ keeper keeper.Keeper
+ app app.TestApp
+ ctx sdk.Context
+ addrs []sdk.AccAddress
+}
+
+func (suite *SavingsTestSuite) SetupTest() {
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+ _, addrs := app.GeneratePrivKeyAddressPairs(3)
+ authGS := app.NewAuthGenState(
+ addrs,
+ []sdk.Coins{
+ cs(c("usdx", 100000)), cs(c("usdx", 50000)), cs(c("usdx", 50000)),
+ },
+ )
+ tApp.InitializeFromGenesisStates(
+ authGS,
+ NewPricefeedGenStateMulti(),
+ NewCDPGenStateMulti(),
+ )
+ sk := tApp.GetSupplyKeeper()
+ macc := sk.GetModuleAccount(ctx, types.SavingsRateMacc)
+ err := sk.MintCoins(ctx, macc.GetName(), cs(c("usdx", 10000)))
+ suite.NoError(err)
+ keeper := tApp.GetCDPKeeper()
+ suite.app = tApp
+ suite.keeper = keeper
+ suite.ctx = ctx
+ suite.addrs = addrs
+}
+
+func (suite *SavingsTestSuite) TestApplySavingsRate() {
+ err := suite.keeper.DistributeSavingsRate(suite.ctx, "usdx")
+ suite.NoError(err)
+ ak := suite.app.GetAccountKeeper()
+ acc0 := ak.GetAccount(suite.ctx, suite.addrs[0])
+ suite.Equal(cs(c("usdx", 105000)), acc0.GetCoins())
+ acc1 := ak.GetAccount(suite.ctx, suite.addrs[1])
+ suite.Equal(cs(c("usdx", 52500)), acc1.GetCoins())
+ acc2 := ak.GetAccount(suite.ctx, suite.addrs[2])
+ suite.Equal(cs(c("usdx", 52500)), acc2.GetCoins())
+ sk := suite.app.GetSupplyKeeper()
+ macc := sk.GetModuleAccount(suite.ctx, types.SavingsRateMacc)
+ suite.True(macc.GetCoins().AmountOf("usdx").IsZero())
+}
+
+func (suite *SavingsTestSuite) TestGetSetPreviousDistributionTime() {
+ now := tmtime.Now()
+
+ _, f := suite.keeper.GetPreviousSavingsDistribution(suite.ctx)
+ suite.False(f)
+
+ suite.NotPanics(func() { suite.keeper.SetPreviousSavingsDistribution(suite.ctx, now) })
+
+ pdt, f := suite.keeper.GetPreviousSavingsDistribution(suite.ctx)
+ suite.True(f)
+ suite.Equal(now, pdt)
+
+}
+
+func TestSavingsTestSuite(t *testing.T) {
+ suite.Run(t, new(SavingsTestSuite))
+}
diff --git a/x/cdp/keeper/seize.go b/x/cdp/keeper/seize.go
index 08645a2b..daf6a813 100644
--- a/x/cdp/keeper/seize.go
+++ b/x/cdp/keeper/seize.go
@@ -15,14 +15,9 @@ import (
// 3. moves debt coins from the cdp module to the liquidator module account,
// 4. decrements the total amount of principal outstanding for that collateral type
// (this is the equivalent of saying that fees are no longer accumulated by a cdp once it gets liquidated)
-func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) sdk.Error {
+func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) error {
// Calculate the previous collateral ratio
- oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
- // Update fees
- periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix()))
- fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom)
- cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees)
- cdp.FeesUpdated = ctx.BlockTime()
+ oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
// Move debt coins from cdp to liquidator account
deposits := k.GetDeposits(ctx, cdp.ID)
@@ -70,7 +65,7 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) sdk.Error {
coinsToDecrement := sdk.NewCoins(dc)
if feeAmount.IsPositive() {
feeCoins := sdk.NewCoins(sdk.NewCoin(dc.Denom, feeAmount))
- coinsToDecrement = coinsToDecrement.Add(feeCoins)
+ coinsToDecrement = coinsToDecrement.Add(feeCoins...)
}
k.DecrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, coinsToDecrement)
}
@@ -80,27 +75,20 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) sdk.Error {
return nil
}
-// HandleNewDebt compounds the accumulated fees for the input collateral and principal coins.
-// the following operations are performed:
-// 1. mints the fee coins in the liquidator module account,
-// 2. mints the same amount of debt coins in the cdp module account
-// 3. updates the total amount of principal for the input collateral type in the store,
-func (k Keeper) HandleNewDebt(ctx sdk.Context, collateralDenom string, principalDenom string, periods sdk.Int) {
- previousDebt := k.GetTotalPrincipal(ctx, collateralDenom, principalDenom)
- feeCoins := sdk.NewCoins(sdk.NewCoin(principalDenom, previousDebt))
- newFees := k.CalculateFees(ctx, feeCoins, periods, collateralDenom)
- k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), newFees)
- k.supplyKeeper.MintCoins(ctx, types.LiquidatorMacc, newFees)
- k.SetTotalPrincipal(ctx, collateralDenom, principalDenom, feeCoins.Add(newFees).AmountOf(principalDenom))
-}
-
// LiquidateCdps seizes collateral from all CDPs below the input liquidation ratio
-func (k Keeper) LiquidateCdps(ctx sdk.Context, marketID string, denom string, liquidationRatio sdk.Dec) sdk.Error {
+func (k Keeper) LiquidateCdps(ctx sdk.Context, marketID string, denom string, liquidationRatio sdk.Dec) error {
price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID)
if err != nil {
return err
}
- normalizedRatio := sdk.OneDec().Quo(price.Price.Quo(liquidationRatio))
+ priceDivLiqRatio := price.Price.Quo(liquidationRatio)
+ if priceDivLiqRatio.IsZero() {
+ priceDivLiqRatio = sdk.SmallestDec()
+ }
+ // price = $0.5
+ // liquidation ratio = 1.5
+ // normalizedRatio = (1/(0.5/1.5)) = 3
+ normalizedRatio := sdk.OneDec().Quo(priceDivLiqRatio)
cdpsToLiquidate := k.GetAllCdpsByDenomAndRatio(ctx, denom, normalizedRatio)
for _, c := range cdpsToLiquidate {
err := k.SeizeCollateral(ctx, c)
diff --git a/x/cdp/keeper/seize_test.go b/x/cdp/keeper/seize_test.go
index 084423fc..579905d6 100644
--- a/x/cdp/keeper/seize_test.go
+++ b/x/cdp/keeper/seize_test.go
@@ -1,6 +1,7 @@
package keeper_test
import (
+ "errors"
"math/rand"
"testing"
"time"
@@ -145,7 +146,7 @@ func (suite *SeizeTestSuite) TestSeizeCollateral() {
acc := ak.GetAccount(suite.ctx, suite.addrs[1])
suite.Equal(p.Int64(), acc.GetCoins().AmountOf("usdx").Int64())
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], cs(c("xrp", 10)))
- suite.Equal(types.CodeCdpNotFound, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
}
func (suite *SeizeTestSuite) TestSeizeCollateralMultiDeposit() {
@@ -170,7 +171,7 @@ func (suite *SeizeTestSuite) TestSeizeCollateralMultiDeposit() {
acc := ak.GetAccount(suite.ctx, suite.addrs[1])
suite.Equal(p.Int64(), acc.GetCoins().AmountOf("usdx").Int64())
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], cs(c("xrp", 10)))
- suite.Equal(types.CodeCdpNotFound, err.Result().Code)
+ suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
}
func (suite *SeizeTestSuite) TestLiquidateCdps() {
@@ -188,14 +189,6 @@ func (suite *SeizeTestSuite) TestLiquidateCdps() {
suite.Equal(len(suite.liquidations.xrp), xrpLiquidations)
}
-func (suite *SeizeTestSuite) TestHandleNewDebt() {
- suite.createCdps()
- tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
- suite.keeper.HandleNewDebt(suite.ctx, "xrp", "usdx", i(31536000))
- tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
- suite.Equal(sdk.NewDec(tpb.Int64()).Mul(d("1.05")).TruncateInt().Int64(), tpa.Int64())
-}
-
func (suite *SeizeTestSuite) TestApplyLiquidationPenalty() {
penalty := suite.keeper.ApplyLiquidationPenalty(suite.ctx, "xrp", i(1000))
suite.Equal(i(50), penalty)
diff --git a/x/cdp/module.go b/x/cdp/module.go
index 108189de..1794aba2 100644
--- a/x/cdp/module.go
+++ b/x/cdp/module.go
@@ -11,6 +11,7 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
+ "github.com/cosmos/cosmos-sdk/x/auth"
sim "github.com/cosmos/cosmos-sdk/x/simulation"
abci "github.com/tendermint/tendermint/abci/types"
@@ -22,7 +23,7 @@ import (
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
- _ module.AppModuleSimulation = AppModuleSimulation{}
+ _ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic app module basics object
@@ -70,41 +71,24 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
//____________________________________________________________________________
-// AppModuleSimulation defines the module simulation functions used by the cdp module.
-type AppModuleSimulation struct{}
-
-// RegisterStoreDecoder registers a decoder for cdp module's types
-func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
- sdr[StoreKey] = simulation.DecodeStore
-}
-
-// GenerateGenesisState creates a randomized GenState of the cdp module
-func (AppModuleSimulation) GenerateGenesisState(simState *module.SimulationState) {
- simulation.RandomizedGenState(simState)
-}
-
-// RandomizedParams creates randomized cdp param changes for the simulator.
-func (AppModuleSimulation) RandomizedParams(r *rand.Rand) []sim.ParamChange {
- return simulation.ParamChanges(r)
-}
-
-//____________________________________________________________________________
-
// AppModule app module type
type AppModule struct {
AppModuleBasic
- AppModuleSimulation
keeper Keeper
+ accountKeeper auth.AccountKeeper
pricefeedKeeper PricefeedKeeper
+ supplyKeeper SupplyKeeper
}
// NewAppModule creates a new AppModule object
-func NewAppModule(keeper Keeper, pricefeedKeeper PricefeedKeeper) AppModule {
+func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper, pricefeedKeeper PricefeedKeeper, supplyKeeper SupplyKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
+ accountKeeper: accountKeeper,
pricefeedKeeper: pricefeedKeeper,
+ supplyKeeper: supplyKeeper,
}
}
@@ -140,7 +124,7 @@ func (am AppModule) NewQuerierHandler() sdk.Querier {
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
- InitGenesis(ctx, am.keeper, am.pricefeedKeeper, genesisState)
+ InitGenesis(ctx, am.keeper, am.pricefeedKeeper, am.supplyKeeper, genesisState)
return []abci.ValidatorUpdate{}
}
@@ -160,3 +144,30 @@ func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
+
+//____________________________________________________________________________
+
+// GenerateGenesisState creates a randomized GenState of the cdp module
+func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
+ simulation.RandomizedGenState(simState)
+}
+
+// ProposalContents doesn't return any content functions for governance proposals.
+func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
+ return nil
+}
+
+// RandomizedParams returns nil because cdp has no params.
+func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
+ return simulation.ParamChanges(r)
+}
+
+// RegisterStoreDecoder registers a decoder for cdp module's types
+func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
+ sdr[StoreKey] = simulation.DecodeStore
+}
+
+// WeightedOperations returns the all the cdp module operations with their respective weights.
+func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
+ return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper, am.pricefeedKeeper)
+}
diff --git a/x/cdp/simulation/decoder.go b/x/cdp/simulation/decoder.go
index edef9051..91c89c10 100644
--- a/x/cdp/simulation/decoder.go
+++ b/x/cdp/simulation/decoder.go
@@ -1,12 +1,66 @@
package simulation
import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "time"
+
+ "github.com/tendermint/tendermint/libs/kv"
+
"github.com/cosmos/cosmos-sdk/codec"
- cmn "github.com/tendermint/tendermint/libs/common"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/x/cdp/types"
)
// DecodeStore unmarshals the KVPair's Value to the corresponding cdp type
-func DecodeStore(cdc *codec.Codec, kvA, kvB cmn.KVPair) string {
- // TODO implement this
- return ""
+func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
+ switch {
+ case bytes.Equal(kvA.Key[:1], types.CdpIDKeyPrefix):
+ var cdpIDsA, cdpIDsB []uint64
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &cdpIDsA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &cdpIDsB)
+ return fmt.Sprintf("%v\n%v", cdpIDsA, cdpIDsB)
+
+ case bytes.Equal(kvA.Key[:1], types.CdpKeyPrefix):
+ var cdpA, cdpB types.CDP
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &cdpA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &cdpB)
+ return fmt.Sprintf("%v\n%v", cdpA, cdpB)
+
+ case bytes.Equal(kvA.Key[:1], types.CdpIDKey),
+ bytes.Equal(kvA.Key[:1], types.CollateralRatioIndexPrefix):
+ idA := binary.BigEndian.Uint64(kvA.Value)
+ idB := binary.BigEndian.Uint64(kvB.Value)
+ return fmt.Sprintf("%d\n%d", idA, idB)
+
+ case bytes.Equal(kvA.Key[:1], types.DebtDenomKey),
+ bytes.Equal(kvA.Key[:1], types.GovDenomKey):
+ var denomA, denomB string
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &denomA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &denomB)
+ return fmt.Sprintf("%s\n%s", denomA, denomB)
+
+ case bytes.Equal(kvA.Key[:1], types.DepositKeyPrefix):
+ var depositA, depositB types.Deposit
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &depositA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &depositB)
+ return fmt.Sprintf("%s\n%s", depositA, depositB)
+
+ case bytes.Equal(kvA.Key[:1], types.PrincipalKeyPrefix):
+ var totalA, totalB sdk.Int
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &totalA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &totalB)
+ return fmt.Sprintf("%s\n%s", totalA, totalB)
+
+ case bytes.Equal(kvA.Key[:1], types.PreviousDistributionTimeKey):
+ var timeA, timeB time.Time
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &timeA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &timeB)
+ return fmt.Sprintf("%s\n%s", timeA, timeB)
+
+ default:
+ panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
+ }
}
diff --git a/x/cdp/simulation/decoder_test.go b/x/cdp/simulation/decoder_test.go
new file mode 100644
index 00000000..f1ab67cc
--- /dev/null
+++ b/x/cdp/simulation/decoder_test.go
@@ -0,0 +1,75 @@
+package simulation
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/x/cdp/types"
+)
+
+func makeTestCodec() (cdc *codec.Codec) {
+ cdc = codec.New()
+ sdk.RegisterCodec(cdc)
+ codec.RegisterCrypto(cdc)
+ types.RegisterCodec(cdc)
+ return
+}
+
+func TestDecodeDistributionStore(t *testing.T) {
+ cdc := makeTestCodec()
+
+ cdpIds := []uint64{1, 2, 3, 4, 5}
+ denom := "denom"
+ oneCoins := sdk.NewCoins(sdk.NewCoin(denom, sdk.OneInt()))
+ deposit := types.Deposit{CdpID: 1, Amount: oneCoins}
+ principal := sdk.OneInt()
+ prevDistTime := time.Now().UTC()
+ cdp := types.CDP{ID: 1, FeesUpdated: prevDistTime, Collateral: oneCoins, Principal: oneCoins, AccumulatedFees: oneCoins}
+
+ kvPairs := kv.Pairs{
+ kv.Pair{Key: types.CdpIDKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(cdpIds)},
+ kv.Pair{Key: types.CdpKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(cdp)},
+ kv.Pair{Key: types.CdpIDKey, Value: sdk.Uint64ToBigEndian(2)},
+ kv.Pair{Key: types.CollateralRatioIndexPrefix, Value: sdk.Uint64ToBigEndian(10)},
+ kv.Pair{Key: []byte(types.DebtDenomKey), Value: cdc.MustMarshalBinaryLengthPrefixed(denom)},
+ kv.Pair{Key: []byte(types.GovDenomKey), Value: cdc.MustMarshalBinaryLengthPrefixed(denom)},
+ kv.Pair{Key: []byte(types.DepositKeyPrefix), Value: cdc.MustMarshalBinaryLengthPrefixed(deposit)},
+ kv.Pair{Key: []byte(types.PrincipalKeyPrefix), Value: cdc.MustMarshalBinaryLengthPrefixed(principal)},
+ kv.Pair{Key: []byte(types.PreviousDistributionTimeKey), Value: cdc.MustMarshalBinaryLengthPrefixed(prevDistTime)},
+ kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
+ }
+
+ tests := []struct {
+ name string
+ expectedLog string
+ }{
+ {"CdpIDs", fmt.Sprintf("%v\n%v", cdpIds, cdpIds)},
+ {"CDP", fmt.Sprintf("%v\n%v", cdp, cdp)},
+ {"CdpID", "2\n2"},
+ {"CollateralRatioIndex", "10\n10"},
+ {"DebtDenom", fmt.Sprintf("%s\n%s", denom, denom)},
+ {"GovDenom", fmt.Sprintf("%s\n%s", denom, denom)},
+ {"DepositKeyPrefix", fmt.Sprintf("%v\n%v", deposit, deposit)},
+ {"Principal", fmt.Sprintf("%v\n%v", principal, principal)},
+ {"PreviousDistributionTime", fmt.Sprintf("%s\n%s", prevDistTime, prevDistTime)},
+ {"other", ""},
+ }
+ for i, tt := range tests {
+ i, tt := i, tt
+ t.Run(tt.name, func(t *testing.T) {
+ switch i {
+ case len(tests) - 1:
+ require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
+ default:
+ require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
+ }
+ })
+ }
+}
diff --git a/x/cdp/simulation/genesis.go b/x/cdp/simulation/genesis.go
index 96bdfb21..655e7018 100644
--- a/x/cdp/simulation/genesis.go
+++ b/x/cdp/simulation/genesis.go
@@ -4,19 +4,163 @@ import (
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
-
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ "github.com/cosmos/cosmos-sdk/x/supply"
+ supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
"github.com/kava-labs/kava/x/cdp/types"
)
// RandomizedGenState generates a random GenesisState for cdp
func RandomizedGenState(simState *module.SimulationState) {
- // TODO implement this fully
- // - randomly generating the genesis params
- // - overwriting with genesis provided to simulation
- cdpGenesis := types.DefaultGenesisState()
+ cdpGenesis := randomCdpGenState(simState.Rand.Intn(2))
+
+ // hacky way to give accounts coins so they can create cdps (coins includes usdx so it's possible to have sufficient balance to close a cdp)
+ var authGenesis auth.GenesisState
+ simState.Cdc.MustUnmarshalJSON(simState.GenState[auth.ModuleName], &authGenesis)
+ totalCdpCoins := sdk.NewCoins()
+ for _, acc := range authGenesis.Accounts {
+ _, ok := acc.(supplyexported.ModuleAccountI)
+ if ok {
+ continue
+ }
+ coinsToAdd := sdk.NewCoins(
+ sdk.NewCoin("bnb", sdk.NewInt(int64(simState.Rand.Intn(100000000000)))),
+ sdk.NewCoin("xrp", sdk.NewInt(int64(simState.Rand.Intn(100000000000)))),
+ sdk.NewCoin("btc", sdk.NewInt(int64(simState.Rand.Intn(500000000)))),
+ sdk.NewCoin("usdx", sdk.NewInt(int64(simState.Rand.Intn(1000000000)))),
+ sdk.NewCoin("ukava", sdk.NewInt(int64(simState.Rand.Intn(500000000000)))),
+ )
+ err := acc.SetCoins(acc.GetCoins().Add(coinsToAdd...))
+ if err != nil {
+ panic(err)
+ }
+ totalCdpCoins = totalCdpCoins.Add(coinsToAdd...)
+ authGenesis.Accounts = replaceOrAppendAccount(authGenesis.Accounts, acc)
+ }
+ simState.GenState[auth.ModuleName] = simState.Cdc.MustMarshalJSON(authGenesis)
+
+ var supplyGenesis supply.GenesisState
+ simState.Cdc.MustUnmarshalJSON(simState.GenState[supply.ModuleName], &supplyGenesis)
+ supplyGenesis.Supply = supplyGenesis.Supply.Add(totalCdpCoins...)
+ simState.GenState[supply.ModuleName] = simState.Cdc.MustMarshalJSON(supplyGenesis)
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, cdpGenesis))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(cdpGenesis)
}
+
+// In a list of accounts, replace the first account found with the same address. If not found, append the account.
+func replaceOrAppendAccount(accounts []authexported.GenesisAccount, acc authexported.GenesisAccount) []authexported.GenesisAccount {
+ newAccounts := accounts
+ for i, a := range accounts {
+ if a.GetAddress().Equals(acc.GetAddress()) {
+ newAccounts[i] = acc
+ return newAccounts
+ }
+ }
+ return append(newAccounts, acc)
+}
+
+func randomCdpGenState(selection int) types.GenesisState {
+ switch selection {
+ case 0:
+ return types.GenesisState{
+ Params: types.Params{
+ GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 100000000000000)),
+ SurplusAuctionThreshold: types.DefaultSurplusThreshold,
+ DebtAuctionThreshold: types.DefaultDebtThreshold,
+ SavingsDistributionFrequency: types.DefaultSavingsDistributionFrequency,
+ CollateralParams: types.CollateralParams{
+ {
+ Denom: "xrp",
+ LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
+ DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 20000000000000)),
+ StabilityFee: sdk.MustNewDecFromStr("1.000000004431822130"),
+ LiquidationPenalty: sdk.MustNewDecFromStr("0.075"),
+ AuctionSize: sdk.NewInt(100000000000),
+ Prefix: 0x20,
+ MarketID: "xrp:usd",
+ ConversionFactor: sdk.NewInt(6),
+ },
+ {
+ Denom: "btc",
+ LiquidationRatio: sdk.MustNewDecFromStr("1.25"),
+ DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000)),
+ StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"),
+ LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
+ AuctionSize: sdk.NewInt(1000000000),
+ Prefix: 0x21,
+ MarketID: "btc:usd",
+ ConversionFactor: sdk.NewInt(8),
+ },
+ {
+ Denom: "bnb",
+ LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
+ DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 30000000000000)),
+ StabilityFee: sdk.MustNewDecFromStr("1.000000002293273137"),
+ LiquidationPenalty: sdk.MustNewDecFromStr("0.15"),
+ AuctionSize: sdk.NewInt(1000000000000),
+ Prefix: 0x22,
+ MarketID: "bnb:usd",
+ ConversionFactor: sdk.NewInt(8),
+ },
+ },
+ DebtParams: types.DebtParams{
+ {
+ Denom: "usdx",
+ ReferenceAsset: "usd",
+ ConversionFactor: sdk.NewInt(6),
+ DebtFloor: sdk.NewInt(10000000),
+ SavingsRate: sdk.MustNewDecFromStr("0.95"),
+ },
+ },
+ },
+ StartingCdpID: types.DefaultCdpStartingID,
+ DebtDenom: types.DefaultDebtDenom,
+ GovDenom: types.DefaultGovDenom,
+ CDPs: types.CDPs{},
+ PreviousDistributionTime: types.DefaultPreviousDistributionTime,
+ }
+ case 1:
+ return types.GenesisState{
+ Params: types.Params{
+ GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 100000000000000)),
+ SurplusAuctionThreshold: types.DefaultSurplusThreshold,
+ DebtAuctionThreshold: types.DefaultDebtThreshold,
+ SavingsDistributionFrequency: types.DefaultSavingsDistributionFrequency,
+ CollateralParams: types.CollateralParams{
+ {
+ Denom: "bnb",
+ LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
+ DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 100000000000000)),
+ StabilityFee: sdk.MustNewDecFromStr("1.000000002293273137"),
+ LiquidationPenalty: sdk.MustNewDecFromStr("0.075"),
+ AuctionSize: sdk.NewInt(10000000000),
+ Prefix: 0x20,
+ MarketID: "bnb:usd",
+ ConversionFactor: sdk.NewInt(8),
+ },
+ },
+ DebtParams: types.DebtParams{
+ {
+ Denom: "usdx",
+ ReferenceAsset: "usd",
+ ConversionFactor: sdk.NewInt(6),
+ DebtFloor: sdk.NewInt(10000000),
+ SavingsRate: sdk.MustNewDecFromStr("0.95"),
+ },
+ },
+ },
+ StartingCdpID: types.DefaultCdpStartingID,
+ DebtDenom: types.DefaultDebtDenom,
+ GovDenom: types.DefaultGovDenom,
+ CDPs: types.CDPs{},
+ PreviousDistributionTime: types.DefaultPreviousDistributionTime,
+ }
+ default:
+ panic("invalid genesis state selector")
+ }
+}
diff --git a/x/cdp/simulation/operations.go b/x/cdp/simulation/operations.go
new file mode 100644
index 00000000..014b7fa1
--- /dev/null
+++ b/x/cdp/simulation/operations.go
@@ -0,0 +1,289 @@
+package simulation
+
+import (
+ "math/rand"
+
+ "github.com/cosmos/cosmos-sdk/baseapp"
+ "github.com/cosmos/cosmos-sdk/codec"
+ "github.com/cosmos/cosmos-sdk/simapp/helpers"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+
+ appparams "github.com/kava-labs/kava/app/params"
+ "github.com/kava-labs/kava/x/cdp/keeper"
+ "github.com/kava-labs/kava/x/cdp/types"
+)
+
+// Simulation operation weights constants
+const (
+ OpWeightMsgCdp = "op_weight_msg_cdp"
+)
+
+// WeightedOperations returns all the operations from the module with their respective weights
+func WeightedOperations(
+ appParams simulation.AppParams, cdc *codec.Codec, ak auth.AccountKeeper,
+ k keeper.Keeper, pfk types.PricefeedKeeper,
+) simulation.WeightedOperations {
+ var weightMsgCdp int
+
+ appParams.GetOrGenerate(cdc, OpWeightMsgCdp, &weightMsgCdp, nil,
+ func(_ *rand.Rand) {
+ weightMsgCdp = appparams.DefaultWeightMsgCdp
+ },
+ )
+
+ return simulation.WeightedOperations{
+ simulation.NewWeightedOperation(
+ weightMsgCdp,
+ SimulateMsgCdp(ak, k, pfk),
+ ),
+ }
+}
+
+// SimulateMsgCdp generates a MsgCreateCdp or MsgDepositCdp with random values.
+func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedKeeper) simulation.Operation {
+ return func(
+ r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
+ ) (simulation.OperationMsg, []simulation.FutureOperation, error) {
+
+ simAccount, _ := simulation.RandomAcc(r, accs)
+ acc := ak.GetAccount(ctx, simAccount.Address)
+ if acc == nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, nil
+ }
+
+ coins := acc.GetCoins()
+ collateralParams := k.GetParams(ctx).CollateralParams
+ if len(collateralParams) == 0 {
+ return simulation.NoOpMsg(types.ModuleName), nil, nil
+ }
+
+ randCollateralParam := collateralParams[r.Intn(len(collateralParams))]
+ randDebtAsset := randCollateralParam.DebtLimit[r.Intn(len(randCollateralParam.DebtLimit))]
+ randDebtParam, _ := k.GetDebtParam(ctx, randDebtAsset.Denom)
+ if coins.AmountOf(randCollateralParam.Denom).IsZero() {
+ return simulation.NoOpMsg(types.ModuleName), nil, nil
+ }
+
+ price, err := pfk.GetCurrentPrice(ctx, randCollateralParam.MarketID)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+ // convert the price to the same units as the debt param
+ priceShifted := ShiftDec(price.Price, randDebtParam.ConversionFactor)
+
+ spendableCoins := acc.SpendableCoins(ctx.BlockTime())
+ fees, err := simulation.RandomFees(r, ctx, spendableCoins)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+ spendableCoins = spendableCoins.Sub(fees)
+
+ existingCDP, found := k.GetCdpByOwnerAndDenom(ctx, acc.GetAddress(), randCollateralParam.Denom)
+ if !found {
+ // calculate the minimum amount of collateral that is needed to create a cdp with the debt floor amount of debt and the minimum liquidation ratio
+ // (debtFloor * liquidationRatio)/priceShifted
+ minCollateralDeposit := (sdk.NewDecFromInt(randDebtParam.DebtFloor).Mul(randCollateralParam.LiquidationRatio)).Quo(priceShifted)
+ // convert to proper collateral units
+ minCollateralDeposit = ShiftDec(minCollateralDeposit, randCollateralParam.ConversionFactor)
+ // convert to integer and always round up
+ minCollateralDepositRounded := minCollateralDeposit.TruncateInt().Add(sdk.OneInt())
+ if spendableCoins.AmountOf(randCollateralParam.Denom).LT(minCollateralDepositRounded) {
+ // account doesn't have enough funds to open a cdp for the min debt amount
+ return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "insufficient funds to open cdp", false, nil), nil, nil
+ }
+ // set the max collateral deposit to the amount of coins in the account
+ maxCollateralDeposit := spendableCoins.AmountOf(randCollateralParam.Denom)
+
+ // randomly select a collateral deposit amount
+ collateralDeposit := sdk.NewInt(int64(simulation.RandIntBetween(r, int(minCollateralDepositRounded.Int64()), int(maxCollateralDeposit.Int64()))))
+ // calculate how much the randomly selected deposit is worth
+ collateralDepositValue := ShiftDec(sdk.NewDecFromInt(collateralDeposit), randCollateralParam.ConversionFactor.Neg()).Mul(priceShifted)
+ // calculate the max amount of debt that could be drawn for the chosen deposit
+ maxDebtDraw := collateralDepositValue.Quo(randCollateralParam.LiquidationRatio).TruncateInt()
+ // check that the debt limit hasn't been reached
+ availableAssetDebt := randCollateralParam.DebtLimit.AmountOf(randDebtParam.Denom).Sub(k.GetTotalPrincipal(ctx, randCollateralParam.Denom, randDebtParam.Denom))
+ if availableAssetDebt.LTE(randDebtParam.DebtFloor) {
+ // debt limit has been reached
+ return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "debt limit reached, cannot open cdp", false, nil), nil, nil
+ }
+ // ensure that the debt draw does not exceed the debt limit
+ maxDebtDraw = sdk.MinInt(maxDebtDraw, availableAssetDebt)
+ // randomly select a debt draw amount
+ debtDraw := sdk.NewInt(int64(simulation.RandIntBetween(r, int(randDebtParam.DebtFloor.Int64()), int(maxDebtDraw.Int64()))))
+
+ msg := types.NewMsgCreateCDP(acc.GetAddress(), sdk.NewCoins(sdk.NewCoin(randCollateralParam.Denom, collateralDeposit)), sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, debtDraw)))
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ fees,
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{acc.GetAccountNumber()},
+ []uint64{acc.GetSequence()},
+ simAccount.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
+ }
+
+ // a cdp already exists, deposit to it, draw debt from it, or repay debt to it
+ // close 25% of the time
+ if canClose(acc, existingCDP, randDebtParam.Denom) && shouldClose(r) {
+ repaymentAmount := spendableCoins.AmountOf(randDebtParam.Denom)
+ msg := types.NewMsgRepayDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, repaymentAmount)))
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ fees,
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{acc.GetAccountNumber()},
+ []uint64{acc.GetSequence()},
+ simAccount.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
+ }
+
+ // deposit 25% of the time
+ if hasCoins(acc, randCollateralParam.Denom) && shouldDeposit(r) {
+ randDepositAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(spendableCoins.AmountOf(randCollateralParam.Denom).Int64()))))
+ msg := types.NewMsgDeposit(acc.GetAddress(), acc.GetAddress(), sdk.NewCoins(sdk.NewCoin(randCollateralParam.Denom, randDepositAmount)))
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ fees,
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{acc.GetAccountNumber()},
+ []uint64{acc.GetSequence()},
+ simAccount.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
+ }
+
+ // draw debt 25% of the time
+ if shouldDraw(r) {
+ collateralShifted := ShiftDec(sdk.NewDecFromInt(existingCDP.Collateral.AmountOf(randCollateralParam.Denom)), randCollateralParam.ConversionFactor.Neg())
+ collateralValue := collateralShifted.Mul(priceShifted)
+ newFeesAccumulated := k.CalculateFees(ctx, existingCDP.Principal, sdk.NewInt(ctx.BlockTime().Unix()-existingCDP.FeesUpdated.Unix()), randCollateralParam.Denom).AmountOf(randDebtParam.Denom)
+ totalFees := existingCDP.AccumulatedFees.AmountOf(randCollateralParam.Denom).Add(newFeesAccumulated)
+ // given the current collateral value, calculate how much debt we could add while maintaining a valid liquidation ratio
+ debt := existingCDP.Principal.AmountOf(randDebtParam.Denom).Add(totalFees)
+ maxTotalDebt := collateralValue.Quo(randCollateralParam.LiquidationRatio)
+ maxDebt := (maxTotalDebt.Sub(sdk.NewDecFromInt(debt))).Mul(sdk.MustNewDecFromStr("0.95")).TruncateInt()
+ if maxDebt.LTE(sdk.OneInt()) {
+ // debt in cdp is maxed out
+ return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "cdp debt maxed out, cannot draw more debt", false, nil), nil, nil
+ }
+ // check if the debt limit has been reached
+ availableAssetDebt := randCollateralParam.DebtLimit.AmountOf(randDebtParam.Denom).Sub(k.GetTotalPrincipal(ctx, randCollateralParam.Denom, randDebtParam.Denom))
+ if availableAssetDebt.LTE(sdk.OneInt()) {
+ // debt limit has been reached
+ return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "debt limit reached, cannot draw more debt", false, nil), nil, nil
+ }
+ maxDraw := sdk.MinInt(maxDebt, availableAssetDebt)
+
+ randDrawAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(maxDraw.Int64()))))
+ msg := types.NewMsgDrawDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, randDrawAmount)))
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ fees,
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{acc.GetAccountNumber()},
+ []uint64{acc.GetSequence()},
+ simAccount.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
+ }
+
+ // repay debt 25% of the time
+ if hasCoins(acc, randDebtParam.Denom) {
+ debt := existingCDP.Principal.AmountOf(randDebtParam.Denom)
+ maxRepay := spendableCoins.AmountOf(randDebtParam.Denom)
+ payableDebt := debt.Sub(randDebtParam.DebtFloor)
+ if maxRepay.GT(payableDebt) {
+ maxRepay = payableDebt
+ }
+ randRepayAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(maxRepay.Int64()))))
+ if debt.Equal(randDebtParam.DebtFloor) && spendableCoins.AmountOf(randDebtParam.Denom).GTE(debt) {
+ randRepayAmount = debt
+ }
+
+ msg := types.NewMsgRepayDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, randRepayAmount)))
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ fees,
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{acc.GetAccountNumber()},
+ []uint64{acc.GetSequence()},
+ simAccount.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
+ }
+
+ return simulation.NoOpMsg(types.ModuleName), nil, nil
+ }
+}
+
+func shouldDraw(r *rand.Rand) bool {
+ threshold := 50
+ value := simulation.RandIntBetween(r, 1, 100)
+ return value > threshold
+}
+
+func shouldDeposit(r *rand.Rand) bool {
+ threshold := 66
+ value := simulation.RandIntBetween(r, 1, 100)
+ return value > threshold
+}
+
+func hasCoins(acc authexported.Account, denom string) bool {
+ return acc.GetCoins().AmountOf(denom).IsPositive()
+}
+
+func shouldClose(r *rand.Rand) bool {
+ threshold := 75
+ value := simulation.RandIntBetween(r, 1, 100)
+ return value > threshold
+}
+
+func canClose(acc authexported.Account, c types.CDP, denom string) bool {
+ repaymentAmount := c.Principal.Add(c.AccumulatedFees...).AmountOf(denom)
+ return acc.GetCoins().AmountOf(denom).GTE(repaymentAmount)
+}
diff --git a/x/cdp/simulation/params.go b/x/cdp/simulation/params.go
index 8c1f7aff..36ff612a 100644
--- a/x/cdp/simulation/params.go
+++ b/x/cdp/simulation/params.go
@@ -9,6 +9,5 @@ import (
// ParamChanges defines the parameters that can be modified by param change proposals
// on the simulation
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
- // TODO implement this
return []simulation.ParamChange{}
}
diff --git a/x/cdp/simulation/utils.go b/x/cdp/simulation/utils.go
new file mode 100644
index 00000000..f9daac89
--- /dev/null
+++ b/x/cdp/simulation/utils.go
@@ -0,0 +1,26 @@
+package simulation
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+func ShiftDec(x sdk.Dec, places sdk.Int) sdk.Dec {
+ neg := places.IsNegative()
+ for i := 0; i < int(abs(places.Int64())); i++ {
+ if neg {
+ x = x.Mul(sdk.MustNewDecFromStr("0.1"))
+ } else {
+ x = x.Mul(sdk.NewDecFromInt(sdk.NewInt(10)))
+ }
+
+ }
+ return x
+}
+
+// abs returns the absolute value of x.
+func abs(x int64) int64 {
+ if x < 0 {
+ return -x
+ }
+ return x
+}
diff --git a/x/cdp/simulation/utils_test.go b/x/cdp/simulation/utils_test.go
new file mode 100644
index 00000000..448366fa
--- /dev/null
+++ b/x/cdp/simulation/utils_test.go
@@ -0,0 +1,28 @@
+package simulation_test
+
+import (
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/x/cdp/simulation"
+ "github.com/stretchr/testify/require"
+)
+
+func TestShiftDec(t *testing.T) {
+ tests := []struct {
+ value sdk.Dec
+ shift sdk.Int
+ expected sdk.Dec
+ }{
+ {sdk.MustNewDecFromStr("5.5"), sdk.NewInt(1), sdk.MustNewDecFromStr("55")},
+ {sdk.MustNewDecFromStr("5.5"), sdk.NewInt(-1), sdk.MustNewDecFromStr("0.55")},
+ {sdk.MustNewDecFromStr("5.5"), sdk.NewInt(2), sdk.MustNewDecFromStr("550")},
+ {sdk.MustNewDecFromStr("5.5"), sdk.NewInt(-2), sdk.MustNewDecFromStr("0.055")},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.value.String(), func(t *testing.T) {
+ require.Equal(t, tt.expected, simulation.ShiftDec(tt.value, tt.shift))
+ })
+ }
+}
diff --git a/x/cdp/spec/04_begin_block.md b/x/cdp/spec/04_begin_block.md
index e8257c0c..77cd41e4 100644
--- a/x/cdp/spec/04_begin_block.md
+++ b/x/cdp/spec/04_begin_block.md
@@ -3,6 +3,7 @@
At the start of every block the BeginBlocker of the cdp module:
- updates total CDP fees
+- update fees for individual "risky" CDPs
- liquidates CDPs under the collateral ratio
- nets out system debt and, if necessary, starts auctions to re-balance it
- records the last block time
@@ -13,6 +14,13 @@ At the start of every block the BeginBlocker of the cdp module:
- An equal amount of debt coins are minted and sent to the system's CDP module account.
- An equal amount of stable asset coins are minted and sent to the system's liquidator module account
+## Update risky cdps
+
+- UpdateFeesForRiskyCdps calculates fees for risky CDPs
+- Select the CDPs with 10% of the liquidation ratio - the risky CDPs
+- Calculate additional accumulated fees on each of those CDPs
+- Update the fees updated time for the CDP to the current block time
+
## Liquidate CDP
- Get every cdp that is under the liquidation ratio for its collateral type.
@@ -26,9 +34,15 @@ At the start of every block the BeginBlocker of the cdp module:
- Burn the maximum possible equal amount of debt and stable asset from the liquidator module account.
- If there is enough debt remaining for an auction, start one.
-- If there is enough surplus stable asset remaining for an auction, start one.
+- If there is enough surplus stable asset, minus surplus reserved for the savings rate, remaining for an auction, start one.
- Otherwise do nothing, leave debt/surplus to accumulate over subsequent blocks.
+## Distribute Surplus Stable Asset According to the Savings Rate
+
+- If `SavingsDistributionFrequency` seconds have elapsed since the previous distribution, the savings rate is applied to all accounts that hold stable asset.
+- Each account that holds stable asset is distributed a ratable portion of the surplus that is apportioned to the savings rate.
+- If distribution occurred, the time of the distribution is recorded.
+
## Update Previous Block Time
The current block time is recorded.
diff --git a/x/cdp/spec/06_params.md b/x/cdp/spec/06_params.md
index fbde95d7..4297619c 100644
--- a/x/cdp/spec/06_params.md
+++ b/x/cdp/spec/06_params.md
@@ -2,12 +2,13 @@
The cdp module contains the following parameters:
-| Key | Type | Example | Description |
-|------------------|-------------------------|------------------------------------|------------------------------------------------------------------|
-| CollateralParams | array (CollateralParam) | [{see below}] | array of params for each enabled collateral type |
-| DebtParams | array (DebtParam) | [{see below}] | array of params for each enabled pegged asset |
-| GlobalDebtLimit | array (coin) | [{"denom":"usdx","amount":"1000"}] | maximum pegged assets that can be minted across the whole system |
-| CircuitBreaker | bool | false | flag to disable user interactions with the system |
+| Key | Type | Example | Description |
+|------------------ |-------------------------|------------------------------------|------------------------------------------------------------------|
+| CollateralParams | array (CollateralParam) | [{see below}] | array of params for each enabled collateral type |
+| DebtParams | array (DebtParam) | [{see below}] | array of params for each enabled pegged asset |
+| GlobalDebtLimit | array (coin) | [{"denom":"usdx","amount":"1000"}] | maximum pegged assets that can be minted across the whole system |
+| SavingsDistributionFrequency | string (int) | "84600" | number of seconds between distribution of the savings rate|
+| CircuitBreaker | bool | false | flag to disable user interactions with the system |
Each CollateralParam has the following parameters:
diff --git a/x/cdp/types/cdp.go b/x/cdp/types/cdp.go
index 8c5365be..da06c836 100644
--- a/x/cdp/types/cdp.go
+++ b/x/cdp/types/cdp.go
@@ -39,7 +39,7 @@ func (cdp CDP) String() string {
Collateral Type: %s
Collateral: %s
Principal: %s
- Fees: %s
+ AccumulatedFees: %s
Fees Last Updated: %s`,
cdp.Owner,
cdp.ID,
diff --git a/x/cdp/types/errors.go b/x/cdp/types/errors.go
index 95dc05b9..92953230 100644
--- a/x/cdp/types/errors.go
+++ b/x/cdp/types/errors.go
@@ -1,115 +1,42 @@
-// DONTCOVER
package types
import (
- "fmt"
-
- sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
-// Error codes specific to cdp module
-const (
- DefaultCodespace sdk.CodespaceType = ModuleName
- CodeCdpAlreadyExists sdk.CodeType = 1
- CodeCollateralLengthInvalid sdk.CodeType = 2
- CodeCollateralNotSupported sdk.CodeType = 3
- CodeDebtNotSupported sdk.CodeType = 4
- CodeExceedsDebtLimit sdk.CodeType = 5
- CodeInvalidCollateralRatio sdk.CodeType = 6
- CodeCdpNotFound sdk.CodeType = 7
- CodeDepositNotFound sdk.CodeType = 8
- CodeInvalidDepositDenom sdk.CodeType = 9
- CodeInvalidPaymentDenom sdk.CodeType = 10
- CodeDepositNotAvailable sdk.CodeType = 11
- CodeInvalidCollateralDenom sdk.CodeType = 12
- CodeInvalidWithdrawAmount sdk.CodeType = 13
- CodeCdpNotAvailable sdk.CodeType = 14
- CodeBelowDebtFloor sdk.CodeType = 15
- CodePaymentExceedsDebt sdk.CodeType = 16
- CodeLoadingAugmentedCDP sdk.CodeType = 17
+// DONTCOVER
+
+var (
+ // ErrCdpAlreadyExists error for duplicate cdps
+ ErrCdpAlreadyExists = sdkerrors.Register(ModuleName, 2, "cdp already exists")
+ // ErrInvalidCollateralLength error for invalid collateral input length
+ ErrInvalidCollateralLength = sdkerrors.Register(ModuleName, 3, "only one collateral type per cdp")
+ // ErrCollateralNotSupported error for unsupported collateral
+ ErrCollateralNotSupported = sdkerrors.Register(ModuleName, 4, "collateral not supported")
+ // ErrDebtNotSupported error for unsupported debt
+ ErrDebtNotSupported = sdkerrors.Register(ModuleName, 5, "debt not supported")
+ // ErrExceedsDebtLimit error for attempted draws that exceed debt limit
+ ErrExceedsDebtLimit = sdkerrors.Register(ModuleName, 6, "proposed debt increase would exceed debt limit")
+ // ErrInvalidCollateralRatio error for attempted draws that are below liquidation ratio
+ ErrInvalidCollateralRatio = sdkerrors.Register(ModuleName, 7, "proposed collateral ratio is below liquidation ratio")
+ // ErrCdpNotFound error cdp not found
+ ErrCdpNotFound = sdkerrors.Register(ModuleName, 8, "cdp not found")
+ // ErrDepositNotFound error for deposit not found
+ ErrDepositNotFound = sdkerrors.Register(ModuleName, 9, "deposit not found")
+ // ErrInvalidDeposit error for invalid deposit
+ ErrInvalidDeposit = sdkerrors.Register(ModuleName, 10, "invalid deposit")
+ // ErrInvalidCollateral error for invalid collateral
+ ErrInvalidCollateral = sdkerrors.Register(ModuleName, 11, "collateral not supported")
+ // ErrInvalidPayment error for invalid payment
+ ErrInvalidPayment = sdkerrors.Register(ModuleName, 12, "invalid payment")
+ //ErrDepositNotAvailable error for withdrawing deposits in liquidation
+ ErrDepositNotAvailable = sdkerrors.Register(ModuleName, 13, "deposit in liquidation")
+ // ErrInvalidWithdrawAmount error for invalid withdrawal amount
+ ErrInvalidWithdrawAmount = sdkerrors.Register(ModuleName, 14, "withdrawal amount exceeds deposit")
+ //ErrCdpNotAvailable error for depositing to a CDP in liquidation
+ ErrCdpNotAvailable = sdkerrors.Register(ModuleName, 15, "cannot modify cdp in liquidation")
+ // ErrBelowDebtFloor error for creating a cdp with debt below the minimum
+ ErrBelowDebtFloor = sdkerrors.Register(ModuleName, 16, "proposed cdp debt is below minimum")
+ // ErrLoadingAugmentedCDP error loading augmented cdp
+ ErrLoadingAugmentedCDP = sdkerrors.Register(ModuleName, 17, "augmented cdp could not be loaded from cdp")
)
-
-// ErrCdpAlreadyExists error for duplicate cdps
-func ErrCdpAlreadyExists(codespace sdk.CodespaceType, owner sdk.AccAddress, denom string) sdk.Error {
- return sdk.NewError(codespace, CodeCdpAlreadyExists, fmt.Sprintf("cdp for owner %s and collateral %s already exists", owner, denom))
-}
-
-// ErrInvalidCollateralLength error for invalid collateral input length
-func ErrInvalidCollateralLength(codespace sdk.CodespaceType, length int) sdk.Error {
- return sdk.NewError(codespace, CodeCollateralLengthInvalid, fmt.Sprintf("only one collateral type per cdp, has %d", length))
-}
-
-// ErrCollateralNotSupported error for unsupported collateral
-func ErrCollateralNotSupported(codespace sdk.CodespaceType, denom string) sdk.Error {
- return sdk.NewError(codespace, CodeCollateralNotSupported, fmt.Sprintf("collateral %s not supported", denom))
-}
-
-// ErrDebtNotSupported error for unsupported debt
-func ErrDebtNotSupported(codespace sdk.CodespaceType, denom string) sdk.Error {
- return sdk.NewError(codespace, CodeDebtNotSupported, fmt.Sprintf("collateral %s not supported", denom))
-}
-
-// ErrExceedsDebtLimit error for attempted draws that exceed debt limit
-func ErrExceedsDebtLimit(codespace sdk.CodespaceType, proposed sdk.Coins, limit sdk.Coins) sdk.Error {
- return sdk.NewError(codespace, CodeExceedsDebtLimit, fmt.Sprintf("proposed debt increase %s would exceed debt limit of %s", proposed, limit))
-}
-
-// ErrInvalidCollateralRatio error for attempted draws that are below liquidation ratio
-func ErrInvalidCollateralRatio(codespace sdk.CodespaceType, denom string, collateralRatio sdk.Dec, liquidationRatio sdk.Dec) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidCollateralRatio, fmt.Sprintf("proposed collateral ratio of %s is below liqudation ratio of %s for collateral %s", collateralRatio, liquidationRatio, denom))
-}
-
-// ErrCdpNotFound error cdp not found
-func ErrCdpNotFound(codespace sdk.CodespaceType, owner sdk.AccAddress, denom string) sdk.Error {
- return sdk.NewError(codespace, CodeCdpNotFound, fmt.Sprintf("cdp for owner %s and collateral %s not found", owner, denom))
-}
-
-// ErrDepositNotFound error for deposit not found
-func ErrDepositNotFound(codespace sdk.CodespaceType, depositor sdk.AccAddress, cdpID uint64) sdk.Error {
- return sdk.NewError(codespace, CodeDepositNotFound, fmt.Sprintf("deposit for cdp %d not found for %s", cdpID, depositor))
-}
-
-// ErrInvalidDepositDenom error for invalid deposit denoms
-func ErrInvalidDepositDenom(codespace sdk.CodespaceType, cdpID uint64, expected string, actual string) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidDepositDenom, fmt.Sprintf("invalid deposit for cdp %d, expects %s, got %s", cdpID, expected, actual))
-}
-
-// ErrInvalidPaymentDenom error for invalid payment denoms
-func ErrInvalidPaymentDenom(codespace sdk.CodespaceType, cdpID uint64, expected []string, actual []string) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidPaymentDenom, fmt.Sprintf("invalid payment for cdp %d, expects %s, got %s", cdpID, expected, actual))
-}
-
-//ErrDepositNotAvailable error for withdrawing deposits in liquidation
-func ErrDepositNotAvailable(codespace sdk.CodespaceType, cdpID uint64, depositor sdk.AccAddress) sdk.Error {
- return sdk.NewError(codespace, CodeDepositNotAvailable, fmt.Sprintf("deposit from %s for cdp %d in liquidation", depositor, cdpID))
-}
-
-// ErrInvalidCollateralDenom error for invalid collateral denoms
-func ErrInvalidCollateralDenom(codespace sdk.CodespaceType, denom string) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidDepositDenom, fmt.Sprintf("invalid denom: %s", denom))
-}
-
-// ErrInvalidWithdrawAmount error for invalid withdrawal amount
-func ErrInvalidWithdrawAmount(codespace sdk.CodespaceType, withdraw sdk.Coins, deposit sdk.Coins) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidWithdrawAmount, fmt.Sprintf("withdrawal amount of %s exceeds deposit of %s", withdraw, deposit))
-}
-
-//ErrCdpNotAvailable error for depositing to a CDP in liquidation
-func ErrCdpNotAvailable(codespace sdk.CodespaceType, cdpID uint64) sdk.Error {
- return sdk.NewError(codespace, CodeCdpNotAvailable, fmt.Sprintf("cannot modify cdp %d, in liquidation", cdpID))
-}
-
-// ErrBelowDebtFloor error for creating a cdp with debt below the minimum
-func ErrBelowDebtFloor(codespace sdk.CodespaceType, debt sdk.Coins, floor sdk.Int) sdk.Error {
- return sdk.NewError(codespace, CodeBelowDebtFloor, fmt.Sprintf("proposed cdp debt of %s is below the minimum of %s", debt, floor))
-}
-
-// ErrPaymentExceedsDebt error for repayments that are greater than the debt amount
-func ErrPaymentExceedsDebt(codespace sdk.CodespaceType, payment sdk.Coins, principal sdk.Coins) sdk.Error {
- return sdk.NewError(codespace, CodePaymentExceedsDebt, fmt.Sprintf("payment of %s exceeds debt of %s", payment, principal))
-}
-
-// ErrLoadingAugmentedCDP error loading augmented cdp
-func ErrLoadingAugmentedCDP(codespace sdk.CodespaceType, cdpID uint64) sdk.Error {
- return sdk.NewError(codespace, CodeCdpNotFound, fmt.Sprintf("augmented cdp could not be loaded from cdp id %d", cdpID))
-}
diff --git a/x/cdp/types/expected_keepers.go b/x/cdp/types/expected_keepers.go
index 402458dc..ae5bb5bd 100644
--- a/x/cdp/types/expected_keepers.go
+++ b/x/cdp/types/expected_keepers.go
@@ -4,6 +4,7 @@ import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
pftypes "github.com/kava-labs/kava/x/pricefeed/types"
)
@@ -16,26 +17,32 @@ type SupplyKeeper interface {
// TODO remove with genesis 2-phases refactor https://github.com/cosmos/cosmos-sdk/issues/2862
SetModuleAccount(sdk.Context, supplyexported.ModuleAccountI)
- SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error
- SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error
- SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) sdk.Error
- BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
- MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
+ SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
+ SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
+ SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error
+ BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error
+ MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error
+ GetSupply(ctx sdk.Context) (supply supplyexported.SupplyI)
}
// PricefeedKeeper defines the expected interface for the pricefeed
type PricefeedKeeper interface {
- GetCurrentPrice(sdk.Context, string) (pftypes.CurrentPrice, sdk.Error)
+ GetCurrentPrice(sdk.Context, string) (pftypes.CurrentPrice, error)
GetParams(sdk.Context) pftypes.Params
// These are used for testing TODO replace mockApp with keeper in tests to remove these
SetParams(sdk.Context, pftypes.Params)
- SetPrice(sdk.Context, sdk.AccAddress, string, sdk.Dec, time.Time) (pftypes.PostedPrice, sdk.Error)
- SetCurrentPrices(sdk.Context, string) sdk.Error
+ SetPrice(sdk.Context, sdk.AccAddress, string, sdk.Dec, time.Time) (pftypes.PostedPrice, error)
+ SetCurrentPrices(sdk.Context, string) error
}
// AuctionKeeper expected interface for the auction keeper (noalias)
type AuctionKeeper interface {
- StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error)
- StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, sdk.Error)
- StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin) (uint64, sdk.Error)
+ StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, error)
+ StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, error)
+ StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin) (uint64, error)
+}
+
+// AccountKeeper expected interface for the account keeper (noalias)
+type AccountKeeper interface {
+ IterateAccounts(ctx sdk.Context, cb func(account authexported.Account) (stop bool))
}
diff --git a/x/cdp/types/genesis.go b/x/cdp/types/genesis.go
index 4097e6d4..a1e73a3f 100644
--- a/x/cdp/types/genesis.go
+++ b/x/cdp/types/genesis.go
@@ -8,39 +8,39 @@ import (
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
- Params Params `json:"params" yaml:"params"`
- CDPs CDPs `json:"cdps" yaml:"cdps"`
- Deposits Deposits `json:"deposits" yaml:"deposits"`
- StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"`
- DebtDenom string `json:"debt_denom" yaml:"debt_denom"`
- GovDenom string `json:"gov_denom" yaml:"gov_denom"`
- PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
+ Params Params `json:"params" yaml:"params"`
+ CDPs CDPs `json:"cdps" yaml:"cdps"`
+ Deposits Deposits `json:"deposits" yaml:"deposits"`
+ StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"`
+ DebtDenom string `json:"debt_denom" yaml:"debt_denom"`
+ GovDenom string `json:"gov_denom" yaml:"gov_denom"`
+ PreviousDistributionTime time.Time `json:"previous_distribution_time" yaml:"previous_distribution_time"`
}
// NewGenesisState returns a new genesis state
-func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID uint64, debtDenom, govDenom string, previousBlockTime time.Time) GenesisState {
+func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID uint64, debtDenom, govDenom string, previousDistTime time.Time) GenesisState {
return GenesisState{
- Params: params,
- CDPs: cdps,
- Deposits: deposits,
- StartingCdpID: startingCdpID,
- DebtDenom: debtDenom,
- GovDenom: govDenom,
- PreviousBlockTime: previousBlockTime,
+ Params: params,
+ CDPs: cdps,
+ Deposits: deposits,
+ StartingCdpID: startingCdpID,
+ DebtDenom: debtDenom,
+ GovDenom: govDenom,
+ PreviousDistributionTime: previousDistTime,
}
}
// DefaultGenesisState returns a default genesis state
func DefaultGenesisState() GenesisState {
- return GenesisState{
- Params: DefaultParams(),
- CDPs: CDPs{},
- Deposits: Deposits{},
- StartingCdpID: DefaultCdpStartingID,
- DebtDenom: DefaultDebtDenom,
- GovDenom: DefaultGovDenom,
- PreviousBlockTime: DefaultPreviousBlockTime,
- }
+ return NewGenesisState(
+ DefaultParams(),
+ CDPs{},
+ Deposits{},
+ DefaultCdpStartingID,
+ DefaultDebtDenom,
+ DefaultGovDenom,
+ DefaultPreviousDistributionTime,
+ )
}
// Validate performs basic validation of genesis data returning an
@@ -51,8 +51,8 @@ func (gs GenesisState) Validate() error {
return err
}
- if gs.PreviousBlockTime.Equal(time.Time{}) {
- return fmt.Errorf("previous block time not set")
+ if gs.PreviousDistributionTime.Equal(time.Time{}) {
+ return fmt.Errorf("previous distribution time not set")
}
if gs.DebtDenom == "" {
diff --git a/x/cdp/types/keys.go b/x/cdp/types/keys.go
index 6f34524a..8f4a8097 100644
--- a/x/cdp/types/keys.go
+++ b/x/cdp/types/keys.go
@@ -25,6 +25,9 @@ const (
// LiquidatorMacc module account for liquidator
LiquidatorMacc = "liquidator"
+
+ // SavingsRateMacc module account for savings rate
+ SavingsRateMacc = "savings"
)
var sep = []byte(":")
@@ -42,19 +45,19 @@ var sep = []byte(":")
// - 0x05::: Deposit
// - 0x06:totalPrincipal
// - 0x07:feeRate
-// - 0x08:previousBlockTime
+// - 0x08:previousDistributionTime
// KVStore key prefixes
var (
- CdpIDKeyPrefix = []byte{0x00}
- CdpKeyPrefix = []byte{0x01}
- CollateralRatioIndexPrefix = []byte{0x02}
- CdpIDKey = []byte{0x03}
- DebtDenomKey = []byte{0x04}
- GovDenomKey = []byte{0x05}
- DepositKeyPrefix = []byte{0x06}
- PrincipalKeyPrefix = []byte{0x07}
- PreviousBlockTimeKey = []byte{0x08}
+ CdpIDKeyPrefix = []byte{0x00}
+ CdpKeyPrefix = []byte{0x01}
+ CollateralRatioIndexPrefix = []byte{0x02}
+ CdpIDKey = []byte{0x03}
+ DebtDenomKey = []byte{0x04}
+ GovDenomKey = []byte{0x05}
+ DepositKeyPrefix = []byte{0x06}
+ PrincipalKeyPrefix = []byte{0x07}
+ PreviousDistributionTimeKey = []byte{0x08}
)
var lenPositiveDec = len(SortableDecBytes(sdk.OneDec()))
diff --git a/x/cdp/types/msg.go b/x/cdp/types/msg.go
index 1fdcc669..9b6df344 100644
--- a/x/cdp/types/msg.go
+++ b/x/cdp/types/msg.go
@@ -1,9 +1,12 @@
package types
import (
+ "errors"
"fmt"
+ "strings"
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// ensure Msg interface compliance at compile time
@@ -38,24 +41,18 @@ func (msg MsgCreateCDP) Route() string { return RouterKey }
func (msg MsgCreateCDP) Type() string { return "create_cdp" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
-func (msg MsgCreateCDP) ValidateBasic() sdk.Error {
+func (msg MsgCreateCDP) ValidateBasic() error {
if msg.Sender.Empty() {
- return sdk.ErrInternal("invalid (empty) sender address")
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
- if len(msg.Collateral) != 1 {
- return sdk.ErrInvalidCoins(fmt.Sprintf("cdps do not support multiple collateral types: received %s", msg.Collateral))
+ if msg.Collateral.Len() != 1 {
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "cdps do not support multiple collateral types: %s", msg.Collateral)
}
if !msg.Collateral.IsValid() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("invalid collateral amount: %s", msg.Collateral))
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "collateral amount %s", msg.Collateral)
}
- if !msg.Collateral.IsAllPositive() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("negative collateral amount: %s", msg.Collateral))
- }
- if !msg.Principal.IsValid() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("invalid principal amount: %s", msg.Principal))
- }
- if !msg.Principal.IsAllPositive() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("negative principal amount: %s", msg.Principal))
+ if msg.Principal.Empty() || !msg.Principal.IsValid() {
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "principal amount %s", msg.Principal)
}
return nil
}
@@ -103,21 +100,18 @@ func (msg MsgDeposit) Route() string { return RouterKey }
func (msg MsgDeposit) Type() string { return "deposit_cdp" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
-func (msg MsgDeposit) ValidateBasic() sdk.Error {
+func (msg MsgDeposit) ValidateBasic() error {
if msg.Owner.Empty() {
- return sdk.ErrInternal("invalid (empty) sender address")
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty")
}
if msg.Depositor.Empty() {
- return sdk.ErrInternal("invalid (empty) owner address")
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
- if len(msg.Collateral) != 1 {
- return sdk.ErrInvalidCoins(fmt.Sprintf("cdps do not support multiple collateral types: received %s", msg.Collateral))
+ if msg.Collateral.Len() != 1 {
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "cdps do not support multiple collateral types: %s", msg.Collateral)
}
if !msg.Collateral.IsValid() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("invalid collateral amount: %s", msg.Collateral))
- }
- if !msg.Collateral.IsAllPositive() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("negative collateral amount: %s", msg.Collateral))
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "collateral amount %s", msg.Collateral)
}
return nil
}
@@ -165,21 +159,18 @@ func (msg MsgWithdraw) Route() string { return RouterKey }
func (msg MsgWithdraw) Type() string { return "withdraw_cdp" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
-func (msg MsgWithdraw) ValidateBasic() sdk.Error {
+func (msg MsgWithdraw) ValidateBasic() error {
if msg.Owner.Empty() {
- return sdk.ErrInternal("invalid (empty) sender address")
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty")
}
if msg.Depositor.Empty() {
- return sdk.ErrInternal("invalid (empty) owner address")
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
- if len(msg.Collateral) != 1 {
- return sdk.ErrInvalidCoins(fmt.Sprintf("cdps do not support multiple collateral types: received %s", msg.Collateral))
+ if msg.Collateral.Len() != 1 {
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "cdps do not support multiple collateral types: %s", msg.Collateral)
}
if !msg.Collateral.IsValid() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("invalid collateral amount: %s", msg.Collateral))
- }
- if !msg.Collateral.IsAllPositive() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("negative collateral amount: %s", msg.Collateral))
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "collateral amount %s", msg.Collateral)
}
return nil
}
@@ -227,18 +218,15 @@ func (msg MsgDrawDebt) Route() string { return RouterKey }
func (msg MsgDrawDebt) Type() string { return "draw_cdp" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
-func (msg MsgDrawDebt) ValidateBasic() sdk.Error {
+func (msg MsgDrawDebt) ValidateBasic() error {
if msg.Sender.Empty() {
- return sdk.ErrInternal("invalid (empty) sender address")
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
- if msg.CdpDenom == "" {
- return sdk.ErrInternal("invalid (empty) cdp denom")
+ if strings.TrimSpace(msg.CdpDenom) == "" {
+ return errors.New("cdp denom cannot be blank")
}
- if !msg.Principal.IsValid() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("invalid principal amount: %s", msg.Principal))
- }
- if !msg.Principal.IsAllPositive() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("negative principal amount: %s", msg.Principal))
+ if msg.Principal.Empty() || !msg.Principal.IsValid() {
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "principal amount %s", msg.Principal)
}
return nil
}
@@ -286,18 +274,15 @@ func (msg MsgRepayDebt) Route() string { return RouterKey }
func (msg MsgRepayDebt) Type() string { return "repay_cdp" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
-func (msg MsgRepayDebt) ValidateBasic() sdk.Error {
+func (msg MsgRepayDebt) ValidateBasic() error {
if msg.Sender.Empty() {
- return sdk.ErrInternal("invalid (empty) sender address")
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
- if msg.CdpDenom == "" {
- return sdk.ErrInternal("invalid (empty) cdp denom")
+ if strings.TrimSpace(msg.CdpDenom) == "" {
+ return errors.New("cdp denom cannot be blank")
}
- if !msg.Payment.IsValid() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("invalid payment amount: %s", msg.Payment))
- }
- if !msg.Payment.IsAllPositive() {
- return sdk.ErrInvalidCoins(fmt.Sprintf("negative payment amount: %s", msg.Payment))
+ if msg.Payment.Empty() || !msg.Payment.IsValid() {
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "payment amount %s", msg.Payment)
}
return nil
}
diff --git a/x/cdp/types/msg_test.go b/x/cdp/types/msg_test.go
index dd034a0d..814f6c68 100644
--- a/x/cdp/types/msg_test.go
+++ b/x/cdp/types/msg_test.go
@@ -33,16 +33,16 @@ func TestMsgCreateCDP(t *testing.T) {
{"create cdp empty owner", sdk.AccAddress{}, coinsSingle, coinsSingle, false},
}
- for i, tc := range tests {
+ for _, tc := range tests {
msg := NewMsgCreateCDP(
tc.sender,
tc.collateral,
tc.principal,
)
if tc.expectPass {
- require.NoError(t, msg.ValidateBasic(), "test: %v", i)
+ require.NoError(t, msg.ValidateBasic(), "test: %v", tc.description)
} else {
- require.Error(t, msg.ValidateBasic(), "test: %v", i)
+ require.Error(t, msg.ValidateBasic(), "test: %v", tc.description)
}
}
}
@@ -63,16 +63,16 @@ func TestMsgDeposit(t *testing.T) {
{"deposit empty depositor", addrs[0], sdk.AccAddress{}, coinsSingle, false},
}
- for i, tc := range tests {
+ for _, tc := range tests {
msg := NewMsgDeposit(
tc.sender,
tc.depositor,
tc.collateral,
)
if tc.expectPass {
- require.NoError(t, msg.ValidateBasic(), "test: %v", i)
+ require.NoError(t, msg.ValidateBasic(), "test: %v", tc.description)
} else {
- require.Error(t, msg.ValidateBasic(), "test: %v", i)
+ require.Error(t, msg.ValidateBasic(), "test: %v", tc.description)
}
}
}
@@ -93,16 +93,16 @@ func TestMsgWithdraw(t *testing.T) {
{"withdraw empty depositor", addrs[0], sdk.AccAddress{}, coinsSingle, false},
}
- for i, tc := range tests {
+ for _, tc := range tests {
msg := NewMsgWithdraw(
tc.sender,
tc.depositor,
tc.collateral,
)
if tc.expectPass {
- require.NoError(t, msg.ValidateBasic(), "test: %v", i)
+ require.NoError(t, msg.ValidateBasic(), "test: %v", tc.description)
} else {
- require.Error(t, msg.ValidateBasic(), "test: %v", i)
+ require.Error(t, msg.ValidateBasic(), "test: %v", tc.description)
}
}
}
@@ -122,16 +122,16 @@ func TestMsgDrawDebt(t *testing.T) {
{"draw debt empty denom", sdk.AccAddress{}, "", coinsSingle, false},
}
- for i, tc := range tests {
+ for _, tc := range tests {
msg := NewMsgDrawDebt(
tc.sender,
tc.denom,
tc.principal,
)
if tc.expectPass {
- require.NoError(t, msg.ValidateBasic(), "test: %v", i)
+ require.NoError(t, msg.ValidateBasic(), "test: %v", tc.description)
} else {
- require.Error(t, msg.ValidateBasic(), "test: %v", i)
+ require.Error(t, msg.ValidateBasic(), "test: %v", tc.description)
}
}
}
@@ -151,16 +151,16 @@ func TestMsgRepayDebt(t *testing.T) {
{"repay debt empty denom", sdk.AccAddress{}, "", coinsSingle, false},
}
- for i, tc := range tests {
+ for _, tc := range tests {
msg := NewMsgRepayDebt(
tc.sender,
tc.denom,
tc.payment,
)
if tc.expectPass {
- require.NoError(t, msg.ValidateBasic(), "test: %v", i)
+ require.NoError(t, msg.ValidateBasic(), "test: %v", tc.description)
} else {
- require.Error(t, msg.ValidateBasic(), "test: %v", i)
+ require.Error(t, msg.ValidateBasic(), "test: %v", tc.description)
}
}
}
diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go
index 2d8c78d4..5246dba4 100644
--- a/x/cdp/types/params.go
+++ b/x/cdp/types/params.go
@@ -2,43 +2,48 @@ package types
import (
"fmt"
+ "strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/params"
tmtime "github.com/tendermint/tendermint/types/time"
)
// Parameter keys
var (
- KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
- KeyCollateralParams = []byte("CollateralParams")
- KeyDebtParams = []byte("DebtParams")
- KeyCircuitBreaker = []byte("CircuitBreaker")
- KeyDebtThreshold = []byte("DebtThreshold")
- KeySurplusThreshold = []byte("SurplusThreshold")
- DefaultGlobalDebt = sdk.Coins{}
- DefaultCircuitBreaker = false
- DefaultCollateralParams = CollateralParams{}
- DefaultDebtParams = DebtParams{}
- DefaultCdpStartingID = uint64(1)
- DefaultDebtDenom = "debt"
- DefaultGovDenom = "ukava"
- DefaultSurplusThreshold = sdk.NewInt(1000000000)
- DefaultDebtThreshold = sdk.NewInt(1000000000)
- DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
- minCollateralPrefix = 0
- maxCollateralPrefix = 255
+ KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
+ KeyCollateralParams = []byte("CollateralParams")
+ KeyDebtParams = []byte("DebtParams")
+ KeyDistributionFrequency = []byte("DistributionFrequency")
+ KeyCircuitBreaker = []byte("CircuitBreaker")
+ KeyDebtThreshold = []byte("DebtThreshold")
+ KeySurplusThreshold = []byte("SurplusThreshold")
+ DefaultGlobalDebt = sdk.Coins{}
+ DefaultCircuitBreaker = false
+ DefaultCollateralParams = CollateralParams{}
+ DefaultDebtParams = DebtParams{}
+ DefaultCdpStartingID = uint64(1)
+ DefaultDebtDenom = "debt"
+ DefaultGovDenom = "ukava"
+ DefaultSurplusThreshold = sdk.NewInt(1000000000)
+ DefaultDebtThreshold = sdk.NewInt(1000000000)
+ DefaultPreviousDistributionTime = tmtime.Canonical(time.Unix(0, 0))
+ DefaultSavingsDistributionFrequency = time.Hour * 24 * 2
+ minCollateralPrefix = 0
+ maxCollateralPrefix = 255
)
// Params governance parameters for cdp module
type Params struct {
- CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"`
- DebtParams DebtParams `json:"debt_params" yaml:"debt_params"`
- GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"`
- SurplusAuctionThreshold sdk.Int `json:"surplus_auction_threshold" yaml:"surplus_auction_threshold"`
- DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"`
- CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"`
+ CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"`
+ DebtParams DebtParams `json:"debt_params" yaml:"debt_params"`
+ GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"`
+ SurplusAuctionThreshold sdk.Int `json:"surplus_auction_threshold" yaml:"surplus_auction_threshold"`
+ DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"`
+ SavingsDistributionFrequency time.Duration `json:"savings_distribution_frequency" yaml:"savings_distribution_frequency"`
+ CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"`
}
// String implements fmt.Stringer
@@ -49,26 +54,28 @@ func (p Params) String() string {
Debt Params: %s
Surplus Auction Threshold: %s
Debt Auction Threshold: %s
+ Savings Distribution Frequency: %s
Circuit Breaker: %t`,
- p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.SurplusAuctionThreshold, p.DebtAuctionThreshold, p.CircuitBreaker,
+ p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.SurplusAuctionThreshold, p.DebtAuctionThreshold, p.SavingsDistributionFrequency, p.CircuitBreaker,
)
}
// NewParams returns a new params object
-func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, surplusThreshold sdk.Int, debtThreshold sdk.Int, breaker bool) Params {
+func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, surplusThreshold sdk.Int, debtThreshold sdk.Int, distributionFreq time.Duration, breaker bool) Params {
return Params{
- GlobalDebtLimit: debtLimit,
- CollateralParams: collateralParams,
- DebtParams: debtParams,
- DebtAuctionThreshold: debtThreshold,
- SurplusAuctionThreshold: surplusThreshold,
- CircuitBreaker: breaker,
+ GlobalDebtLimit: debtLimit,
+ CollateralParams: collateralParams,
+ DebtParams: debtParams,
+ DebtAuctionThreshold: debtThreshold,
+ SurplusAuctionThreshold: surplusThreshold,
+ SavingsDistributionFrequency: distributionFreq,
+ CircuitBreaker: breaker,
}
}
// DefaultParams returns default params for cdp module
func DefaultParams() Params {
- return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultSurplusThreshold, DefaultDebtThreshold, DefaultCircuitBreaker)
+ return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultSurplusThreshold, DefaultDebtThreshold, DefaultSavingsDistributionFrequency, DefaultCircuitBreaker)
}
// CollateralParam governance parameters for each collateral type within the cdp module
@@ -116,7 +123,8 @@ type DebtParam struct {
Denom string `json:"denom" yaml:"denom"`
ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"`
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"`
- DebtFloor sdk.Int `json:"debt_floor" yaml:"debt_floor"` // minimum active loan size, used to prevent dust
+ DebtFloor sdk.Int `json:"debt_floor" yaml:"debt_floor"` // minimum active loan size, used to prevent dust
+ SavingsRate sdk.Dec `json:"savings_rate" yaml:"savings_rate"` // the percentage of stability fees that are redirected to savings rate
}
func (dp DebtParam) String() string {
@@ -149,26 +157,49 @@ func ParamKeyTable() params.KeyTable {
// nolint
func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{
- {Key: KeyGlobalDebtLimit, Value: &p.GlobalDebtLimit},
- {Key: KeyCollateralParams, Value: &p.CollateralParams},
- {Key: KeyDebtParams, Value: &p.DebtParams},
- {Key: KeyCircuitBreaker, Value: &p.CircuitBreaker},
- {Key: KeySurplusThreshold, Value: &p.SurplusAuctionThreshold},
- {Key: KeyDebtThreshold, Value: &p.DebtAuctionThreshold},
+ params.NewParamSetPair(KeyGlobalDebtLimit, &p.GlobalDebtLimit, validateGlobalDebtLimitParam),
+ params.NewParamSetPair(KeyCollateralParams, &p.CollateralParams, validateCollateralParams),
+ params.NewParamSetPair(KeyDebtParams, &p.DebtParams, validateDebtParams),
+ params.NewParamSetPair(KeyCircuitBreaker, &p.CircuitBreaker, validateCircuitBreakerParam),
+ params.NewParamSetPair(KeySurplusThreshold, &p.SurplusAuctionThreshold, validateSurplusAuctionThresholdParam),
+ params.NewParamSetPair(KeyDebtThreshold, &p.DebtAuctionThreshold, validateDebtAuctionThresholdParam),
+ params.NewParamSetPair(KeyDistributionFrequency, &p.SavingsDistributionFrequency, validateSavingsDistributionFrequencyParam),
}
}
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
- // validate debt params
- debtDenoms := make(map[string]int)
- for _, dp := range p.DebtParams {
- _, found := debtDenoms[dp.Denom]
- if found {
- return fmt.Errorf("duplicate debt denom: %s", dp.Denom)
- }
- debtDenoms[dp.Denom] = 1
+ if err := validateGlobalDebtLimitParam(p.GlobalDebtLimit); err != nil {
+ return err
+ }
+ if err := validateCollateralParams(p.CollateralParams); err != nil {
+ return err
+ }
+
+ if err := validateDebtParams(p.DebtParams); err != nil {
+ return err
+ }
+
+ if err := validateCircuitBreakerParam(p.CircuitBreaker); err != nil {
+ return err
+ }
+
+ if err := validateSurplusAuctionThresholdParam(p.SurplusAuctionThreshold); err != nil {
+ return err
+ }
+
+ if err := validateDebtAuctionThresholdParam(p.DebtAuctionThreshold); err != nil {
+ return err
+ }
+
+ if err := validateSavingsDistributionFrequencyParam(p.SavingsDistributionFrequency); err != nil {
+ return err
+ }
+
+ debtDenoms := make(map[string]bool)
+ for _, dp := range p.DebtParams {
+ debtDenoms[dp.Denom] = true
}
// validate collateral params
@@ -176,38 +207,76 @@ func (p Params) Validate() error {
prefixDupMap := make(map[int]int)
collateralParamsDebtLimit := sdk.Coins{}
for _, cp := range p.CollateralParams {
+
+ prefix := int(cp.Prefix)
+ prefixDupMap[prefix] = 1
+ collateralDupMap[cp.Denom] = 1
+
+ collateralParamsDebtLimit = collateralParamsDebtLimit.Add(cp.DebtLimit...)
+
+ if cp.DebtLimit.IsAnyGT(p.GlobalDebtLimit) {
+ return fmt.Errorf("collateral debt limit for %s exceeds global debt limit: \n\tglobal debt limit: %s\n\tcollateral debt limits: %s",
+ cp.Denom, p.GlobalDebtLimit, cp.DebtLimit)
+ }
+ }
+
+ if collateralParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) {
+ return fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s",
+ p.GlobalDebtLimit, collateralParamsDebtLimit)
+ }
+
+ return nil
+}
+
+func validateGlobalDebtLimitParam(i interface{}) error {
+ globalDebtLimit, ok := i.(sdk.Coins)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if !globalDebtLimit.IsValid() {
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "global debt limit %s", globalDebtLimit.String())
+ }
+
+ return nil
+}
+
+func validateCollateralParams(i interface{}) error {
+ collateralParams, ok := i.(CollateralParams)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ collateralDupMap := make(map[string]bool)
+ prefixDupMap := make(map[int]bool)
+ for _, cp := range collateralParams {
+ if strings.TrimSpace(cp.Denom) == "" {
+ return fmt.Errorf("debt denom cannot be blank %s", cp)
+ }
+
prefix := int(cp.Prefix)
if prefix < minCollateralPrefix || prefix > maxCollateralPrefix {
return fmt.Errorf("invalid prefix for collateral denom %s: %b", cp.Denom, cp.Prefix)
}
+
_, found := prefixDupMap[prefix]
if found {
return fmt.Errorf("duplicate prefix for collateral denom %s: %v", cp.Denom, []byte{cp.Prefix})
}
- prefixDupMap[prefix] = 1
- _, found = collateralDupMap[cp.Denom]
+ prefixDupMap[prefix] = true
+ _, found = collateralDupMap[cp.Denom]
if found {
return fmt.Errorf("duplicate collateral denom: %s", cp.Denom)
}
- collateralDupMap[cp.Denom] = 1
- if cp.DebtLimit.IsAnyNegative() {
+ collateralDupMap[cp.Denom] = true
+
+ if !cp.DebtLimit.IsValid() {
return fmt.Errorf("debt limit for all collaterals should be positive, is %s for %s", cp.DebtLimit, cp.Denom)
}
- collateralParamsDebtLimit = collateralParamsDebtLimit.Add(cp.DebtLimit)
- for _, dc := range cp.DebtLimit {
- _, found := debtDenoms[dc.Denom]
- if !found {
- return fmt.Errorf("debt limit for collateral %s contains invalid debt denom %s", cp.Denom, dc.Denom)
- }
- }
- if cp.DebtLimit.IsAnyGT(p.GlobalDebtLimit) {
- return fmt.Errorf("collateral debt limit for %s exceeds global debt limit: \n\tglobal debt limit: %s\n\tcollateral debt limits: %s",
- cp.Denom, p.GlobalDebtLimit, cp.DebtLimit)
- }
if cp.LiquidationPenalty.LT(sdk.ZeroDec()) || cp.LiquidationPenalty.GT(sdk.OneDec()) {
return fmt.Errorf("liquidation penalty should be between 0 and 1, is %s for %s", cp.LiquidationPenalty, cp.Denom)
}
@@ -218,20 +287,82 @@ func (p Params) Validate() error {
return fmt.Errorf("stability fee must be ≥ 1.0, is %s for %s", cp.StabilityFee, cp.Denom)
}
}
- if collateralParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) {
- return fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s",
- p.GlobalDebtLimit, collateralParamsDebtLimit)
- }
- // validate global params
- if p.GlobalDebtLimit.IsAnyNegative() {
- return fmt.Errorf("global debt limit should be positive for all debt tokens, is %s", p.GlobalDebtLimit)
- }
- if !p.SurplusAuctionThreshold.IsPositive() {
- return fmt.Errorf("surplus auction threshold should be positive, is %s", p.SurplusAuctionThreshold)
- }
- if !p.DebtAuctionThreshold.IsPositive() {
- return fmt.Errorf("debt auction threshold should be positive, is %s", p.DebtAuctionThreshold)
- }
+ return nil
+}
+
+func validateDebtParams(i interface{}) error {
+ debtParams, ok := i.(DebtParams)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ // validate debt params
+ debtDenoms := make(map[string]bool)
+ for _, dp := range debtParams {
+ if strings.TrimSpace(dp.Denom) == "" {
+ return fmt.Errorf("debt denom cannot be blank %s", dp)
+ }
+
+ _, found := debtDenoms[dp.Denom]
+ if found {
+ return fmt.Errorf("duplicate debt denom: %s", dp.Denom)
+ }
+
+ if dp.SavingsRate.LT(sdk.ZeroDec()) || dp.SavingsRate.GT(sdk.OneDec()) {
+ return fmt.Errorf("savings rate should be between 0 and 1, is %s for %s", dp.SavingsRate, dp.Denom)
+ }
+
+ debtDenoms[dp.Denom] = true
+ }
+
+ return nil
+}
+
+func validateCircuitBreakerParam(i interface{}) error {
+ _, ok := i.(bool)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ return nil
+}
+
+func validateSurplusAuctionThresholdParam(i interface{}) error {
+ sat, ok := i.(sdk.Int)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if !sat.IsPositive() {
+ return fmt.Errorf("surplus auction threshold should be positive: %s", sat)
+ }
+
+ return nil
+}
+
+func validateDebtAuctionThresholdParam(i interface{}) error {
+ dat, ok := i.(sdk.Int)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if !dat.IsPositive() {
+ return fmt.Errorf("debt auction threshold should be positive: %s", dat)
+ }
+
+ return nil
+}
+
+func validateSavingsDistributionFrequencyParam(i interface{}) error {
+ sdf, ok := i.(time.Duration)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ if sdf.Seconds() <= float64(0) {
+ return fmt.Errorf("savings distribution frequency should be positive: %s", sdf)
+ }
+
return nil
}
diff --git a/x/incentive/abci.go b/x/incentive/abci.go
new file mode 100644
index 00000000..e6c0b293
--- /dev/null
+++ b/x/incentive/abci.go
@@ -0,0 +1,13 @@
+package incentive
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/x/incentive/keeper"
+)
+
+// BeginBlocker runs at the start of every block
+func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
+ k.DeleteExpiredClaimsAndClaimPeriods(ctx)
+ k.ApplyRewardsToCdps(ctx)
+ k.CreateAndDeleteRewardPeriods(ctx)
+}
diff --git a/x/incentive/alias.go b/x/incentive/alias.go
new file mode 100644
index 00000000..877ddba3
--- /dev/null
+++ b/x/incentive/alias.go
@@ -0,0 +1,92 @@
+// nolint
+// autogenerated code using github.com/rigelrozanski/multitool
+// aliases generated for the following subdirectories:
+// ALIASGEN: github.com/kava-labs/kava/x/incentive/keeper
+// ALIASGEN: github.com/kava-labs/kava/x/incentive/types
+package incentive
+
+import (
+ "github.com/kava-labs/kava/x/incentive/keeper"
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+const (
+ EventTypeClaim = types.EventTypeClaim
+ AttributeValueCategory = types.AttributeValueCategory
+ AttributeKeySender = types.AttributeKeySender
+ ModuleName = types.ModuleName
+ StoreKey = types.StoreKey
+ RouterKey = types.RouterKey
+ DefaultParamspace = types.DefaultParamspace
+ QuerierRoute = types.QuerierRoute
+ QueryGetClaims = types.QueryGetClaims
+ RestClaimOwner = types.RestClaimOwner
+ RestClaimDenom = types.RestClaimDenom
+ QueryGetParams = types.QueryGetParams
+)
+
+var (
+ // functions aliases
+ NewKeeper = keeper.NewKeeper
+ NewQuerier = keeper.NewQuerier
+ GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength
+ RegisterCodec = types.RegisterCodec
+ NewGenesisState = types.NewGenesisState
+ DefaultGenesisState = types.DefaultGenesisState
+ BytesToUint64 = types.BytesToUint64
+ GetClaimPeriodPrefix = types.GetClaimPeriodPrefix
+ GetClaimPrefix = types.GetClaimPrefix
+ NewMsgClaimReward = types.NewMsgClaimReward
+ NewParams = types.NewParams
+ DefaultParams = types.DefaultParams
+ ParamKeyTable = types.ParamKeyTable
+ NewReward = types.NewReward
+ NewPeriod = types.NewPeriod
+ NewQueryClaimsParams = types.NewQueryClaimsParams
+ NewRewardPeriod = types.NewRewardPeriod
+ NewClaimPeriod = types.NewClaimPeriod
+ NewClaim = types.NewClaim
+
+ // variable aliases
+ ModuleCdc = types.ModuleCdc
+ ErrClaimNotFound = types.ErrClaimNotFound
+ ErrClaimPeriodNotFound = types.ErrClaimPeriodNotFound
+ ErrInvalidAccountType = types.ErrInvalidAccountType
+ ErrNoClaimsFound = types.ErrNoClaimsFound
+ ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance
+ RewardPeriodKeyPrefix = types.RewardPeriodKeyPrefix
+ ClaimPeriodKeyPrefix = types.ClaimPeriodKeyPrefix
+ ClaimKeyPrefix = types.ClaimKeyPrefix
+ NextClaimPeriodIDPrefix = types.NextClaimPeriodIDPrefix
+ PreviousBlockTimeKey = types.PreviousBlockTimeKey
+ KeyActive = types.KeyActive
+ KeyRewards = types.KeyRewards
+ DefaultActive = types.DefaultActive
+ DefaultRewards = types.DefaultRewards
+ DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
+ GovDenom = types.GovDenom
+ PrincipalDenom = types.PrincipalDenom
+ IncentiveMacc = types.IncentiveMacc
+)
+
+type (
+ Keeper = keeper.Keeper
+ SupplyKeeper = types.SupplyKeeper
+ CdpKeeper = types.CdpKeeper
+ AccountKeeper = types.AccountKeeper
+ GenesisClaimPeriodID = types.GenesisClaimPeriodID
+ GenesisClaimPeriodIDs = types.GenesisClaimPeriodIDs
+ GenesisState = types.GenesisState
+ MsgClaimReward = types.MsgClaimReward
+ Params = types.Params
+ Reward = types.Reward
+ Rewards = types.Rewards
+ QueryClaimsParams = types.QueryClaimsParams
+ PostClaimReq = types.PostClaimReq
+ RewardPeriod = types.RewardPeriod
+ RewardPeriods = types.RewardPeriods
+ ClaimPeriod = types.ClaimPeriod
+ ClaimPeriods = types.ClaimPeriods
+ Claim = types.Claim
+ Claims = types.Claims
+)
diff --git a/x/incentive/client/cli/query.go b/x/incentive/client/cli/query.go
new file mode 100644
index 00000000..c9201c53
--- /dev/null
+++ b/x/incentive/client/cli/query.go
@@ -0,0 +1,98 @@
+package cli
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/version"
+ "github.com/kava-labs/kava/x/incentive/types"
+ "github.com/spf13/cobra"
+)
+
+// GetQueryCmd returns the cli query commands for the incentive module
+func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
+ incentiveQueryCmd := &cobra.Command{
+ Use: types.ModuleName,
+ Short: "Querying commands for the incentive module",
+ }
+
+ incentiveQueryCmd.AddCommand(flags.GetCommands(
+ queryParamsCmd(queryRoute, cdc),
+ queryClaimsCmd(queryRoute, cdc),
+ )...)
+
+ return incentiveQueryCmd
+
+}
+
+func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "claims [owner-addr] [denom]",
+ Short: "get claims by onwer and denom",
+ Long: strings.TrimSpace(
+ fmt.Sprintf(`Get all claims owned by the owner address for the particular collateral type.
+
+ Example:
+ $ %s query %s claims kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb`, version.ClientName, types.ModuleName)),
+ Args: cobra.ExactArgs(2),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+ // Prepare params for querier
+ ownerAddress, err := sdk.AccAddressFromBech32(args[0])
+ if err != nil {
+ return err
+ }
+ bz, err := cdc.MarshalJSON(types.QueryClaimsParams{
+ Owner: ownerAddress,
+ Denom: args[1],
+ })
+ if err != nil {
+ return err
+ }
+
+ // Query
+ route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetClaims)
+ res, _, err := cliCtx.QueryWithData(route, bz)
+ if err != nil {
+ return err
+ }
+
+ var claims types.Claims
+ if err := cdc.UnmarshalJSON(res, &claims); err != nil {
+ return fmt.Errorf("failed to unmarshal claims: %w", err)
+ }
+ return cliCtx.PrintOutput(claims)
+
+ },
+ }
+}
+
+func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "params",
+ Short: "get the incentive module parameters",
+ Long: "Get the current global incentive module parameters.",
+ Args: cobra.NoArgs,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+
+ // Query
+ route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
+ res, _, err := cliCtx.QueryWithData(route, nil)
+ if err != nil {
+ return err
+ }
+
+ // Decode and print results
+ var params types.Params
+ if err := cdc.UnmarshalJSON(res, ¶ms); err != nil {
+ return fmt.Errorf("failed to unmarshal params: %w", err)
+ }
+ return cliCtx.PrintOutput(params)
+ },
+ }
+}
diff --git a/x/incentive/client/cli/tx.go b/x/incentive/client/cli/tx.go
new file mode 100644
index 00000000..559cbd37
--- /dev/null
+++ b/x/incentive/client/cli/tx.go
@@ -0,0 +1,63 @@
+package cli
+
+import (
+ "bufio"
+ "fmt"
+ "strings"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/version"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
+ "github.com/kava-labs/kava/x/incentive/types"
+ "github.com/spf13/cobra"
+)
+
+// GetTxCmd returns the transaction cli commands for the incentive module
+func GetTxCmd(cdc *codec.Codec) *cobra.Command {
+ incentiveTxCmd := &cobra.Command{
+ Use: types.ModuleName,
+ Short: "transaction commands for the incentive module",
+ }
+
+ incentiveTxCmd.AddCommand(flags.PostCommands(
+ getCmdClaim(cdc),
+ )...)
+
+ return incentiveTxCmd
+
+}
+
+func getCmdClaim(cdc *codec.Codec) *cobra.Command {
+ return &cobra.Command{
+ Use: "claim [owner] [denom]",
+ Short: "claim rewards for owner and denom",
+ Long: strings.TrimSpace(
+ fmt.Sprintf(`Claim any outstanding rewards owned by owner for the input denom,
+
+ Example:
+ $ %s tx %s claim kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb
+ `, version.ClientName, types.ModuleName),
+ ),
+ Args: cobra.ExactArgs(2),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
+ cliCtx := context.NewCLIContext().WithCodec(cdc)
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
+ owner, err := sdk.AccAddressFromBech32(args[0])
+ if err != nil {
+ return err
+ }
+
+ msg := types.NewMsgClaimReward(owner, args[1])
+ err = msg.ValidateBasic()
+ if err != nil {
+ return err
+ }
+ return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
+ },
+ }
+}
diff --git a/x/incentive/client/rest/query.go b/x/incentive/client/rest/query.go
new file mode 100644
index 00000000..e8e0e5e1
--- /dev/null
+++ b/x/incentive/client/rest/query.go
@@ -0,0 +1,70 @@
+package rest
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/rest"
+ "github.com/gorilla/mux"
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
+ r.HandleFunc(fmt.Sprintf("/%s/claims", types.ModuleName), queryClaimsHandlerFn(cliCtx)).Methods("GET")
+ r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET")
+}
+
+func queryClaimsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
+ if !ok {
+ return
+ }
+ vars := mux.Vars(r)
+ ownerBech32 := vars[types.RestClaimOwner]
+ denom := vars[types.RestClaimDenom]
+
+ owner, err := sdk.AccAddressFromBech32(ownerBech32)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ queryParams := types.NewQueryClaimsParams(owner, denom)
+ bz, err := cliCtx.Codec.MarshalJSON(queryParams)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/incentive/%s", types.QueryGetClaims), bz)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+ cliCtx = cliCtx.WithHeight(height)
+ rest.PostProcessResponse(w, cliCtx, res)
+
+ }
+}
+
+func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
+ if !ok {
+ return
+ }
+
+ route := fmt.Sprintf("custom/%s/parameters", types.QuerierRoute)
+
+ res, height, err := cliCtx.QueryWithData(route, nil)
+ if err != nil {
+ rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ cliCtx = cliCtx.WithHeight(height)
+ rest.PostProcessResponse(w, cliCtx, res)
+ }
+}
diff --git a/x/incentive/client/rest/rest.go b/x/incentive/client/rest/rest.go
new file mode 100644
index 00000000..9c41f78f
--- /dev/null
+++ b/x/incentive/client/rest/rest.go
@@ -0,0 +1,13 @@
+package rest
+
+import (
+ "github.com/gorilla/mux"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+)
+
+// RegisterRoutes registers incentive-related REST handlers to a router
+func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
+ registerQueryRoutes(cliCtx, r)
+ registerTxRoutes(cliCtx, r)
+}
diff --git a/x/incentive/client/rest/tx.go b/x/incentive/client/rest/tx.go
new file mode 100644
index 00000000..13ccd94a
--- /dev/null
+++ b/x/incentive/client/rest/tx.go
@@ -0,0 +1,36 @@
+package rest
+
+import (
+ "net/http"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/rest"
+ "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
+ "github.com/gorilla/mux"
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
+ r.HandleFunc("/incentive/claim", postClaimHandlerFn(cliCtx)).Methods("POST")
+
+}
+
+func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ var requestBody types.PostClaimReq
+ if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
+ return
+ }
+ requestBody.BaseReq = requestBody.BaseReq.Sanitize()
+ if !requestBody.BaseReq.ValidateBasic(w) {
+ return
+ }
+ msg := types.NewMsgClaimReward(requestBody.Sender, requestBody.Denom)
+ if err := msg.ValidateBasic(); err != nil {
+ rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
+ }
+}
diff --git a/x/incentive/doc.go b/x/incentive/doc.go
new file mode 100644
index 00000000..f2b92511
--- /dev/null
+++ b/x/incentive/doc.go
@@ -0,0 +1,40 @@
+/*
+Package incentive implements a Cosmos SDK module, per ADR 009, that provides governance-controlled,
+on-chain incentives for users who open cdps and mint stablecoins (USDX).
+
+For the background and motivation of this module, see the governance proposal that was voted on by
+KAVA token holders: https://ipfs.io/ipfs/QmSYedssC3nyQacDJmNcREtgmTPyaMx2JX7RNkMdAVkdkr/user-growth-fund-proposal.pdf
+
+The 'Reward' parameter is used to control how much incentives are given.
+For example, the following reward:
+
+ Reward{
+ Active: true,
+ Denom: "bnb",
+ AvailableRewards: sdk.NewCoin("ukava", 1000000000),
+ Duration: time.Hour*7*24,
+ TimeLock: time.Hour*24*365,
+ ClaimDuration: time.Hour*7*24,
+ }
+
+will distribute 1000 KAVA each week (Duration) to users who mint USDX using collateral bnb.
+That KAVA can be claimed by the user for one week (ClaimDuration) after the reward period expires,
+and all KAVA rewards will be timelocked for 1 year (TimeLock). If a user does not claim them during
+the claim duration period, they are forgone.
+
+Rewards are accumulated by users continuously and proportionally - ie. if a user holds a CDP that has
+minted 10% of all USDX backed by bnb for the entire reward period, they will be eligible to claim 10%
+of rewards for that period.
+
+Once a reward period ends, but not before, users can claim the rewards they have accumulated. Users claim rewards
+using a MsgClaimReward transaction. The following msg:
+
+ MsgClaimReward {
+ "kava1..."
+ "bnb"
+ }
+
+will claim all outstanding rewards for minting USDX backed by bnb for the input user.
+
+*/
+package incentive
diff --git a/x/incentive/genesis.go b/x/incentive/genesis.go
new file mode 100644
index 00000000..5a38e146
--- /dev/null
+++ b/x/incentive/genesis.go
@@ -0,0 +1,73 @@
+package incentive
+
+import (
+ "fmt"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/x/incentive/keeper"
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+// InitGenesis initializes the store state from a genesis state.
+func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeeper, gs types.GenesisState) {
+
+ // check if the module account exists
+ moduleAcc := supplyKeeper.GetModuleAccount(ctx, types.IncentiveMacc)
+ if moduleAcc == nil {
+ panic(fmt.Sprintf("%s module account has not been set", types.IncentiveMacc))
+ }
+
+ if err := gs.Validate(); err != nil {
+ panic(fmt.Sprintf("failed to validate %s genesis state: %s", types.ModuleName, err))
+ }
+
+ k.SetParams(ctx, gs.Params)
+
+ for _, r := range gs.Params.Rewards {
+ k.SetNextClaimPeriodID(ctx, r.Denom, 1)
+ }
+
+ // only set the previous block time if it's different than default
+ if !gs.PreviousBlockTime.Equal(types.DefaultPreviousBlockTime) {
+ k.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
+ }
+
+ // set store objects
+ for _, rp := range gs.RewardPeriods {
+ k.SetRewardPeriod(ctx, rp)
+ }
+
+ for _, cp := range gs.ClaimPeriods {
+ k.SetClaimPeriod(ctx, cp)
+ }
+
+ for _, c := range gs.Claims {
+ k.SetClaim(ctx, c)
+ }
+
+ for _, id := range gs.NextClaimPeriodIDs {
+ k.SetNextClaimPeriodID(ctx, id.Denom, id.ID)
+ }
+
+}
+
+// ExportGenesis export genesis state for incentive module
+func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState {
+ // get all objects out of the store
+ params := k.GetParams(ctx)
+ previousBlockTime, found := k.GetPreviousBlockTime(ctx)
+
+ // since it is not set in genesis, if somehow the chain got started and was exported
+ // immediately after InitGenesis, there would be no previousBlockTime value.
+ if !found {
+ previousBlockTime = types.DefaultPreviousBlockTime
+ }
+
+ // Get all objects from the store
+ rewardPeriods := k.GetAllRewardPeriods(ctx)
+ claimPeriods := k.GetAllClaimPeriods(ctx)
+ claims := k.GetAllClaims(ctx)
+ claimPeriodIDs := k.GetAllClaimPeriodIDPairs(ctx)
+
+ return types.NewGenesisState(params, previousBlockTime, rewardPeriods, claimPeriods, claims, claimPeriodIDs)
+}
diff --git a/x/incentive/handler.go b/x/incentive/handler.go
new file mode 100644
index 00000000..1fdf8a58
--- /dev/null
+++ b/x/incentive/handler.go
@@ -0,0 +1,48 @@
+package incentive
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+ "github.com/kava-labs/kava/x/incentive/keeper"
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+// NewHandler creates an sdk.Handler for incentive module messages
+func NewHandler(k keeper.Keeper) sdk.Handler {
+ return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
+ ctx = ctx.WithEventManager(sdk.NewEventManager())
+ switch msg := msg.(type) {
+ case types.MsgClaimReward:
+ return handleMsgClaimReward(ctx, k, msg)
+ default:
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
+
+ }
+ }
+}
+
+func handleMsgClaimReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimReward) (*sdk.Result, error) {
+
+ claims, found := k.GetClaimsByAddressAndDenom(ctx, msg.Sender, msg.Denom)
+ if !found {
+ return nil, sdkerrors.Wrapf(types.ErrNoClaimsFound, "address: %s, denom: %s", msg.Sender, msg.Denom)
+ }
+
+ for _, claim := range claims {
+ err := k.PayoutClaim(ctx, claim.Owner, claim.Denom, claim.ClaimPeriodID)
+ if err != nil {
+ return nil, err
+ }
+ }
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ sdk.EventTypeMessage,
+ sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
+ sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
+ ),
+ )
+
+ return &sdk.Result{
+ Events: ctx.EventManager().Events(),
+ }, nil
+}
diff --git a/x/incentive/handler_test.go b/x/incentive/handler_test.go
new file mode 100644
index 00000000..70253ef3
--- /dev/null
+++ b/x/incentive/handler_test.go
@@ -0,0 +1,74 @@
+package incentive_test
+
+import (
+ "testing"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/incentive"
+ "github.com/kava-labs/kava/x/kavadist"
+ "github.com/stretchr/testify/suite"
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
+func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
+
+type HandlerTestSuite struct {
+ suite.Suite
+
+ ctx sdk.Context
+ app app.TestApp
+ handler sdk.Handler
+ keeper incentive.Keeper
+ addrs []sdk.AccAddress
+}
+
+func (suite *HandlerTestSuite) SetupTest() {
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+ keeper := tApp.GetIncentiveKeeper()
+
+ // Set up genesis state and initialize
+ _, addrs := app.GeneratePrivKeyAddressPairs(3)
+ coins := []sdk.Coins{}
+ for j := 0; j < 3; j++ {
+ coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000)))
+ }
+ authGS := app.NewAuthGenState(addrs, coins)
+ tApp.InitializeFromGenesisStates(authGS)
+
+ suite.addrs = addrs
+ suite.handler = incentive.NewHandler(keeper)
+ suite.keeper = keeper
+ suite.app = tApp
+ suite.ctx = ctx
+}
+
+func (suite *HandlerTestSuite) addClaim() {
+ supplyKeeper := suite.app.GetSupplyKeeper()
+ macc := supplyKeeper.GetModuleAccount(suite.ctx, kavadist.ModuleName)
+ err := supplyKeeper.MintCoins(suite.ctx, macc.GetName(), cs(c("ukava", 1000000)))
+ suite.Require().NoError(err)
+ cp := incentive.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766)
+ suite.NotPanics(func() {
+ suite.keeper.SetClaimPeriod(suite.ctx, cp)
+ })
+ c1 := incentive.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
+ suite.NotPanics(func() {
+ suite.keeper.SetClaim(suite.ctx, c1)
+ })
+}
+
+func (suite *HandlerTestSuite) TestMsgClaimReward() {
+ suite.addClaim()
+ msg := incentive.NewMsgClaimReward(suite.addrs[0], "bnb")
+ res, err := suite.handler(suite.ctx, msg)
+ suite.NoError(err)
+ suite.Require().NotNil(res)
+}
+func TestHandlerTestSuite(t *testing.T) {
+ suite.Run(t, new(HandlerTestSuite))
+}
diff --git a/x/incentive/keeper/keeper.go b/x/incentive/keeper/keeper.go
new file mode 100644
index 00000000..3c2aa383
--- /dev/null
+++ b/x/incentive/keeper/keeper.go
@@ -0,0 +1,243 @@
+package keeper
+
+import (
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ "github.com/cosmos/cosmos-sdk/store/prefix"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/params/subspace"
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+// Keeper keeper for the incentive module
+type Keeper struct {
+ accountKeeper types.AccountKeeper
+ cdc *codec.Codec
+ cdpKeeper types.CdpKeeper
+ key sdk.StoreKey
+ paramSubspace subspace.Subspace
+ supplyKeeper types.SupplyKeeper
+}
+
+// NewKeeper creates a new keeper
+func NewKeeper(
+ cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, sk types.SupplyKeeper,
+ cdpk types.CdpKeeper, ak types.AccountKeeper,
+) Keeper {
+
+ return Keeper{
+ accountKeeper: ak,
+ cdc: cdc,
+ cdpKeeper: cdpk,
+ key: key,
+ paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
+ supplyKeeper: sk,
+ }
+}
+
+// GetRewardPeriod returns the reward period from the store for the input denom and a boolean for if it was found
+func (k Keeper) GetRewardPeriod(ctx sdk.Context, denom string) (types.RewardPeriod, bool) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
+ bz := store.Get([]byte(denom))
+ if bz == nil {
+ return types.RewardPeriod{}, false
+ }
+ var rp types.RewardPeriod
+ k.cdc.MustUnmarshalBinaryBare(bz, &rp)
+ return rp, true
+}
+
+// SetRewardPeriod sets the reward period in the store for the input deno,
+func (k Keeper) SetRewardPeriod(ctx sdk.Context, rp types.RewardPeriod) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
+ bz := k.cdc.MustMarshalBinaryBare(rp)
+ store.Set([]byte(rp.Denom), bz)
+}
+
+// DeleteRewardPeriod deletes the reward period in the store for the input denom,
+func (k Keeper) DeleteRewardPeriod(ctx sdk.Context, denom string) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
+ store.Delete([]byte(denom))
+}
+
+// IterateRewardPeriods iterates over all reward period objects in the store and preforms a callback function
+func (k Keeper) IterateRewardPeriods(ctx sdk.Context, cb func(rp types.RewardPeriod) (stop bool)) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
+ iterator := sdk.KVStorePrefixIterator(store, []byte{})
+ defer iterator.Close()
+ for ; iterator.Valid(); iterator.Next() {
+ var rp types.RewardPeriod
+ k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &rp)
+ if cb(rp) {
+ break
+ }
+ }
+}
+
+// GetAllRewardPeriods returns all reward periods in the store
+func (k Keeper) GetAllRewardPeriods(ctx sdk.Context) types.RewardPeriods {
+ rps := types.RewardPeriods{}
+ k.IterateRewardPeriods(ctx, func(rp types.RewardPeriod) (stop bool) {
+ rps = append(rps, rp)
+ return false
+ })
+ return rps
+}
+
+// GetNextClaimPeriodID returns the highest claim period id in the store for the input denom
+func (k Keeper) GetNextClaimPeriodID(ctx sdk.Context, denom string) uint64 {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
+ bz := store.Get([]byte(denom))
+ return types.BytesToUint64(bz)
+}
+
+// SetNextClaimPeriodID sets the highest claim period id in the store for the input denom
+func (k Keeper) SetNextClaimPeriodID(ctx sdk.Context, denom string, id uint64) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
+ store.Set([]byte(denom), sdk.Uint64ToBigEndian(id))
+}
+
+// IterateClaimPeriodIDKeysAndValues iterates over the claim period id (value) and denom (key) of each claim period id in the store and performs a callback function
+func (k Keeper) IterateClaimPeriodIDKeysAndValues(ctx sdk.Context, cb func(denom string, id uint64) (stop bool)) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
+ iterator := sdk.KVStorePrefixIterator(store, []byte{})
+ defer iterator.Close()
+ for ; iterator.Valid(); iterator.Next() {
+ id := types.BytesToUint64(iterator.Value())
+ denom := string(iterator.Key())
+ if cb(denom, id) {
+ break
+ }
+ }
+}
+
+// GetAllClaimPeriodIDPairs returns all denom:nextClaimPeriodID pairs in the store
+func (k Keeper) GetAllClaimPeriodIDPairs(ctx sdk.Context) types.GenesisClaimPeriodIDs {
+ ids := types.GenesisClaimPeriodIDs{}
+ k.IterateClaimPeriodIDKeysAndValues(ctx, func(denom string, id uint64) (stop bool) {
+ genID := types.GenesisClaimPeriodID{
+ Denom: denom,
+ ID: id,
+ }
+ ids = append(ids, genID)
+ return false
+ })
+ return ids
+}
+
+// GetClaimPeriod returns claim period in the store for the input ID and denom and a boolean for if it was found
+func (k Keeper) GetClaimPeriod(ctx sdk.Context, id uint64, denom string) (types.ClaimPeriod, bool) {
+ var cp types.ClaimPeriod
+ store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
+ bz := store.Get(types.GetClaimPeriodPrefix(denom, id))
+ if bz == nil {
+ return types.ClaimPeriod{}, false
+ }
+ k.cdc.MustUnmarshalBinaryBare(bz, &cp)
+ return cp, true
+}
+
+// SetClaimPeriod sets the claim period in the store for the input ID and denom
+func (k Keeper) SetClaimPeriod(ctx sdk.Context, cp types.ClaimPeriod) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
+ bz := k.cdc.MustMarshalBinaryBare(cp)
+ store.Set(types.GetClaimPeriodPrefix(cp.Denom, cp.ID), bz)
+}
+
+// DeleteClaimPeriod deletes the claim period in the store for the input ID and denom
+func (k Keeper) DeleteClaimPeriod(ctx sdk.Context, id uint64, denom string) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
+ store.Delete(types.GetClaimPeriodPrefix(denom, id))
+}
+
+// IterateClaimPeriods iterates over all claim period objects in the store and preforms a callback function
+func (k Keeper) IterateClaimPeriods(ctx sdk.Context, cb func(cp types.ClaimPeriod) (stop bool)) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
+ iterator := sdk.KVStorePrefixIterator(store, []byte{})
+ defer iterator.Close()
+ for ; iterator.Valid(); iterator.Next() {
+ var cp types.ClaimPeriod
+ k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &cp)
+ if cb(cp) {
+ break
+ }
+ }
+}
+
+// GetAllClaimPeriods returns all ClaimPeriod objects in the store
+func (k Keeper) GetAllClaimPeriods(ctx sdk.Context) types.ClaimPeriods {
+ cps := types.ClaimPeriods{}
+ k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
+ cps = append(cps, cp)
+ return false
+ })
+ return cps
+}
+
+// GetClaim returns the claim in the store corresponding the the input address denom and id and a boolean for if the claim was found
+func (k Keeper) GetClaim(ctx sdk.Context, addr sdk.AccAddress, denom string, id uint64) (types.Claim, bool) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
+ bz := store.Get(types.GetClaimPrefix(addr, denom, id))
+ if bz == nil {
+ return types.Claim{}, false
+ }
+ var c types.Claim
+ k.cdc.MustUnmarshalBinaryBare(bz, &c)
+ return c, true
+}
+
+// SetClaim sets the claim in the store corresponding to the input address, denom, and id
+func (k Keeper) SetClaim(ctx sdk.Context, c types.Claim) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
+ bz := k.cdc.MustMarshalBinaryBare(c)
+ store.Set(types.GetClaimPrefix(c.Owner, c.Denom, c.ClaimPeriodID), bz)
+
+}
+
+// DeleteClaim deletes the claim in the store corresponding to the input address, denom, and id
+func (k Keeper) DeleteClaim(ctx sdk.Context, owner sdk.AccAddress, denom string, id uint64) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
+ store.Delete(types.GetClaimPrefix(owner, denom, id))
+}
+
+// IterateClaims iterates over all claim objects in the store and preforms a callback function
+func (k Keeper) IterateClaims(ctx sdk.Context, cb func(c types.Claim) (stop bool)) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
+ iterator := sdk.KVStorePrefixIterator(store, []byte{})
+ defer iterator.Close()
+ for ; iterator.Valid(); iterator.Next() {
+ var c types.Claim
+ k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &c)
+ if cb(c) {
+ break
+ }
+ }
+}
+
+// GetAllClaims returns all Claim objects in the store
+func (k Keeper) GetAllClaims(ctx sdk.Context) types.Claims {
+ cs := types.Claims{}
+ k.IterateClaims(ctx, func(c types.Claim) (stop bool) {
+ cs = append(cs, c)
+ return false
+ })
+ return cs
+}
+
+// GetPreviousBlockTime get the blocktime for the previous block
+func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
+ b := store.Get([]byte{})
+ if b == nil {
+ return time.Time{}, false
+ }
+ k.cdc.MustUnmarshalBinaryBare(b, &blockTime)
+ return blockTime, true
+}
+
+// SetPreviousBlockTime set the time of the previous block
+func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
+ store.Set([]byte{}, k.cdc.MustMarshalBinaryBare(blockTime))
+}
diff --git a/x/incentive/keeper/keeper_test.go b/x/incentive/keeper/keeper_test.go
new file mode 100644
index 00000000..0d56b2d5
--- /dev/null
+++ b/x/incentive/keeper/keeper_test.go
@@ -0,0 +1,174 @@
+package keeper_test
+
+import (
+ "testing"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/incentive/keeper"
+ "github.com/kava-labs/kava/x/incentive/types"
+ "github.com/stretchr/testify/suite"
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+// Test suite used for all keeper tests
+type KeeperTestSuite struct {
+ suite.Suite
+
+ keeper keeper.Keeper
+ app app.TestApp
+ ctx sdk.Context
+ addrs []sdk.AccAddress
+}
+
+// The default state used by each test
+func (suite *KeeperTestSuite) SetupTest() {
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+ tApp.InitializeFromGenesisStates()
+ _, addrs := app.GeneratePrivKeyAddressPairs(1)
+ keeper := tApp.GetIncentiveKeeper()
+ suite.app = tApp
+ suite.ctx = ctx
+ suite.keeper = keeper
+ suite.addrs = addrs
+}
+
+func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account {
+ ak := suite.app.GetAccountKeeper()
+ return ak.GetAccount(suite.ctx, addr)
+}
+
+func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.ModuleAccountI {
+ sk := suite.app.GetSupplyKeeper()
+ return sk.GetModuleAccount(suite.ctx, name)
+}
+
+func (suite *KeeperTestSuite) TestGetSetDeleteRewardPeriod() {
+ rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
+ _, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
+ suite.False(found)
+ suite.NotPanics(func() {
+ suite.keeper.SetRewardPeriod(suite.ctx, rp)
+ })
+ testRP, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
+ suite.True(found)
+ suite.Equal(rp, testRP)
+ suite.NotPanics(func() {
+ suite.keeper.DeleteRewardPeriod(suite.ctx, "bnb")
+ })
+ _, found = suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
+ suite.False(found)
+}
+
+func (suite *KeeperTestSuite) TestGetSetDeleteClaimPeriod() {
+ cp := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766)
+ _, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
+ suite.False(found)
+ suite.NotPanics(func() {
+ suite.keeper.SetClaimPeriod(suite.ctx, cp)
+ })
+ testCP, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
+ suite.True(found)
+ suite.Equal(cp, testCP)
+ suite.NotPanics(func() {
+ suite.keeper.DeleteClaimPeriod(suite.ctx, 1, "bnb")
+ })
+ _, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
+ suite.False(found)
+}
+
+func (suite *KeeperTestSuite) TestGetSetClaimPeriodID() {
+ suite.Panics(func() {
+ suite.keeper.GetNextClaimPeriodID(suite.ctx, "bnb")
+ })
+ suite.NotPanics(func() {
+ suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
+ })
+ testID := suite.keeper.GetNextClaimPeriodID(suite.ctx, "bnb")
+ suite.Equal(uint64(1), testID)
+}
+
+func (suite *KeeperTestSuite) TestGetSetDeleteClaim() {
+ c := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
+ _, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
+ suite.False(found)
+ suite.NotPanics(func() {
+ suite.keeper.SetClaim(suite.ctx, c)
+ })
+ testC, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
+ suite.True(found)
+ suite.Equal(c, testC)
+ suite.NotPanics(func() {
+ suite.keeper.DeleteClaim(suite.ctx, suite.addrs[0], "bnb", 1)
+ })
+ _, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
+ suite.False(found)
+}
+
+func (suite *KeeperTestSuite) TestIterateMethods() {
+ suite.addObjectsToStore() // adds 2 objects of each type to the store
+
+ var rewardPeriods types.RewardPeriods
+ suite.keeper.IterateRewardPeriods(suite.ctx, func(rp types.RewardPeriod) (stop bool) {
+ rewardPeriods = append(rewardPeriods, rp)
+ return false
+ })
+ suite.Equal(2, len(rewardPeriods))
+
+ var claimPeriods types.ClaimPeriods
+ suite.keeper.IterateClaimPeriods(suite.ctx, func(cp types.ClaimPeriod) (stop bool) {
+ claimPeriods = append(claimPeriods, cp)
+ return false
+ })
+ suite.Equal(2, len(claimPeriods))
+
+ var claims types.Claims
+ suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
+ claims = append(claims, c)
+ return false
+ })
+ suite.Equal(2, len(claims))
+
+ var genIDs types.GenesisClaimPeriodIDs
+ suite.keeper.IterateClaimPeriodIDKeysAndValues(suite.ctx, func(denom string, id uint64) (stop bool) {
+ genID := types.GenesisClaimPeriodID{Denom: denom, ID: id}
+ genIDs = append(genIDs, genID)
+ return false
+ })
+ suite.Equal(2, len(genIDs))
+}
+
+func (suite *KeeperTestSuite) addObjectsToStore() {
+ rp1 := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
+ rp2 := types.NewRewardPeriod("xrp", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
+ suite.keeper.SetRewardPeriod(suite.ctx, rp1)
+ suite.keeper.SetRewardPeriod(suite.ctx, rp2)
+
+ cp1 := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766)
+ cp2 := types.NewClaimPeriod("xrp", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766)
+ suite.keeper.SetClaimPeriod(suite.ctx, cp1)
+ suite.keeper.SetClaimPeriod(suite.ctx, cp2)
+
+ suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
+ suite.keeper.SetNextClaimPeriodID(suite.ctx, "xrp", 1)
+
+ c1 := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
+ c2 := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "xrp", 1)
+ suite.keeper.SetClaim(suite.ctx, c1)
+ suite.keeper.SetClaim(suite.ctx, c2)
+
+ params := types.NewParams(
+ true, types.Rewards{types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)},
+ )
+ suite.keeper.SetParams(suite.ctx, params)
+
+}
+
+func TestKeeperTestSuite(t *testing.T) {
+ suite.Run(t, new(KeeperTestSuite))
+}
diff --git a/x/incentive/keeper/params.go b/x/incentive/keeper/params.go
new file mode 100644
index 00000000..ad80a0e8
--- /dev/null
+++ b/x/incentive/keeper/params.go
@@ -0,0 +1,18 @@
+package keeper
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+// GetParams returns the params from the store
+func (k Keeper) GetParams(ctx sdk.Context) types.Params {
+ var p types.Params
+ k.paramSubspace.GetParamSet(ctx, &p)
+ return p
+}
+
+// SetParams sets params on the store
+func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
+ k.paramSubspace.SetParamSet(ctx, ¶ms)
+}
diff --git a/x/incentive/keeper/payout.go b/x/incentive/keeper/payout.go
new file mode 100644
index 00000000..ac3142b8
--- /dev/null
+++ b/x/incentive/keeper/payout.go
@@ -0,0 +1,204 @@
+package keeper
+
+import (
+ "fmt"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
+ "github.com/cosmos/cosmos-sdk/x/auth/vesting"
+ supplyExported "github.com/cosmos/cosmos-sdk/x/supply/exported"
+ "github.com/kava-labs/kava/x/incentive/types"
+ validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
+)
+
+// PayoutClaim sends the timelocked claim coins to the input address
+func (k Keeper) PayoutClaim(ctx sdk.Context, addr sdk.AccAddress, denom string, id uint64) error {
+ claim, found := k.GetClaim(ctx, addr, denom, id)
+ if !found {
+ return sdkerrors.Wrapf(types.ErrClaimNotFound, "id: %d, denom %s, address: %s", id, denom, addr)
+ }
+ claimPeriod, found := k.GetClaimPeriod(ctx, id, denom)
+ if !found {
+ return sdkerrors.Wrapf(types.ErrClaimPeriodNotFound, "id: %d, denom: %s", id, denom)
+ }
+ err := k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, sdk.NewCoins(claim.Reward), int64(claimPeriod.TimeLock.Seconds()))
+ if err != nil {
+ return err
+ }
+
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ types.EventTypeClaim,
+ sdk.NewAttribute(types.AttributeKeySender, fmt.Sprintf("%s", addr)),
+ ),
+ )
+ return nil
+}
+
+// SendTimeLockedCoinsToAccount sends time-locked coins from the input module account to the recipient. If the recipients account is not a vesting account, it is converted to a periodic vesting account and the coins are added to the vesting balance as a vesting period with the input length.
+func (k Keeper) SendTimeLockedCoinsToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
+ macc := k.supplyKeeper.GetModuleAccount(ctx, senderModule)
+ if !macc.GetCoins().IsAllGTE(amt) {
+ return sdkerrors.Wrapf(types.ErrInsufficientModAccountBalance, "%s", senderModule)
+ }
+
+ // 0. Get the account from the account keeper and do a type switch, error if it's a validator vesting account or module account (can make this work for validator vesting later if necessary)
+ acc := k.accountKeeper.GetAccount(ctx, recipientAddr)
+
+ switch acc.(type) {
+ case *validatorvesting.ValidatorVestingAccount, supplyExported.ModuleAccountI:
+ return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", acc)
+ case *vesting.PeriodicVestingAccount:
+ return k.SendTimeLockedCoinsToPeriodicVestingAccount(ctx, senderModule, recipientAddr, amt, length)
+ case *auth.BaseAccount:
+ return k.SendTimeLockedCoinsToBaseAccount(ctx, senderModule, recipientAddr, amt, length)
+ default:
+ return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", acc)
+ }
+}
+
+// SendTimeLockedCoinsToPeriodicVestingAccount sends time-locked coins from the input module account to the recipient
+func (k Keeper) SendTimeLockedCoinsToPeriodicVestingAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
+ err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
+ if err != nil {
+ return err
+ }
+ k.addCoinsToVestingSchedule(ctx, recipientAddr, amt, length)
+ return nil
+}
+
+// SendTimeLockedCoinsToBaseAccount sends time-locked coins from the input module account to the recipient, converting the recipient account to a vesting account
+func (k Keeper) SendTimeLockedCoinsToBaseAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
+ err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
+ if err != nil {
+ return err
+ }
+ acc := k.accountKeeper.GetAccount(ctx, recipientAddr)
+ // transition the account to a periodic vesting account:
+ bacc := authtypes.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
+ newPeriods := vesting.Periods{types.NewPeriod(amt, length)}
+ bva, err := vesting.NewBaseVestingAccount(bacc, amt, ctx.BlockTime().Unix()+length)
+ if err != nil {
+ return err
+ }
+ pva := vesting.NewPeriodicVestingAccountRaw(bva, ctx.BlockTime().Unix(), newPeriods)
+ k.accountKeeper.SetAccount(ctx, pva)
+ return nil
+}
+
+// DeleteExpiredClaimsAndClaimPeriods deletes expired claim periods and their associated claims
+func (k Keeper) DeleteExpiredClaimsAndClaimPeriods(ctx sdk.Context) {
+ k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
+ if !cp.End.Before(ctx.BlockTime()) {
+ return false
+ }
+ k.IterateClaims(ctx, func(c types.Claim) (stop bool) {
+ if !(c.Denom == cp.Denom && c.ClaimPeriodID == cp.ID) {
+ return false
+ }
+ k.DeleteClaim(ctx, c.Owner, c.Denom, c.ClaimPeriodID)
+ return false
+ })
+ k.DeleteClaimPeriod(ctx, cp.ID, cp.Denom)
+ return false
+ })
+}
+
+// GetClaimsByAddressAndDenom returns all claims for a specific user and address and a bool for if any were found
+func (k Keeper) GetClaimsByAddressAndDenom(ctx sdk.Context, addr sdk.AccAddress, denom string) (claims types.Claims, found bool) {
+ found = false
+ k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
+ if cp.Denom != denom {
+ return false
+ }
+ c, hasClaim := k.GetClaim(ctx, addr, cp.Denom, cp.ID)
+ if !hasClaim {
+ return false
+ }
+ found = true
+ claims = append(claims, c)
+ return false
+ })
+ return claims, found
+}
+
+// addCoinsToVestingSchedule adds coins to the input account's vesting schedule where length is the amount of time (from the current block time), in seconds, that the coins will be vesting for
+// the input address must be a periodic vesting account
+func (k Keeper) addCoinsToVestingSchedule(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, length int64) {
+ acc := k.accountKeeper.GetAccount(ctx, addr)
+ vacc := acc.(*vesting.PeriodicVestingAccount)
+ // Add the new vesting coins to OriginalVesting
+ vacc.OriginalVesting = vacc.OriginalVesting.Add(amt...)
+ // update vesting periods
+ if vacc.EndTime < ctx.BlockTime().Unix() {
+ // edge case one - the vesting account's end time is in the past (ie, all previous vesting periods have completed)
+ // append a new period to the vesting account, update the end time, update the account in the store and return
+ newPeriodLength := (ctx.BlockTime().Unix() - vacc.EndTime) + length
+ newPeriod := types.NewPeriod(amt, newPeriodLength)
+ vacc.VestingPeriods = append(vacc.VestingPeriods, newPeriod)
+ vacc.EndTime = ctx.BlockTime().Unix() + length
+ k.accountKeeper.SetAccount(ctx, vacc)
+ return
+ }
+ if vacc.StartTime > ctx.BlockTime().Unix() {
+ // edge case two - the vesting account's start time is in the future (all periods have not started)
+ // update the start time to now and adjust the period lengths in place - a new period will be inserted in the next code block
+ updatedPeriods := vesting.Periods{}
+ for i, period := range vacc.VestingPeriods {
+ updatedPeriod := period
+ if i == 0 {
+ updatedPeriod = types.NewPeriod(period.Amount, (vacc.StartTime-ctx.BlockTime().Unix())+period.Length)
+ }
+ updatedPeriods = append(updatedPeriods, updatedPeriod)
+ }
+ vacc.VestingPeriods = updatedPeriods
+ vacc.StartTime = ctx.BlockTime().Unix()
+ }
+
+ // logic for inserting a new vesting period into the existing vesting schedule
+ totalPeriodLength := types.GetTotalVestingPeriodLength(vacc.VestingPeriods)
+ proposedEndTime := ctx.BlockTime().Unix() + length
+ if totalPeriodLength < length {
+ // in the case that the proposed length is longer than the sum of all previous period lengths, create a new period with length equal to the difference between the proposed length and the previous total length
+ newPeriodLength := length - totalPeriodLength
+ newPeriod := types.NewPeriod(amt, newPeriodLength)
+ vacc.VestingPeriods = append(vacc.VestingPeriods, newPeriod)
+ // update the end time so that the sum of all period lengths equals endTime - startTime
+ vacc.EndTime = proposedEndTime
+ } else {
+ // In the case that the proposed length is less than or equal to the sum of all previous period lengths, insert the period and update other periods as necessary.
+ // EXAMPLE (l is length, a is amount)
+ // Original Periods: {[l: 1 a: 1], [l: 2, a: 1], [l:8, a:3], [l: 5, a: 3]}
+ // Period we want to insert [l: 5, a: x]
+ // Expected result:
+ // {[l: 1, a: 1], [l:2, a: 1], [l:2, a:x], [l:6, a:3], [l:5, a:3]}
+
+ newPeriods := vesting.Periods{}
+ lengthCounter := int64(0)
+ appendRemaining := false
+ for _, period := range vacc.VestingPeriods {
+ if appendRemaining {
+ newPeriods = append(newPeriods, period)
+ continue
+ }
+ lengthCounter += period.Length
+ if lengthCounter < length {
+ newPeriods = append(newPeriods, period)
+ } else if lengthCounter == length {
+ newPeriod := types.NewPeriod(period.Amount.Add(amt...), period.Length)
+ newPeriods = append(newPeriods, newPeriod)
+ appendRemaining = true
+ } else {
+ newPeriod := types.NewPeriod(amt, length-types.GetTotalVestingPeriodLength(newPeriods))
+ previousPeriod := types.NewPeriod(period.Amount, period.Length-newPeriod.Length)
+ newPeriods = append(newPeriods, newPeriod, previousPeriod)
+ appendRemaining = true
+ }
+ }
+ vacc.VestingPeriods = newPeriods
+ }
+ k.accountKeeper.SetAccount(ctx, vacc)
+ return
+}
diff --git a/x/incentive/keeper/payout_test.go b/x/incentive/keeper/payout_test.go
new file mode 100644
index 00000000..caefc6dd
--- /dev/null
+++ b/x/incentive/keeper/payout_test.go
@@ -0,0 +1,357 @@
+package keeper_test
+
+import (
+ "errors"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ "github.com/cosmos/cosmos-sdk/x/auth/vesting"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/cdp"
+ "github.com/kava-labs/kava/x/incentive/types"
+ "github.com/kava-labs/kava/x/kavadist"
+ validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
+ abci "github.com/tendermint/tendermint/abci/types"
+)
+
+func (suite *KeeperTestSuite) setupChain() {
+ // creates a new app state with 4 funded addresses and 1 module account
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: time.Unix(100, 0)})
+ _, addrs := app.GeneratePrivKeyAddressPairs(4)
+ authGS := app.NewAuthGenState(
+ addrs,
+ []sdk.Coins{
+ cs(c("ukava", 400)),
+ cs(c("ukava", 400)),
+ cs(c("ukava", 400)),
+ cs(c("ukava", 400)),
+ })
+ tApp.InitializeFromGenesisStates(
+ authGS,
+ )
+ supplyKeeper := tApp.GetSupplyKeeper()
+ macc := supplyKeeper.GetModuleAccount(ctx, kavadist.ModuleName)
+ err := supplyKeeper.MintCoins(ctx, macc.GetName(), cs(c("ukava", 500)))
+ suite.Require().NoError(err)
+
+ // sets addrs[0] to be a periodic vesting account
+ ak := tApp.GetAccountKeeper()
+ acc := ak.GetAccount(ctx, addrs[0])
+ bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
+ periods := vesting.Periods{
+ vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
+ }
+ bva, err2 := vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), ctx.BlockTime().Unix()+16)
+ suite.Require().NoError(err2)
+ pva := vesting.NewPeriodicVestingAccountRaw(bva, ctx.BlockTime().Unix(), periods)
+ ak.SetAccount(ctx, pva)
+
+ // sets addrs[2] to be a validator vesting account
+ acc = ak.GetAccount(ctx, addrs[2])
+ bacc = auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
+ bva, err2 = vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), ctx.BlockTime().Unix()+16)
+ suite.Require().NoError(err2)
+ vva := validatorvesting.NewValidatorVestingAccountRaw(bva, ctx.BlockTime().Unix(), periods, sdk.ConsAddress{}, nil, 90)
+ ak.SetAccount(ctx, vva)
+ suite.app = tApp
+ suite.keeper = tApp.GetIncentiveKeeper()
+ suite.ctx = ctx
+ suite.addrs = addrs
+}
+
+func (suite *KeeperTestSuite) setupExpiredClaims() {
+ // creates a new app state with 4 funded addresses
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: time.Unix(100, 0)})
+ _, addrs := app.GeneratePrivKeyAddressPairs(4)
+ authGS := app.NewAuthGenState(
+ addrs,
+ []sdk.Coins{
+ cs(c("ukava", 400)),
+ cs(c("ukava", 400)),
+ cs(c("ukava", 400)),
+ cs(c("ukava", 400)),
+ })
+ tApp.InitializeFromGenesisStates(
+ authGS,
+ )
+
+ // creates two claim periods, one expired, and one that expires in the future
+ cp1 := types.NewClaimPeriod("bnb", 1, time.Unix(90, 0), time.Hour*8766)
+ cp2 := types.NewClaimPeriod("xrp", 1, time.Unix(110, 0), time.Hour*8766)
+ suite.keeper = tApp.GetIncentiveKeeper()
+ suite.keeper.SetClaimPeriod(ctx, cp1)
+ suite.keeper.SetClaimPeriod(ctx, cp2)
+ // creates one claim for the non-expired claim period and one claim for the expired claim period
+ c1 := types.NewClaim(addrs[0], c("ukava", 1000000), "bnb", 1)
+ c2 := types.NewClaim(addrs[0], c("ukava", 1000000), "xrp", 1)
+ suite.keeper.SetClaim(ctx, c1)
+ suite.keeper.SetClaim(ctx, c2)
+ suite.app = tApp
+ suite.ctx = ctx
+ suite.addrs = addrs
+}
+
+func (suite *KeeperTestSuite) TestSendCoinsToPeriodicVestingAccount() {
+ suite.setupChain()
+
+ type args struct {
+ coins sdk.Coins
+ length int64
+ }
+
+ type errArgs struct {
+ expectErr bool
+ errType error
+ }
+
+ type vestingAccountTest struct {
+ name string
+ blockTime time.Time
+ args args
+ errArgs errArgs
+ expectedPeriods vesting.Periods
+ expectedOriginalVesting sdk.Coins
+ expectedCoins sdk.Coins
+ expectedStartTime int64
+ expectedEndTime int64
+ }
+
+ type vestingAccountTests []vestingAccountTest
+
+ testCases := vestingAccountTests{
+ vestingAccountTest{
+ name: "insert period into an existing vesting schedule",
+ blockTime: time.Unix(100, 0),
+ args: args{coins: cs(c("ukava", 100)), length: 5},
+ errArgs: errArgs{expectErr: false, errType: nil},
+ expectedPeriods: vesting.Periods{
+ vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
+ },
+ expectedOriginalVesting: cs(c("ukava", 500)),
+ expectedCoins: cs(c("ukava", 500)),
+ expectedStartTime: int64(100),
+ expectedEndTime: int64(116),
+ },
+ vestingAccountTest{
+ name: "append period to the end of an existing vesting schedule",
+ blockTime: time.Unix(100, 0),
+ args: args{coins: cs(c("ukava", 100)), length: 17},
+ errArgs: errArgs{expectErr: false, errType: nil},
+ expectedPeriods: vesting.Periods{
+ vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
+ },
+ expectedOriginalVesting: cs(c("ukava", 600)),
+ expectedCoins: cs(c("ukava", 600)),
+ expectedStartTime: int64(100),
+ expectedEndTime: int64(117),
+ },
+ vestingAccountTest{
+ name: "append period to the end of a completed vesting schedule",
+ blockTime: time.Unix(120, 0),
+ args: args{coins: cs(c("ukava", 100)), length: 5},
+ errArgs: errArgs{expectErr: false, errType: nil},
+ expectedPeriods: vesting.Periods{
+ vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))},
+ },
+ expectedOriginalVesting: cs(c("ukava", 700)),
+ expectedCoins: cs(c("ukava", 700)),
+ expectedStartTime: int64(100),
+ expectedEndTime: int64(125),
+ },
+ vestingAccountTest{
+ name: "prepend period to to an upcoming vesting schedule",
+ blockTime: time.Unix(90, 0),
+ args: args{coins: cs(c("ukava", 100)), length: 5},
+ errArgs: errArgs{expectErr: false, errType: nil},
+ expectedPeriods: vesting.Periods{
+ vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))},
+ },
+ expectedOriginalVesting: cs(c("ukava", 800)),
+ expectedCoins: cs(c("ukava", 800)),
+ expectedStartTime: int64(90),
+ expectedEndTime: int64(125),
+ },
+ vestingAccountTest{
+ name: "add period that coincides with an existing end time",
+ blockTime: time.Unix(90, 0),
+ args: args{coins: cs(c("ukava", 100)), length: 11},
+ errArgs: errArgs{expectErr: false, errType: nil},
+ expectedPeriods: vesting.Periods{
+ vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(6), Amount: cs(c("ukava", 200))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
+ vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))},
+ },
+ expectedOriginalVesting: cs(c("ukava", 900)),
+ expectedCoins: cs(c("ukava", 900)),
+ expectedStartTime: int64(90),
+ expectedEndTime: int64(125),
+ },
+ vestingAccountTest{
+ name: "insufficient module account balance",
+ blockTime: time.Unix(90, 0),
+ args: args{coins: cs(c("ukava", 1000)), length: 11},
+ errArgs: errArgs{expectErr: true, errType: types.ErrInsufficientModAccountBalance},
+ expectedPeriods: vesting.Periods{},
+ expectedOriginalVesting: sdk.Coins{},
+ expectedCoins: sdk.Coins{},
+ expectedStartTime: int64(0),
+ expectedEndTime: int64(0),
+ },
+ }
+
+ for _, tc := range testCases {
+ suite.Run(tc.name, func() {
+ suite.ctx = suite.ctx.WithBlockTime(tc.blockTime)
+ err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[0], tc.args.coins, tc.args.length)
+ if tc.errArgs.expectErr {
+ suite.Require().True(errors.Is(err, tc.errArgs.errType))
+ } else {
+ suite.Require().NoError(err)
+ acc := suite.getAccount(suite.addrs[0])
+ vacc, ok := acc.(*vesting.PeriodicVestingAccount)
+ suite.True(ok)
+ suite.Equal(tc.expectedPeriods, vacc.VestingPeriods)
+ suite.Equal(tc.expectedOriginalVesting, vacc.OriginalVesting)
+ suite.Equal(tc.expectedCoins, vacc.Coins)
+ suite.Equal(tc.expectedStartTime, vacc.StartTime)
+ suite.Equal(tc.expectedEndTime, vacc.EndTime)
+ }
+ })
+ }
+}
+
+func (suite *KeeperTestSuite) TestSendCoinsToBaseAccount() {
+ suite.setupChain()
+ // send coins to base account
+ err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[1], cs(c("ukava", 100)), 5)
+ suite.Require().NoError(err)
+ acc := suite.getAccount(suite.addrs[1])
+ vacc, ok := acc.(*vesting.PeriodicVestingAccount)
+ suite.True(ok)
+ expectedPeriods := vesting.Periods{
+ vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
+ }
+ suite.Equal(expectedPeriods, vacc.VestingPeriods)
+ suite.Equal(cs(c("ukava", 100)), vacc.OriginalVesting)
+ suite.Equal(cs(c("ukava", 500)), vacc.Coins)
+ suite.Equal(int64(105), vacc.EndTime)
+ suite.Equal(int64(100), vacc.StartTime)
+
+}
+
+func (suite *KeeperTestSuite) TestSendCoinsToInvalidAccount() {
+ suite.setupChain()
+ err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[2], cs(c("ukava", 100)), 5)
+ suite.Require().True(errors.Is(err, types.ErrInvalidAccountType))
+ macc := suite.getModuleAccount(cdp.ModuleName)
+ err = suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, macc.GetAddress(), cs(c("ukava", 100)), 5)
+ suite.Require().True(errors.Is(err, types.ErrInvalidAccountType))
+}
+
+func (suite *KeeperTestSuite) TestPayoutClaim() {
+ suite.setupChain() // adds 3 accounts - 1 periodic vesting account, 1 base account, and 1 validator vesting account
+
+ // add 2 claims that correspond to an existing claim period and one claim that has no corresponding claim period
+ cp1 := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766)
+ suite.keeper.SetClaimPeriod(suite.ctx, cp1)
+ // valid claim for addrs[0]
+ c1 := types.NewClaim(suite.addrs[0], c("ukava", 100), "bnb", 1)
+ // invalid claim for addrs[0]
+ c2 := types.NewClaim(suite.addrs[0], c("ukava", 100), "xrp", 1)
+ // valid claim for addrs[1]
+ c3 := types.NewClaim(suite.addrs[1], c("ukava", 100), "bnb", 1)
+ suite.keeper.SetClaim(suite.ctx, c1)
+ suite.keeper.SetClaim(suite.ctx, c2)
+ suite.keeper.SetClaim(suite.ctx, c3)
+
+ // existing claim with corresponding claim period successfully claimed by existing periodic vesting account
+ err := suite.keeper.PayoutClaim(suite.ctx, suite.addrs[0], "bnb", 1)
+ suite.Require().NoError(err)
+ acc := suite.getAccount(suite.addrs[0])
+ // account is a periodic vesting account
+ vacc, ok := acc.(*vesting.PeriodicVestingAccount)
+ suite.True(ok)
+ // vesting balance is correct
+ suite.Equal(cs(c("ukava", 500)), vacc.OriginalVesting)
+
+ // existing claim with corresponding claim period successfully claimed by base account
+ err = suite.keeper.PayoutClaim(suite.ctx, suite.addrs[1], "bnb", 1)
+ suite.Require().NoError(err)
+ acc = suite.getAccount(suite.addrs[1])
+ // account has become a periodic vesting account
+ vacc, ok = acc.(*vesting.PeriodicVestingAccount)
+ suite.True(ok)
+ // vesting balance is correct
+ suite.Equal(cs(c("ukava", 100)), vacc.OriginalVesting)
+
+ // addrs[3] has no claims
+ err = suite.keeper.PayoutClaim(suite.ctx, suite.addrs[3], "bnb", 1)
+ suite.Require().True(errors.Is(err, types.ErrClaimNotFound))
+ // addrs[0] has an xrp claim, but there is not corresponding claim period
+ err = suite.keeper.PayoutClaim(suite.ctx, suite.addrs[0], "xrp", 1)
+ suite.Require().True(errors.Is(err, types.ErrClaimPeriodNotFound))
+}
+
+func (suite *KeeperTestSuite) TestDeleteExpiredClaimPeriods() {
+ suite.setupExpiredClaims() // creates new app state with one non-expired claim period (xrp) and one expired claim period (bnb) as well as a claim that corresponds to each claim period
+
+ // both claim periods are present
+ _, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
+ suite.True(found)
+ _, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "xrp")
+ suite.True(found)
+ // both claims are present
+ _, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
+ suite.True(found)
+ _, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "xrp", 1)
+ suite.True(found)
+
+ // expired claim period and associated claims should get deleted
+ suite.NotPanics(func() {
+ suite.keeper.DeleteExpiredClaimsAndClaimPeriods(suite.ctx)
+ })
+ // expired claim period and claim are not found
+ _, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
+ suite.False(found)
+ _, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
+ suite.False(found)
+ // non-expired claim period and claim are found
+ _, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "xrp")
+ suite.True(found)
+ _, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "xrp", 1)
+ suite.True(found)
+
+}
diff --git a/x/incentive/keeper/querier.go b/x/incentive/keeper/querier.go
new file mode 100644
index 00000000..f5a3654c
--- /dev/null
+++ b/x/incentive/keeper/querier.go
@@ -0,0 +1,51 @@
+package keeper
+
+import (
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+ "github.com/kava-labs/kava/x/incentive/types"
+ abci "github.com/tendermint/tendermint/abci/types"
+)
+
+// NewQuerier is the module level router for state queries
+func NewQuerier(k Keeper) sdk.Querier {
+ return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
+ switch path[0] {
+ case types.QueryGetParams:
+ return queryGetParams(ctx, req, k)
+ case types.QueryGetClaims:
+ return queryGetClaims(ctx, req, k)
+ default:
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
+ }
+ }
+}
+
+// query params in the store
+func queryGetParams(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
+ // Get params
+ params := k.GetParams(ctx)
+
+ // Encode results
+ bz, err := codec.MarshalJSONIndent(k.cdc, params)
+ if err != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
+ }
+ return bz, nil
+}
+
+func queryGetClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
+ var requestParams types.QueryClaimsParams
+ err := k.cdc.UnmarshalJSON(req.Data, &requestParams)
+ if err != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
+ }
+ claims, _ := k.GetClaimsByAddressAndDenom(ctx, requestParams.Owner, requestParams.Denom)
+
+ bz, err := codec.MarshalJSONIndent(k.cdc, claims)
+ if err != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
+ }
+ return bz, nil
+}
diff --git a/x/incentive/keeper/querier_test.go b/x/incentive/keeper/querier_test.go
new file mode 100644
index 00000000..156d0e8d
--- /dev/null
+++ b/x/incentive/keeper/querier_test.go
@@ -0,0 +1,32 @@
+package keeper_test
+
+import (
+ "strings"
+
+ "github.com/kava-labs/kava/x/incentive/keeper"
+ "github.com/kava-labs/kava/x/incentive/types"
+ abci "github.com/tendermint/tendermint/abci/types"
+)
+
+func (suite *KeeperTestSuite) TestQuerier() {
+ suite.addObjectsToStore()
+ querier := keeper.NewQuerier(suite.keeper)
+ bz, err := querier(suite.ctx, []string{types.QueryGetParams}, abci.RequestQuery{})
+ suite.Require().NoError(err)
+ suite.NotNil(bz)
+
+ var p types.Params
+ suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p))
+
+ claimQueryParams := types.NewQueryClaimsParams(suite.addrs[0], "bnb")
+ query := abci.RequestQuery{
+ Path: strings.Join([]string{"custom", types.QuerierRoute, types.QueryGetClaims}, "/"),
+ Data: types.ModuleCdc.MustMarshalJSON(claimQueryParams),
+ }
+ bz, err = querier(suite.ctx, []string{types.QueryGetClaims}, query)
+
+ var claims types.Claims
+ suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &claims))
+ suite.Equal(1, len(claims))
+ suite.Equal(types.Claims{types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)}, claims)
+}
diff --git a/x/incentive/keeper/rewards.go b/x/incentive/keeper/rewards.go
new file mode 100644
index 00000000..116c8755
--- /dev/null
+++ b/x/incentive/keeper/rewards.go
@@ -0,0 +1,110 @@
+package keeper
+
+import (
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/store/prefix"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ cdptypes "github.com/kava-labs/kava/x/cdp/types"
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+// HandleRewardPeriodExpiry deletes expired RewardPeriods from the store and creates a ClaimPeriod in the store for each expired RewardPeriod
+func (k Keeper) HandleRewardPeriodExpiry(ctx sdk.Context, rp types.RewardPeriod) {
+ k.CreateUniqueClaimPeriod(ctx, rp.Denom, rp.ClaimEnd, rp.ClaimTimeLock)
+ store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
+ store.Delete([]byte(rp.Denom))
+ return
+}
+
+// CreateNewRewardPeriod creates a new reward period from the input reward
+func (k Keeper) CreateNewRewardPeriod(ctx sdk.Context, reward types.Reward) {
+ // reward periods store the amount of rewards payed PER SECOND
+ rewardsPerSecond := sdk.NewDecFromInt(reward.AvailableRewards.Amount).Quo(sdk.NewDecFromInt(sdk.NewInt(int64(reward.Duration.Seconds())))).TruncateInt()
+ rewardCoinPerSecond := sdk.NewCoin(reward.AvailableRewards.Denom, rewardsPerSecond)
+ rp := types.RewardPeriod{
+ Denom: reward.Denom,
+ Start: ctx.BlockTime(),
+ End: ctx.BlockTime().Add(reward.Duration),
+ Reward: rewardCoinPerSecond,
+ ClaimEnd: ctx.BlockTime().Add(reward.Duration).Add(reward.ClaimDuration),
+ ClaimTimeLock: reward.TimeLock,
+ }
+ k.SetRewardPeriod(ctx, rp)
+}
+
+// CreateAndDeleteRewardPeriods creates reward periods for active rewards that don't already have a reward period and deletes reward periods for inactive rewards that currently have a reward period
+func (k Keeper) CreateAndDeleteRewardPeriods(ctx sdk.Context) {
+ params := k.GetParams(ctx)
+
+ for _, r := range params.Rewards {
+ _, found := k.GetRewardPeriod(ctx, r.Denom)
+ // if governance has made a reward inactive, delete the current period
+ if found && !r.Active {
+ k.DeleteRewardPeriod(ctx, r.Denom)
+ }
+ // if a reward period for an active reward is not found, create one
+ if !found && r.Active {
+ k.CreateNewRewardPeriod(ctx, r)
+ }
+ }
+}
+
+// ApplyRewardsToCdps iterates over the reward periods and creates a claim for each cdp owner that created usdx with the collateral specified in the reward period
+func (k Keeper) ApplyRewardsToCdps(ctx sdk.Context) {
+ previousBlockTime, found := k.GetPreviousBlockTime(ctx)
+ if !found {
+ previousBlockTime = ctx.BlockTime()
+ k.SetPreviousBlockTime(ctx, previousBlockTime)
+ return
+ }
+ k.IterateRewardPeriods(ctx, func(rp types.RewardPeriod) bool {
+ expired := false
+ // the total amount of usdx created with the collateral type being incentivized
+ totalPrincipal := k.cdpKeeper.GetTotalPrincipal(ctx, rp.Denom, types.PrincipalDenom)
+ // the number of seconds since last payout
+ timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix())
+ if rp.End.Before(ctx.BlockTime()) {
+ timeElapsed = sdk.NewInt(rp.End.Unix() - previousBlockTime.Unix())
+ expired = true
+ }
+ // the amount of rewards to pay (rewardAmount * timeElapsed)
+ rewardsThisPeriod := rp.Reward.Amount.Mul(timeElapsed)
+ id := k.GetNextClaimPeriodID(ctx, rp.Denom)
+ k.cdpKeeper.IterateCdpsByDenom(ctx, rp.Denom, func(cdp cdptypes.CDP) bool {
+ rewardsShare := sdk.NewDecFromInt(cdp.Principal.AmountOf(types.PrincipalDenom).Add(cdp.AccumulatedFees.AmountOf(types.PrincipalDenom))).Quo(sdk.NewDecFromInt(totalPrincipal))
+ // sanity check - don't create zero claims
+ if rewardsShare.IsZero() {
+ return false
+ }
+ rewardsEarned := rewardsShare.Mul(sdk.NewDecFromInt(rewardsThisPeriod)).RoundInt()
+ k.AddToClaim(ctx, cdp.Owner, rp.Denom, id, sdk.NewCoin(types.GovDenom, rewardsEarned))
+ return false
+ })
+ if !expired {
+ return false
+ }
+ k.HandleRewardPeriodExpiry(ctx, rp)
+ return false
+ })
+ k.SetPreviousBlockTime(ctx, ctx.BlockTime())
+}
+
+// CreateUniqueClaimPeriod creates a new claim period in the store and updates the highest claim period id
+func (k Keeper) CreateUniqueClaimPeriod(ctx sdk.Context, denom string, end time.Time, timeLock time.Duration) {
+ id := k.GetNextClaimPeriodID(ctx, denom)
+ claimPeriod := types.NewClaimPeriod(denom, id, end, timeLock)
+ k.SetClaimPeriod(ctx, claimPeriod)
+ k.SetNextClaimPeriodID(ctx, denom, id+1)
+}
+
+// AddToClaim adds the amount to an existing claim or creates a new one for that amount
+func (k Keeper) AddToClaim(ctx sdk.Context, addr sdk.AccAddress, denom string, id uint64, amount sdk.Coin) {
+ claim, found := k.GetClaim(ctx, addr, denom, id)
+ if found {
+ claim.Reward = claim.Reward.Add(amount)
+ } else {
+ claim = types.NewClaim(addr, amount, denom, id)
+ }
+ k.SetClaim(ctx, claim)
+}
diff --git a/x/incentive/keeper/rewards_test.go b/x/incentive/keeper/rewards_test.go
new file mode 100644
index 00000000..2bea860f
--- /dev/null
+++ b/x/incentive/keeper/rewards_test.go
@@ -0,0 +1,258 @@
+package keeper_test
+
+import (
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/cdp"
+ "github.com/kava-labs/kava/x/incentive/types"
+ "github.com/kava-labs/kava/x/pricefeed"
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+func (suite *KeeperTestSuite) TestExpireRewardPeriod() {
+ rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
+ suite.keeper.SetRewardPeriod(suite.ctx, rp)
+ suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
+ suite.NotPanics(func() {
+ suite.keeper.HandleRewardPeriodExpiry(suite.ctx, rp)
+ })
+ _, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
+ suite.True(found)
+}
+
+func (suite *KeeperTestSuite) TestAddToClaim() {
+ rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
+ suite.keeper.SetRewardPeriod(suite.ctx, rp)
+ suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
+ suite.keeper.HandleRewardPeriodExpiry(suite.ctx, rp)
+ c1 := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
+ suite.keeper.SetClaim(suite.ctx, c1)
+ suite.NotPanics(func() {
+ suite.keeper.AddToClaim(suite.ctx, suite.addrs[0], "bnb", 1, c("ukava", 1000000))
+ })
+ testC, _ := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
+ suite.Equal(c("ukava", 2000000), testC.Reward)
+
+ suite.NotPanics(func() {
+ suite.keeper.AddToClaim(suite.ctx, suite.addrs[0], "xpr", 1, c("ukava", 1000000))
+ })
+}
+
+func (suite *KeeperTestSuite) TestCreateRewardPeriod() {
+ reward := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
+ suite.NotPanics(func() {
+ suite.keeper.CreateNewRewardPeriod(suite.ctx, reward)
+ })
+ _, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
+ suite.True(found)
+}
+
+func (suite *KeeperTestSuite) TestCreateAndDeleteRewardsPeriods() {
+ reward1 := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
+ reward2 := types.NewReward(false, "xrp", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
+ reward3 := types.NewReward(false, "btc", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
+ // add a reward period to the store for a non-active reward
+ suite.NotPanics(func() {
+ suite.keeper.CreateNewRewardPeriod(suite.ctx, reward3)
+ })
+ params := types.NewParams(true, types.Rewards{reward1, reward2, reward3})
+ suite.keeper.SetParams(suite.ctx, params)
+
+ suite.NotPanics(func() {
+ suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
+ })
+ testCases := []struct {
+ name string
+ arg string
+ expectFound bool
+ }{
+ {
+ "active reward period",
+ "bnb",
+ true,
+ },
+ {
+ "attempt to add inactive reward period",
+ "xrp",
+ false,
+ },
+ {
+ "remove inactive reward period",
+ "btc",
+ false,
+ },
+ }
+ for _, tc := range testCases {
+ suite.Run(tc.name, func() {
+ _, found := suite.keeper.GetRewardPeriod(suite.ctx, tc.arg)
+ if tc.expectFound {
+ suite.True(found)
+ } else {
+ suite.False(found)
+ }
+ })
+ }
+}
+
+func (suite *KeeperTestSuite) TestApplyRewardsToCdps() {
+ suite.setupCdpChain() // creates a test app with 3 BNB cdps and usdx incentives for bnb - each reward period is one week
+
+ // move the context forward by 100 periods
+ suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 100))
+ // apply rewards to BNB cdps
+ suite.NotPanics(func() {
+ suite.keeper.ApplyRewardsToCdps(suite.ctx)
+ })
+ // each cdp should have a claim
+ claims := types.Claims{}
+ suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
+ claims = append(claims, c)
+ return false
+ })
+ suite.Equal(3, len(claims))
+ // there should be no associated claim period, because the reward period has not ended yet
+ _, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
+ suite.False(found)
+
+ // move ctx to the reward period expiry and check that the claim period has been created and the next claim period id has increased
+ suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 24 * 7))
+
+ suite.NotPanics(func() {
+ // apply rewards to cdps
+ suite.keeper.ApplyRewardsToCdps(suite.ctx)
+ // delete the old reward period amd create a new one
+ suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
+ })
+ _, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
+ suite.True(found)
+ testID := suite.keeper.GetNextClaimPeriodID(suite.ctx, "bnb")
+ suite.Equal(uint64(2), testID)
+
+ // move the context forward by 100 periods
+ suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 100))
+ // run the begin blocker functions
+ suite.NotPanics(func() {
+ suite.keeper.DeleteExpiredClaimsAndClaimPeriods(suite.ctx)
+ suite.keeper.ApplyRewardsToCdps(suite.ctx)
+ suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
+ })
+ // each cdp should now have two claims
+ claims = types.Claims{}
+ suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
+ claims = append(claims, c)
+ return false
+ })
+ suite.Equal(6, len(claims))
+}
+
+func (suite *KeeperTestSuite) setupCdpChain() {
+ // creates a new test app with bnb as the only asset the pricefeed and cdp modules
+ // funds three addresses and creates 3 cdps, funded with 100 BNB, 1000 BNB, and 10000 BNB
+ // each CDP draws 10, 100, and 1000 USDX respectively
+ // adds usdx incentives for bnb - 1000 KAVA per week with a 1 year time lock
+
+ tApp := app.NewTestApp()
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+ // need pricefeed and cdp gen state with one collateral
+ pricefeedGS := pricefeed.GenesisState{
+ Params: pricefeed.Params{
+ Markets: []pricefeed.Market{
+ pricefeed.Market{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
+ },
+ },
+ PostedPrices: []pricefeed.PostedPrice{
+ pricefeed.PostedPrice{
+ MarketID: "bnb:usd",
+ OracleAddress: sdk.AccAddress{},
+ Price: d("12.29"),
+ Expiry: time.Now().Add(100000 * time.Hour),
+ },
+ },
+ }
+ // need incentive params for one collateral
+ cdpGS := cdp.GenesisState{
+ Params: cdp.Params{
+ GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
+ SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
+ DebtAuctionThreshold: cdp.DefaultDebtThreshold,
+ SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
+ CollateralParams: cdp.CollateralParams{
+ {
+ Denom: "bnb",
+ LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
+ DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
+ StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
+ LiquidationPenalty: d("0.05"),
+ AuctionSize: i(10000000000),
+ Prefix: 0x20,
+ MarketID: "bnb:usd",
+ ConversionFactor: i(8),
+ },
+ },
+ DebtParams: cdp.DebtParams{
+ {
+ Denom: "usdx",
+ ReferenceAsset: "usd",
+ ConversionFactor: i(6),
+ DebtFloor: i(10000000),
+ SavingsRate: d("0.95"),
+ },
+ },
+ },
+ StartingCdpID: cdp.DefaultCdpStartingID,
+ DebtDenom: cdp.DefaultDebtDenom,
+ GovDenom: cdp.DefaultGovDenom,
+ CDPs: cdp.CDPs{},
+ PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
+ }
+ incentiveGS := types.NewGenesisState(
+ types.NewParams(
+ true, types.Rewards{types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)},
+ ),
+ types.DefaultPreviousBlockTime,
+ types.RewardPeriods{types.NewRewardPeriod("bnb", ctx.BlockTime(), ctx.BlockTime().Add(time.Hour*7*24), c("ukava", 1000), ctx.BlockTime().Add(time.Hour*7*24*2), time.Hour*365*24)},
+ types.ClaimPeriods{},
+ types.Claims{},
+ types.GenesisClaimPeriodIDs{})
+ pricefeedAppGs := app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)}
+ cdpAppGs := app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGS)}
+ incentiveAppGs := app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(incentiveGS)}
+ _, addrs := app.GeneratePrivKeyAddressPairs(3)
+ authGS := app.NewAuthGenState(
+ addrs[0:3],
+ []sdk.Coins{
+ cs(c("bnb", 10000000000)),
+ cs(c("bnb", 100000000000)),
+ cs(c("bnb", 1000000000000)),
+ })
+ tApp.InitializeFromGenesisStates(
+ authGS,
+ pricefeedAppGs,
+ incentiveAppGs,
+ cdpAppGs,
+ )
+ suite.app = tApp
+ suite.keeper = tApp.GetIncentiveKeeper()
+ suite.ctx = ctx
+ // create 3 cdps
+ cdpKeeper := tApp.GetCDPKeeper()
+ err := cdpKeeper.AddCdp(suite.ctx, addrs[0], cs(c("bnb", 10000000000)), cs(c("usdx", 10000000)))
+ suite.Require().NoError(err)
+ err = cdpKeeper.AddCdp(suite.ctx, addrs[1], cs(c("bnb", 100000000000)), cs(c("usdx", 100000000)))
+ suite.Require().NoError(err)
+ err = cdpKeeper.AddCdp(suite.ctx, addrs[2], cs(c("bnb", 1000000000000)), cs(c("usdx", 1000000000)))
+ suite.Require().NoError(err)
+ // total usd is 1110
+
+ // set the previous block time
+ suite.keeper.SetPreviousBlockTime(suite.ctx, suite.ctx.BlockTime())
+}
+
+// Avoid cluttering test cases with long function names
+func i(in int64) sdk.Int { return sdk.NewInt(in) }
+func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
+func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
+func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
diff --git a/x/incentive/module.go b/x/incentive/module.go
new file mode 100644
index 00000000..9c464e00
--- /dev/null
+++ b/x/incentive/module.go
@@ -0,0 +1,167 @@
+package incentive
+
+import (
+ "encoding/json"
+ "math/rand"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/module"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ sim "github.com/cosmos/cosmos-sdk/x/simulation"
+ "github.com/gorilla/mux"
+ "github.com/kava-labs/kava/x/incentive/client/cli"
+ "github.com/kava-labs/kava/x/incentive/client/rest"
+ "github.com/kava-labs/kava/x/incentive/keeper"
+ "github.com/kava-labs/kava/x/incentive/simulation"
+ "github.com/kava-labs/kava/x/incentive/types"
+ "github.com/spf13/cobra"
+ abci "github.com/tendermint/tendermint/abci/types"
+)
+
+var (
+ _ module.AppModule = AppModule{}
+ _ module.AppModuleBasic = AppModuleBasic{}
+ _ module.AppModuleSimulation = AppModule{}
+)
+
+// AppModuleBasic defines the basic application module used by the incentive module.
+type AppModuleBasic struct{}
+
+// Name returns the incentive module's name.
+func (AppModuleBasic) Name() string {
+ return types.ModuleName
+}
+
+// RegisterCodec registers the incentive module's types for the given codec.
+func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
+ types.RegisterCodec(cdc)
+}
+
+// DefaultGenesis returns default genesis state as raw bytes for the incentive
+// module.
+func (AppModuleBasic) DefaultGenesis() json.RawMessage {
+ return types.ModuleCdc.MustMarshalJSON(types.DefaultGenesisState())
+}
+
+// ValidateGenesis performs genesis state validation for the incentive module.
+func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
+ var gs types.GenesisState
+ err := types.ModuleCdc.UnmarshalJSON(bz, &gs)
+ if err != nil {
+ return err
+ }
+ return gs.Validate()
+}
+
+// RegisterRESTRoutes registers the REST routes for the incentive module.
+func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
+ rest.RegisterRoutes(ctx, rtr)
+}
+
+// GetTxCmd returns the root tx command for the incentive module.
+func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
+ return cli.GetTxCmd(cdc)
+}
+
+// GetQueryCmd returns no root query command for the crisis module.
+func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
+ return cli.GetQueryCmd(types.StoreKey, cdc)
+}
+
+// RegisterStoreDecoder registers a decoder for cdp module's types
+func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
+ sdr[StoreKey] = simulation.DecodeStore
+}
+
+// GenerateGenesisState creates a randomized GenState of the cdp module
+func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
+ simulation.RandomizedGenState(simState)
+}
+
+// RandomizedParams creates randomized cdp param changes for the simulator.
+func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
+ return simulation.ParamChanges(r)
+}
+
+// ProposalContents doesn't return any content functions for governance proposals.
+func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
+ return nil
+}
+
+// WeightedOperations returns the all the bep3 module operations with their respective weights.
+func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
+ return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.supplyKeeper, am.keeper)
+}
+
+// AppModule implements the sdk.AppModule interface.
+type AppModule struct {
+ AppModuleBasic
+
+ keeper Keeper
+ accountKeeper auth.AccountKeeper
+ supplyKeeper SupplyKeeper
+}
+
+// NewAppModule creates a new AppModule object
+func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper, supplyKeeper SupplyKeeper) AppModule {
+ return AppModule{
+ AppModuleBasic: AppModuleBasic{},
+ keeper: keeper,
+ accountKeeper: accountKeeper,
+ supplyKeeper: supplyKeeper,
+ }
+}
+
+// Name returns the incentive module's name.
+func (AppModule) Name() string {
+ return types.ModuleName
+}
+
+// RegisterInvariants registers the incentive module invariants.
+func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
+
+// Route returns the message routing key for the incentive module.
+func (AppModule) Route() string {
+ return types.RouterKey
+}
+
+// NewHandler returns an sdk.Handler for the incentive module.
+func (am AppModule) NewHandler() sdk.Handler {
+ return NewHandler(am.keeper)
+}
+
+// QuerierRoute returns the incentive module's querier route name.
+func (AppModule) QuerierRoute() string {
+ return types.QuerierRoute
+}
+
+// NewQuerierHandler returns the incentive module sdk.Querier.
+func (am AppModule) NewQuerierHandler() sdk.Querier {
+ return keeper.NewQuerier(am.keeper)
+}
+
+// InitGenesis performs genesis initialization for the incentive module. It returns no validator updates.
+func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
+ var gs types.GenesisState
+ types.ModuleCdc.MustUnmarshalJSON(data, &gs)
+ InitGenesis(ctx, am.keeper, am.supplyKeeper, gs)
+ return []abci.ValidatorUpdate{}
+}
+
+// ExportGenesis returns the exported genesis state as raw bytes for the incentive module
+func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
+ gs := ExportGenesis(ctx, am.keeper)
+ return types.ModuleCdc.MustMarshalJSON(gs)
+}
+
+// BeginBlock returns the begin blocker for the incentive module.
+func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
+ BeginBlocker(ctx, am.keeper)
+}
+
+// EndBlock returns the end blocker for the incentive module. It returns no validator updates.
+func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
+ return []abci.ValidatorUpdate{}
+}
diff --git a/x/incentive/simulation/decoder.go b/x/incentive/simulation/decoder.go
new file mode 100644
index 00000000..dda10310
--- /dev/null
+++ b/x/incentive/simulation/decoder.go
@@ -0,0 +1,50 @@
+package simulation
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+// DecodeStore unmarshals the KVPair's Value to the module's corresponding type
+func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
+ switch {
+ case bytes.Equal(kvA.Key[:1], types.RewardPeriodKeyPrefix):
+ var rewardPeriodA, rewardPeriodB types.RewardPeriod
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &rewardPeriodA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &rewardPeriodB)
+ return fmt.Sprintf("%v\n%v", rewardPeriodA, rewardPeriodB)
+
+ case bytes.Equal(kvA.Key[:1], types.ClaimPeriodKeyPrefix):
+ var claimPeriodA, claimPeriodB types.ClaimPeriod
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &claimPeriodA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &claimPeriodB)
+ return fmt.Sprintf("%v\n%v", claimPeriodA, claimPeriodB)
+
+ case bytes.Equal(kvA.Key[:1], types.ClaimKeyPrefix):
+ var claimA, claimB types.Claim
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &claimA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &claimB)
+ return fmt.Sprintf("%v\n%v", claimA, claimB)
+
+ case bytes.Equal(kvA.Key[:1], types.NextClaimPeriodIDPrefix):
+ claimPeriodIDA := binary.BigEndian.Uint64(kvA.Value)
+ claimPeriodIDB := binary.BigEndian.Uint64(kvB.Value)
+ return fmt.Sprintf("%d\n%d", claimPeriodIDA, claimPeriodIDB)
+
+ case bytes.Equal(kvA.Key[:1], types.PreviousBlockTimeKey):
+ var timeA, timeB time.Time
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &timeA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &timeB)
+ return fmt.Sprintf("%s\n%s", timeA, timeB)
+
+ default:
+ panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
+ }
+}
diff --git a/x/incentive/simulation/decoder_test.go b/x/incentive/simulation/decoder_test.go
new file mode 100644
index 00000000..7eb08ebd
--- /dev/null
+++ b/x/incentive/simulation/decoder_test.go
@@ -0,0 +1,65 @@
+package simulation
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/stretchr/testify/require"
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+func makeTestCodec() (cdc *codec.Codec) {
+ cdc = codec.New()
+ sdk.RegisterCodec(cdc)
+ types.RegisterCodec(cdc)
+ return
+}
+
+func TestDecodeDistributionStore(t *testing.T) {
+ cdc := makeTestCodec()
+
+ // Set up RewardPeriod, ClaimPeriod, Claim, and previous block time
+ rewardPeriod := types.NewRewardPeriod("btc", time.Now().UTC(), time.Now().Add(time.Hour*1).UTC(),
+ sdk.NewCoin("ukava", sdk.NewInt(10000000000)), time.Now().Add(time.Hour*2).UTC(), time.Duration(time.Hour*2))
+ claimPeriod := types.NewClaimPeriod("btc", 1, time.Now().Add(time.Hour*24).UTC(), time.Duration(time.Hour*24))
+ addr, _ := sdk.AccAddressFromBech32("kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw")
+ claim := types.NewClaim(addr, sdk.NewCoin("ukava", sdk.NewInt(1000000)), "bnb", 1)
+ prevBlockTime := time.Now().Add(time.Hour * -1).UTC()
+
+ kvPairs := kv.Pairs{
+ kv.Pair{Key: types.RewardPeriodKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&rewardPeriod)},
+ kv.Pair{Key: types.ClaimPeriodKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&claimPeriod)},
+ kv.Pair{Key: types.ClaimKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&claim)},
+ kv.Pair{Key: types.NextClaimPeriodIDPrefix, Value: sdk.Uint64ToBigEndian(10)},
+ kv.Pair{Key: []byte(types.PreviousBlockTimeKey), Value: cdc.MustMarshalBinaryLengthPrefixed(prevBlockTime)},
+ kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
+ }
+
+ tests := []struct {
+ name string
+ expectedLog string
+ }{
+ {"RewardPeriod", fmt.Sprintf("%v\n%v", rewardPeriod, rewardPeriod)},
+ {"ClaimPeriod", fmt.Sprintf("%v\n%v", claimPeriod, claimPeriod)},
+ {"Claim", fmt.Sprintf("%v\n%v", claim, claim)},
+ {"NextClaimPeriodID", "10\n10"},
+ {"PreviousBlockTime", fmt.Sprintf("%v\n%v", prevBlockTime, prevBlockTime)},
+ {"other", ""},
+ }
+ for i, tt := range tests {
+ i, tt := i, tt
+ t.Run(tt.name, func(t *testing.T) {
+ switch i {
+ case len(tests) - 1:
+ require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
+ default:
+ require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
+ }
+ })
+ }
+}
diff --git a/x/incentive/simulation/genesis.go b/x/incentive/simulation/genesis.go
new file mode 100644
index 00000000..88a28bd6
--- /dev/null
+++ b/x/incentive/simulation/genesis.go
@@ -0,0 +1,140 @@
+package simulation
+
+import (
+ "fmt"
+ "math/rand"
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/module"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+var (
+ CollateralDenoms = [3]string{"bnb", "xrp", "btc"}
+ RewardDenom = "ukava"
+ MaxTotalAssetReward = sdk.NewInt(1000000000)
+)
+
+// RandomizedGenState generates a random GenesisState for incentive module
+func RandomizedGenState(simState *module.SimulationState) {
+ params := genParams(simState.Rand)
+ rewardPeriods := genRewardPeriods(simState.Rand, simState.GenTimestamp, params.Rewards)
+ claimPeriods := genClaimPeriods(rewardPeriods)
+ claimPeriodIDs := genNextClaimPeriodIds(claimPeriods)
+
+ // New genesis state holds valid, linked reward periods, claim periods, and claim period IDs
+ incentiveGenesis := types.NewGenesisState(params, types.DefaultPreviousBlockTime,
+ rewardPeriods, claimPeriods, types.Claims{}, claimPeriodIDs)
+ if err := incentiveGenesis.Validate(); err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, incentiveGenesis))
+ simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(incentiveGenesis)
+}
+
+// genParams generates random rewards and is active by default
+func genParams(r *rand.Rand) types.Params {
+ params := types.NewParams(true, genRewards(r))
+ if err := params.Validate(); err != nil {
+ panic(err)
+ }
+ return params
+}
+
+// genRewards generates rewards for each specified collateral type
+func genRewards(r *rand.Rand) types.Rewards {
+ var rewards types.Rewards
+ for _, denom := range CollateralDenoms {
+ active := true
+ // total reward is in range (half max total reward, max total reward)
+ amount := simulation.RandIntBetween(r, int(MaxTotalAssetReward.Int64()/2), int(MaxTotalAssetReward.Int64()))
+ totalRewards := sdk.NewInt64Coin(RewardDenom, int64(amount))
+ // generate a random number of hours between 6-48 to use for reward's times
+ numbHours := simulation.RandIntBetween(r, 6, 48)
+ duration := time.Duration(time.Hour * time.Duration(numbHours))
+ timeLock := time.Duration(time.Hour * time.Duration(numbHours/2)) // half as long as duration
+ claimDuration := time.Hour * time.Duration(numbHours*2) // twice as long as duration
+ reward := types.NewReward(active, denom, totalRewards, duration, timeLock, claimDuration)
+ rewards = append(rewards, reward)
+ }
+ return rewards
+}
+
+// genRewardPeriods generates chronological reward periods for each given reward type
+func genRewardPeriods(r *rand.Rand, timestamp time.Time, rewards types.Rewards) types.RewardPeriods {
+ var rewardPeriods types.RewardPeriods
+ for _, reward := range rewards {
+ rewardPeriodStart := timestamp
+ for i := 10; i >= simulation.RandIntBetween(r, 2, 9); i-- {
+ // Set up reward period parameters
+ start := rewardPeriodStart
+ end := start.Add(reward.Duration).UTC()
+ baseRewardAmount := reward.AvailableRewards.Amount.Quo(sdk.NewInt(100)) // base period reward is 1/100 total reward
+ // Earlier periods have larger rewards
+ amount := sdk.NewCoin(reward.Denom, baseRewardAmount.Mul(sdk.NewInt(int64(i))))
+ claimEnd := end.Add(reward.ClaimDuration)
+ claimTimeLock := reward.TimeLock
+ // Create reward period and append to array
+ rewardPeriod := types.NewRewardPeriod(reward.Denom, start, end, amount, claimEnd, claimTimeLock)
+ rewardPeriods = append(rewardPeriods, rewardPeriod)
+ // Update start time of next reward period
+ rewardPeriodStart = end
+ }
+ }
+ return rewardPeriods
+}
+
+// genClaimPeriods loads valid claim periods for an array of reward periods
+func genClaimPeriods(rewardPeriods types.RewardPeriods) types.ClaimPeriods {
+ denomRewardPeriodsCount := make(map[string]uint64)
+ var claimPeriods types.ClaimPeriods
+ for _, rewardPeriod := range rewardPeriods {
+ // Increment reward period count for this denom (this is our claim period's ID)
+ denom := rewardPeriod.Denom
+ numbRewardPeriods := denomRewardPeriodsCount[denom] + 1
+ denomRewardPeriodsCount[denom] = numbRewardPeriods
+ // Set end and timelock from the associated reward period
+ end := rewardPeriod.ClaimEnd
+ claimTimeLock := rewardPeriod.ClaimTimeLock
+ // Create the new claim period for this reward period
+ claimPeriod := types.NewClaimPeriod(denom, numbRewardPeriods, end, claimTimeLock)
+ claimPeriods = append(claimPeriods, claimPeriod)
+ }
+ return claimPeriods
+}
+
+// genNextClaimPeriodIds returns an array of the most recent claim period IDs for each denom
+func genNextClaimPeriodIds(cps types.ClaimPeriods) types.GenesisClaimPeriodIDs {
+ // Build a map of the most recent claim periods by denom
+ mostRecentClaimPeriodByDenom := make(map[string]uint64)
+ for _, cp := range cps {
+ if cp.ID > mostRecentClaimPeriodByDenom[cp.Denom] {
+ mostRecentClaimPeriodByDenom[cp.Denom] = cp.ID
+ }
+ }
+ // Write map contents to an array of GenesisClaimPeriodIDs
+ var claimPeriodIDs types.GenesisClaimPeriodIDs
+ for key, value := range mostRecentClaimPeriodByDenom {
+ claimPeriodID := types.GenesisClaimPeriodID{Denom: key, ID: value}
+ claimPeriodIDs = append(claimPeriodIDs, claimPeriodID)
+ }
+ return claimPeriodIDs
+}
+
+// In a list of accounts, replace the first account found with the same address. If not found, append the account.
+func replaceOrAppendAccount(accounts []authexported.GenesisAccount, acc authexported.GenesisAccount) []authexported.GenesisAccount {
+ newAccounts := accounts
+ for i, a := range accounts {
+ if a.GetAddress().Equals(acc.GetAddress()) {
+ newAccounts[i] = acc
+ return newAccounts
+ }
+ }
+ return append(newAccounts, acc)
+}
diff --git a/x/incentive/simulation/operations.go b/x/incentive/simulation/operations.go
new file mode 100644
index 00000000..03e143d8
--- /dev/null
+++ b/x/incentive/simulation/operations.go
@@ -0,0 +1,146 @@
+package simulation
+
+import (
+ "fmt"
+ "math/rand"
+
+ "github.com/cosmos/cosmos-sdk/baseapp"
+ "github.com/cosmos/cosmos-sdk/codec"
+ "github.com/cosmos/cosmos-sdk/simapp/helpers"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ "github.com/cosmos/cosmos-sdk/x/auth/vesting"
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+
+ appparams "github.com/kava-labs/kava/app/params"
+ "github.com/kava-labs/kava/x/incentive/keeper"
+ "github.com/kava-labs/kava/x/incentive/types"
+ "github.com/kava-labs/kava/x/kavadist"
+)
+
+// Simulation operation weights constants
+const (
+ OpWeightMsgClaimReward = "op_weight_msg_claim_reward"
+)
+
+// WeightedOperations returns all the operations from the module with their respective weights
+func WeightedOperations(
+ appParams simulation.AppParams, cdc *codec.Codec, ak auth.AccountKeeper, sk types.SupplyKeeper, k keeper.Keeper,
+) simulation.WeightedOperations {
+ var weightMsgClaimReward int
+
+ appParams.GetOrGenerate(cdc, OpWeightMsgClaimReward, &weightMsgClaimReward, nil,
+ func(_ *rand.Rand) {
+ weightMsgClaimReward = appparams.DefaultWeightMsgClaimReward
+ },
+ )
+
+ return simulation.WeightedOperations{
+ simulation.NewWeightedOperation(
+ weightMsgClaimReward,
+ SimulateMsgClaimReward(ak, sk, k),
+ ),
+ }
+}
+
+// SimulateMsgClaimReward generates a MsgClaimReward
+func SimulateMsgClaimReward(ak auth.AccountKeeper, sk types.SupplyKeeper, k keeper.Keeper) simulation.Operation {
+ return func(
+ r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
+ ) (simulation.OperationMsg, []simulation.FutureOperation, error) {
+
+ // Load only account types that can claim rewards
+ var accounts []authexported.Account
+ validAccounts := make(map[string]bool)
+ for _, acc := range accs {
+ account := ak.GetAccount(ctx, acc.Address)
+ switch account.(type) {
+ case *vesting.PeriodicVestingAccount, *auth.BaseAccount: // Valid: BaseAccount, PeriodicVestingAccount
+ accounts = append(accounts, account)
+ validAccounts[account.GetAddress().String()] = true
+ break
+ default: // Invalid: ValidatorVestingAccount, DelayedVestingAccount, ContinuousVestingAccount
+ break
+ }
+ }
+
+ // Load open claims and shuffle them to randomize
+ openClaims := types.Claims{}
+ k.IterateClaims(ctx, func(claim types.Claim) bool {
+ openClaims = append(openClaims, claim)
+ return false
+ })
+ r.Shuffle(len(openClaims), func(i, j int) {
+ openClaims[i], openClaims[j] = openClaims[j], openClaims[i]
+ })
+
+ kavadistMacc := sk.GetModuleAccount(ctx, kavadist.KavaDistMacc)
+ kavadistBalance := kavadistMacc.SpendableCoins(ctx.BlockTime())
+
+ // Find address that has a claim of the same reward denom, then confirm it's distributable
+ claimer, claim, found := findValidAccountClaimPair(accs, openClaims, func(acc simulation.Account, claim types.Claim) bool {
+ if validAccounts[acc.Address.String()] { // Address must be valid type
+ if claim.Owner.Equals(acc.Address) { // Account must be claim owner
+ allClaims, found := k.GetClaimsByAddressAndDenom(ctx, claim.Owner, claim.Denom)
+ if found { // found should always be true
+ var rewards sdk.Coins
+ for _, individualClaim := range allClaims {
+ rewards = rewards.Add(individualClaim.Reward)
+ }
+ if rewards.AmountOf(claim.Reward.Denom).IsPositive() { // Can't distribute 0 coins
+ // Validate that kavadist module has enough coins to distribute rewards
+ if kavadistBalance.AmountOf(claim.Reward.Denom).GTE(rewards.AmountOf(claim.Reward.Denom)) {
+ return true
+ }
+ }
+ }
+ }
+ }
+ return false
+ })
+ if !found {
+ return simulation.NewOperationMsgBasic(types.ModuleName,
+ "no-operation (no accounts currently have fulfillable claims)", "", false, nil), nil, nil
+ }
+
+ claimerAcc := ak.GetAccount(ctx, claimer.Address)
+ if claimerAcc == nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("couldn't find account %s", claimer.Address)
+ }
+
+ msg := types.NewMsgClaimReward(claimer.Address, claim.Denom)
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ sdk.NewCoins(),
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{claimerAcc.GetAccountNumber()},
+ []uint64{claimerAcc.GetSequence()},
+ claimer.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ // to aid debugging, add the stack trace to the comment field of the returned opMsg
+ return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
+ }
+
+ // to aid debugging, add the result log to the comment field
+ return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
+ }
+}
+
+// findValidAccountClaimPair finds an account and reward claim for which the callback func returns true
+func findValidAccountClaimPair(accounts []simulation.Account, claims types.Claims,
+ cb func(simulation.Account, types.Claim) bool) (simulation.Account, types.Claim, bool) {
+ for _, claim := range claims {
+ for _, acc := range accounts {
+ if isValid := cb(acc, claim); isValid {
+ return acc, claim, true
+ }
+ }
+ }
+ return simulation.Account{}, types.Claim{}, false
+}
diff --git a/x/incentive/simulation/params.go b/x/incentive/simulation/params.go
new file mode 100644
index 00000000..dd47e83f
--- /dev/null
+++ b/x/incentive/simulation/params.go
@@ -0,0 +1,40 @@
+package simulation
+
+import (
+ "fmt"
+ "math/rand"
+
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+ "github.com/kava-labs/kava/x/incentive/types"
+)
+
+const (
+ keyActive = "Active"
+ keyRewards = "Rewards"
+)
+
+// genActive generates active bool with 80% chance of true
+func genActive(r *rand.Rand) bool {
+ threshold := 80
+ value := simulation.RandIntBetween(r, 1, 100)
+ if value > threshold {
+ return false
+ }
+ return true
+}
+
+// ParamChanges defines the parameters that can be modified by param change proposals
+func ParamChanges(r *rand.Rand) []simulation.ParamChange {
+ return []simulation.ParamChange{
+ simulation.NewSimParamChange(types.ModuleName, keyActive,
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("\"%t\"", genActive(r))
+ },
+ ),
+ simulation.NewSimParamChange(types.ModuleName, keyRewards,
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("\"%v\"", genRewards(r))
+ },
+ ),
+ }
+}
diff --git a/x/incentive/types/account.go b/x/incentive/types/account.go
new file mode 100644
index 00000000..d651d7ee
--- /dev/null
+++ b/x/incentive/types/account.go
@@ -0,0 +1,14 @@
+package types
+
+import (
+ "github.com/cosmos/cosmos-sdk/x/auth/vesting"
+)
+
+// GetTotalVestingPeriodLength returns the summed length of all vesting periods
+func GetTotalVestingPeriodLength(periods vesting.Periods) int64 {
+ length := int64(0)
+ for _, period := range periods {
+ length += period.Length
+ }
+ return length
+}
diff --git a/x/incentive/types/account_test.go b/x/incentive/types/account_test.go
new file mode 100644
index 00000000..525a3d02
--- /dev/null
+++ b/x/incentive/types/account_test.go
@@ -0,0 +1,51 @@
+package types_test
+
+import (
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/auth/vesting"
+ "github.com/kava-labs/kava/x/incentive/types"
+ "github.com/stretchr/testify/suite"
+)
+
+type accountTest struct {
+ periods vesting.Periods
+ expectedVal int64
+}
+
+type AccountTestSuite struct {
+ suite.Suite
+
+ tests []accountTest
+}
+
+func (suite *AccountTestSuite) SetupTest() {
+ tests := []accountTest{
+ accountTest{
+ periods: vesting.Periods{
+ vesting.Period{
+ Length: int64(100),
+ Amount: sdk.Coins{},
+ },
+ vesting.Period{
+ Length: int64(200),
+ Amount: sdk.Coins{},
+ },
+ },
+ expectedVal: int64(300),
+ },
+ }
+ suite.tests = tests
+}
+
+func (suite *AccountTestSuite) TestGetTotalPeriodLength() {
+ for _, t := range suite.tests {
+ length := types.GetTotalVestingPeriodLength(t.periods)
+ suite.Equal(t.expectedVal, length)
+ }
+}
+
+func TestAccountTestSuite(t *testing.T) {
+ suite.Run(t, new(AccountTestSuite))
+}
diff --git a/x/incentive/types/codec.go b/x/incentive/types/codec.go
new file mode 100644
index 00000000..31f066e0
--- /dev/null
+++ b/x/incentive/types/codec.go
@@ -0,0 +1,22 @@
+package types
+
+import "github.com/cosmos/cosmos-sdk/codec"
+
+// ModuleCdc generic sealed codec to be used throughout module
+var ModuleCdc *codec.Codec
+
+func init() {
+ cdc := codec.New()
+ RegisterCodec(cdc)
+ codec.RegisterCrypto(cdc)
+ ModuleCdc = cdc.Seal()
+}
+
+// RegisterCodec registers the necessary types for incentive module
+func RegisterCodec(cdc *codec.Codec) {
+ cdc.RegisterConcrete(MsgClaimReward{}, "incentive/MsgClaimReward", nil)
+ cdc.RegisterConcrete(GenesisClaimPeriodID{}, "incentive/GenesisClaimPeriodID", nil)
+ cdc.RegisterConcrete(RewardPeriod{}, "incentive/RewardPeriod", nil)
+ cdc.RegisterConcrete(ClaimPeriod{}, "incentive/ClaimPeriod", nil)
+ cdc.RegisterConcrete(Claim{}, "incentive/Claim", nil)
+}
diff --git a/x/incentive/types/errors.go b/x/incentive/types/errors.go
new file mode 100644
index 00000000..f42cb5b6
--- /dev/null
+++ b/x/incentive/types/errors.go
@@ -0,0 +1,15 @@
+package types
+
+import (
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+)
+
+// DONTCOVER
+
+var (
+ ErrClaimNotFound = sdkerrors.Register(ModuleName, 1, "no claim with input id found for owner and denom")
+ ErrClaimPeriodNotFound = sdkerrors.Register(ModuleName, 2, "no claim period found for id and denom")
+ ErrInvalidAccountType = sdkerrors.Register(ModuleName, 3, "account type not supported")
+ ErrNoClaimsFound = sdkerrors.Register(ModuleName, 4, "no claims with denom found for address")
+ ErrInsufficientModAccountBalance = sdkerrors.Register(ModuleName, 5, "module account has insufficient balance to pay claim")
+)
diff --git a/x/incentive/types/events.go b/x/incentive/types/events.go
new file mode 100644
index 00000000..09d0cd47
--- /dev/null
+++ b/x/incentive/types/events.go
@@ -0,0 +1,8 @@
+package types
+
+const (
+ EventTypeClaim = "claim_reward"
+
+ AttributeValueCategory = ModuleName
+ AttributeKeySender = "sender"
+)
diff --git a/x/incentive/types/expected_keepers.go b/x/incentive/types/expected_keepers.go
new file mode 100644
index 00000000..ccb20830
--- /dev/null
+++ b/x/incentive/types/expected_keepers.go
@@ -0,0 +1,27 @@
+package types
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
+ supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
+ cdptypes "github.com/kava-labs/kava/x/cdp/types"
+)
+
+// SupplyKeeper defines the expected supply keeper for module accounts
+type SupplyKeeper interface {
+ GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI
+
+ SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
+}
+
+// CdpKeeper defines the expected cdp keeper for interacting with cdps
+type CdpKeeper interface {
+ IterateCdpsByDenom(ctx sdk.Context, denom string, cb func(cdp cdptypes.CDP) (stop bool))
+ GetTotalPrincipal(ctx sdk.Context, collateralDenom string, principalDenom string) (total sdk.Int)
+}
+
+// AccountKeeper defines the expected keeper interface for interacting with account
+type AccountKeeper interface {
+ GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
+ SetAccount(ctx sdk.Context, acc authexported.Account)
+}
diff --git a/x/incentive/types/genesis.go b/x/incentive/types/genesis.go
new file mode 100644
index 00000000..d73d1b5e
--- /dev/null
+++ b/x/incentive/types/genesis.go
@@ -0,0 +1,75 @@
+package types
+
+import (
+ "bytes"
+ "fmt"
+ "time"
+)
+
+// GenesisClaimPeriodID stores the next claim id and its corresponding denom
+type GenesisClaimPeriodID struct {
+ Denom string `json:"denom" yaml:"denom"`
+ ID uint64 `json:"id" yaml:"id"`
+}
+
+// GenesisClaimPeriodIDs array of GenesisClaimPeriodID
+type GenesisClaimPeriodIDs []GenesisClaimPeriodID
+
+// GenesisState is the state that must be provided at genesis.
+type GenesisState struct {
+ Params Params `json:"params" yaml:"params"`
+ PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
+ RewardPeriods RewardPeriods `json:"reward_periods" yaml:"reward_periods"`
+ ClaimPeriods ClaimPeriods `json:"claim_periods" yaml:"claim_periods"`
+ Claims Claims `json:"claims" yaml:"claims"`
+ NextClaimPeriodIDs GenesisClaimPeriodIDs `json:"next_claim_period_ids" yaml:"next_claim_period_ids"`
+}
+
+// NewGenesisState returns a new genesis state
+func NewGenesisState(params Params, previousBlockTime time.Time, rp RewardPeriods, cp ClaimPeriods, c Claims, ids GenesisClaimPeriodIDs) GenesisState {
+ return GenesisState{
+ Params: params,
+ PreviousBlockTime: previousBlockTime,
+ RewardPeriods: rp,
+ ClaimPeriods: cp,
+ Claims: c,
+ NextClaimPeriodIDs: ids,
+ }
+}
+
+// DefaultGenesisState returns a default genesis state
+func DefaultGenesisState() GenesisState {
+ return GenesisState{
+ Params: DefaultParams(),
+ PreviousBlockTime: DefaultPreviousBlockTime,
+ RewardPeriods: RewardPeriods{},
+ ClaimPeriods: ClaimPeriods{},
+ Claims: Claims{},
+ NextClaimPeriodIDs: GenesisClaimPeriodIDs{},
+ }
+}
+
+// Validate performs basic validation of genesis data returning an
+// error for any failed validation criteria.
+func (gs GenesisState) Validate() error {
+
+ if err := gs.Params.Validate(); err != nil {
+ return err
+ }
+ if gs.PreviousBlockTime.Equal(time.Time{}) {
+ return fmt.Errorf("previous block time not set")
+ }
+ return nil
+}
+
+// Equal checks whether two gov GenesisState structs are equivalent
+func (gs GenesisState) Equal(gs2 GenesisState) bool {
+ b1 := ModuleCdc.MustMarshalBinaryBare(gs)
+ b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
+ return bytes.Equal(b1, b2)
+}
+
+// IsEmpty returns true if a GenesisState is empty
+func (gs GenesisState) IsEmpty() bool {
+ return gs.Equal(GenesisState{})
+}
diff --git a/x/incentive/types/keys.go b/x/incentive/types/keys.go
new file mode 100644
index 00000000..1185ff1d
--- /dev/null
+++ b/x/incentive/types/keys.go
@@ -0,0 +1,61 @@
+package types
+
+import (
+ "encoding/binary"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+const (
+ // ModuleName The name that will be used throughout the module
+ ModuleName = "incentive"
+
+ // StoreKey Top level store key where all module items will be stored
+ StoreKey = ModuleName
+
+ // RouterKey Top level router key
+ RouterKey = ModuleName
+
+ // DefaultParamspace default name for parameter store
+ DefaultParamspace = ModuleName
+
+ // QuerierRoute route used for abci queries
+ QuerierRoute = ModuleName
+)
+
+// Key Prefixes
+var (
+ RewardPeriodKeyPrefix = []byte{0x01} // prefix for keys that store reward periods
+ ClaimPeriodKeyPrefix = []byte{0x02} // prefix for keys that store claim periods
+ ClaimKeyPrefix = []byte{0x03} // prefix for keys that store claims
+ NextClaimPeriodIDPrefix = []byte{0x04} // prefix for keys that store the next ID for claims periods
+ PreviousBlockTimeKey = []byte{0x05} // prefix for key that stores the previous blocktime
+)
+
+// Keys
+// 0x00:Denom <> RewardPeriod the current active reward period (max 1 reward period per denom)
+// 0x01:Denom:ID <> ClaimPeriod object for that ID, indexed by denom and ID
+// 0x02:Denom:ID:Owner <> Claim object, indexed by Denom, ID and owner
+// 0x03:Denom <> NextClaimPeriodIDPrefix the ID of the next claim period, indexed by denom
+
+// BytesToUint64 returns uint64 format from a byte array
+func BytesToUint64(bz []byte) uint64 {
+ return binary.BigEndian.Uint64(bz)
+}
+
+// GetClaimPeriodPrefix returns the key (denom + id) for a claim prefix
+func GetClaimPeriodPrefix(denom string, id uint64) []byte {
+ return createKey([]byte(denom), sdk.Uint64ToBigEndian(id))
+}
+
+// GetClaimPrefix returns the key (denom + id + address) for a claim
+func GetClaimPrefix(addr sdk.AccAddress, denom string, id uint64) []byte {
+ return createKey([]byte(denom), sdk.Uint64ToBigEndian(id), addr)
+}
+
+func createKey(bytes ...[]byte) (r []byte) {
+ for _, b := range bytes {
+ r = append(r, b...)
+ }
+ return
+}
diff --git a/x/incentive/types/msg.go b/x/incentive/types/msg.go
new file mode 100644
index 00000000..446389d5
--- /dev/null
+++ b/x/incentive/types/msg.go
@@ -0,0 +1,54 @@
+package types
+
+import (
+ "errors"
+ "strings"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+)
+
+// ensure Msg interface compliance at compile time
+var _ sdk.Msg = &MsgClaimReward{}
+
+// MsgClaimReward message type used to claim rewards
+type MsgClaimReward struct {
+ Sender sdk.AccAddress `json:"sender" yaml:"sender"`
+ Denom string `json:"denom" yaml:"denom"`
+}
+
+// NewMsgClaimReward returns a new MsgClaimReward.
+func NewMsgClaimReward(sender sdk.AccAddress, denom string) MsgClaimReward {
+ return MsgClaimReward{
+ Sender: sender,
+ Denom: denom,
+ }
+}
+
+// Route return the message type used for routing the message.
+func (msg MsgClaimReward) Route() string { return RouterKey }
+
+// Type returns a human-readable string for the message, intended for utilization within tags.
+func (msg MsgClaimReward) Type() string { return "claim_reward" }
+
+// ValidateBasic does a simple validation check that doesn't require access to state.
+func (msg MsgClaimReward) ValidateBasic() error {
+ if msg.Sender.Empty() {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
+ }
+ if strings.TrimSpace(msg.Denom) == "" {
+ return errors.New("invalid (empty) denom")
+ }
+ return nil
+}
+
+// GetSignBytes gets the canonical byte representation of the Msg.
+func (msg MsgClaimReward) GetSignBytes() []byte {
+ bz := ModuleCdc.MustMarshalJSON(msg)
+ return sdk.MustSortJSON(bz)
+}
+
+// GetSigners returns the addresses of signers that must sign.
+func (msg MsgClaimReward) GetSigners() []sdk.AccAddress {
+ return []sdk.AccAddress{msg.Sender}
+}
diff --git a/x/incentive/types/msg_test.go b/x/incentive/types/msg_test.go
new file mode 100644
index 00000000..dbe06fe7
--- /dev/null
+++ b/x/incentive/types/msg_test.go
@@ -0,0 +1,59 @@
+package types_test
+
+import (
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/x/incentive/types"
+ "github.com/stretchr/testify/suite"
+ "github.com/tendermint/tendermint/crypto"
+)
+
+type msgTest struct {
+ from sdk.AccAddress
+ denom string
+ expectPass bool
+}
+
+type MsgTestSuite struct {
+ suite.Suite
+
+ tests []msgTest
+}
+
+func (suite *MsgTestSuite) SetupTest() {
+ tests := []msgTest{
+ msgTest{
+ from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
+ denom: "bnb",
+ expectPass: true,
+ },
+ msgTest{
+ from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
+ denom: "",
+ expectPass: false,
+ },
+ msgTest{
+ from: sdk.AccAddress{},
+ denom: "bnb",
+ expectPass: false,
+ },
+ }
+ suite.tests = tests
+}
+
+func (suite *MsgTestSuite) TestMsgValidation() {
+ for _, t := range suite.tests {
+ msg := types.NewMsgClaimReward(t.from, t.denom)
+ err := msg.ValidateBasic()
+ if t.expectPass {
+ suite.Require().NoError(err)
+ } else {
+ suite.Require().Error(err)
+ }
+ }
+}
+
+func TestMsgTestSuite(t *testing.T) {
+ suite.Run(t, new(MsgTestSuite))
+}
diff --git a/x/incentive/types/params.go b/x/incentive/types/params.go
new file mode 100644
index 00000000..524cfb83
--- /dev/null
+++ b/x/incentive/types/params.go
@@ -0,0 +1,165 @@
+package types
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/params"
+ cdptypes "github.com/kava-labs/kava/x/cdp/types"
+ kavadistTypes "github.com/kava-labs/kava/x/kavadist/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+// Parameter keys and default values
+var (
+ KeyActive = []byte("Active")
+ KeyRewards = []byte("Rewards")
+ DefaultActive = false
+ DefaultRewards = Rewards{}
+ DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
+ GovDenom = cdptypes.DefaultGovDenom
+ PrincipalDenom = "usdx"
+ IncentiveMacc = kavadistTypes.ModuleName
+)
+
+// Params governance parameters for the incentive module
+type Params struct {
+ Active bool `json:"active" yaml:"active"` // top level governance switch to disable all rewards
+ Rewards Rewards `json:"rewards" yaml:"rewards"`
+}
+
+// NewParams returns a new params object
+func NewParams(active bool, rewards Rewards) Params {
+ return Params{
+ Active: active,
+ Rewards: rewards,
+ }
+}
+
+// DefaultParams returns default params for kavadist module
+func DefaultParams() Params {
+ return NewParams(DefaultActive, DefaultRewards)
+}
+
+// String implements fmt.Stringer
+func (p Params) String() string {
+ return fmt.Sprintf(`Params:
+ Active: %t
+ Rewards: %s`, p.Active, p.Rewards)
+}
+
+// ParamKeyTable Key declaration for parameters
+func ParamKeyTable() params.KeyTable {
+ return params.NewKeyTable().RegisterParamSet(&Params{})
+}
+
+// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
+func (p *Params) ParamSetPairs() params.ParamSetPairs {
+ return params.ParamSetPairs{
+ params.NewParamSetPair(KeyActive, &p.Active, validateActiveParam),
+ params.NewParamSetPair(KeyRewards, &p.Rewards, validateRewardsParam),
+ }
+}
+
+// Validate checks that the parameters have valid values.
+func (p Params) Validate() error {
+ if err := validateActiveParam(p.Active); err != nil {
+ return err
+ }
+
+ if err := validateRewardsParam(p.Rewards); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func validateActiveParam(i interface{}) error {
+ _, ok := i.(bool)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+ return nil
+}
+
+func validateRewardsParam(i interface{}) error {
+ rewards, ok := i.(Rewards)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+ rewardDenoms := make(map[string]bool)
+
+ for _, reward := range rewards {
+ if strings.TrimSpace(reward.Denom) == "" {
+ return fmt.Errorf("cannot have empty reward denom: %s", reward)
+ }
+ if rewardDenoms[reward.Denom] {
+ return fmt.Errorf("cannot have duplicate reward denoms: %s", reward.Denom)
+ }
+ rewardDenoms[reward.Denom] = true
+ if !reward.AvailableRewards.IsValid() {
+ return fmt.Errorf("invalid reward coins %s for %s", reward.AvailableRewards, reward.Denom)
+ }
+ if !reward.AvailableRewards.IsPositive() {
+ return fmt.Errorf("reward amount must be positive, is %s for %s", reward.AvailableRewards, reward.Denom)
+ }
+ if int(reward.Duration.Seconds()) <= 0 {
+ return fmt.Errorf("reward duration must be positive, is %s for %s", reward.Duration.String(), reward.Denom)
+ }
+ if int(reward.TimeLock.Seconds()) < 0 {
+ return fmt.Errorf("reward timelock must be non-negative, is %s for %s", reward.TimeLock.String(), reward.Denom)
+ }
+ if int(reward.ClaimDuration.Seconds()) <= 0 {
+ return fmt.Errorf("claim duration must be positive, is %s for %s", reward.ClaimDuration.String(), reward.Denom)
+ }
+ }
+ return nil
+}
+
+// Reward stores the specified state for a single reward period.
+type Reward struct {
+ Active bool `json:"active" yaml:"active"` // governance switch to disable a period
+ Denom string `json:"denom" yaml:"denom"` // the collateral denom rewards apply to, must be found in the cdp collaterals
+ AvailableRewards sdk.Coin `json:"available_rewards" yaml:"available_rewards"` // the total amount of coins distributed per period
+ Duration time.Duration `json:"duration" yaml:"duration"` // the duration of the period
+ TimeLock time.Duration `json:"time_lock" yaml:"time_lock"` // how long rewards for this period are timelocked
+ ClaimDuration time.Duration `json:"claim_duration" yaml:"claim_duration"` // how long users have after the period ends to claim their rewards
+}
+
+// NewReward returns a new Reward
+func NewReward(active bool, denom string, reward sdk.Coin, duration time.Duration, timelock time.Duration, claimDuration time.Duration) Reward {
+ return Reward{
+ Active: active,
+ Denom: denom,
+ AvailableRewards: reward,
+ Duration: duration,
+ TimeLock: timelock,
+ ClaimDuration: claimDuration,
+ }
+}
+
+// String implements fmt.Stringer
+func (r Reward) String() string {
+ return fmt.Sprintf(`Reward:
+ Active: %t,
+ Denom: %s,
+ Available Rewards: %s,
+ Duration: %s,
+ Time Lock: %s,
+ Claim Duration: %s`,
+ r.Active, r.Denom, r.AvailableRewards, r.Duration, r.TimeLock, r.ClaimDuration)
+}
+
+// Rewards array of Reward
+type Rewards []Reward
+
+// String implements fmt.Stringer
+func (rs Rewards) String() string {
+ out := "Rewards\n"
+ for _, r := range rs {
+ out += fmt.Sprintf("%s\n", r)
+ }
+ return out
+}
diff --git a/x/incentive/types/params_test.go b/x/incentive/types/params_test.go
new file mode 100644
index 00000000..a35a2508
--- /dev/null
+++ b/x/incentive/types/params_test.go
@@ -0,0 +1,219 @@
+package types_test
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/x/incentive/types"
+ "github.com/stretchr/testify/suite"
+)
+
+type paramTest struct {
+ name string
+ params types.Params
+ errResult errResult
+}
+
+type errResult struct {
+ expectPass bool
+ contains string
+}
+
+type ParamTestSuite struct {
+ suite.Suite
+
+ tests []paramTest
+}
+
+func (suite *ParamTestSuite) SetupTest() {
+ suite.tests = []paramTest{
+ paramTest{
+ name: "valid - active",
+ params: types.Params{
+ Active: true,
+ Rewards: types.Rewards{
+ types.Reward{
+ Active: true,
+ Denom: "bnb",
+ AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
+ Duration: time.Hour * 24 * 7,
+ TimeLock: time.Hour * 8766,
+ ClaimDuration: time.Hour * 24 * 14,
+ },
+ },
+ },
+ errResult: errResult{
+ expectPass: true,
+ contains: "",
+ },
+ },
+ paramTest{
+ name: "valid - inactive",
+ params: types.Params{
+ Active: false,
+ Rewards: types.Rewards{
+ types.Reward{
+ Active: true,
+ Denom: "bnb",
+ AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
+ Duration: time.Hour * 24 * 7,
+ TimeLock: time.Hour * 8766,
+ ClaimDuration: time.Hour * 24 * 14,
+ },
+ },
+ },
+ errResult: errResult{
+ expectPass: true,
+ contains: "",
+ },
+ },
+ paramTest{
+ name: "duplicate reward",
+ params: types.Params{
+ Active: true,
+ Rewards: types.Rewards{
+ types.Reward{
+ Active: true,
+ Denom: "bnb",
+ AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
+ Duration: time.Hour * 24 * 7,
+ TimeLock: time.Hour * 8766,
+ ClaimDuration: time.Hour * 24 * 14,
+ },
+ types.Reward{
+ Active: true,
+ Denom: "bnb",
+ AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
+ Duration: time.Hour * 24 * 7,
+ TimeLock: time.Hour * 8766,
+ ClaimDuration: time.Hour * 24 * 14,
+ },
+ },
+ },
+ errResult: errResult{
+ expectPass: false,
+ contains: "cannot have duplicate reward denoms",
+ },
+ },
+ paramTest{
+ name: "negative reward duration",
+ params: types.Params{
+ Active: true,
+ Rewards: types.Rewards{
+ types.Reward{
+ Active: true,
+ Denom: "bnb",
+ AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
+ Duration: time.Hour * -24 * 7,
+ TimeLock: time.Hour * 8766,
+ ClaimDuration: time.Hour * 24 * 14,
+ },
+ },
+ },
+ errResult: errResult{
+ expectPass: false,
+ contains: "reward duration must be positive",
+ },
+ },
+ paramTest{
+ name: "negative time lock",
+ params: types.Params{
+ Active: true,
+ Rewards: types.Rewards{
+ types.Reward{
+ Active: true,
+ Denom: "bnb",
+ AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
+ Duration: time.Hour * 24 * 7,
+ TimeLock: time.Hour * -8766,
+ ClaimDuration: time.Hour * 24 * 14,
+ },
+ },
+ },
+ errResult: errResult{
+ expectPass: false,
+ contains: "reward timelock must be non-negative",
+ },
+ },
+ paramTest{
+ name: "zero claim duration",
+ params: types.Params{
+ Active: true,
+ Rewards: types.Rewards{
+ types.Reward{
+ Active: true,
+ Denom: "bnb",
+ AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
+ Duration: time.Hour * 24 * 7,
+ TimeLock: time.Hour * 8766,
+ ClaimDuration: time.Hour * 0,
+ },
+ },
+ },
+ errResult: errResult{
+ expectPass: false,
+ contains: "claim duration must be positive",
+ },
+ },
+ paramTest{
+ name: "zero reward",
+ params: types.Params{
+ Active: true,
+ Rewards: types.Rewards{
+ types.Reward{
+ Active: true,
+ Denom: "bnb",
+ AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(0)),
+ Duration: time.Hour * 24 * 7,
+ TimeLock: time.Hour * 8766,
+ ClaimDuration: time.Hour * 24 * 14,
+ },
+ },
+ },
+ errResult: errResult{
+ expectPass: false,
+ contains: "reward amount must be positive",
+ },
+ },
+ paramTest{
+ name: "empty reward denom",
+ params: types.Params{
+ Active: true,
+ Rewards: types.Rewards{
+ types.Reward{
+ Active: true,
+ Denom: "",
+ AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(0)),
+ Duration: time.Hour * 24 * 7,
+ TimeLock: time.Hour * 8766,
+ ClaimDuration: time.Hour * 24 * 14,
+ },
+ },
+ },
+ errResult: errResult{
+ expectPass: false,
+ contains: "cannot have empty reward denom",
+ },
+ },
+ }
+}
+
+func (suite *ParamTestSuite) TestParamValidation() {
+ for _, t := range suite.tests {
+ suite.Run(t.name, func() {
+ err := t.params.Validate()
+ if t.errResult.expectPass {
+ suite.Require().NoError(err)
+ } else {
+ suite.Require().Error(err)
+ suite.Require().True(strings.Contains(err.Error(), t.errResult.contains))
+ }
+ })
+ }
+}
+
+func TestParamTestSuite(t *testing.T) {
+ suite.Run(t, new(ParamTestSuite))
+}
diff --git a/x/incentive/types/period.go b/x/incentive/types/period.go
new file mode 100644
index 00000000..c2894750
--- /dev/null
+++ b/x/incentive/types/period.go
@@ -0,0 +1,11 @@
+package types
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/auth/vesting"
+)
+
+// NewPeriod returns a new vesting period
+func NewPeriod(amount sdk.Coins, length int64) vesting.Period {
+ return vesting.Period{Amount: amount, Length: length}
+}
diff --git a/x/incentive/types/querier.go b/x/incentive/types/querier.go
new file mode 100644
index 00000000..d6271048
--- /dev/null
+++ b/x/incentive/types/querier.go
@@ -0,0 +1,35 @@
+package types
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/rest"
+)
+
+// Querier routes for the incentive module
+const (
+ QueryGetClaims = "claims"
+ RestClaimOwner = "owner"
+ RestClaimDenom = "denom"
+ QueryGetParams = "parameters"
+)
+
+// QueryClaimsParams params for query /incentive/claims
+type QueryClaimsParams struct {
+ Owner sdk.AccAddress
+ Denom string
+}
+
+// NewQueryClaimsParams returns QueryClaimsParams
+func NewQueryClaimsParams(owner sdk.AccAddress, denom string) QueryClaimsParams {
+ return QueryClaimsParams{
+ Owner: owner,
+ Denom: denom,
+ }
+}
+
+// PostClaimReq defines the properties of claim transaction's request body.
+type PostClaimReq struct {
+ BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
+ Sender sdk.AccAddress `json:"sender" yaml:"sender"`
+ Denom string `json:"denom" yaml:"denom"`
+}
diff --git a/x/incentive/types/rewards.go b/x/incentive/types/rewards.go
new file mode 100644
index 00000000..237b222a
--- /dev/null
+++ b/x/incentive/types/rewards.go
@@ -0,0 +1,97 @@
+package types
+
+import (
+ "fmt"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+// RewardPeriod stores the state of an ongoing reward
+type RewardPeriod struct {
+ Denom string `json:"denom" yaml:"denom"`
+ Start time.Time `json:"start" yaml:"start"`
+ End time.Time `json:"end" yaml:"end"`
+ Reward sdk.Coin `json:"reward" yaml:"reward"` // per second reward payouts
+ ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"`
+ ClaimTimeLock time.Duration `json:"claim_time_lock" yaml:"claim_time_lock"` // the amount of time rewards are timelocked once they are sent to users
+}
+
+// String implements fmt.Stringer
+func (rp RewardPeriod) String() string {
+ return fmt.Sprintf(`Reward Period:
+ Denom: %s,
+ Start: %s,
+ End: %s,
+ Reward: %s,
+ Claim End: %s,
+ Claim Time Lock: %s`,
+ rp.Denom, rp.Start, rp.End, rp.Reward, rp.ClaimEnd, rp.ClaimTimeLock)
+}
+
+// NewRewardPeriod returns a new RewardPeriod
+func NewRewardPeriod(denom string, start time.Time, end time.Time, reward sdk.Coin, claimEnd time.Time, claimTimeLock time.Duration) RewardPeriod {
+ return RewardPeriod{
+ Denom: denom,
+ Start: start,
+ End: end,
+ Reward: reward,
+ ClaimEnd: claimEnd,
+ ClaimTimeLock: claimTimeLock,
+ }
+}
+
+// RewardPeriods array of RewardPeriod
+type RewardPeriods []RewardPeriod
+
+// ClaimPeriod stores the state of an ongoing claim period
+type ClaimPeriod struct {
+ Denom string `json:"denom" yaml:"denom"`
+ ID uint64 `json:"id" yaml:"id"`
+ End time.Time `json:"end" yaml:"end"`
+ TimeLock time.Duration `json:"time_lock" yaml:"time_lock"`
+}
+
+// NewClaimPeriod returns a new ClaimPeriod
+func NewClaimPeriod(denom string, id uint64, end time.Time, timeLock time.Duration) ClaimPeriod {
+ return ClaimPeriod{
+ Denom: denom,
+ ID: id,
+ End: end,
+ TimeLock: timeLock,
+ }
+}
+
+// ClaimPeriods array of ClaimPeriod
+type ClaimPeriods []ClaimPeriod
+
+// Claim stores the rewards that can be claimed by owner
+type Claim struct {
+ Owner sdk.AccAddress `json:"owner" yaml:"owner"`
+ Reward sdk.Coin `json:"reward" yaml:"reward"`
+ Denom string `json:"denom" yaml:"denom"`
+ ClaimPeriodID uint64 `json:"claim_period_id" yaml:"claim_period_id"`
+}
+
+// NewClaim returns a new Claim
+func NewClaim(owner sdk.AccAddress, reward sdk.Coin, denom string, claimPeriodID uint64) Claim {
+ return Claim{
+ Owner: owner,
+ Reward: reward,
+ Denom: denom,
+ ClaimPeriodID: claimPeriodID,
+ }
+}
+
+// String implements fmt.Stringer
+func (c Claim) String() string {
+ return fmt.Sprintf(`Claim:
+ Owner: %s,
+ Denom: %s,
+ Reward: %s,
+ Claim Period ID: %d,`,
+ c.Owner, c.Denom, c.Reward, c.ClaimPeriodID)
+}
+
+// Claims array of Claim
+type Claims []Claim
diff --git a/x/kavadist/abci.go b/x/kavadist/abci.go
new file mode 100644
index 00000000..de7b80ce
--- /dev/null
+++ b/x/kavadist/abci.go
@@ -0,0 +1,12 @@
+package kavadist
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+func BeginBlocker(ctx sdk.Context, k Keeper) {
+ err := k.MintPeriodInflation(ctx)
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/x/kavadist/alias.go b/x/kavadist/alias.go
new file mode 100644
index 00000000..04cdf035
--- /dev/null
+++ b/x/kavadist/alias.go
@@ -0,0 +1,56 @@
+package kavadist
+
+import (
+ "github.com/kava-labs/kava/x/kavadist/keeper"
+ "github.com/kava-labs/kava/x/kavadist/types"
+)
+
+// autogenerated code using github.com/rigelrozanski/multitool
+// aliases generated for the following subdirectories:
+// ALIASGEN: github.com/kava-labs/kava/x/kavadist/keeper
+// ALIASGEN: github.com/kava-labs/kava/x/kavadist/types
+
+// nolint
+const (
+ EventTypeKavaDist = types.EventTypeKavaDist
+ AttributeKeyInflation = types.AttributeKeyInflation
+ ModuleName = types.ModuleName
+ StoreKey = types.StoreKey
+ RouterKey = types.RouterKey
+ QuerierRoute = types.QuerierRoute
+ DefaultParamspace = types.DefaultParamspace
+ KavaDistMacc = types.KavaDistMacc
+ QueryGetParams = types.QueryGetParams
+)
+
+// nolint
+var (
+ // functions aliases
+ NewKeeper = keeper.NewKeeper
+ RegisterCodec = types.RegisterCodec
+ NewGenesisState = types.NewGenesisState
+ DefaultGenesisState = types.DefaultGenesisState
+ NewParams = types.NewParams
+ DefaultParams = types.DefaultParams
+ ParamKeyTable = types.ParamKeyTable
+
+ // variable aliases
+ ModuleCdc = types.ModuleCdc
+ CurrentDistPeriodKey = types.CurrentDistPeriodKey
+ PreviousBlockTimeKey = types.PreviousBlockTimeKey
+ KeyActive = types.KeyActive
+ KeyPeriods = types.KeyPeriods
+ DefaultActive = types.DefaultActive
+ DefaultPeriods = types.DefaultPeriods
+ DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
+ GovDenom = types.GovDenom
+)
+
+// nolint
+type (
+ Keeper = keeper.Keeper
+ GenesisState = types.GenesisState
+ Params = types.Params
+ Period = types.Period
+ Periods = types.Periods
+)
diff --git a/x/kavadist/doc.go b/x/kavadist/doc.go
new file mode 100644
index 00000000..5926f7e4
--- /dev/null
+++ b/x/kavadist/doc.go
@@ -0,0 +1 @@
+package kavadist
diff --git a/x/kavadist/genesis.go b/x/kavadist/genesis.go
new file mode 100644
index 00000000..c4219a02
--- /dev/null
+++ b/x/kavadist/genesis.go
@@ -0,0 +1,39 @@
+package kavadist
+
+import (
+ "fmt"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/x/kavadist/types"
+)
+
+// InitGenesis initializes the store state from a genesis state.
+func InitGenesis(ctx sdk.Context, k Keeper, supplyKeeper types.SupplyKeeper, gs GenesisState) {
+ if err := gs.Validate(); err != nil {
+ panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
+ }
+
+ k.SetParams(ctx, gs.Params)
+
+ // only set the previous block time if it's different than default
+ if !gs.PreviousBlockTime.Equal(DefaultPreviousBlockTime) {
+ k.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
+ }
+
+ // check if the module account exists
+ moduleAcc := supplyKeeper.GetModuleAccount(ctx, KavaDistMacc)
+ if moduleAcc == nil {
+ panic(fmt.Sprintf("%s module account has not been set", KavaDistMacc))
+ }
+
+}
+
+// ExportGenesis export genesis state for cdp module
+func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
+ params := k.GetParams(ctx)
+ previousBlockTime, found := k.GetPreviousBlockTime(ctx)
+ if !found {
+ previousBlockTime = DefaultPreviousBlockTime
+ }
+ return NewGenesisState(params,previousBlockTime)
+}
diff --git a/x/kavadist/handler.go b/x/kavadist/handler.go
new file mode 100644
index 00000000..ed7a1b60
--- /dev/null
+++ b/x/kavadist/handler.go
@@ -0,0 +1,16 @@
+package kavadist
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+)
+
+// NewHandler creates an sdk.Handler for kavadist messages
+func NewHandler(k Keeper) sdk.Handler {
+ return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
+ switch msg := msg.(type) {
+ default:
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
+ }
+ }
+}
diff --git a/x/kavadist/keeper/keeper.go b/x/kavadist/keeper/keeper.go
new file mode 100644
index 00000000..34faa329
--- /dev/null
+++ b/x/kavadist/keeper/keeper.go
@@ -0,0 +1,50 @@
+package keeper
+
+import (
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ "github.com/cosmos/cosmos-sdk/store/prefix"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/params/subspace"
+ "github.com/kava-labs/kava/x/kavadist/types"
+)
+
+// Keeper keeper for the cdp module
+type Keeper struct {
+ key sdk.StoreKey
+ cdc *codec.Codec
+ paramSubspace subspace.Subspace
+ supplyKeeper types.SupplyKeeper
+}
+
+// NewKeeper creates a new keeper
+func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, sk types.SupplyKeeper) Keeper {
+ if !paramstore.HasKeyTable() {
+ paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
+ }
+
+ return Keeper{
+ key: key,
+ cdc: cdc,
+ paramSubspace: paramstore,
+ supplyKeeper: sk,
+ }
+}
+
+// GetPreviousBlockTime get the blocktime for the previous block
+func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
+ b := store.Get([]byte{})
+ if b == nil {
+ return time.Time{}, false
+ }
+ k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &blockTime)
+ return blockTime, true
+}
+
+// SetPreviousBlockTime set the time of the previous block
+func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) {
+ store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
+ store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(blockTime))
+}
diff --git a/x/kavadist/keeper/mint.go b/x/kavadist/keeper/mint.go
new file mode 100644
index 00000000..c5d19215
--- /dev/null
+++ b/x/kavadist/keeper/mint.go
@@ -0,0 +1,87 @@
+package keeper
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ cdptypes "github.com/kava-labs/kava/x/cdp/types"
+ "github.com/kava-labs/kava/x/kavadist/types"
+)
+
+// MintPeriodInflation mints new tokens according to the inflation schedule specified in the paramters
+func (k Keeper) MintPeriodInflation(ctx sdk.Context) error {
+ params := k.GetParams(ctx)
+ if !params.Active {
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ types.EventTypeKavaDist,
+ sdk.NewAttribute(types.AttributeKeyStatus, types.AttributeValueInactive),
+ ),
+ )
+ return nil
+ }
+ previousBlockTime, found := k.GetPreviousBlockTime(ctx)
+ if !found {
+ previousBlockTime = ctx.BlockTime()
+ k.SetPreviousBlockTime(ctx, previousBlockTime)
+ return nil
+ }
+ for _, period := range params.Periods {
+ // Case 1 - period is fully expired
+ if period.End.Before(previousBlockTime) {
+ continue
+ }
+ // Case 2 - period has ended since the previous block time
+ if period.End.After(previousBlockTime) && period.End.Before(ctx.BlockTime()) {
+ // calculate time elapsed relative to the periods end time
+ timeElapsed := sdk.NewInt(period.End.Unix() - previousBlockTime.Unix())
+ err := k.mintInflationaryCoins(ctx, period.Inflation, timeElapsed, types.GovDenom)
+ if err != nil {
+ return err
+ }
+ // update the value of previousBlockTime so that the next period starts from the end of the last
+ // period and not the original value of previousBlockTime
+ previousBlockTime = period.End
+ }
+ // Case 3 - period is ongoing
+ if (period.Start.Before(previousBlockTime) || period.Start.Equal(previousBlockTime)) && period.End.After(ctx.BlockTime()) {
+ // calculate time elapsed relative to the current block time
+ timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix())
+ err := k.mintInflationaryCoins(ctx, period.Inflation, timeElapsed, types.GovDenom)
+ if err != nil {
+ return err
+ }
+ }
+ // Case 4 - period hasn't started
+ if period.Start.After(ctx.BlockTime()) || period.Start.Equal(ctx.BlockTime()) {
+ continue
+ }
+ }
+ k.SetPreviousBlockTime(ctx, ctx.BlockTime())
+ return nil
+}
+
+func (k Keeper) mintInflationaryCoins(ctx sdk.Context, inflationRate sdk.Dec, timePeriods sdk.Int, denom string) error {
+ totalSupply := k.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(denom)
+ // used to scale accumulator calculations by 10^18
+ scalar := sdk.NewInt(1000000000000000000)
+ // convert inflation rate to integer
+ inflationInt := inflationRate.Mul(sdk.NewDecFromInt(scalar)).TruncateInt()
+ // calculate the multiplier (amount to multiply the total supply by to achieve the desired inflation)
+ // multiply the result by 10^-18 because RelativePow returns the result scaled by 10^18
+ accumulator := sdk.NewDecFromInt(cdptypes.RelativePow(inflationInt, timePeriods, scalar)).Mul(sdk.SmallestDec())
+ // calculate the number of coins to mint
+ amountToMint := (sdk.NewDecFromInt(totalSupply).Mul(accumulator)).Sub(sdk.NewDecFromInt(totalSupply)).TruncateInt()
+ if amountToMint.IsZero() {
+ return nil
+ }
+ err := k.supplyKeeper.MintCoins(ctx, types.KavaDistMacc, sdk.NewCoins(sdk.NewCoin(denom, amountToMint)))
+ if err != nil {
+ return err
+ }
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ types.EventTypeKavaDist,
+ sdk.NewAttribute(types.AttributeKeyInflation, amountToMint.String()),
+ ),
+ )
+ return nil
+}
diff --git a/x/kavadist/keeper/mint_test.go b/x/kavadist/keeper/mint_test.go
new file mode 100644
index 00000000..c9b77753
--- /dev/null
+++ b/x/kavadist/keeper/mint_test.go
@@ -0,0 +1,144 @@
+package keeper_test
+
+import (
+ "testing"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/app"
+ "github.com/kava-labs/kava/x/kavadist/keeper"
+ "github.com/kava-labs/kava/x/kavadist/types"
+ "github.com/stretchr/testify/suite"
+ abci "github.com/tendermint/tendermint/abci/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+type MintTestSuite struct {
+ suite.Suite
+
+ keeper keeper.Keeper
+ supplyKeeper types.SupplyKeeper
+ app app.TestApp
+ ctx sdk.Context
+}
+
+var (
+ p = types.Periods{
+ types.Period{
+ Start: time.Date(2020, time.March, 1, 1, 0, 0, 0, time.UTC),
+ End: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC),
+ Inflation: sdk.MustNewDecFromStr("1.000000003022265980"),
+ },
+ }
+)
+
+func (suite *MintTestSuite) SetupTest() {
+ config := sdk.GetConfig()
+ app.SetBech32AddressPrefixes(config)
+ tApp := app.NewTestApp()
+ _, addrs := app.GeneratePrivKeyAddressPairs(1)
+ coins := []sdk.Coins{sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000000000000)))}
+ authGS := app.NewAuthGenState(
+ addrs, coins)
+
+ ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
+
+ params := types.NewParams(true, p)
+ gs := app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(types.NewGenesisState(params, types.DefaultPreviousBlockTime))}
+ tApp.InitializeFromGenesisStates(
+ authGS,
+ gs,
+ )
+ keeper := tApp.GetKavadistKeeper()
+ sk := tApp.GetSupplyKeeper()
+ suite.app = tApp
+ suite.ctx = ctx
+ suite.keeper = keeper
+ suite.supplyKeeper = sk
+
+}
+
+func (suite *MintTestSuite) TestMintExpiredPeriod() {
+ initialSupply := suite.supplyKeeper.GetSupply(suite.ctx).GetTotal().AmountOf(types.GovDenom)
+ suite.NotPanics(func() { suite.keeper.SetPreviousBlockTime(suite.ctx, time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)) })
+ ctx := suite.ctx.WithBlockTime(time.Date(2022, 1, 1, 0, 7, 0, 0, time.UTC))
+ err := suite.keeper.MintPeriodInflation(ctx)
+ suite.NoError(err)
+ finalSupply := suite.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(types.GovDenom)
+ suite.Equal(initialSupply, finalSupply)
+}
+
+func (suite *MintTestSuite) TestMintPeriodNotStarted() {
+ initialSupply := suite.supplyKeeper.GetSupply(suite.ctx).GetTotal().AmountOf(types.GovDenom)
+ suite.NotPanics(func() { suite.keeper.SetPreviousBlockTime(suite.ctx, time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)) })
+ ctx := suite.ctx.WithBlockTime(time.Date(2019, 1, 1, 0, 7, 0, 0, time.UTC))
+ err := suite.keeper.MintPeriodInflation(ctx)
+ suite.NoError(err)
+ finalSupply := suite.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(types.GovDenom)
+ suite.Equal(initialSupply, finalSupply)
+}
+
+func (suite *MintTestSuite) TestMintOngoingPeriod() {
+ initialSupply := suite.supplyKeeper.GetSupply(suite.ctx).GetTotal().AmountOf(types.GovDenom)
+ suite.NotPanics(func() {
+ suite.keeper.SetPreviousBlockTime(suite.ctx, time.Date(2020, time.March, 1, 1, 0, 1, 0, time.UTC))
+ })
+ ctx := suite.ctx.WithBlockTime(time.Date(2021, 2, 28, 23, 59, 59, 0, time.UTC))
+ err := suite.keeper.MintPeriodInflation(ctx)
+ suite.NoError(err)
+ finalSupply := suite.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(types.GovDenom)
+ suite.True(finalSupply.GT(initialSupply))
+ mAcc := suite.supplyKeeper.GetModuleAccount(ctx, types.ModuleName)
+ mAccSupply := mAcc.GetCoins().AmountOf(types.GovDenom)
+ suite.True(mAccSupply.Equal(finalSupply.Sub(initialSupply)))
+ // expect that inflation is ~10%
+ expectedSupply := sdk.NewDecFromInt(initialSupply).Mul(sdk.MustNewDecFromStr("1.1"))
+ supplyError := sdk.OneDec().Sub((sdk.NewDecFromInt(finalSupply).Quo(expectedSupply))).Abs()
+ suite.True(supplyError.LTE(sdk.MustNewDecFromStr("0.001")))
+}
+
+func (suite *MintTestSuite) TestMintPeriodTransition() {
+ initialSupply := suite.supplyKeeper.GetSupply(suite.ctx).GetTotal().AmountOf(types.GovDenom)
+ params := suite.keeper.GetParams(suite.ctx)
+ periods := types.Periods{
+ p[0],
+ types.Period{
+ Start: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC),
+ End: time.Date(2022, time.March, 1, 1, 0, 0, 0, time.UTC),
+ Inflation: sdk.MustNewDecFromStr("1.000000003022265980"),
+ },
+ }
+ params.Periods = periods
+ suite.NotPanics(func() {
+ suite.keeper.SetParams(suite.ctx, params)
+ })
+ suite.NotPanics(func() {
+ suite.keeper.SetPreviousBlockTime(suite.ctx, time.Date(2020, time.March, 1, 1, 0, 1, 0, time.UTC))
+ })
+ ctx := suite.ctx.WithBlockTime(time.Date(2021, 3, 10, 0, 0, 0, 0, time.UTC))
+ err := suite.keeper.MintPeriodInflation(ctx)
+ suite.NoError(err)
+ finalSupply := suite.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(types.GovDenom)
+ suite.True(finalSupply.GT(initialSupply))
+}
+
+func (suite *MintTestSuite) TestMintNotActive() {
+ initialSupply := suite.supplyKeeper.GetSupply(suite.ctx).GetTotal().AmountOf(types.GovDenom)
+ params := suite.keeper.GetParams(suite.ctx)
+ params.Active = false
+ suite.NotPanics(func() {
+ suite.keeper.SetParams(suite.ctx, params)
+ })
+ suite.NotPanics(func() {
+ suite.keeper.SetPreviousBlockTime(suite.ctx, time.Date(2020, time.March, 1, 1, 0, 1, 0, time.UTC))
+ })
+ ctx := suite.ctx.WithBlockTime(time.Date(2021, 2, 28, 23, 59, 59, 0, time.UTC))
+ err := suite.keeper.MintPeriodInflation(ctx)
+ suite.NoError(err)
+ finalSupply := suite.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(types.GovDenom)
+ suite.Equal(initialSupply, finalSupply)
+}
+
+func TestMintTestSuite(t *testing.T) {
+ suite.Run(t, new(MintTestSuite))
+}
diff --git a/x/kavadist/keeper/params.go b/x/kavadist/keeper/params.go
new file mode 100644
index 00000000..526cb255
--- /dev/null
+++ b/x/kavadist/keeper/params.go
@@ -0,0 +1,18 @@
+package keeper
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/x/kavadist/types"
+)
+
+// GetParams returns the params from the store
+func (k Keeper) GetParams(ctx sdk.Context) types.Params {
+ var p types.Params
+ k.paramSubspace.GetParamSet(ctx, &p)
+ return p
+}
+
+// SetParams sets params on the store
+func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
+ k.paramSubspace.SetParamSet(ctx, ¶ms)
+}
diff --git a/x/kavadist/module.go b/x/kavadist/module.go
new file mode 100644
index 00000000..f24005c8
--- /dev/null
+++ b/x/kavadist/module.go
@@ -0,0 +1,158 @@
+package kavadist
+
+import (
+ "encoding/json"
+ "math/rand"
+
+ "github.com/gorilla/mux"
+ "github.com/spf13/cobra"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/module"
+ sim "github.com/cosmos/cosmos-sdk/x/simulation"
+ "github.com/kava-labs/kava/x/kavadist/simulation"
+ "github.com/kava-labs/kava/x/kavadist/types"
+ abci "github.com/tendermint/tendermint/abci/types"
+)
+
+var (
+ _ module.AppModule = AppModule{}
+ _ module.AppModuleBasic = AppModuleBasic{}
+ _ module.AppModuleSimulation = AppModule{}
+)
+
+// AppModuleBasic app module basics object
+type AppModuleBasic struct{}
+
+// Name get module name
+func (AppModuleBasic) Name() string {
+ return ModuleName
+}
+
+// RegisterCodec register module codec
+func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
+ RegisterCodec(cdc)
+}
+
+// DefaultGenesis default genesis state
+func (AppModuleBasic) DefaultGenesis() json.RawMessage {
+ return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
+}
+
+// ValidateGenesis module validate genesis
+func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
+ var gs GenesisState
+ err := ModuleCdc.UnmarshalJSON(bz, &gs)
+ if err != nil {
+ return err
+ }
+ return gs.Validate()
+}
+
+// RegisterRESTRoutes registers no REST routes for the crisis module.
+func (AppModuleBasic) RegisterRESTRoutes(_ context.CLIContext, _ *mux.Router) {}
+
+// GetTxCmd returns the root tx command for the crisis module.
+func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { return nil }
+
+// GetQueryCmd returns no root query command for the crisis module.
+func (AppModuleBasic) GetQueryCmd(_ *codec.Codec) *cobra.Command { return nil }
+
+//____________________________________________________________________________
+
+// AppModule app module type
+type AppModule struct {
+ AppModuleBasic
+
+ keeper Keeper
+ supplyKeeper types.SupplyKeeper
+}
+
+// NewAppModule creates a new AppModule object
+func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule {
+ return AppModule{
+ AppModuleBasic: AppModuleBasic{},
+ keeper: keeper,
+ supplyKeeper: supplyKeeper,
+ }
+}
+
+// Name module name
+func (AppModule) Name() string {
+ return ModuleName
+}
+
+// RegisterInvariants register module invariants
+func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
+
+// Route module message route name
+func (AppModule) Route() string {
+ return ModuleName
+}
+
+// NewHandler module handler
+func (am AppModule) NewHandler() sdk.Handler {
+ return NewHandler(am.keeper)
+}
+
+// QuerierRoute module querier route name
+func (AppModule) QuerierRoute() string {
+ return ModuleName
+}
+
+// NewQuerierHandler returns no sdk.Querier.
+func (AppModule) NewQuerierHandler() sdk.Querier { return nil }
+
+// InitGenesis module init-genesis
+func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
+ var genesisState GenesisState
+ ModuleCdc.MustUnmarshalJSON(data, &genesisState)
+ InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState)
+
+ return []abci.ValidatorUpdate{}
+}
+
+// ExportGenesis module export genesis
+func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
+ gs := ExportGenesis(ctx, am.keeper)
+ return ModuleCdc.MustMarshalJSON(gs)
+}
+
+// BeginBlock module begin-block
+func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) {
+ BeginBlocker(ctx, am.keeper)
+}
+
+// EndBlock module end-block
+func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
+ return []abci.ValidatorUpdate{}
+}
+
+//____________________________________________________________________________
+
+// GenerateGenesisState creates a randomized GenState of the auction module
+func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
+ simulation.RandomizedGenState(simState)
+}
+
+// ProposalContents doesn't return any content functions for governance proposals.
+func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
+ return nil
+}
+
+// RandomizedParams returns nil because auction has no params.
+func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
+ return simulation.ParamChanges(r)
+}
+
+// RegisterStoreDecoder registers a decoder for auction module's types
+func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
+ sdr[StoreKey] = simulation.DecodeStore
+}
+
+// WeightedOperations returns the all the auction module operations with their respective weights.
+func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
+ return nil
+}
diff --git a/x/kavadist/simulation/decoder.go b/x/kavadist/simulation/decoder.go
new file mode 100644
index 00000000..bc3787a0
--- /dev/null
+++ b/x/kavadist/simulation/decoder.go
@@ -0,0 +1,27 @@
+package simulation
+
+import (
+ "bytes"
+ "fmt"
+ "time"
+
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+
+ "github.com/kava-labs/kava/x/kavadist/types"
+)
+
+// DecodeStore unmarshals the KVPair's Value to the corresponding cdp type
+func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
+ switch {
+ case bytes.Equal(kvA.Key[:1], types.PreviousBlockTimeKey):
+ var timeA, timeB time.Time
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &timeA)
+ cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &timeB)
+ return fmt.Sprintf("%s\n%s", timeA, timeB)
+
+ default:
+ panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
+ }
+}
diff --git a/x/kavadist/simulation/decoder_test.go b/x/kavadist/simulation/decoder_test.go
new file mode 100644
index 00000000..0ed937dc
--- /dev/null
+++ b/x/kavadist/simulation/decoder_test.go
@@ -0,0 +1,54 @@
+package simulation
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/x/kavadist/types"
+)
+
+func makeTestCodec() (cdc *codec.Codec) {
+ cdc = codec.New()
+ sdk.RegisterCodec(cdc)
+ codec.RegisterCrypto(cdc)
+ types.RegisterCodec(cdc)
+ return
+}
+
+func TestDecodeDistributionStore(t *testing.T) {
+ cdc := makeTestCodec()
+
+ prevBlockTime := time.Now().UTC()
+
+ kvPairs := kv.Pairs{
+ kv.Pair{Key: []byte(types.PreviousBlockTimeKey), Value: cdc.MustMarshalBinaryLengthPrefixed(prevBlockTime)},
+ kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
+ }
+
+ tests := []struct {
+ name string
+ expectedLog string
+ }{
+ {"PreviousBlockTime", fmt.Sprintf("%s\n%s", prevBlockTime, prevBlockTime)},
+ {"other", ""},
+ }
+ for i, tt := range tests {
+ i, tt := i, tt
+ t.Run(tt.name, func(t *testing.T) {
+ switch i {
+ case len(tests) - 1:
+ require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
+ default:
+ require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
+ }
+ })
+ }
+}
diff --git a/x/kavadist/simulation/genesis.go b/x/kavadist/simulation/genesis.go
new file mode 100644
index 00000000..ebaa2ff7
--- /dev/null
+++ b/x/kavadist/simulation/genesis.go
@@ -0,0 +1,86 @@
+package simulation
+
+import (
+ "fmt"
+ "math/rand"
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/types/module"
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+
+ "github.com/kava-labs/kava/x/kavadist/types"
+)
+
+// SecondsPerYear is the number of seconds in a year
+const (
+ SecondsPerYear = 31536000
+ // BaseAprPadding sets the minimum inflation to the calculated SPR inflation rate from being 0.0
+ BaseAprPadding = "0.000000003022265980"
+)
+
+// RandomizedGenState generates a random GenesisState for kavadist module
+func RandomizedGenState(simState *module.SimulationState) {
+ params := genRandomParams(simState)
+ if err := params.Validate(); err != nil {
+ panic(err)
+ }
+
+ kavadistGenesis := types.NewGenesisState(params, types.DefaultPreviousBlockTime)
+ if err := kavadistGenesis.Validate(); err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, kavadistGenesis))
+ simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(kavadistGenesis)
+}
+
+func genRandomParams(simState *module.SimulationState) types.Params {
+ periods := genRandomPeriods(simState.Rand, simState.GenTimestamp)
+ params := types.NewParams(true, periods)
+ return params
+}
+
+func genRandomPeriods(r *rand.Rand, timestamp time.Time) types.Periods {
+ var periods types.Periods
+ numPeriods := simulation.RandIntBetween(r, 1, 10)
+ periodStart := timestamp
+ for i := 0; i < numPeriods; i++ {
+ // set periods to be between 1-3 days
+ durationMultiplier := simulation.RandIntBetween(r, 1, 3)
+ duration := time.Duration(int64(24*durationMultiplier)) * time.Hour
+ periodEnd := periodStart.Add(duration)
+ inflation := genRandomInflation(r)
+ period := types.NewPeriod(periodStart, periodEnd, inflation)
+ periods = append(periods, period)
+ periodStart = periodEnd
+ }
+ return periods
+}
+
+func genRandomInflation(r *rand.Rand) sdk.Dec {
+ // If sim.RandomDecAmount is less than base apr padding, add base apr padding
+ aprPadding, _ := sdk.NewDecFromStr(BaseAprPadding)
+ extraAprInflation := simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("0.25"))
+ for extraAprInflation.LT(aprPadding) {
+ extraAprInflation = extraAprInflation.Add(aprPadding)
+ }
+ aprInflation := sdk.OneDec().Add(extraAprInflation)
+
+ // convert APR inflation to SPR (inflation per second)
+ inflationSpr, err := approxRoot(aprInflation, uint64(SecondsPerYear))
+ if err != nil {
+ panic(fmt.Sprintf("error generating random inflation %v", err))
+ }
+ return inflationSpr
+}
+
+func genRandomActive(r *rand.Rand) bool {
+ threshold := 50
+ value := simulation.RandIntBetween(r, 1, 100)
+ if value > threshold {
+ return true
+ }
+ return false
+}
diff --git a/x/kavadist/simulation/params.go b/x/kavadist/simulation/params.go
new file mode 100644
index 00000000..af0b75e4
--- /dev/null
+++ b/x/kavadist/simulation/params.go
@@ -0,0 +1,34 @@
+package simulation
+
+import (
+ "fmt"
+ "math/rand"
+
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+
+ "github.com/kava-labs/kava/x/kavadist/types"
+)
+
+// ParamChanges defines the parameters that can be modified by param change proposals
+// on the simulation
+func ParamChanges(r *rand.Rand) []simulation.ParamChange {
+ // Hacky way to validate periods since validation is wrapped in params
+ active := genRandomActive(r)
+ periods := genRandomPeriods(r, simulation.RandTimestamp(r))
+ if err := types.NewParams(active, periods).Validate(); err != nil {
+ panic(err)
+ }
+
+ return []simulation.ParamChange{
+ simulation.NewSimParamChange(types.ModuleName, string(types.KeyActive),
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("%t", active)
+ },
+ ),
+ simulation.NewSimParamChange(types.ModuleName, string(types.KeyPeriods),
+ func(r *rand.Rand) string {
+ return fmt.Sprintf("%v", periods)
+ },
+ ),
+ }
+}
diff --git a/x/kavadist/simulation/utils.go b/x/kavadist/simulation/utils.go
new file mode 100644
index 00000000..b6f771b7
--- /dev/null
+++ b/x/kavadist/simulation/utils.go
@@ -0,0 +1,64 @@
+package simulation
+
+import (
+ "errors"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+// TODO use the official version available in v0.38.2 of the sdk
+// https://github.com/cosmos/cosmos-sdk/blob/e8d89a2fe26175b73545a3e79ae783032b4e975e/types/decimal.go#L328
+func approxRoot(d sdk.Dec, root uint64) (guess sdk.Dec, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ var ok bool
+ err, ok = r.(error)
+ if !ok {
+ err = errors.New("out of bounds")
+ }
+ }
+ }()
+
+ if d.IsNegative() {
+ absRoot, err := approxRoot(d.MulInt64(-1), root)
+ return absRoot.MulInt64(-1), err
+ }
+ if root == 1 || d.IsZero() || d.Equal(sdk.OneDec()) {
+ return d, nil
+ }
+ if root == 0 {
+ return sdk.OneDec(), nil
+ }
+ rootInt := sdk.NewInt(int64(root))
+ guess, delta := sdk.OneDec(), sdk.OneDec()
+ for delta.Abs().GT(sdk.SmallestDec()) {
+ prev := power(guess, (root - 1))
+ if prev.IsZero() {
+ prev = sdk.SmallestDec()
+ }
+ delta = d.Quo(prev)
+ delta = delta.Sub(guess)
+ delta = delta.QuoInt(rootInt)
+
+ guess = guess.Add(delta)
+ }
+ return guess, nil
+}
+
+// Power returns a the result of raising to a positive integer power
+func power(d sdk.Dec, power uint64) sdk.Dec {
+ if power == 0 {
+ return sdk.OneDec()
+ }
+ tmp := sdk.OneDec()
+ for i := power; i > 1; {
+ if i%2 == 0 {
+ i /= 2
+ } else {
+ tmp = tmp.Mul(d)
+ i = (i - 1) / 2
+ }
+ d = d.Mul(d)
+ }
+ return d.Mul(tmp)
+}
diff --git a/x/kavadist/simulation/utils_test.go b/x/kavadist/simulation/utils_test.go
new file mode 100644
index 00000000..81a76c39
--- /dev/null
+++ b/x/kavadist/simulation/utils_test.go
@@ -0,0 +1,30 @@
+package simulation
+
+import (
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/stretchr/testify/require"
+)
+
+func TestApproxRoot(t *testing.T) {
+ testCases := []struct {
+ input sdk.Dec
+ root uint64
+ expected sdk.Dec
+ }{
+ {sdk.OneDec(), 10, sdk.OneDec()}, // 1.0 ^ (0.1) => 1.0
+ {sdk.NewDecWithPrec(25, 2), 2, sdk.NewDecWithPrec(5, 1)}, // 0.25 ^ (0.5) => 0.5
+ {sdk.NewDecWithPrec(4, 2), 2, sdk.NewDecWithPrec(2, 1)}, // 0.04 => 0.2
+ {sdk.NewDecFromInt(sdk.NewInt(27)), 3, sdk.NewDecFromInt(sdk.NewInt(3))}, // 27 ^ (1/3) => 3
+ {sdk.NewDecFromInt(sdk.NewInt(-81)), 4, sdk.NewDecFromInt(sdk.NewInt(-3))}, // -81 ^ (0.25) => -3
+ {sdk.NewDecFromInt(sdk.NewInt(2)), 2, sdk.NewDecWithPrec(1414213562373095049, 18)}, // 2 ^ (0.5) => 1.414213562373095049
+ {sdk.NewDecWithPrec(1005, 3), 31536000, sdk.MustNewDecFromStr("1.000000000158153904")}, // 1.005 ^ (1/31536000) = 1.000000000158153904
+ }
+
+ for i, tc := range testCases {
+ res, err := approxRoot(tc.input, tc.root)
+ require.NoError(t, err)
+ require.True(t, tc.expected.Sub(res).Abs().LTE(sdk.SmallestDec()), "unexpected result for test case %d, input: %v", i, tc.input)
+ }
+}
diff --git a/x/kavadist/types/codec.go b/x/kavadist/types/codec.go
new file mode 100644
index 00000000..16afc367
--- /dev/null
+++ b/x/kavadist/types/codec.go
@@ -0,0 +1,17 @@
+package types
+
+import "github.com/cosmos/cosmos-sdk/codec"
+
+// ModuleCdc generic sealed codec to be used throughout module
+var ModuleCdc *codec.Codec
+
+func init() {
+ cdc := codec.New()
+ RegisterCodec(cdc)
+ codec.RegisterCrypto(cdc)
+ ModuleCdc = cdc.Seal()
+}
+
+// RegisterCodec registers the necessary types for cdp module
+func RegisterCodec(cdc *codec.Codec) {
+}
diff --git a/x/kavadist/types/events.go b/x/kavadist/types/events.go
new file mode 100644
index 00000000..9bdb0fed
--- /dev/null
+++ b/x/kavadist/types/events.go
@@ -0,0 +1,9 @@
+package types
+
+// Event types for cdp module
+const (
+ EventTypeKavaDist = ModuleName
+ AttributeKeyInflation = "kava_dist_inflation"
+ AttributeKeyStatus = "kava_dist_status"
+ AttributeValueInactive = "inactive"
+)
diff --git a/x/kavadist/types/expected_keepers.go b/x/kavadist/types/expected_keepers.go
new file mode 100644
index 00000000..cbbc3cef
--- /dev/null
+++ b/x/kavadist/types/expected_keepers.go
@@ -0,0 +1,15 @@
+package types // noalias
+
+import (
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/supply/exported"
+)
+
+// SupplyKeeper defines the expected supply keeper
+type SupplyKeeper interface {
+ GetModuleAddress(name string) sdk.AccAddress
+ GetModuleAccount(ctx sdk.Context, name string) exported.ModuleAccountI
+ GetSupply(ctx sdk.Context) (supply exported.SupplyI)
+ SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error
+ MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error
+}
diff --git a/x/kavadist/types/genesis.go b/x/kavadist/types/genesis.go
new file mode 100644
index 00000000..e857ab0a
--- /dev/null
+++ b/x/kavadist/types/genesis.go
@@ -0,0 +1,54 @@
+package types
+
+import (
+ "bytes"
+ "fmt"
+ "time"
+)
+
+// GenesisState is the state that must be provided at genesis.
+type GenesisState struct {
+ Params Params `json:"params" yaml:"params"`
+ PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
+}
+
+// NewGenesisState returns a new genesis state
+func NewGenesisState(params Params, previousBlockTime time.Time) GenesisState {
+ return GenesisState{
+ Params: params,
+ PreviousBlockTime: previousBlockTime,
+ }
+}
+
+// DefaultGenesisState returns a default genesis state
+func DefaultGenesisState() GenesisState {
+ return GenesisState{
+ Params: DefaultParams(),
+ PreviousBlockTime: DefaultPreviousBlockTime,
+ }
+}
+
+// Validate performs basic validation of genesis data returning an
+// error for any failed validation criteria.
+func (gs GenesisState) Validate() error {
+
+ if err := gs.Params.Validate(); err != nil {
+ return err
+ }
+ if gs.PreviousBlockTime.Equal(time.Time{}) {
+ return fmt.Errorf("previous block time not set")
+ }
+ return nil
+}
+
+// Equal checks whether two gov GenesisState structs are equivalent
+func (gs GenesisState) Equal(gs2 GenesisState) bool {
+ b1 := ModuleCdc.MustMarshalBinaryBare(gs)
+ b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
+ return bytes.Equal(b1, b2)
+}
+
+// IsEmpty returns true if a GenesisState is empty
+func (gs GenesisState) IsEmpty() bool {
+ return gs.Equal(GenesisState{})
+}
diff --git a/x/kavadist/types/keys.go b/x/kavadist/types/keys.go
new file mode 100644
index 00000000..82ed8361
--- /dev/null
+++ b/x/kavadist/types/keys.go
@@ -0,0 +1,26 @@
+package types
+
+const (
+ // ModuleName name that will be used throughout the module
+ ModuleName = "kavadist"
+
+ // StoreKey Top level store key where all module items will be stored
+ StoreKey = ModuleName
+
+ // RouterKey Top level router key
+ RouterKey = ModuleName
+
+ // QuerierRoute Top level query string
+ QuerierRoute = ModuleName
+
+ // DefaultParamspace default name for parameter store
+ DefaultParamspace = ModuleName
+
+ // KavaDistMacc module account for kavadist
+ KavaDistMacc = ModuleName
+)
+
+var (
+ CurrentDistPeriodKey = []byte{0x00}
+ PreviousBlockTimeKey = []byte{0x01}
+)
diff --git a/x/kavadist/types/params.go b/x/kavadist/types/params.go
new file mode 100644
index 00000000..9eb01b08
--- /dev/null
+++ b/x/kavadist/types/params.go
@@ -0,0 +1,141 @@
+package types
+
+import (
+ "fmt"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/params"
+ cdptypes "github.com/kava-labs/kava/x/cdp/types"
+ tmtime "github.com/tendermint/tendermint/types/time"
+)
+
+// Parameter keys and default values
+var (
+ KeyActive = []byte("Active")
+ KeyPeriods = []byte("Periods")
+ DefaultActive = false
+ DefaultPeriods = Periods{}
+ DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
+ GovDenom = cdptypes.DefaultGovDenom
+)
+
+// Params governance parameters for kavadist module
+type Params struct {
+ Active bool `json:"active" yaml:"active"`
+ Periods Periods `json:"periods" yaml:"periods"`
+}
+
+// Period stores the specified start and end dates, and the inflation, expressed as a decimal representing the yearly APR of KAVA tokens that will be minted during that period
+type Period struct {
+ Start time.Time `json:"start" yaml:"start"` // example "2020-03-01T15:20:00Z"
+ End time.Time `json:"end" yaml:"end"` // example "2020-06-01T15:20:00Z"
+ Inflation sdk.Dec `json:"inflation" yaml:"inflation"` // example "1.000000003022265980" - 10% inflation
+}
+
+// NewPeriod returns a new instance of Period
+func NewPeriod(start time.Time, end time.Time, inflation sdk.Dec) Period {
+ return Period{
+ Start: start,
+ End: end,
+ Inflation: inflation,
+ }
+}
+
+// String implements fmt.Stringer
+func (pr Period) String() string {
+ return fmt.Sprintf(`Period:
+ Start: %s
+ End: %s
+ Inflation: %s`, pr.Start, pr.End, pr.Inflation)
+}
+
+// Periods array of Period
+type Periods []Period
+
+// String implements fmt.Stringer
+func (prs Periods) String() string {
+ out := "Periods\n"
+ for _, pr := range prs {
+ out += fmt.Sprintf("%s\n", pr)
+ }
+ return out
+}
+
+// NewParams returns a new params object
+func NewParams(active bool, periods Periods) Params {
+ return Params{
+ Active: active,
+ Periods: periods,
+ }
+}
+
+// DefaultParams returns default params for kavadist module
+func DefaultParams() Params {
+ return NewParams(DefaultActive, DefaultPeriods)
+}
+
+// String implements fmt.Stringer
+func (p Params) String() string {
+ return fmt.Sprintf(`Params:
+ Active: %t
+ Periods %s`, p.Active, p.Periods)
+}
+
+// ParamKeyTable Key declaration for parameters
+func ParamKeyTable() params.KeyTable {
+ return params.NewKeyTable().RegisterParamSet(&Params{})
+}
+
+// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
+func (p *Params) ParamSetPairs() params.ParamSetPairs {
+ return params.ParamSetPairs{
+ params.NewParamSetPair(KeyActive, &p.Active, validateActiveParam),
+ params.NewParamSetPair(KeyPeriods, &p.Periods, validatePeriodsParams),
+ }
+}
+
+// Validate checks that the parameters have valid values.
+func (p Params) Validate() error {
+ if err := validateActiveParam(p.Active); err != nil {
+ return err
+ }
+
+ return validatePeriodsParams(p.Periods)
+}
+
+func validateActiveParam(i interface{}) error {
+ _, ok := i.(bool)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ return nil
+}
+
+func validatePeriodsParams(i interface{}) error {
+ periods, ok := i.(Periods)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
+ prevEnd := tmtime.Canonical(time.Unix(0, 0))
+ for _, pr := range periods {
+ if pr.End.Before(pr.Start) {
+ return fmt.Errorf("end time for period is before start time: %s", pr)
+ }
+
+ if pr.Start.Before(prevEnd) {
+ return fmt.Errorf("periods must be in chronological order: %s", periods)
+ }
+ prevEnd = pr.End
+
+ if pr.Start.IsZero() || pr.End.IsZero() {
+ return fmt.Errorf("start or end time cannot be zero: %s", pr)
+ }
+
+ //TODO: validate period Inflation?
+ }
+
+ return nil
+}
diff --git a/x/kavadist/types/params_test.go b/x/kavadist/types/params_test.go
new file mode 100644
index 00000000..f2e02b90
--- /dev/null
+++ b/x/kavadist/types/params_test.go
@@ -0,0 +1,99 @@
+package types_test
+
+import (
+ "testing"
+ "time"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/kava-labs/kava/x/kavadist/types"
+ "github.com/stretchr/testify/suite"
+)
+
+type paramTest struct {
+ params types.Params
+ expectPass bool
+}
+
+type ParamTestSuite struct {
+ suite.Suite
+
+ tests []paramTest
+}
+
+func (suite *ParamTestSuite) SetupTest() {
+ p1 := types.Params{
+ Active: true,
+ Periods: types.Periods{
+ types.Period{
+ Start: time.Date(2020, time.March, 1, 1, 0, 0, 0, time.UTC),
+ End: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC),
+ Inflation: sdk.MustNewDecFromStr("1.000000003022265980"),
+ },
+ types.Period{
+ Start: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC),
+ End: time.Date(2022, time.March, 1, 1, 0, 0, 0, time.UTC),
+ Inflation: sdk.MustNewDecFromStr("1.000000003022265980"),
+ },
+ },
+ }
+ p2 := types.Params{
+ Active: true,
+ Periods: types.Periods{
+ types.Period{
+ Start: time.Date(2022, time.March, 1, 1, 0, 0, 0, time.UTC),
+ End: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC),
+ Inflation: sdk.MustNewDecFromStr("1.000000003022265980"),
+ },
+ types.Period{
+ Start: time.Date(2023, time.March, 1, 1, 0, 0, 0, time.UTC),
+ End: time.Date(2024, time.March, 1, 1, 0, 0, 0, time.UTC),
+ Inflation: sdk.MustNewDecFromStr("1.000000003022265980"),
+ },
+ },
+ }
+ p3 := types.Params{
+ Active: true,
+ Periods: types.Periods{
+ types.Period{
+ Start: time.Date(2020, time.March, 1, 1, 0, 0, 0, time.UTC),
+ End: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC),
+ Inflation: sdk.MustNewDecFromStr("1.000000003022265980"),
+ },
+ types.Period{
+ Start: time.Date(2020, time.March, 1, 1, 0, 0, 0, time.UTC),
+ End: time.Date(2022, time.March, 1, 1, 0, 0, 0, time.UTC),
+ Inflation: sdk.MustNewDecFromStr("1.000000003022265980"),
+ },
+ },
+ }
+
+ suite.tests = []paramTest{
+ paramTest{
+ params: p1,
+ expectPass: true,
+ },
+ paramTest{
+ params: p2,
+ expectPass: false,
+ },
+ paramTest{
+ params: p3,
+ expectPass: false,
+ },
+ }
+}
+
+func (suite *ParamTestSuite) TestParamValidation() {
+ for _, t := range suite.tests {
+ err := t.params.Validate()
+ if t.expectPass {
+ suite.NoError(err)
+ } else {
+ suite.Error(err)
+ }
+ }
+}
+
+func TestGenesisTestSuite(t *testing.T) {
+ suite.Run(t, new(ParamTestSuite))
+}
diff --git a/x/kavadist/types/querier.go b/x/kavadist/types/querier.go
new file mode 100644
index 00000000..104ab89a
--- /dev/null
+++ b/x/kavadist/types/querier.go
@@ -0,0 +1,6 @@
+package types
+
+// Querier routes for the kavadist module
+const (
+ QueryGetParams = "params"
+)
diff --git a/x/pricefeed/alias.go b/x/pricefeed/alias.go
index be764b2c..75affe59 100644
--- a/x/pricefeed/alias.go
+++ b/x/pricefeed/alias.go
@@ -1,8 +1,3 @@
-// nolint
-// autogenerated code using github.com/rigelrozanski/multitool
-// aliases generated for the following subdirectories:
-// ALIASGEN: github.com/kava-labs/kava/x/pricefeed/types/
-// ALIASGEN: github.com/kava-labs/kava/x/pricefeed/keeper/
package pricefeed
import (
@@ -10,13 +5,13 @@ import (
"github.com/kava-labs/kava/x/pricefeed/types"
)
+// autogenerated code using github.com/rigelrozanski/multitool
+// aliases generated for the following subdirectories:
+// ALIASGEN: github.com/kava-labs/kava/x/pricefeed/types/
+// ALIASGEN: github.com/kava-labs/kava/x/pricefeed/keeper/
+
+// nolint
const (
- DefaultCodespace = types.DefaultCodespace
- CodeEmptyInput = types.CodeEmptyInput
- CodeExpired = types.CodeExpired
- CodeInvalidPrice = types.CodeInvalidPrice
- CodeInvalidAsset = types.CodeInvalidAsset
- CodeInvalidOracle = types.CodeInvalidOracle
EventTypeMarketPriceUpdated = types.EventTypeMarketPriceUpdated
EventTypeOracleUpdatedPrice = types.EventTypeOracleUpdatedPrice
EventTypeNoValidPrices = types.EventTypeNoValidPrices
@@ -31,48 +26,56 @@ const (
RouterKey = types.RouterKey
QuerierRoute = types.QuerierRoute
DefaultParamspace = types.DefaultParamspace
- RawPriceFeedPrefix = types.RawPriceFeedPrefix
- CurrentPricePrefix = types.CurrentPricePrefix
- MarketPrefix = types.MarketPrefix
- OraclePrefix = types.OraclePrefix
TypeMsgPostPrice = types.TypeMsgPostPrice
- QueryPrice = types.QueryPrice
- QueryRawPrices = types.QueryRawPrices
+ QueryGetParams = types.QueryGetParams
QueryMarkets = types.QueryMarkets
+ QueryOracles = types.QueryOracles
+ QueryRawPrices = types.QueryRawPrices
+ QueryPrice = types.QueryPrice
)
+// nolint
var (
// functions aliases
- RegisterCodec = types.RegisterCodec
- ErrEmptyInput = types.ErrEmptyInput
- ErrExpired = types.ErrExpired
- ErrNoValidPrice = types.ErrNoValidPrice
- ErrInvalidMarket = types.ErrInvalidMarket
- ErrInvalidOracle = types.ErrInvalidOracle
- NewGenesisState = types.NewGenesisState
- DefaultGenesisState = types.DefaultGenesisState
- NewMsgPostPrice = types.NewMsgPostPrice
- NewParams = types.NewParams
- DefaultParams = types.DefaultParams
- ParamKeyTable = types.ParamKeyTable
- NewKeeper = keeper.NewKeeper
- NewQuerier = keeper.NewQuerier
+ NewKeeper = keeper.NewKeeper
+ NewQuerier = keeper.NewQuerier
+ RegisterCodec = types.RegisterCodec
+ ErrEmptyInput = types.ErrEmptyInput
+ ErrExpired = types.ErrExpired
+ ErrNoValidPrice = types.ErrNoValidPrice
+ ErrInvalidMarket = types.ErrInvalidMarket
+ ErrInvalidOracle = types.ErrInvalidOracle
+ NewGenesisState = types.NewGenesisState
+ DefaultGenesisState = types.DefaultGenesisState
+ CurrentPriceKey = types.CurrentPriceKey
+ RawPriceKey = types.RawPriceKey
+ NewCurrentPrice = types.NewCurrentPrice
+ NewPostedPrice = types.NewPostedPrice
+ NewMsgPostPrice = types.NewMsgPostPrice
+ NewParams = types.NewParams
+ DefaultParams = types.DefaultParams
+ ParamKeyTable = types.ParamKeyTable
+ NewQueryWithMarketIDParams = types.NewQueryWithMarketIDParams
// variable aliases
- ModuleCdc = types.ModuleCdc
- KeyMarkets = types.KeyMarkets
- DefaultMarkets = types.DefaultMarkets
+ ModuleCdc = types.ModuleCdc
+ CurrentPricePrefix = types.CurrentPricePrefix
+ RawPriceFeedPrefix = types.RawPriceFeedPrefix
+ KeyMarkets = types.KeyMarkets
+ DefaultMarkets = types.DefaultMarkets
)
+// nolint
type (
+ Keeper = keeper.Keeper
GenesisState = types.GenesisState
Market = types.Market
Markets = types.Markets
CurrentPrice = types.CurrentPrice
PostedPrice = types.PostedPrice
+ PostedPrices = types.PostedPrices
SortDecs = types.SortDecs
MsgPostPrice = types.MsgPostPrice
Params = types.Params
QueryWithMarketIDParams = types.QueryWithMarketIDParams
- Keeper = keeper.Keeper
)
diff --git a/x/pricefeed/client/cli/query.go b/x/pricefeed/client/cli/query.go
index d1a125c5..b41d388a 100644
--- a/x/pricefeed/client/cli/query.go
+++ b/x/pricefeed/client/cli/query.go
@@ -5,6 +5,7 @@ import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/pricefeed/types"
@@ -22,7 +23,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
RunE: client.ValidateCmd,
}
- pricefeedQueryCmd.AddCommand(client.GetCommands(
+ pricefeedQueryCmd.AddCommand(flags.GetCommands(
GetCmdPrice(queryRoute, cdc),
GetCmdRawPrices(queryRoute, cdc),
GetCmdOracles(queryRoute, cdc),
diff --git a/x/pricefeed/client/cli/tx.go b/x/pricefeed/client/cli/tx.go
index 0b46a6af..2aec48d6 100644
--- a/x/pricefeed/client/cli/tx.go
+++ b/x/pricefeed/client/cli/tx.go
@@ -1,6 +1,7 @@
package cli
import (
+ "bufio"
"fmt"
"time"
@@ -8,6 +9,7 @@ import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
@@ -26,7 +28,7 @@ func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command {
RunE: client.ValidateCmd,
}
- pricefeedTxCmd.AddCommand(client.PostCommands(
+ pricefeedTxCmd.AddCommand(flags.PostCommands(
GetCmdPostPrice(cdc),
)...)
@@ -40,8 +42,9 @@ func GetCmdPostPrice(cdc *codec.Codec) *cobra.Command {
Short: "post the latest price for a particular market",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
+ inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
- txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
+ txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
price, err := sdk.NewDecFromStr(args[1])
if err != nil {
diff --git a/x/pricefeed/client/rest/tx.go b/x/pricefeed/client/rest/tx.go
index 5ba7b8dc..6e0b87e6 100644
--- a/x/pricefeed/client/rest/tx.go
+++ b/x/pricefeed/client/rest/tx.go
@@ -15,7 +15,7 @@ import (
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
- r.HandleFunc(fmt.Sprintf("/%s/postprice", types.ModuleName), postPriceHandlerFn(cliCtx)).Methods("PUT")
+ r.HandleFunc(fmt.Sprintf("/%s/postprice", types.ModuleName), postPriceHandlerFn(cliCtx)).Methods("POST")
}
diff --git a/x/pricefeed/genesis.go b/x/pricefeed/genesis.go
index 9919f6a7..476d4c56 100644
--- a/x/pricefeed/genesis.go
+++ b/x/pricefeed/genesis.go
@@ -25,7 +25,10 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, gs GenesisState) {
// Set the current price (if any) based on what's now in the store
for _, market := range params.Markets {
if market.Active {
- rps := keeper.GetRawPrices(ctx, market.MarketID)
+ rps, err := keeper.GetRawPrices(ctx, market.MarketID)
+ if err != nil {
+ panic(err)
+ }
if len(rps) > 0 {
err := keeper.SetCurrentPrices(ctx, market.MarketID)
if err != nil {
@@ -44,7 +47,10 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
var postedPrices []PostedPrice
for _, market := range keeper.GetMarkets(ctx) {
- pp := keeper.GetRawPrices(ctx, market.MarketID)
+ pp, err := keeper.GetRawPrices(ctx, market.MarketID)
+ if err != nil {
+ panic(err)
+ }
postedPrices = append(postedPrices, pp...)
}
diff --git a/x/pricefeed/handler.go b/x/pricefeed/handler.go
index 628f8951..c43277e3 100644
--- a/x/pricefeed/handler.go
+++ b/x/pricefeed/handler.go
@@ -1,20 +1,18 @@
package pricefeed
import (
- "fmt"
-
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// NewHandler handles all pricefeed type messages
func NewHandler(k Keeper) sdk.Handler {
- return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
+ return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
switch msg := msg.(type) {
case MsgPostPrice:
return HandleMsgPostPrice(ctx, k, msg)
default:
- errMsg := fmt.Sprintf("unrecognized pricefeed message type: %T", msg)
- return sdk.ErrUnknownRequest(errMsg).Result()
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
}
}
}
@@ -26,15 +24,15 @@ func NewHandler(k Keeper) sdk.Handler {
func HandleMsgPostPrice(
ctx sdk.Context,
k Keeper,
- msg MsgPostPrice) sdk.Result {
+ msg MsgPostPrice) (*sdk.Result, error) {
_, err := k.GetOracle(ctx, msg.MarketID, msg.From)
if err != nil {
- return err.Result()
+ return nil, err
}
_, err = k.SetPrice(ctx, msg.From, msg.MarketID, msg.Price, msg.Expiry)
if err != nil {
- return err.Result()
+ return nil, err
}
ctx.EventManager().EmitEvent(
@@ -45,5 +43,5 @@ func HandleMsgPostPrice(
),
)
- return sdk.Result{Events: ctx.EventManager().Events()}
+ return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}
diff --git a/x/pricefeed/keeper/keeper.go b/x/pricefeed/keeper/keeper.go
index 0e4fc8e9..c25e08f2 100644
--- a/x/pricefeed/keeper/keeper.go
+++ b/x/pricefeed/keeper/keeper.go
@@ -1,12 +1,12 @@
package keeper
import (
- "fmt"
"sort"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/pricefeed/types"
@@ -20,19 +20,20 @@ type Keeper struct {
cdc *codec.Codec
// The reference to the Paramstore to get and set pricefeed specific params
paramSubspace subspace.Subspace
- // Reserved codespace
- codespace sdk.CodespaceType
}
// NewKeeper returns a new keeper for the pricefeed module.
func NewKeeper(
- cdc *codec.Codec, key sdk.StoreKey, paramSubspace subspace.Subspace, codespace sdk.CodespaceType,
+ cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace,
) Keeper {
+ if !paramstore.HasKeyTable() {
+ paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
+ }
+
return Keeper{
- paramSubspace: paramSubspace.WithKeyTable(types.ParamKeyTable()),
- key: key,
cdc: cdc,
- codespace: codespace,
+ key: key,
+ paramSubspace: paramstore,
}
}
@@ -42,56 +43,55 @@ func (k Keeper) SetPrice(
oracle sdk.AccAddress,
marketID string,
price sdk.Dec,
- expiry time.Time) (types.PostedPrice, sdk.Error) {
+ expiry time.Time) (types.PostedPrice, error) {
// If the expiry is less than or equal to the current blockheight, we consider the price valid
- if expiry.After(ctx.BlockTime()) {
- store := ctx.KVStore(k.key)
- prices := k.GetRawPrices(ctx, marketID)
- var index int
- found := false
- for i := range prices {
- if prices[i].OracleAddress.Equals(oracle) {
- index = i
- found = true
- break
- }
- }
- // set the price for that particular oracle
- if found {
- prices[index] = types.PostedPrice{
- MarketID: marketID, OracleAddress: oracle,
- Price: price, Expiry: expiry}
- } else {
- prices = append(prices, types.PostedPrice{
- MarketID: marketID, OracleAddress: oracle,
- Price: price, Expiry: expiry})
- index = len(prices) - 1
- }
-
- // Emit an event containing the oracle's new price
- ctx.EventManager().EmitEvent(
- sdk.NewEvent(
- types.EventTypeOracleUpdatedPrice,
- sdk.NewAttribute(types.AttributeMarketID, marketID),
- sdk.NewAttribute(types.AttributeOracle, oracle.String()),
- sdk.NewAttribute(types.AttributeMarketPrice, price.String()),
- sdk.NewAttribute(types.AttributeExpiry, fmt.Sprintf("%d", expiry.Unix())),
- ),
- )
- store.Set(
- []byte(types.RawPriceFeedPrefix+marketID), k.cdc.MustMarshalBinaryBare(prices),
- )
- return prices[index], nil
+ if !expiry.After(ctx.BlockTime()) {
+ return types.PostedPrice{}, types.ErrExpired
}
- return types.PostedPrice{}, types.ErrExpired(k.codespace)
+ store := ctx.KVStore(k.key)
+ prices, err := k.GetRawPrices(ctx, marketID)
+ if err != nil {
+ return types.PostedPrice{}, err
+ }
+ var index int
+ found := false
+ for i := range prices {
+ if prices[i].OracleAddress.Equals(oracle) {
+ index = i
+ found = true
+ break
+ }
+ }
+
+ // set the price for that particular oracle
+ if found {
+ prices[index] = types.NewPostedPrice(marketID, oracle, price, expiry)
+ } else {
+ prices = append(prices, types.NewPostedPrice(marketID, oracle, price, expiry))
+ index = len(prices) - 1
+ }
+
+ // Emit an event containing the oracle's new price
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ types.EventTypeOracleUpdatedPrice,
+ sdk.NewAttribute(types.AttributeMarketID, marketID),
+ sdk.NewAttribute(types.AttributeOracle, oracle.String()),
+ sdk.NewAttribute(types.AttributeMarketPrice, price.String()),
+ sdk.NewAttribute(types.AttributeExpiry, expiry.UTC().String()),
+ ),
+ )
+
+ store.Set(types.RawPriceKey(marketID), k.cdc.MustMarshalBinaryBare(prices))
+ return prices[index], nil
}
// SetCurrentPrices updates the price of an asset to the median of all valid oracle inputs
-func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error {
+func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) error {
_, ok := k.GetMarket(ctx, marketID)
if !ok {
- return types.ErrInvalidMarket(k.codespace, marketID)
+ return sdkerrors.Wrap(types.ErrInvalidMarket, marketID)
}
// store current price
validPrevPrice := true
@@ -100,24 +100,25 @@ func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error {
validPrevPrice = false
}
- prices := k.GetRawPrices(ctx, marketID)
- var notExpiredPrices []types.CurrentPrice
+ prices, err := k.GetRawPrices(ctx, marketID)
+ if err != nil {
+ return err
+ }
+ var notExpiredPrices types.CurrentPrices
// filter out expired prices
for _, v := range prices {
if v.Expiry.After(ctx.BlockTime()) {
- notExpiredPrices = append(notExpiredPrices, types.CurrentPrice{
- MarketID: v.MarketID,
- Price: v.Price,
- })
+ notExpiredPrices = append(notExpiredPrices, types.NewCurrentPrice(v.MarketID, v.Price))
}
}
if len(notExpiredPrices) == 0 {
store := ctx.KVStore(k.key)
store.Set(
- []byte(types.CurrentPricePrefix+marketID), k.cdc.MustMarshalBinaryBare(types.CurrentPrice{}),
+ types.CurrentPriceKey(marketID), k.cdc.MustMarshalBinaryBare(types.CurrentPrice{}),
)
- return types.ErrNoValidPrice(k.codespace)
+ return types.ErrNoValidPrice
}
+
medianPrice := k.CalculateMedianPrice(ctx, notExpiredPrices)
// check case that market price was not set in genesis
@@ -127,28 +128,25 @@ func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeMarketPriceUpdated,
- sdk.NewAttribute(types.AttributeMarketID, fmt.Sprintf("%s", marketID)),
- sdk.NewAttribute(types.AttributeMarketPrice, fmt.Sprintf("%s", medianPrice.String())),
+ sdk.NewAttribute(types.AttributeMarketID, marketID),
+ sdk.NewAttribute(types.AttributeMarketPrice, medianPrice.String()),
),
)
}
}
store := ctx.KVStore(k.key)
- currentPrice := types.CurrentPrice{
- MarketID: marketID,
- Price: medianPrice,
- }
+ currentPrice := types.NewCurrentPrice(marketID, medianPrice)
store.Set(
- []byte(types.CurrentPricePrefix+marketID), k.cdc.MustMarshalBinaryBare(currentPrice),
+ types.CurrentPriceKey(marketID), k.cdc.MustMarshalBinaryBare(currentPrice),
)
return nil
}
// CalculateMedianPrice calculates the median prices for the input prices.
-func (k Keeper) CalculateMedianPrice(ctx sdk.Context, prices []types.CurrentPrice) sdk.Dec {
+func (k Keeper) CalculateMedianPrice(ctx sdk.Context, prices types.CurrentPrices) sdk.Dec {
l := len(prices)
if l == 1 {
@@ -169,38 +167,42 @@ func (k Keeper) CalculateMedianPrice(ctx sdk.Context, prices []types.CurrentPric
}
-func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices []types.CurrentPrice) sdk.Dec {
+func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices types.CurrentPrices) sdk.Dec {
sum := prices[0].Price.Add(prices[1].Price)
mean := sum.Quo(sdk.NewDec(2))
return mean
}
// GetCurrentPrice fetches the current median price of all oracles for a specific market
-func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) (types.CurrentPrice, sdk.Error) {
+func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) (types.CurrentPrice, error) {
store := ctx.KVStore(k.key)
- bz := store.Get([]byte(types.CurrentPricePrefix + marketID))
+ bz := store.Get(types.CurrentPriceKey(marketID))
if bz == nil {
- return types.CurrentPrice{}, types.ErrNoValidPrice(k.codespace)
+ return types.CurrentPrice{}, types.ErrNoValidPrice
}
var price types.CurrentPrice
- k.cdc.MustUnmarshalBinaryBare(bz, &price)
+ err := k.cdc.UnmarshalBinaryBare(bz, &price)
+ if err != nil {
+ return types.CurrentPrice{}, err
+ }
if price.Price.Equal(sdk.ZeroDec()) {
- return types.CurrentPrice{}, types.ErrNoValidPrice(k.codespace)
+ return types.CurrentPrice{}, types.ErrNoValidPrice
}
return price, nil
}
// GetRawPrices fetches the set of all prices posted by oracles for an asset
-func (k Keeper) GetRawPrices(ctx sdk.Context, marketID string) []types.PostedPrice {
+func (k Keeper) GetRawPrices(ctx sdk.Context, marketID string) (types.PostedPrices, error) {
store := ctx.KVStore(k.key)
- bz := store.Get([]byte(types.RawPriceFeedPrefix + marketID))
- var prices []types.PostedPrice
- k.cdc.MustUnmarshalBinaryBare(bz, &prices)
- return prices
-}
-
-// Codespace return the codespace for the keeper
-func (k Keeper) Codespace() sdk.CodespaceType {
- return k.codespace
+ bz := store.Get(types.RawPriceKey(marketID))
+ if bz == nil {
+ return types.PostedPrices{}, nil
+ }
+ var prices types.PostedPrices
+ err := k.cdc.UnmarshalBinaryBare(bz, &prices)
+ if err != nil {
+ return types.PostedPrices{}, err
+ }
+ return prices, nil
}
diff --git a/x/pricefeed/keeper/keeper_test.go b/x/pricefeed/keeper/keeper_test.go
index 0f79b026..2288fa87 100644
--- a/x/pricefeed/keeper/keeper_test.go
+++ b/x/pricefeed/keeper/keeper_test.go
@@ -67,7 +67,8 @@ func TestKeeper_GetSetPrice(t *testing.T) {
time.Now().Add(1*time.Hour))
require.NoError(t, err)
// Get raw prices
- rawPrices := keeper.GetRawPrices(ctx, "tstusd")
+ rawPrices, err := keeper.GetRawPrices(ctx, "tstusd")
+ require.NoError(t, err)
require.Equal(t, len(rawPrices), 1)
require.Equal(t, rawPrices[0].Price.Equal(sdk.MustNewDecFromStr("0.33")), true)
// Set price by oracle 2
@@ -77,7 +78,8 @@ func TestKeeper_GetSetPrice(t *testing.T) {
time.Now().Add(time.Hour*1))
require.NoError(t, err)
- rawPrices = keeper.GetRawPrices(ctx, "tstusd")
+ rawPrices, err = keeper.GetRawPrices(ctx, "tstusd")
+ require.NoError(t, err)
require.Equal(t, len(rawPrices), 2)
require.Equal(t, rawPrices[1].Price.Equal(sdk.MustNewDecFromStr("0.35")), true)
@@ -87,7 +89,8 @@ func TestKeeper_GetSetPrice(t *testing.T) {
sdk.MustNewDecFromStr("0.37"),
time.Now().Add(time.Hour*1))
require.NoError(t, err)
- rawPrices = keeper.GetRawPrices(ctx, "tstusd")
+ rawPrices, err = keeper.GetRawPrices(ctx, "tstusd")
+ require.NoError(t, err)
require.Equal(t, rawPrices[0].Price.Equal(sdk.MustNewDecFromStr("0.37")), true)
}
diff --git a/x/pricefeed/keeper/params.go b/x/pricefeed/keeper/params.go
index 8b297f57..16836359 100644
--- a/x/pricefeed/keeper/params.go
+++ b/x/pricefeed/keeper/params.go
@@ -2,6 +2,7 @@ package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/pricefeed/types"
)
@@ -24,28 +25,27 @@ func (k Keeper) GetMarkets(ctx sdk.Context) types.Markets {
}
// GetOracles returns the oracles in the pricefeed store
-func (k Keeper) GetOracles(ctx sdk.Context, marketID string) ([]sdk.AccAddress, sdk.Error) {
-
+func (k Keeper) GetOracles(ctx sdk.Context, marketID string) ([]sdk.AccAddress, error) {
for _, m := range k.GetMarkets(ctx) {
if marketID == m.MarketID {
return m.Oracles, nil
}
}
- return []sdk.AccAddress{}, types.ErrInvalidMarket(k.Codespace(), marketID)
+ return []sdk.AccAddress{}, sdkerrors.Wrap(types.ErrInvalidMarket, marketID)
}
// GetOracle returns the oracle from the store or an error if not found
-func (k Keeper) GetOracle(ctx sdk.Context, marketID string, address sdk.AccAddress) (sdk.AccAddress, sdk.Error) {
+func (k Keeper) GetOracle(ctx sdk.Context, marketID string, address sdk.AccAddress) (sdk.AccAddress, error) {
oracles, err := k.GetOracles(ctx, marketID)
if err != nil {
- return sdk.AccAddress{}, types.ErrInvalidMarket(k.Codespace(), marketID)
+ return sdk.AccAddress{}, sdkerrors.Wrap(types.ErrInvalidMarket, marketID)
}
for _, addr := range oracles {
if address.Equals(addr) {
return addr, nil
}
}
- return sdk.AccAddress{}, types.ErrInvalidOracle(k.codespace, address)
+ return sdk.AccAddress{}, sdkerrors.Wrap(types.ErrInvalidOracle, address.String())
}
// GetMarket returns the market if it is in the pricefeed system
diff --git a/x/pricefeed/keeper/querier.go b/x/pricefeed/keeper/querier.go
index 94015535..cef65d4c 100644
--- a/x/pricefeed/keeper/querier.go
+++ b/x/pricefeed/keeper/querier.go
@@ -1,19 +1,18 @@
package keeper
import (
- "fmt"
+ abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/codec"
-
sdk "github.com/cosmos/cosmos-sdk/types"
- abci "github.com/tendermint/tendermint/abci/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/pricefeed/types"
)
// NewQuerier is the module level router for state queries
func NewQuerier(keeper Keeper) sdk.Querier {
- return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
+ return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
switch path[0] {
case types.QueryPrice:
return queryPrice(ctx, req, keeper)
@@ -26,93 +25,97 @@ func NewQuerier(keeper Keeper) sdk.Querier {
case types.QueryGetParams:
return queryGetParams(ctx, req, keeper)
default:
- return nil, sdk.ErrUnknownRequest("unknown pricefeed query endpoint")
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
}
}
}
-func queryPrice(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr sdk.Error) {
+func queryPrice(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr error) {
var requestParams types.QueryWithMarketIDParams
- err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
+ err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
- return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
_, found := keeper.GetMarket(ctx, requestParams.MarketID)
if !found {
- return []byte{}, sdk.ErrUnknownRequest("asset not found")
+ return []byte{}, sdkerrors.Wrap(types.ErrAssetNotFound, requestParams.MarketID)
}
currentPrice, sdkErr := keeper.GetCurrentPrice(ctx, requestParams.MarketID)
if sdkErr != nil {
return nil, sdkErr
}
- bz, err := codec.MarshalJSONIndent(keeper.cdc, currentPrice)
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, currentPrice)
if err != nil {
- panic("could not marshal result to JSON")
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
-func queryRawPrices(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr sdk.Error) {
+func queryRawPrices(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr error) {
var requestParams types.QueryWithMarketIDParams
- err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
+ err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
- return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
_, found := keeper.GetMarket(ctx, requestParams.MarketID)
if !found {
- return []byte{}, sdk.ErrUnknownRequest("asset not found")
+ return []byte{}, sdkerrors.Wrap(types.ErrAssetNotFound, requestParams.MarketID)
}
- rawPrices := keeper.GetRawPrices(ctx, requestParams.MarketID)
- bz, err := codec.MarshalJSONIndent(keeper.cdc, rawPrices)
+ rawPrices, err := keeper.GetRawPrices(ctx, requestParams.MarketID)
if err != nil {
- panic("could not marshal result to JSON")
+ return nil, err
+ }
+
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, rawPrices)
+ if err != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
-func queryOracles(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr sdk.Error) {
+func queryOracles(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr error) {
var requestParams types.QueryWithMarketIDParams
- err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
+ err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
- return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
oracles, err := keeper.GetOracles(ctx, requestParams.MarketID)
if err != nil {
- return []byte{}, sdk.ErrUnknownRequest("market not found")
+ return []byte{}, sdkerrors.Wrap(types.ErrAssetNotFound, requestParams.MarketID)
}
- bz, err := codec.MarshalJSONIndent(keeper.cdc, oracles)
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, oracles)
if err != nil {
- panic("could not marshal result to JSON")
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
-func queryMarkets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr sdk.Error) {
+func queryMarkets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr error) {
markets := keeper.GetMarkets(ctx)
- bz, err := codec.MarshalJSONIndent(keeper.cdc, markets)
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, markets)
if err != nil {
- panic("could not marshal result to JSON")
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// query params in the pricefeed store
-func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
params := keeper.GetParams(ctx)
// Encode results
- bz, err := codec.MarshalJSONIndent(keeper.cdc, params)
+ bz, err := codec.MarshalJSONIndent(types.ModuleCdc, params)
if err != nil {
- return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
diff --git a/x/pricefeed/module.go b/x/pricefeed/module.go
index ab167e02..3426a860 100644
--- a/x/pricefeed/module.go
+++ b/x/pricefeed/module.go
@@ -11,6 +11,7 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
+ "github.com/cosmos/cosmos-sdk/x/auth"
sim "github.com/cosmos/cosmos-sdk/x/simulation"
abci "github.com/tendermint/tendermint/abci/types"
@@ -22,7 +23,7 @@ import (
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
- _ module.AppModuleSimulation = AppModuleSimulation{}
+ _ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic app module basics object
@@ -70,39 +71,20 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
//____________________________________________________________________________
-// AppModuleSimulation defines the module simulation functions used by the pricefeed module.
-type AppModuleSimulation struct{}
-
-// RegisterStoreDecoder registers a decoder for pricefeed module's types
-func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
- sdr[StoreKey] = simulation.DecodeStore
-}
-
-// GenerateGenesisState creates a randomized GenState of the pricefeed module
-func (AppModuleSimulation) GenerateGenesisState(simState *module.SimulationState) {
- simulation.RandomizedGenState(simState)
-}
-
-// RandomizedParams creates randomized pricefeed param changes for the simulator.
-func (AppModuleSimulation) RandomizedParams(r *rand.Rand) []sim.ParamChange {
- return simulation.ParamChanges(r)
-}
-
-//____________________________________________________________________________
-
// AppModule app module type
type AppModule struct {
AppModuleBasic
- AppModuleSimulation
- keeper Keeper
+ keeper Keeper
+ accountKeeper auth.AccountKeeper
}
// NewAppModule creates a new AppModule object
-func NewAppModule(keeper Keeper) AppModule {
+func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
+ accountKeeper: accountKeeper,
}
}
@@ -156,3 +138,32 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
+
+//____________________________________________________________________________
+
+// AppModuleSimulation functions
+
+// GenerateGenesisState creates a randomized GenState of the price feed module
+func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
+ simulation.RandomizedGenState(simState)
+}
+
+// ProposalContents doesn't return any content functions for governance proposals.
+func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
+ return nil
+}
+
+// RandomizedParams returns nil because price feed has no params.
+func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
+ return simulation.ParamChanges(r)
+}
+
+// RegisterStoreDecoder registers a decoder for price feed module's types
+func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
+ sdr[StoreKey] = simulation.DecodeStore
+}
+
+// WeightedOperations returns the all the price feed module operations with their respective weights.
+func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
+ return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper)
+}
diff --git a/x/pricefeed/simulation/decoder.go b/x/pricefeed/simulation/decoder.go
index 115565b3..e40c1932 100644
--- a/x/pricefeed/simulation/decoder.go
+++ b/x/pricefeed/simulation/decoder.go
@@ -1,12 +1,31 @@
package simulation
import (
+ "bytes"
+ "fmt"
+
"github.com/cosmos/cosmos-sdk/codec"
- cmn "github.com/tendermint/tendermint/libs/common"
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/kava-labs/kava/x/pricefeed/types"
)
// DecodeStore unmarshals the KVPair's Value to the corresponding pricefeed type
-func DecodeStore(cdc *codec.Codec, kvA, kvB cmn.KVPair) string {
- // TODO implement this
- return ""
+func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
+ switch {
+ case bytes.Contains(kvA.Key, []byte(types.CurrentPricePrefix)):
+ var priceA, priceB types.CurrentPrice
+ cdc.MustUnmarshalBinaryBare(kvA.Value, &priceA)
+ cdc.MustUnmarshalBinaryBare(kvB.Value, &priceB)
+ return fmt.Sprintf("%s\n%s", priceA, priceB)
+
+ case bytes.Contains(kvA.Key, []byte(types.RawPriceFeedPrefix)):
+ var postedPriceA, postedPriceB []types.PostedPrice
+ cdc.MustUnmarshalBinaryBare(kvA.Value, &postedPriceA)
+ cdc.MustUnmarshalBinaryBare(kvB.Value, &postedPriceB)
+ return fmt.Sprintf("%s\n%s", postedPriceA, postedPriceB)
+
+ default:
+ panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
+ }
}
diff --git a/x/pricefeed/simulation/decoder_test.go b/x/pricefeed/simulation/decoder_test.go
new file mode 100644
index 00000000..33ef65a8
--- /dev/null
+++ b/x/pricefeed/simulation/decoder_test.go
@@ -0,0 +1,56 @@
+package simulation
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+
+ "github.com/kava-labs/kava/x/pricefeed/types"
+)
+
+func makeTestCodec() (cdc *codec.Codec) {
+ cdc = codec.New()
+ sdk.RegisterCodec(cdc)
+ codec.RegisterCrypto(cdc)
+ types.RegisterCodec(cdc)
+ return
+}
+
+func TestDecodeDistributionStore(t *testing.T) {
+ cdc := makeTestCodec()
+
+ currentPrice := types.CurrentPrice{MarketID: "current", Price: sdk.OneDec()}
+ postedPrice := []types.PostedPrice{{MarketID: "posted", Price: sdk.OneDec(), Expiry: time.Now().UTC()}}
+
+ kvPairs := kv.Pairs{
+ kv.Pair{Key: []byte(types.CurrentPricePrefix), Value: cdc.MustMarshalBinaryBare(currentPrice)},
+ kv.Pair{Key: []byte(types.RawPriceFeedPrefix), Value: cdc.MustMarshalBinaryBare(postedPrice)},
+ kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
+ }
+
+ tests := []struct {
+ name string
+ expectedLog string
+ }{
+ {"CurrentPrice", fmt.Sprintf("%v\n%v", currentPrice, currentPrice)},
+ {"PostedPrice", fmt.Sprintf("%s\n%s", postedPrice, postedPrice)},
+ {"other", ""},
+ }
+ for i, tt := range tests {
+ i, tt := i, tt
+ t.Run(tt.name, func(t *testing.T) {
+ switch i {
+ case len(tests) - 1:
+ require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
+ default:
+ require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
+ }
+ })
+ }
+}
diff --git a/x/pricefeed/simulation/genesis.go b/x/pricefeed/simulation/genesis.go
index 756fc988..a1d049a0 100644
--- a/x/pricefeed/simulation/genesis.go
+++ b/x/pricefeed/simulation/genesis.go
@@ -2,21 +2,72 @@ package simulation
import (
"fmt"
+ "time"
"github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
+ "github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/pricefeed/types"
+ pricefeed "github.com/kava-labs/kava/x/pricefeed/types"
+)
+
+var (
+ // BaseAssets is a list of collateral asset denoms
+ BaseAssets = [3]string{"bnb", "xrp", "btc"}
+ QuoteAsset = "usd"
)
// RandomizedGenState generates a random GenesisState for pricefeed
func RandomizedGenState(simState *module.SimulationState) {
-
- // TODO implement this fully
- // - randomly generating the genesis params
- // - overwriting with genesis provided to simulation
- pricefeedGenesis := types.DefaultGenesisState()
-
+ pricefeedGenesis := loadPricefeedGenState(simState)
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, pricefeedGenesis))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(pricefeedGenesis)
}
+
+// loadPricefeedGenState loads a valid pricefeed gen state
+func loadPricefeedGenState(simState *module.SimulationState) pricefeed.GenesisState {
+ var markets []pricefeed.Market
+ var postedPrices []pricefeed.PostedPrice
+ for _, denom := range BaseAssets {
+ // Select an account to be the oracle
+ oracle, _ := simulation.RandomAcc(simState.Rand, simState.Accounts)
+
+ marketID := fmt.Sprintf("%s:%s", denom, QuoteAsset)
+ // Construct market for asset
+ market := pricefeed.Market{
+ MarketID: marketID,
+ BaseAsset: denom,
+ QuoteAsset: QuoteAsset,
+ Oracles: []sdk.AccAddress{oracle.Address},
+ Active: true,
+ }
+
+ // Construct posted price for asset
+ postedPrice := pricefeed.PostedPrice{
+ MarketID: market.MarketID,
+ OracleAddress: oracle.Address,
+ Price: getInitialPrice(marketID),
+ Expiry: simState.GenTimestamp.Add(time.Hour * 24),
+ }
+ markets = append(markets, market)
+ postedPrices = append(postedPrices, postedPrice)
+ }
+ params := pricefeed.NewParams(markets)
+ return pricefeed.NewGenesisState(params, postedPrices)
+}
+
+// getInitialPrice gets the starting price for each of the base assets
+func getInitialPrice(marketID string) (price sdk.Dec) {
+ switch marketID {
+ case "btc:usd":
+ return sdk.MustNewDecFromStr("7000")
+ case "bnb:usd":
+ return sdk.MustNewDecFromStr("14")
+ case "xrp:usd":
+ return sdk.MustNewDecFromStr("0.2")
+ default:
+ return sdk.MustNewDecFromStr("20") // Catch future additional assets
+ }
+}
diff --git a/x/pricefeed/simulation/operations.go b/x/pricefeed/simulation/operations.go
new file mode 100644
index 00000000..f620d172
--- /dev/null
+++ b/x/pricefeed/simulation/operations.go
@@ -0,0 +1,201 @@
+package simulation
+
+import (
+ "math/rand"
+ "sync"
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/baseapp"
+ "github.com/cosmos/cosmos-sdk/codec"
+ "github.com/cosmos/cosmos-sdk/simapp/helpers"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+ "github.com/cosmos/cosmos-sdk/x/simulation"
+
+ appparams "github.com/kava-labs/kava/app/params"
+ "github.com/kava-labs/kava/x/pricefeed/keeper"
+ "github.com/kava-labs/kava/x/pricefeed/types"
+)
+
+var (
+ noOpMsg = simulation.NoOpMsg(types.ModuleName)
+ btcPrices = []sdk.Dec{}
+ bnbPrices = []sdk.Dec{}
+ xrpPrices = []sdk.Dec{}
+ genPrices sync.Once
+)
+
+// Simulation operation weights constants
+const (
+ OpWeightMsgUpdatePrices = "op_weight_msg_update_prices"
+)
+
+// WeightedOperations returns all the operations from the module with their respective weights
+func WeightedOperations(
+ appParams simulation.AppParams, cdc *codec.Codec, ak auth.AccountKeeper, k keeper.Keeper,
+) simulation.WeightedOperations {
+ var weightMsgUpdatePrices int
+ // var numBlocks int
+
+ appParams.GetOrGenerate(cdc, OpWeightMsgUpdatePrices, &weightMsgUpdatePrices, nil,
+ func(_ *rand.Rand) {
+ weightMsgUpdatePrices = appparams.DefaultWeightMsgUpdatePrices
+ },
+ )
+
+ return simulation.WeightedOperations{
+ simulation.NewWeightedOperation(
+ weightMsgUpdatePrices,
+ SimulateMsgUpdatePrices(ak, k, 10000),
+ ),
+ }
+}
+
+// SimulateMsgUpdatePrices updates the prices of various assets by randomly varying them based on current price
+func SimulateMsgUpdatePrices(ak auth.AccountKeeper, keeper keeper.Keeper, blocks int) simulation.Operation {
+ return func(
+ r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
+ ) (simulation.OperationMsg, []simulation.FutureOperation, error) {
+
+ genPrices.Do(func() {
+ // generate a random walk for each asset exactly once, with observations equal to the number of blocks in the sim
+ for _, m := range keeper.GetMarkets(ctx) {
+ startPrice := getStartPrice(m.MarketID)
+ // allow prices to fluctuate from 10x GAINZ to 100x REKT
+ maxPrice := sdk.MustNewDecFromStr("10.0").Mul(startPrice)
+ minPrice := sdk.MustNewDecFromStr("0.01").Mul(startPrice)
+ previousPrice := startPrice
+ for i := 0; i < blocks; i++ {
+ increment := getIncrement(m.MarketID)
+ // note calling r instead of rand here breaks determinism
+ upDown := rand.Intn(2)
+ if upDown == 0 {
+ if previousPrice.Add(increment).GT(maxPrice) {
+ previousPrice = maxPrice
+ } else {
+ previousPrice = previousPrice.Add(increment)
+ }
+ } else {
+ if previousPrice.Sub(increment).LT(minPrice) {
+ previousPrice = minPrice
+ } else {
+ previousPrice = previousPrice.Sub(increment)
+ }
+ }
+ setPrice(m.MarketID, previousPrice)
+ }
+ }
+ })
+
+ randomMarket := pickRandomAsset(ctx, keeper, r)
+ marketID := randomMarket.MarketID
+ address := getRandomOracle(r, randomMarket)
+
+ oracle, found := simulation.FindAccount(accs, address)
+ if !found {
+ return simulation.NoOpMsg(types.ModuleName), nil, nil
+ }
+
+ oracleAcc := ak.GetAccount(ctx, oracle.Address)
+ if oracleAcc == nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, nil
+ }
+
+ price := pickNewRandomPrice(marketID, int(ctx.BlockHeight()))
+
+ // get the expiry time based off the current time
+ expiry := getExpiryTime(ctx)
+
+ // now create the msg to post price
+ msg := types.NewMsgPostPrice(oracle.Address, marketID, price, expiry)
+
+ spendable := oracleAcc.SpendableCoins(ctx.BlockTime())
+ fees, err := simulation.RandomFees(r, ctx, spendable)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+
+ tx := helpers.GenTx(
+ []sdk.Msg{msg},
+ fees,
+ helpers.DefaultGenTxGas,
+ chainID,
+ []uint64{oracleAcc.GetAccountNumber()},
+ []uint64{oracleAcc.GetSequence()},
+ oracle.PrivKey,
+ )
+
+ _, result, err := app.Deliver(tx)
+ if err != nil {
+ return simulation.NoOpMsg(types.ModuleName), nil, err
+ }
+ return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
+ }
+}
+
+func getStartPrice(marketID string) (startPrice sdk.Dec) {
+ switch marketID {
+ case "btc:usd":
+ return sdk.MustNewDecFromStr("7000")
+ case "bnb:usd":
+ return sdk.MustNewDecFromStr("15")
+ case "xrp:usd":
+ return sdk.MustNewDecFromStr("0.25")
+ }
+ return sdk.MustNewDecFromStr("100")
+}
+
+func getIncrement(marketID string) (increment sdk.Dec) {
+ startPrice := getStartPrice(marketID)
+ divisor := sdk.MustNewDecFromStr("20")
+ increment = startPrice.Quo(divisor)
+ return increment
+}
+
+func setPrice(marketID string, price sdk.Dec) {
+ switch marketID {
+ case "btc:usd":
+ btcPrices = append(btcPrices, price)
+ return
+ case "bnb:usd":
+ bnbPrices = append(bnbPrices, price)
+ return
+ case "xrp:usd":
+ xrpPrices = append(xrpPrices, price)
+ }
+ return
+}
+
+func pickNewRandomPrice(marketID string, blockHeight int) (newPrice sdk.Dec) {
+ switch marketID {
+ case "btc:usd":
+ return btcPrices[blockHeight-1]
+ case "bnb:usd":
+ return bnbPrices[blockHeight-1]
+ case "xrp:usd":
+ return xrpPrices[blockHeight-1]
+ }
+ panic("invalid price request")
+}
+
+// getRandomOracle picks a random oracle from the list of oracles
+func getRandomOracle(r *rand.Rand, market types.Market) sdk.AccAddress {
+ randomIndex := simulation.RandIntBetween(r, 0, len(market.Oracles))
+ return market.Oracles[randomIndex]
+}
+
+// pickRandomAsset picks a random asset out of the assets with equal probability
+// it returns the Market which includes the base asset as one of its fields
+func pickRandomAsset(ctx sdk.Context, keeper keeper.Keeper, r *rand.Rand) (market types.Market) {
+ // get the params
+ params := keeper.GetParams(ctx)
+ // now pick a random asset
+ randomIndex := simulation.RandIntBetween(r, 0, len(params.Markets))
+ return params.Markets[randomIndex]
+}
+
+// getExpiryTime gets a price expiry time by taking the current time and adding a delta to it
+func getExpiryTime(ctx sdk.Context) (t time.Time) {
+ // need to use the blocktime from the context as the context generates random start time when running simulations
+ return ctx.BlockTime().Add(time.Second * 1000000)
+}
diff --git a/x/pricefeed/simulation/params.go b/x/pricefeed/simulation/params.go
index 8c1f7aff..36ff612a 100644
--- a/x/pricefeed/simulation/params.go
+++ b/x/pricefeed/simulation/params.go
@@ -9,6 +9,5 @@ import (
// ParamChanges defines the parameters that can be modified by param change proposals
// on the simulation
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
- // TODO implement this
return []simulation.ParamChange{}
}
diff --git a/x/pricefeed/types/errors.go b/x/pricefeed/types/errors.go
index 6d545f02..1d14aa38 100644
--- a/x/pricefeed/types/errors.go
+++ b/x/pricefeed/types/errors.go
@@ -1,48 +1,22 @@
package types
import (
- "fmt"
-
- sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
-const (
- // DefaultCodespace codespace for the module
- DefaultCodespace sdk.CodespaceType = ModuleName
+// DONTCOVER
- // CodeEmptyInput error code for empty input errors
- CodeEmptyInput sdk.CodeType = 1
- // CodeExpired error code for expired prices
- CodeExpired sdk.CodeType = 2
- // CodeInvalidPrice error code for all input prices expired
- CodeInvalidPrice sdk.CodeType = 3
- // CodeInvalidAsset error code for invalid asset
- CodeInvalidAsset sdk.CodeType = 4
- // CodeInvalidOracle error code for invalid oracle
- CodeInvalidOracle sdk.CodeType = 5
+var (
+ // ErrEmptyInput error for empty input
+ ErrEmptyInput = sdkerrors.Register(ModuleName, 2, "input must not be empty")
+ // ErrExpired error for posted price messages with expired price
+ ErrExpired = sdkerrors.Register(ModuleName, 3, "price is expired")
+ // ErrNoValidPrice error for posted price messages with expired price
+ ErrNoValidPrice = sdkerrors.Register(ModuleName, 4, "all input prices are expired")
+ // ErrInvalidMarket error for posted price messages for invalid markets
+ ErrInvalidMarket = sdkerrors.Register(ModuleName, 5, "market does not exist")
+ // ErrInvalidOracle error for posted price messages for invalid oracles
+ ErrInvalidOracle = sdkerrors.Register(ModuleName, 6, "oracle does not exist or not authorized")
+ // ErrAssetNotFound error for not found asset
+ ErrAssetNotFound = sdkerrors.Register(ModuleName, 7, "asset not found")
)
-
-// ErrEmptyInput Error constructor
-func ErrEmptyInput(codespace sdk.CodespaceType) sdk.Error {
- return sdk.NewError(codespace, CodeEmptyInput, fmt.Sprintf("Input must not be empty."))
-}
-
-// ErrExpired Error constructor for posted price messages with expired price
-func ErrExpired(codespace sdk.CodespaceType) sdk.Error {
- return sdk.NewError(codespace, CodeExpired, fmt.Sprintf("Price is expired."))
-}
-
-// ErrNoValidPrice Error constructor for posted price messages with expired price
-func ErrNoValidPrice(codespace sdk.CodespaceType) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidPrice, fmt.Sprintf("All input prices are expired."))
-}
-
-// ErrInvalidMarket Error constructor for posted price messages for invalid markets
-func ErrInvalidMarket(codespace sdk.CodespaceType, marketId string) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidAsset, fmt.Sprintf("market %s does not exist", marketId))
-}
-
-// ErrInvalidOracle Error constructor for posted price messages for invalid oracles
-func ErrInvalidOracle(codespace sdk.CodespaceType, addr sdk.AccAddress) sdk.Error {
- return sdk.NewError(codespace, CodeInvalidOracle, fmt.Sprintf("oracle %s does not exist or not authorized", addr))
-}
diff --git a/x/pricefeed/types/key.go b/x/pricefeed/types/key.go
index a1107954..74a8a81b 100644
--- a/x/pricefeed/types/key.go
+++ b/x/pricefeed/types/key.go
@@ -15,16 +15,22 @@ const (
// DefaultParamspace default namestore
DefaultParamspace = ModuleName
+)
+
+var (
+ // CurrentPricePrefix prefix for the current price of an asset
+ CurrentPricePrefix = []byte{0x00}
// RawPriceFeedPrefix prefix for the raw pricefeed of an asset
- RawPriceFeedPrefix = StoreKey + ":raw:"
-
- // CurrentPricePrefix prefix for the current price of an asset
- CurrentPricePrefix = StoreKey + ":currentprice:"
-
- // MarketPrefix Prefix for the assets in the pricefeed system
- MarketPrefix = StoreKey + ":markets"
-
- // OraclePrefix store prefix for the oracle accounts
- OraclePrefix = StoreKey + ":oracles"
+ RawPriceFeedPrefix = []byte{0x01}
)
+
+// CurrentPriceKey returns the prefix for the current price
+func CurrentPriceKey(marketID string) []byte {
+ return append(CurrentPricePrefix, []byte(marketID)...)
+}
+
+// RawPriceKey returns the prefix for the raw price
+func RawPriceKey(marketID string) []byte {
+ return append(RawPriceFeedPrefix, []byte(marketID)...)
+}
diff --git a/x/pricefeed/types/market.go b/x/pricefeed/types/market.go
index a357db73..16231be8 100644
--- a/x/pricefeed/types/market.go
+++ b/x/pricefeed/types/market.go
@@ -46,6 +46,14 @@ type CurrentPrice struct {
Price sdk.Dec `json:"price" yaml:"price"`
}
+// NewCurrentPrice returns an instance of CurrentPrice
+func NewCurrentPrice(marketID string, price sdk.Dec) CurrentPrice {
+ return CurrentPrice{MarketID: marketID, Price: price}
+}
+
+// CurrentPrices type for an array of CurrentPrice
+type CurrentPrices []CurrentPrice
+
// PostedPrice price for market posted by a specific oracle
type PostedPrice struct {
MarketID string `json:"market_id" yaml:"market_id"`
@@ -54,6 +62,19 @@ type PostedPrice struct {
Expiry time.Time `json:"expiry" yaml:"expiry"`
}
+// NewPostedPrice returns a new PostedPrice
+func NewPostedPrice(marketID string, oracle sdk.AccAddress, price sdk.Dec, expiry time.Time) PostedPrice {
+ return PostedPrice{
+ MarketID: marketID,
+ OracleAddress: oracle,
+ Price: price,
+ Expiry: expiry,
+ }
+}
+
+// PostedPrices type for an array of PostedPrice
+type PostedPrices []PostedPrice
+
// implement fmt.Stringer
func (cp CurrentPrice) String() string {
return strings.TrimSpace(fmt.Sprintf(`Market ID: %s
@@ -68,6 +89,15 @@ Price: %s
Expiry: %s`, pp.MarketID, pp.OracleAddress, pp.Price, pp.Expiry))
}
+// String implements fmt.Stringer
+func (ps PostedPrices) String() string {
+ out := "Posted Prices:\n"
+ for _, p := range ps {
+ out += fmt.Sprintf("%s\n", p.String())
+ }
+ return strings.TrimSpace(out)
+}
+
// SortDecs provides the interface needed to sort sdk.Dec slices
type SortDecs []sdk.Dec
diff --git a/x/pricefeed/types/msgs.go b/x/pricefeed/types/msgs.go
index 059c3866..77476699 100644
--- a/x/pricefeed/types/msgs.go
+++ b/x/pricefeed/types/msgs.go
@@ -1,9 +1,13 @@
package types
import (
+ "errors"
+ "fmt"
+ "strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
const (
@@ -55,16 +59,18 @@ func (msg MsgPostPrice) GetSigners() []sdk.AccAddress {
}
// ValidateBasic does a simple validation check that doesn't require access to any other information.
-func (msg MsgPostPrice) ValidateBasic() sdk.Error {
+func (msg MsgPostPrice) ValidateBasic() error {
if msg.From.Empty() {
- return sdk.ErrInternal("invalid (empty) from address")
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
- if len(msg.MarketID) == 0 {
- return sdk.ErrInternal("invalid (empty) market id")
+ if strings.TrimSpace(msg.MarketID) == "" {
+ return errors.New("market id cannot be blank")
}
- if msg.Price.LT(sdk.ZeroDec()) {
- return sdk.ErrInternal("invalid (negative) price")
+ if msg.Price.IsNegative() {
+ return fmt.Errorf("price cannot be negative: %s", msg.Price.String())
+ }
+ if msg.Expiry.IsZero() {
+ return errors.New("must set an expiration time")
}
- // TODO check coin denoms
return nil
}
diff --git a/x/pricefeed/types/params.go b/x/pricefeed/types/params.go
index 6236a8eb..acb748d4 100644
--- a/x/pricefeed/types/params.go
+++ b/x/pricefeed/types/params.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/params"
)
@@ -39,7 +40,7 @@ func ParamKeyTable() params.KeyTable {
// pairs of pricefeed module's parameters.
func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{
- {Key: KeyMarkets, Value: &p.Markets},
+ params.NewParamSetPair(KeyMarkets, &p.Markets, validateMarketParams),
}
}
@@ -54,11 +55,21 @@ func (p Params) String() string {
// Validate ensure that params have valid values
func (p Params) Validate() error {
+ return validateMarketParams(p.Markets)
+}
+
+func validateMarketParams(i interface{}) error {
+ markets, ok := i.(Markets)
+ if !ok {
+ return fmt.Errorf("invalid parameter type: %T", i)
+ }
+
// iterate over assets and verify them
- for _, asset := range p.Markets {
- if asset.MarketID == "" {
- return fmt.Errorf("invalid market: %s. missing market ID", asset.String())
+ for _, asset := range markets {
+ if strings.TrimSpace(asset.MarketID) == "" {
+ return sdkerrors.Wrapf(ErrInvalidMarket, "market id for asset %s cannot be blank", asset)
}
}
+
return nil
}
diff --git a/x/validator-vesting/abci.go b/x/validator-vesting/abci.go
index 43a4f0b8..bd896d56 100644
--- a/x/validator-vesting/abci.go
+++ b/x/validator-vesting/abci.go
@@ -7,7 +7,7 @@ import (
tmtime "github.com/tendermint/tendermint/types/time"
sdk "github.com/cosmos/cosmos-sdk/types"
- "github.com/kava-labs/kava/x/validator-vesting/internal/keeper"
+ "github.com/kava-labs/kava/x/validator-vesting/keeper"
abci "github.com/tendermint/tendermint/abci/types"
)
diff --git a/x/validator-vesting/abci_test.go b/x/validator-vesting/abci_test.go
index b2e65667..dcac8266 100644
--- a/x/validator-vesting/abci_test.go
+++ b/x/validator-vesting/abci_test.go
@@ -11,8 +11,8 @@ import (
"github.com/cosmos/cosmos-sdk/x/staking"
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
- "github.com/kava-labs/kava/x/validator-vesting/internal/keeper"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+ "github.com/kava-labs/kava/x/validator-vesting/keeper"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
)
func TestBeginBlockerZeroHeight(t *testing.T) {
diff --git a/x/validator-vesting/alias.go b/x/validator-vesting/alias.go
index 9246d614..5ddc76e1 100644
--- a/x/validator-vesting/alias.go
+++ b/x/validator-vesting/alias.go
@@ -1,13 +1,13 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
-// ALIASGEN: github.com/kava-labs/kava/x/validator-vesting/internal/keeper
-// ALIASGEN: github.com/kava-labs/kava/x/validator-vesting/internal/types
+// ALIASGEN: github.com/kava-labs/kava/x/validator-vesting/keeper
+// ALIASGEN: github.com/kava-labs/kava/x/validator-vesting/types
package validatorvesting
import (
- "github.com/kava-labs/kava/x/validator-vesting/internal/keeper"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+ "github.com/kava-labs/kava/x/validator-vesting/keeper"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
)
const (
diff --git a/x/validator-vesting/client/cli/query.go b/x/validator-vesting/client/cli/query.go
index 5b13bff7..b4d45251 100644
--- a/x/validator-vesting/client/cli/query.go
+++ b/x/validator-vesting/client/cli/query.go
@@ -3,11 +3,12 @@ package cli
import (
"fmt"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
"github.com/spf13/cobra"
- "github.com/cosmos/cosmos-sdk/client"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
+
"github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
)
@@ -19,7 +20,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
Short: "Querying commands for the validator vesting module",
}
- queryValidatorVestingCmd.AddCommand(client.GetCommands(
+ queryValidatorVestingCmd.AddCommand(flags.GetCommands(
QueryCirculatingSupplyCmd(queryRoute, cdc),
QueryTotalSupplyCmd(queryRoute, cdc),
)...)
diff --git a/x/validator-vesting/client/rest/query.go b/x/validator-vesting/client/rest/query.go
index 3dbf8a3e..ebe1de2e 100644
--- a/x/validator-vesting/client/rest/query.go
+++ b/x/validator-vesting/client/rest/query.go
@@ -8,7 +8,7 @@ import (
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/gorilla/mux"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
)
// define routes that get registered by the main application
diff --git a/x/validator-vesting/genesis.go b/x/validator-vesting/genesis.go
index d3e463c6..e987b00c 100644
--- a/x/validator-vesting/genesis.go
+++ b/x/validator-vesting/genesis.go
@@ -2,7 +2,7 @@ package validatorvesting
import (
sdk "github.com/cosmos/cosmos-sdk/types"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
)
// InitGenesis stores the account address of each ValidatorVestingAccount in the validator vesting keeper, for faster lookup.
diff --git a/x/validator-vesting/internal/keeper/keeper.go b/x/validator-vesting/keeper/keeper.go
similarity index 99%
rename from x/validator-vesting/internal/keeper/keeper.go
rename to x/validator-vesting/keeper/keeper.go
index bff59ee4..f6bdade1 100644
--- a/x/validator-vesting/internal/keeper/keeper.go
+++ b/x/validator-vesting/keeper/keeper.go
@@ -7,7 +7,7 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
"github.com/tendermint/tendermint/libs/log"
)
@@ -138,7 +138,7 @@ func (k Keeper) SetVestingProgress(ctx sdk.Context, addr sdk.AccAddress, period
// AddDebt adds the input amount to DebtAfterFailedVesting field
func (k Keeper) AddDebt(ctx sdk.Context, addr sdk.AccAddress, amount sdk.Coins) {
vv := k.GetAccountFromAuthKeeper(ctx, addr)
- vv.DebtAfterFailedVesting = vv.DebtAfterFailedVesting.Add(amount)
+ vv.DebtAfterFailedVesting = vv.DebtAfterFailedVesting.Add(amount...)
k.ak.SetAccount(ctx, vv)
}
diff --git a/x/validator-vesting/internal/keeper/keeper_test.go b/x/validator-vesting/keeper/keeper_test.go
similarity index 99%
rename from x/validator-vesting/internal/keeper/keeper_test.go
rename to x/validator-vesting/keeper/keeper_test.go
index cedf77d3..c3aaafb8 100644
--- a/x/validator-vesting/internal/keeper/keeper_test.go
+++ b/x/validator-vesting/keeper/keeper_test.go
@@ -11,7 +11,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/staking"
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
)
func TestGetSetValidatorVestingAccounts(t *testing.T) {
@@ -349,7 +349,7 @@ func TestHandleVestingDebtReturn(t *testing.T) {
require.Equal(t, sdk.NewCoins(sdk.NewInt64Coin(stakeDenom, 30000000)), vva.DebtAfterFailedVesting)
initialBalance := ak.GetAccount(ctx, TestAddrs[2]).GetCoins()
- expectedBalance := initialBalance.Add(vva.DebtAfterFailedVesting)
+ expectedBalance := initialBalance.Add(vva.DebtAfterFailedVesting...)
// Context needs the block time because bank keeper calls 'SpendableCoins' by getting the header from the context.
ctx = ctx.WithBlockTime(now.Add(12 * time.Hour))
keeper.HandleVestingDebt(ctx, vva.Address, now.Add(12*time.Hour))
diff --git a/x/validator-vesting/internal/keeper/querier.go b/x/validator-vesting/keeper/querier.go
similarity index 75%
rename from x/validator-vesting/internal/keeper/querier.go
rename to x/validator-vesting/keeper/querier.go
index dafc5f5b..4c5727e9 100644
--- a/x/validator-vesting/internal/keeper/querier.go
+++ b/x/validator-vesting/keeper/querier.go
@@ -2,37 +2,40 @@ package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+
+ "github.com/kava-labs/kava/x/validator-vesting/types"
+
abci "github.com/tendermint/tendermint/abci/types"
)
// NewQuerier returns a new querier function
func NewQuerier(keeper Keeper) sdk.Querier {
- return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
+ return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
switch path[0] {
case types.QueryCirculatingSupply:
return queryGetCirculatingSupply(ctx, req, keeper)
case types.QueryTotalSupply:
return queryGetTotalSupply(ctx, req, keeper)
default:
- return nil, sdk.ErrUnknownRequest("unknown cdp query endpoint")
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint: %s", types.ModuleName, path[0])
}
}
}
-func queryGetTotalSupply(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryGetTotalSupply(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
totalSupply := keeper.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf("ukava")
supplyInt := sdk.NewDecFromInt(totalSupply).Mul(sdk.MustNewDecFromStr("0.000001")).TruncateInt64()
- bz, err := keeper.cdc.MarshalJSON(supplyInt)
+ bz, err := types.ModuleCdc.MarshalJSON(supplyInt)
if err != nil {
- return nil, sdk.ErrInternal(err.Error())
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
return bz, nil
}
-func queryGetCirculatingSupply(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
+func queryGetCirculatingSupply(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
circulatingSupply := keeper.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf("ukava")
keeper.ak.IterateAccounts(ctx,
func(acc authexported.Account) (stop bool) {
@@ -54,9 +57,9 @@ func queryGetCirculatingSupply(ctx sdk.Context, req abci.RequestQuery, keeper Ke
return false
})
supplyInt := sdk.NewDecFromInt(circulatingSupply).Mul(sdk.MustNewDecFromStr("0.000001")).TruncateInt64()
- bz, err := keeper.cdc.MarshalJSON(supplyInt)
+ bz, err := types.ModuleCdc.MarshalJSON(supplyInt)
if err != nil {
- return nil, sdk.ErrInternal(err.Error())
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
return bz, nil
}
diff --git a/x/validator-vesting/internal/keeper/test_common.go b/x/validator-vesting/keeper/test_common.go
similarity index 96%
rename from x/validator-vesting/internal/keeper/test_common.go
rename to x/validator-vesting/keeper/test_common.go
index 8c630329..fbb817ca 100644
--- a/x/validator-vesting/internal/keeper/test_common.go
+++ b/x/validator-vesting/keeper/test_common.go
@@ -25,7 +25,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/params"
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/cosmos-sdk/x/supply"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
)
//nolint: deadcode unused
@@ -120,12 +120,12 @@ func CreateTestInput(t *testing.T, isCheckTx bool, initPower int64) (sdk.Context
cdc := MakeTestCodec()
- pk := params.NewKeeper(cdc, keyParams, tkeyParams, params.DefaultCodespace)
+ pk := params.NewKeeper(cdc, keyParams, tkeyParams)
- stakingParams := staking.NewParams(time.Hour, 100, uint16(7), sdk.DefaultBondDenom)
+ stakingParams := staking.NewParams(time.Hour, 100, uint16(7), 0, sdk.DefaultBondDenom)
accountKeeper := auth.NewAccountKeeper(cdc, keyAcc, pk.Subspace(auth.DefaultParamspace), auth.ProtoBaseAccount)
- bankKeeper := bank.NewBaseKeeper(accountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs)
+ bankKeeper := bank.NewBaseKeeper(accountKeeper, pk.Subspace(bank.DefaultParamspace), blacklistedAddrs)
maccPerms := map[string][]string{
auth.FeeCollectorName: nil,
staking.NotBondedPoolName: {supply.Burner, supply.Staking},
@@ -134,7 +134,7 @@ func CreateTestInput(t *testing.T, isCheckTx bool, initPower int64) (sdk.Context
}
supplyKeeper := supply.NewKeeper(cdc, keySupply, accountKeeper, bankKeeper, maccPerms)
- stakingKeeper := staking.NewKeeper(cdc, keyStaking, supplyKeeper, pk.Subspace(staking.DefaultParamspace), staking.DefaultCodespace)
+ stakingKeeper := staking.NewKeeper(cdc, keyStaking, supplyKeeper, pk.Subspace(staking.DefaultParamspace))
stakingKeeper.SetParams(ctx, stakingParams)
keeper := NewKeeper(cdc, keyValidatorVesting, accountKeeper, bankKeeper, supplyKeeper, stakingKeeper)
diff --git a/x/validator-vesting/module.go b/x/validator-vesting/module.go
index 16a081e4..f918d028 100644
--- a/x/validator-vesting/module.go
+++ b/x/validator-vesting/module.go
@@ -15,14 +15,14 @@ import (
sim "github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/validator-vesting/client/cli"
"github.com/kava-labs/kava/x/validator-vesting/client/rest"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
"github.com/kava-labs/kava/x/validator-vesting/simulation"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
- _ module.AppModuleSimulation = AppModuleSimulation{}
+ _ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic defines the basic application module used by the auth module.
@@ -66,28 +66,10 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
-// AppModuleSimulation defines the module simulation functions used by the auth module.
-type AppModuleSimulation struct{}
-
-// RegisterStoreDecoder registers a decoder for auth module's types
-func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
- sdr[StoreKey] = simulation.DecodeStore
-}
-
-// GenerateGenesisState creates a randomized GenState of the auth module
-func (AppModuleSimulation) GenerateGenesisState(simState *module.SimulationState) {
- simulation.RandomizedGenState(simState)
-}
-
-// RandomizedParams returns nil because validatorvesting has no params.
-func (AppModuleSimulation) RandomizedParams(_ *rand.Rand) []sim.ParamChange {
- return []sim.ParamChange{}
-}
-
// AppModule implements an application module for the validator-vesting module.
type AppModule struct {
AppModuleBasic
- AppModuleSimulation
+
keeper Keeper
accountKeeper types.AccountKeeper
}
@@ -95,10 +77,9 @@ type AppModule struct {
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper, ak types.AccountKeeper) AppModule {
return AppModule{
- AppModuleBasic: AppModuleBasic{},
- AppModuleSimulation: AppModuleSimulation{},
- keeper: keeper,
- accountKeeper: ak,
+ AppModuleBasic: AppModuleBasic{},
+ keeper: keeper,
+ accountKeeper: ak,
}
}
@@ -152,3 +133,32 @@ func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
+
+//____________________________________________________________________________
+
+// AppModuleSimulation functions
+
+// GenerateGenesisState creates a randomized GenState of the auth module
+func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
+ simulation.RandomizedGenState(simState)
+}
+
+// ProposalContents doesn't return any content functions for governance proposals.
+func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
+ return nil
+}
+
+// RandomizedParams returns nil because validatorvesting has no params.
+func (AppModuleBasic) RandomizedParams(_ *rand.Rand) []sim.ParamChange {
+ return []sim.ParamChange{}
+}
+
+// RegisterStoreDecoder registers a decoder for auth module's types
+func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
+ sdr[StoreKey] = simulation.DecodeStore
+}
+
+// WeightedOperations returns the all the validator vesting module operations with their respective weights.
+func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
+ return nil
+}
diff --git a/x/validator-vesting/simulation/decoder.go b/x/validator-vesting/simulation/decoder.go
index f6c052f5..6e3207ab 100644
--- a/x/validator-vesting/simulation/decoder.go
+++ b/x/validator-vesting/simulation/decoder.go
@@ -5,15 +5,14 @@ import (
"fmt"
"time"
- cmn "github.com/tendermint/tendermint/libs/common"
-
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/auth/exported"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
+ "github.com/tendermint/tendermint/libs/kv"
)
// DecodeStore unmarshals the KVPair's Value to the corresponding auth type
-func DecodeStore(cdc *codec.Codec, kvA, kvB cmn.KVPair) string {
+func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
switch {
case bytes.Equal(kvA.Key[:1], types.ValidatorVestingAccountPrefix):
var accA, accB exported.Account
@@ -26,6 +25,6 @@ func DecodeStore(cdc *codec.Codec, kvA, kvB cmn.KVPair) string {
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &btB)
return fmt.Sprintf("%v\n%v", btA, btB)
default:
- panic(fmt.Sprintf("invalid account key %X", kvA.Key))
+ panic(fmt.Sprintf("invalid %s key %X", types.ModuleName, kvA.Key))
}
}
diff --git a/x/validator-vesting/simulation/decoder_test.go b/x/validator-vesting/simulation/decoder_test.go
new file mode 100644
index 00000000..adc0cb24
--- /dev/null
+++ b/x/validator-vesting/simulation/decoder_test.go
@@ -0,0 +1,59 @@
+package simulation
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "github.com/tendermint/tendermint/libs/kv"
+
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ "github.com/cosmos/cosmos-sdk/x/auth"
+
+ "github.com/kava-labs/kava/x/validator-vesting/types"
+)
+
+func makeTestCodec() (cdc *codec.Codec) {
+ cdc = codec.New()
+ sdk.RegisterCodec(cdc)
+ auth.RegisterCodec(cdc)
+ codec.RegisterCrypto(cdc)
+ types.RegisterCodec(cdc)
+ codec.RegisterEvidences(cdc)
+ return
+}
+
+func TestDecodeDistributionStore(t *testing.T) {
+ cdc := makeTestCodec()
+
+ acc := types.ValidatorVestingAccount{SigningThreshold: 1}
+ now := time.Now().UTC()
+
+ kvPairs := kv.Pairs{
+ kv.Pair{Key: types.ValidatorVestingAccountPrefix, Value: cdc.MustMarshalBinaryBare(acc)},
+ kv.Pair{Key: types.BlocktimeKey, Value: cdc.MustMarshalBinaryLengthPrefixed(now)},
+ kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
+ }
+
+ tests := []struct {
+ name string
+ expectedLog string
+ }{
+ {"ValidatorVestingAccount", fmt.Sprintf("%v\n%v", acc, acc)},
+ {"BlockTime", fmt.Sprintf("%s\n%s", now, now)},
+ {"other", ""},
+ }
+ for i, tt := range tests {
+ i, tt := i, tt
+ t.Run(tt.name, func(t *testing.T) {
+ switch i {
+ case len(tests) - 1:
+ require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
+ default:
+ require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
+ }
+ })
+ }
+}
diff --git a/x/validator-vesting/simulation/genesis.go b/x/validator-vesting/simulation/genesis.go
index cccdc358..7b133dff 100644
--- a/x/validator-vesting/simulation/genesis.go
+++ b/x/validator-vesting/simulation/genesis.go
@@ -11,7 +11,7 @@ import (
vestexported "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported"
vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
"github.com/cosmos/cosmos-sdk/x/simulation"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
)
// RandomizedGenState generates a random GenesisState for validator-vesting
@@ -101,12 +101,12 @@ func getRandomVestingPeriods(duration int64, r *rand.Rand, origCoins sdk.Coins)
vestingCoins := sdk.NewCoins()
for _, ic := range origCoins {
amountVesting := ic.Amount.Int64() / int64(coinFraction)
- vestingCoins = vestingCoins.Add(sdk.NewCoins(sdk.NewInt64Coin(ic.Denom, amountVesting)))
+ vestingCoins = vestingCoins.Add(sdk.NewInt64Coin(ic.Denom, amountVesting))
}
periodCoins := sdk.NewCoins()
for _, c := range vestingCoins {
amountPeriod := c.Amount.Int64() / int64(numPeriods)
- periodCoins = periodCoins.Add(sdk.NewCoins(sdk.NewInt64Coin(c.Denom, amountPeriod)))
+ periodCoins = periodCoins.Add(sdk.NewInt64Coin(c.Denom, amountPeriod))
}
vestingPeriods := make([]vestingtypes.Period, numPeriods)
@@ -121,7 +121,7 @@ func getRandomVestingPeriods(duration int64, r *rand.Rand, origCoins sdk.Coins)
func getVestingCoins(periods vestingtypes.Periods) sdk.Coins {
vestingCoins := sdk.NewCoins()
for _, p := range periods {
- vestingCoins = vestingCoins.Add(p.Amount)
+ vestingCoins = vestingCoins.Add(p.Amount...)
}
return vestingCoins
}
diff --git a/x/validator-vesting/test_common.go b/x/validator-vesting/test_common.go
index 909ee0bb..fdb4f83f 100644
--- a/x/validator-vesting/test_common.go
+++ b/x/validator-vesting/test_common.go
@@ -18,8 +18,8 @@ import (
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/cosmos-sdk/x/supply"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
- "github.com/kava-labs/kava/x/validator-vesting/internal/keeper"
- "github.com/kava-labs/kava/x/validator-vesting/internal/types"
+ "github.com/kava-labs/kava/x/validator-vesting/keeper"
+ "github.com/kava-labs/kava/x/validator-vesting/types"
)
var (
@@ -60,7 +60,7 @@ func getMockApp(t *testing.T, numGenAccs int, genState types.GenesisState, genAc
pk := mApp.ParamsKeeper
- bk := bank.NewBaseKeeper(mApp.AccountKeeper, mApp.ParamsKeeper.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs)
+ bk := bank.NewBaseKeeper(mApp.AccountKeeper, mApp.ParamsKeeper.Subspace(bank.DefaultParamspace), blacklistedAddrs)
maccPerms := map[string][]string{
types.ModuleName: {supply.Burner},
@@ -69,7 +69,7 @@ func getMockApp(t *testing.T, numGenAccs int, genState types.GenesisState, genAc
}
supplyKeeper := supply.NewKeeper(mApp.Cdc, keySupply, mApp.AccountKeeper, bk, maccPerms)
sk := staking.NewKeeper(
- mApp.Cdc, keyStaking, supplyKeeper, pk.Subspace(staking.DefaultParamspace), staking.DefaultCodespace,
+ mApp.Cdc, keyStaking, supplyKeeper, pk.Subspace(staking.DefaultParamspace),
)
keeper := keeper.NewKeeper(
diff --git a/x/validator-vesting/internal/types/codec.go b/x/validator-vesting/types/codec.go
similarity index 100%
rename from x/validator-vesting/internal/types/codec.go
rename to x/validator-vesting/types/codec.go
diff --git a/x/validator-vesting/internal/types/expected_keepers.go b/x/validator-vesting/types/expected_keepers.go
similarity index 88%
rename from x/validator-vesting/internal/types/expected_keepers.go
rename to x/validator-vesting/types/expected_keepers.go
index 74969143..afdf6c16 100644
--- a/x/validator-vesting/internal/types/expected_keepers.go
+++ b/x/validator-vesting/types/expected_keepers.go
@@ -19,7 +19,7 @@ type AccountKeeper interface {
// BankKeeper defines the expected bank keeper (noalias)
type BankKeeper interface {
- SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) sdk.Error
+ SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
}
// StakingKeeper defines the expected staking keeper (noalias)
@@ -28,13 +28,13 @@ type StakingKeeper interface {
fn func(index int64, delegation stakingexported.DelegationI) (stop bool))
Undelegate(
ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, sharesAmount sdk.Dec,
- ) (time.Time, sdk.Error)
+ ) (time.Time, error)
}
// SupplyKeeper defines the expected supply keeper for module accounts (noalias)
type SupplyKeeper interface {
- SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error
- BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
+ SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
+ BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error
SetModuleAccount(sdk.Context, supplyexported.ModuleAccountI)
GetSupply(ctx sdk.Context) (supply supplyexported.SupplyI)
}
diff --git a/x/validator-vesting/internal/types/genesis.go b/x/validator-vesting/types/genesis.go
similarity index 100%
rename from x/validator-vesting/internal/types/genesis.go
rename to x/validator-vesting/types/genesis.go
diff --git a/x/validator-vesting/internal/types/key.go b/x/validator-vesting/types/key.go
similarity index 100%
rename from x/validator-vesting/internal/types/key.go
rename to x/validator-vesting/types/key.go
diff --git a/x/validator-vesting/internal/types/querier.go b/x/validator-vesting/types/querier.go
similarity index 100%
rename from x/validator-vesting/internal/types/querier.go
rename to x/validator-vesting/types/querier.go
diff --git a/x/validator-vesting/internal/types/test_common.go b/x/validator-vesting/types/test_common.go
similarity index 100%
rename from x/validator-vesting/internal/types/test_common.go
rename to x/validator-vesting/types/test_common.go
diff --git a/x/validator-vesting/internal/types/validator_vesting_account.go b/x/validator-vesting/types/validator_vesting_account.go
similarity index 70%
rename from x/validator-vesting/internal/types/validator_vesting_account.go
rename to x/validator-vesting/types/validator_vesting_account.go
index b09521a4..27eb452e 100644
--- a/x/validator-vesting/internal/types/validator_vesting_account.go
+++ b/x/validator-vesting/types/validator_vesting_account.go
@@ -1,6 +1,7 @@
package types
import (
+ "encoding/json"
"errors"
"time"
@@ -11,6 +12,7 @@ import (
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
vestexported "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported"
vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
+ "github.com/tendermint/tendermint/crypto"
)
// Assert ValidatorVestingAccount implements the vestexported.VestingAccount interface
@@ -143,7 +145,7 @@ func (vva ValidatorVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins
x := blockTime.Unix() - currentPeriodStartTime
if x >= vva.VestingPeriods[i].Length {
if vva.VestingPeriodProgress[i].PeriodComplete {
- vestedCoins = vestedCoins.Add(vva.VestingPeriods[i].Amount)
+ vestedCoins = vestedCoins.Add(vva.VestingPeriods[i].Amount...)
}
currentPeriodStartTime += vva.VestingPeriods[i].Length
} else {
@@ -161,7 +163,7 @@ func (vva ValidatorVestingAccount) GetFailedVestedCoins() sdk.Coins {
for i := 0; i < numberPeriods; i++ {
if vva.VestingPeriodProgress[i].PeriodComplete {
if !vva.VestingPeriodProgress[i].VestingSuccessful {
- failedVestedCoins = failedVestedCoins.Add(vva.VestingPeriods[i].Amount)
+ failedVestedCoins = failedVestedCoins.Add(vva.VestingPeriods[i].Amount...)
}
} else {
break
@@ -199,6 +201,97 @@ func (vva ValidatorVestingAccount) Validate() error {
return vva.PeriodicVestingAccount.Validate()
}
+type validatorVestingAccountPretty struct {
+ Address sdk.AccAddress `json:"address" yaml:"address"`
+ Coins sdk.Coins `json:"coins" yaml:"coins"`
+ PubKey string `json:"public_key" yaml:"public_key"`
+ AccountNumber uint64 `json:"account_number" yaml:"account_number"`
+ Sequence uint64 `json:"sequence" yaml:"sequence"`
+ OriginalVesting sdk.Coins `json:"original_vesting" yaml:"original_vesting"`
+ DelegatedFree sdk.Coins `json:"delegated_free" yaml:"delegated_free"`
+ DelegatedVesting sdk.Coins `json:"delegated_vesting" yaml:"delegated_vesting"`
+ EndTime int64 `json:"end_time" yaml:"end_time"`
+ StartTime int64 `json:"start_time" yaml:"start_time"`
+ VestingPeriods vestingtypes.Periods `json:"vesting_periods" yaml:"vesting_periods"`
+ ValidatorAddress sdk.ConsAddress `json:"validator_address" yaml:"validator_address"`
+ ReturnAddress sdk.AccAddress `json:"return_address" yaml:"return_address"`
+ SigningThreshold int64 `json:"signing_threshold" yaml:"signing_threshold"`
+ CurrentPeriodProgress CurrentPeriodProgress `json:"current_period_progress" yaml:"current_period_progress"`
+ VestingPeriodProgress []VestingProgress `json:"vesting_period_progress" yaml:"vesting_period_progress"`
+ DebtAfterFailedVesting sdk.Coins `json:"debt_after_failed_vesting" yaml:"debt_after_failed_vesting"`
+}
+
+// MarshalJSON returns the JSON representation of a PeriodicVestingAccount.
+func (vva ValidatorVestingAccount) MarshalJSON() ([]byte, error) {
+ alias := validatorVestingAccountPretty{
+ Address: vva.Address,
+ Coins: vva.Coins,
+ AccountNumber: vva.AccountNumber,
+ Sequence: vva.Sequence,
+ OriginalVesting: vva.OriginalVesting,
+ DelegatedFree: vva.DelegatedFree,
+ DelegatedVesting: vva.DelegatedVesting,
+ EndTime: vva.EndTime,
+ StartTime: vva.StartTime,
+ VestingPeriods: vva.VestingPeriods,
+ ValidatorAddress: vva.ValidatorAddress,
+ ReturnAddress: vva.ReturnAddress,
+ SigningThreshold: vva.SigningThreshold,
+ CurrentPeriodProgress: vva.CurrentPeriodProgress,
+ VestingPeriodProgress: vva.VestingPeriodProgress,
+ DebtAfterFailedVesting: vva.DebtAfterFailedVesting,
+ }
+
+ if vva.PubKey != nil {
+ pks, err := sdk.Bech32ifyPubKey(sdk.Bech32PubKeyTypeAccPub, vva.PubKey)
+ if err != nil {
+ return nil, err
+ }
+
+ alias.PubKey = pks
+ }
+
+ return json.Marshal(alias)
+}
+
+// UnmarshalJSON unmarshals raw JSON bytes into a PeriodicVestingAccount.
+func (vva *ValidatorVestingAccount) UnmarshalJSON(bz []byte) error {
+ var alias validatorVestingAccountPretty
+ if err := json.Unmarshal(bz, &alias); err != nil {
+ return err
+ }
+
+ var (
+ pk crypto.PubKey
+ err error
+ )
+
+ if alias.PubKey != "" {
+ pk, err = sdk.GetPubKeyFromBech32(sdk.Bech32PubKeyTypeAccPub, alias.PubKey)
+ if err != nil {
+ return err
+ }
+ }
+
+ ba := authtypes.NewBaseAccount(alias.Address, alias.Coins, pk, alias.AccountNumber, alias.Sequence)
+ bva := &vestingtypes.BaseVestingAccount{
+ BaseAccount: ba,
+ OriginalVesting: alias.OriginalVesting,
+ DelegatedFree: alias.DelegatedFree,
+ DelegatedVesting: alias.DelegatedVesting,
+ EndTime: alias.EndTime,
+ }
+ pva := vestingtypes.NewPeriodicVestingAccountRaw(bva, alias.StartTime, alias.VestingPeriods)
+ vva.PeriodicVestingAccount = pva
+ vva.ValidatorAddress = alias.ValidatorAddress
+ vva.ReturnAddress = alias.ReturnAddress
+ vva.SigningThreshold = alias.SigningThreshold
+ vva.CurrentPeriodProgress = alias.CurrentPeriodProgress
+ vva.VestingPeriodProgress = alias.VestingPeriodProgress
+ vva.DebtAfterFailedVesting = alias.DebtAfterFailedVesting
+ return nil
+}
+
// MarshalYAML returns the YAML representation of an account.
func (vva ValidatorVestingAccount) MarshalYAML() (interface{}, error) {
var bs []byte
@@ -206,7 +299,7 @@ func (vva ValidatorVestingAccount) MarshalYAML() (interface{}, error) {
var pubkey string
if vva.PubKey != nil {
- pubkey, err = sdk.Bech32ifyAccPub(vva.PubKey)
+ pubkey, err = sdk.Bech32ifyPubKey(sdk.Bech32PubKeyTypeAccPub, vva.PubKey)
if err != nil {
return nil, err
}
diff --git a/x/validator-vesting/internal/types/validator_vesting_account_test.go b/x/validator-vesting/types/validator_vesting_account_test.go
similarity index 99%
rename from x/validator-vesting/internal/types/validator_vesting_account_test.go
rename to x/validator-vesting/types/validator_vesting_account_test.go
index 4c19c68e..5bb91d76 100644
--- a/x/validator-vesting/internal/types/validator_vesting_account_test.go
+++ b/x/validator-vesting/types/validator_vesting_account_test.go
@@ -222,7 +222,7 @@ func TestSpendableCoinsValidatorVestingAccount(t *testing.T) {
// receive some coins
recvAmt := sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}
- vva.SetCoins(vva.GetCoins().Add(recvAmt))
+ vva.SetCoins(vva.GetCoins().Add(recvAmt...))
// require that all vested coins (50%) are spendable plus any received after period 1 completes successfully
vva.VestingPeriodProgress[0] = VestingProgress{true, true}