mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 23:35:19 +00:00
Issuance module (#599)
* wip: issuance module * add keeper and module methods * add begin blocker * add client * update events * add simulations * ignore v0.8 migration tests for now * ignore migration tests in ci * add test suite * update spec to match implementation details * add unblock method * address review comments * fix typos
This commit is contained in:
parent
790753f156
commit
e14466547d
@ -71,7 +71,7 @@ jobs:
|
|||||||
export VERSION="$(git describe --tags --long | sed 's/v\(.*\)/\1/')"
|
export VERSION="$(git describe --tags --long | sed 's/v\(.*\)/\1/')"
|
||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
mkdir -p /tmp/logs /tmp/workspace/profiles
|
mkdir -p /tmp/logs /tmp/workspace/profiles
|
||||||
for pkg in $(go list ./... | grep -v '/simulation' | circleci tests split); do
|
for pkg in $(go list ./... | grep -v 'simulation\|migrate\|contrib' | circleci tests split); do
|
||||||
id=$(echo "$pkg" | sed 's|[/.]|_|g')
|
id=$(echo "$pkg" | sed 's|[/.]|_|g')
|
||||||
go test -mod=readonly -timeout 8m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic -tags='ledger test_ledger_mock' "$pkg" | tee "/tmp/logs/$id-$RANDOM.log"
|
go test -mod=readonly -timeout 8m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic -tags='ledger test_ledger_mock' "$pkg" | tee "/tmp/logs/$id-$RANDOM.log"
|
||||||
done
|
done
|
||||||
|
2
Makefile
2
Makefile
@ -171,7 +171,7 @@ test-basic: test
|
|||||||
@go test ./app -run TestAppStateDeterminism -Enabled -Commit -NumBlocks=5 -BlockSize=200 -Seed 4 -v -timeout 2m
|
@go test ./app -run TestAppStateDeterminism -Enabled -Commit -NumBlocks=5 -BlockSize=200 -Seed 4 -v -timeout 2m
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test ./...
|
@go test $$(go list ./... | grep -v 'migrate\|contrib')
|
||||||
|
|
||||||
test-rest:
|
test-rest:
|
||||||
rest_test/./run_all_tests_from_make.sh
|
rest_test/./run_all_tests_from_make.sh
|
||||||
|
20
app/app.go
20
app/app.go
@ -38,6 +38,7 @@ import (
|
|||||||
"github.com/kava-labs/kava/x/cdp"
|
"github.com/kava-labs/kava/x/cdp"
|
||||||
"github.com/kava-labs/kava/x/committee"
|
"github.com/kava-labs/kava/x/committee"
|
||||||
"github.com/kava-labs/kava/x/incentive"
|
"github.com/kava-labs/kava/x/incentive"
|
||||||
|
"github.com/kava-labs/kava/x/issuance"
|
||||||
"github.com/kava-labs/kava/x/kavadist"
|
"github.com/kava-labs/kava/x/kavadist"
|
||||||
"github.com/kava-labs/kava/x/pricefeed"
|
"github.com/kava-labs/kava/x/pricefeed"
|
||||||
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
||||||
@ -80,6 +81,7 @@ var (
|
|||||||
bep3.AppModuleBasic{},
|
bep3.AppModuleBasic{},
|
||||||
kavadist.AppModuleBasic{},
|
kavadist.AppModuleBasic{},
|
||||||
incentive.AppModuleBasic{},
|
incentive.AppModuleBasic{},
|
||||||
|
issuance.AppModuleBasic{},
|
||||||
)
|
)
|
||||||
|
|
||||||
// module account permissions
|
// module account permissions
|
||||||
@ -97,6 +99,7 @@ var (
|
|||||||
cdp.SavingsRateMacc: {supply.Minter},
|
cdp.SavingsRateMacc: {supply.Minter},
|
||||||
bep3.ModuleName: nil,
|
bep3.ModuleName: nil,
|
||||||
kavadist.ModuleName: {supply.Minter},
|
kavadist.ModuleName: {supply.Minter},
|
||||||
|
issuance.ModuleAccountName: {supply.Minter, supply.Burner},
|
||||||
}
|
}
|
||||||
|
|
||||||
// module accounts that are allowed to receive tokens
|
// module accounts that are allowed to receive tokens
|
||||||
@ -140,6 +143,7 @@ type App struct {
|
|||||||
bep3Keeper bep3.Keeper
|
bep3Keeper bep3.Keeper
|
||||||
kavadistKeeper kavadist.Keeper
|
kavadistKeeper kavadist.Keeper
|
||||||
incentiveKeeper incentive.Keeper
|
incentiveKeeper incentive.Keeper
|
||||||
|
issuanceKeeper issuance.Keeper
|
||||||
|
|
||||||
// the module manager
|
// the module manager
|
||||||
mm *module.Manager
|
mm *module.Manager
|
||||||
@ -164,7 +168,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
|
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
|
||||||
gov.StoreKey, params.StoreKey, upgrade.StoreKey, evidence.StoreKey,
|
gov.StoreKey, params.StoreKey, upgrade.StoreKey, evidence.StoreKey,
|
||||||
validatorvesting.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey,
|
validatorvesting.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey,
|
||||||
bep3.StoreKey, kavadist.StoreKey, incentive.StoreKey, committee.StoreKey,
|
bep3.StoreKey, kavadist.StoreKey, incentive.StoreKey, issuance.StoreKey, committee.StoreKey,
|
||||||
)
|
)
|
||||||
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
|
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
|
||||||
|
|
||||||
@ -193,6 +197,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
bep3Subspace := app.paramsKeeper.Subspace(bep3.DefaultParamspace)
|
bep3Subspace := app.paramsKeeper.Subspace(bep3.DefaultParamspace)
|
||||||
kavadistSubspace := app.paramsKeeper.Subspace(kavadist.DefaultParamspace)
|
kavadistSubspace := app.paramsKeeper.Subspace(kavadist.DefaultParamspace)
|
||||||
incentiveSubspace := app.paramsKeeper.Subspace(incentive.DefaultParamspace)
|
incentiveSubspace := app.paramsKeeper.Subspace(incentive.DefaultParamspace)
|
||||||
|
issuanceSubspace := app.paramsKeeper.Subspace(issuance.DefaultParamspace)
|
||||||
|
|
||||||
// add keepers
|
// add keepers
|
||||||
app.accountKeeper = auth.NewAccountKeeper(
|
app.accountKeeper = auth.NewAccountKeeper(
|
||||||
@ -350,6 +355,13 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
app.cdpKeeper,
|
app.cdpKeeper,
|
||||||
app.accountKeeper,
|
app.accountKeeper,
|
||||||
)
|
)
|
||||||
|
app.issuanceKeeper = issuance.NewKeeper(
|
||||||
|
app.cdc,
|
||||||
|
keys[issuance.StoreKey],
|
||||||
|
issuanceSubspace,
|
||||||
|
app.accountKeeper,
|
||||||
|
app.supplyKeeper,
|
||||||
|
)
|
||||||
|
|
||||||
// register the staking hooks
|
// register the staking hooks
|
||||||
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
|
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
|
||||||
@ -379,6 +391,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
|
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
|
||||||
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
|
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
|
||||||
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
|
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
|
||||||
|
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
|
||||||
)
|
)
|
||||||
|
|
||||||
// During begin block slashing happens after distr.BeginBlocker so that
|
// During begin block slashing happens after distr.BeginBlocker so that
|
||||||
@ -389,7 +402,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
app.mm.SetOrderBeginBlockers(
|
app.mm.SetOrderBeginBlockers(
|
||||||
upgrade.ModuleName, mint.ModuleName, distr.ModuleName, slashing.ModuleName,
|
upgrade.ModuleName, mint.ModuleName, distr.ModuleName, slashing.ModuleName,
|
||||||
validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName,
|
validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName,
|
||||||
bep3.ModuleName, incentive.ModuleName, committee.ModuleName,
|
bep3.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
|
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
|
||||||
@ -400,7 +413,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
|
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
|
||||||
gov.ModuleName, mint.ModuleName, evidence.ModuleName,
|
gov.ModuleName, mint.ModuleName, evidence.ModuleName,
|
||||||
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName,
|
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName,
|
||||||
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName,
|
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName,
|
||||||
supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
|
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
|
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.
|
genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts.
|
||||||
@ -430,6 +443,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
|
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
|
||||||
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
|
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
|
||||||
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
|
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
|
||||||
|
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
|
||||||
)
|
)
|
||||||
|
|
||||||
app.sm.RegisterStoreDecoders()
|
app.sm.RegisterStoreDecoders()
|
||||||
|
@ -13,5 +13,9 @@ const (
|
|||||||
DefaultWeightMsgUpdatePrices int = 20
|
DefaultWeightMsgUpdatePrices int = 20
|
||||||
DefaultWeightMsgCdp int = 20
|
DefaultWeightMsgCdp int = 20
|
||||||
DefaultWeightMsgClaimReward int = 20
|
DefaultWeightMsgClaimReward int = 20
|
||||||
|
DefaultWeightMsgIssue int = 20
|
||||||
|
DefaultWeightMsgRedeem int = 20
|
||||||
|
DefaultWeightMsgBlock int = 20
|
||||||
|
DefaultWeightMsgPause int = 20
|
||||||
OpWeightSubmitCommitteeChangeProposal int = 20
|
OpWeightSubmitCommitteeChangeProposal int = 20
|
||||||
)
|
)
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
"github.com/kava-labs/kava/x/cdp"
|
"github.com/kava-labs/kava/x/cdp"
|
||||||
"github.com/kava-labs/kava/x/committee"
|
"github.com/kava-labs/kava/x/committee"
|
||||||
"github.com/kava-labs/kava/x/incentive"
|
"github.com/kava-labs/kava/x/incentive"
|
||||||
|
"github.com/kava-labs/kava/x/issuance"
|
||||||
"github.com/kava-labs/kava/x/kavadist"
|
"github.com/kava-labs/kava/x/kavadist"
|
||||||
"github.com/kava-labs/kava/x/pricefeed"
|
"github.com/kava-labs/kava/x/pricefeed"
|
||||||
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
||||||
@ -84,6 +85,7 @@ func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keep
|
|||||||
func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper }
|
func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper }
|
||||||
func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper }
|
func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper }
|
||||||
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
|
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
|
||||||
|
func (tApp TestApp) GetIssuanceKeeper() issuance.Keeper { return tApp.issuanceKeeper }
|
||||||
|
|
||||||
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
|
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
|
||||||
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {
|
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {
|
||||||
|
17
x/issuance/abci.go
Normal file
17
x/issuance/abci.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package issuance
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/keeper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BeginBlocker iterates over each asset and seizes coins from blocked addresses by returning them to the asset owner
|
||||||
|
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
|
||||||
|
params := k.GetParams(ctx)
|
||||||
|
for _, asset := range params.Assets {
|
||||||
|
err := k.SeizeCoinsFromBlockedAddresses(ctx, asset.Denom)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
x/issuance/alias.go
Normal file
81
x/issuance/alias.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package issuance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kava-labs/kava/x/issuance/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
// autogenerated code using github.com/rigelrozanski/multitool
|
||||||
|
// aliases generated for the following subdirectories:
|
||||||
|
// ALIASGEN: github.com/kava-labs/kava/x/issuance/keeper
|
||||||
|
// ALIASGEN: github.com/kava-labs/kava/x/issuance/types
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventTypeIssue = types.EventTypeIssue
|
||||||
|
EventTypeRedeem = types.EventTypeRedeem
|
||||||
|
EventTypeBlock = types.EventTypeBlock
|
||||||
|
EventTypeUnblock = types.EventTypeUnblock
|
||||||
|
EventTypePause = types.EventTypePause
|
||||||
|
EventTypeSeize = types.EventTypeSeize
|
||||||
|
AttributeValueCategory = types.AttributeValueCategory
|
||||||
|
AttributeKeyDenom = types.AttributeKeyDenom
|
||||||
|
AttributeKeyIssueAmount = types.AttributeKeyIssueAmount
|
||||||
|
AttributeKeyRedeemAmount = types.AttributeKeyRedeemAmount
|
||||||
|
AttributeKeyBlock = types.AttributeKeyBlock
|
||||||
|
AttributeKeyUnblock = types.AttributeKeyUnblock
|
||||||
|
AttributeKeyAddress = types.AttributeKeyAddress
|
||||||
|
AttributeKeyPauseStatus = types.AttributeKeyPauseStatus
|
||||||
|
ModuleName = types.ModuleName
|
||||||
|
StoreKey = types.StoreKey
|
||||||
|
RouterKey = types.RouterKey
|
||||||
|
DefaultParamspace = types.DefaultParamspace
|
||||||
|
QuerierRoute = types.QuerierRoute
|
||||||
|
QueryGetParams = types.QueryGetParams
|
||||||
|
QueryGetAsset = types.QueryGetAsset
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// functions aliases
|
||||||
|
NewKeeper = keeper.NewKeeper
|
||||||
|
NewQuerier = keeper.NewQuerier
|
||||||
|
RegisterCodec = types.RegisterCodec
|
||||||
|
NewGenesisState = types.NewGenesisState
|
||||||
|
DefaultGenesisState = types.DefaultGenesisState
|
||||||
|
NewMsgIssueTokens = types.NewMsgIssueTokens
|
||||||
|
NewMsgRedeemTokens = types.NewMsgRedeemTokens
|
||||||
|
NewMsgBlockAddress = types.NewMsgBlockAddress
|
||||||
|
NewMsgUnblockAddress = types.NewMsgUnblockAddress
|
||||||
|
NewMsgSetPauseStatus = types.NewMsgSetPauseStatus
|
||||||
|
NewParams = types.NewParams
|
||||||
|
DefaultParams = types.DefaultParams
|
||||||
|
ParamKeyTable = types.ParamKeyTable
|
||||||
|
NewAsset = types.NewAsset
|
||||||
|
|
||||||
|
// variable aliases
|
||||||
|
ModuleCdc = types.ModuleCdc
|
||||||
|
ErrAssetNotFound = types.ErrAssetNotFound
|
||||||
|
ErrNotAuthorized = types.ErrNotAuthorized
|
||||||
|
ErrAssetPaused = types.ErrAssetPaused
|
||||||
|
ErrAccountBlocked = types.ErrAccountBlocked
|
||||||
|
ErrAccountAlreadyBlocked = types.ErrAccountAlreadyBlocked
|
||||||
|
ErrAccountAlreadyUnblocked = types.ErrAccountAlreadyUnblocked
|
||||||
|
ErrIssueToModuleAccount = types.ErrIssueToModuleAccount
|
||||||
|
KeyAssets = types.KeyAssets
|
||||||
|
DefaultAssets = types.DefaultAssets
|
||||||
|
ModuleAccountName = types.ModuleAccountName
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Keeper = keeper.Keeper
|
||||||
|
GenesisState = types.GenesisState
|
||||||
|
MsgIssueTokens = types.MsgIssueTokens
|
||||||
|
MsgRedeemTokens = types.MsgRedeemTokens
|
||||||
|
MsgBlockAddress = types.MsgBlockAddress
|
||||||
|
MsgUnblockAddress = types.MsgUnblockAddress
|
||||||
|
MsgSetPauseStatus = types.MsgSetPauseStatus
|
||||||
|
Params = types.Params
|
||||||
|
Asset = types.Asset
|
||||||
|
Assets = types.Assets
|
||||||
|
QueryAssetParams = types.QueryAssetParams
|
||||||
|
)
|
55
x/issuance/client/cli/query.go
Normal file
55
x/issuance/client/cli/query.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"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/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetQueryCmd returns the cli query commands for the issuance module
|
||||||
|
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
issuanceQueryCmd := &cobra.Command{
|
||||||
|
Use: types.ModuleName,
|
||||||
|
Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName),
|
||||||
|
}
|
||||||
|
|
||||||
|
issuanceQueryCmd.AddCommand(flags.GetCommands(
|
||||||
|
queryParamsCmd(queryRoute, cdc),
|
||||||
|
)...)
|
||||||
|
|
||||||
|
return issuanceQueryCmd
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "params",
|
||||||
|
Short: fmt.Sprintf("get the %s module parameters", types.ModuleName),
|
||||||
|
Long: "Get the current global issuance 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, height, err := cliCtx.QueryWithData(route, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cliCtx = cliCtx.WithHeight(height)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
192
x/issuance/client/cli/tx.go
Normal file
192
x/issuance/client/cli/tx.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"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/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTxCmd returns the transaction cli commands for the issuance module
|
||||||
|
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
|
||||||
|
issuanceTxCmd := &cobra.Command{
|
||||||
|
Use: types.ModuleName,
|
||||||
|
Short: "transaction commands for the issuance module",
|
||||||
|
}
|
||||||
|
|
||||||
|
issuanceTxCmd.AddCommand(flags.PostCommands(
|
||||||
|
getCmdIssueTokens(cdc),
|
||||||
|
getCmdRedeemTokens(cdc),
|
||||||
|
getCmdBlockAddress(cdc),
|
||||||
|
getCmdUnblockAddress(cdc),
|
||||||
|
getCmdPauseAsset(cdc),
|
||||||
|
)...)
|
||||||
|
|
||||||
|
return issuanceTxCmd
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCmdIssueTokens(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "issue [tokens] [receiver]",
|
||||||
|
Short: "issue new tokens to the receiver address",
|
||||||
|
Long: "The asset owner issues new tokens that will be credited to the receiver address",
|
||||||
|
Example: fmt.Sprintf(`$ %s tx %s issue 20000000usdtoken kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw
|
||||||
|
`, 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))
|
||||||
|
tokens, err := sdk.ParseCoin(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
receiver, err := sdk.AccAddressFromBech32(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgIssueTokens(cliCtx.GetFromAddress(), tokens, receiver)
|
||||||
|
err = msg.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCmdRedeemTokens(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "redeem [tokens]",
|
||||||
|
Short: "redeem tokens",
|
||||||
|
Long: "The asset owner redeems (burns) tokens, removing them from the circulating supply",
|
||||||
|
Example: fmt.Sprintf(`$ %s tx %s redeem 20000000usdtoken
|
||||||
|
`, 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))
|
||||||
|
tokens, err := sdk.ParseCoin(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgRedeemTokens(cliCtx.GetFromAddress(), tokens)
|
||||||
|
err = msg.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCmdBlockAddress(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "block [address] [denom]",
|
||||||
|
Short: "block an address for the input denom",
|
||||||
|
Long: "The asset owner blocks an address from holding coins of that denomination. Any tokens of the input denomination held by the address will be sent to the owner address",
|
||||||
|
Example: fmt.Sprintf(`$ %s tx %s block kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw usdtoken
|
||||||
|
`, 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))
|
||||||
|
address, err := sdk.AccAddressFromBech32(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sdk.ValidateDenom(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msg := types.NewMsgBlockAddress(cliCtx.GetFromAddress(), args[1], address)
|
||||||
|
err = msg.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCmdUnblockAddress(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "unblock [address] [denom]",
|
||||||
|
Short: "unblock an address for the input denom",
|
||||||
|
Long: "The asset owner unblocks an address from holding coins of that denomination.",
|
||||||
|
Example: fmt.Sprintf(`$ %s tx %s unblock kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw usdtoken
|
||||||
|
`, 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))
|
||||||
|
address, err := sdk.AccAddressFromBech32(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sdk.ValidateDenom(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msg := types.NewMsgUnblockAddress(cliCtx.GetFromAddress(), args[1], address)
|
||||||
|
err = msg.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCmdPauseAsset(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "set-pause-status [denom] [status]",
|
||||||
|
Short: "pause or unpause an asset",
|
||||||
|
Long: "The asset owner pauses or unpauses the input asset, halting new issuance and redemption",
|
||||||
|
Example: fmt.Sprintf(`$ %s tx %s pause usdtoken true
|
||||||
|
`, 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))
|
||||||
|
err := sdk.ValidateDenom(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var status bool
|
||||||
|
if args[1] == "true" {
|
||||||
|
status = true
|
||||||
|
} else if args[1] == "false" {
|
||||||
|
status = false
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf(fmt.Sprintf("status must be true or false, got %s", args[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgSetPauseStatus(cliCtx.GetFromAddress(), args[0], status)
|
||||||
|
err = msg.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
34
x/issuance/client/rest/query.go
Normal file
34
x/issuance/client/rest/query.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// define routes that get registered by the main application
|
||||||
|
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), getParamsHandlerFn(cliCtx)).Methods("GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetParams), nil)
|
||||||
|
cliCtx = cliCtx.WithHeight(height)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rest.PostProcessResponse(w, cliCtx, res)
|
||||||
|
}
|
||||||
|
}
|
49
x/issuance/client/rest/rest.go
Normal file
49
x/issuance/client/rest/rest.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterRoutes - Central function to define routes that get registered by the main application
|
||||||
|
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||||
|
registerQueryRoutes(cliCtx, r)
|
||||||
|
registerTxRoutes(cliCtx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostIssueReq defines the properties of an issue token request's body
|
||||||
|
type PostIssueReq struct {
|
||||||
|
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||||
|
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
|
||||||
|
Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostRedeemReq defines the properties of a redeem token request's body
|
||||||
|
type PostRedeemReq struct {
|
||||||
|
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||||
|
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostBlockAddressReq defines the properties of a block address request's body
|
||||||
|
type PostBlockAddressReq struct {
|
||||||
|
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||||
|
Address sdk.AccAddress `json:"blocked_address" yaml:"blocked_address"`
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostUnblockAddressReq defines the properties of a unblock address request's body
|
||||||
|
type PostUnblockAddressReq struct {
|
||||||
|
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||||
|
Address sdk.AccAddress `json:"blocked_address" yaml:"blocked_address"`
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostPauseReq defines the properties of a pause request's body
|
||||||
|
type PostPauseReq struct {
|
||||||
|
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
Status bool `json:"status" yaml:"status"`
|
||||||
|
}
|
181
x/issuance/client/rest/tx.go
Normal file
181
x/issuance/client/rest/tx.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
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/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/issue", types.ModuleName), postIssueTokensHandlerFn(cliCtx)).Methods("POST")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/redeem", types.ModuleName), postRedeemTokensHandlerFn(cliCtx)).Methods("POST")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/block", types.ModuleName), postBlockAddressHandlerFn(cliCtx)).Methods("POST")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/unblock", types.ModuleName), postUnblockAddressHandlerFn(cliCtx)).Methods("POST")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/pause", types.ModuleName), postPauseHandlerFn(cliCtx)).Methods("POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
func postIssueTokensHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var requestBody PostIssueReq
|
||||||
|
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
|
||||||
|
if !requestBody.BaseReq.ValidateBasic(w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgIssueTokens(
|
||||||
|
fromAddr,
|
||||||
|
requestBody.Tokens,
|
||||||
|
requestBody.Receiver,
|
||||||
|
)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func postRedeemTokensHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var requestBody PostRedeemReq
|
||||||
|
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
|
||||||
|
if !requestBody.BaseReq.ValidateBasic(w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgRedeemTokens(
|
||||||
|
fromAddr,
|
||||||
|
requestBody.Tokens,
|
||||||
|
)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func postBlockAddressHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var requestBody PostBlockAddressReq
|
||||||
|
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
|
||||||
|
if !requestBody.BaseReq.ValidateBasic(w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgBlockAddress(
|
||||||
|
fromAddr,
|
||||||
|
requestBody.Denom,
|
||||||
|
requestBody.Address,
|
||||||
|
)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func postUnblockAddressHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var requestBody PostUnblockAddressReq
|
||||||
|
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
|
||||||
|
if !requestBody.BaseReq.ValidateBasic(w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgUnblockAddress(
|
||||||
|
fromAddr,
|
||||||
|
requestBody.Denom,
|
||||||
|
requestBody.Address,
|
||||||
|
)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func postPauseHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var requestBody PostPauseReq
|
||||||
|
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
|
||||||
|
if !requestBody.BaseReq.ValidateBasic(w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgSetPauseStatus(
|
||||||
|
fromAddr,
|
||||||
|
requestBody.Denom,
|
||||||
|
requestBody.Status,
|
||||||
|
)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
|
||||||
|
}
|
||||||
|
}
|
28
x/issuance/genesis.go
Normal file
28
x/issuance/genesis.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package issuance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitGenesis initializes the store state from a genesis state.
|
||||||
|
func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeeper, gs types.GenesisState) {
|
||||||
|
|
||||||
|
k.SetParams(ctx, gs.Params)
|
||||||
|
|
||||||
|
// check if the module account exists
|
||||||
|
moduleAcc := supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
|
||||||
|
if moduleAcc == nil {
|
||||||
|
panic(fmt.Sprintf("%s module account has not been set", types.ModuleAccountName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportGenesis export genesis state for issuance module
|
||||||
|
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState {
|
||||||
|
params := k.GetParams(ctx)
|
||||||
|
return types.NewGenesisState(params)
|
||||||
|
}
|
116
x/issuance/handler.go
Normal file
116
x/issuance/handler.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package issuance
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHandler creates an sdk.Handler for issuance 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.MsgIssueTokens:
|
||||||
|
return handleMsgIssueTokens(ctx, k, msg)
|
||||||
|
case types.MsgRedeemTokens:
|
||||||
|
return handleMsgRedeemTokens(ctx, k, msg)
|
||||||
|
case types.MsgBlockAddress:
|
||||||
|
return handleMsgBlockAddress(ctx, k, msg)
|
||||||
|
case types.MsgUnblockAddress:
|
||||||
|
return handleMsgUnblockAddress(ctx, k, msg)
|
||||||
|
case types.MsgSetPauseStatus:
|
||||||
|
return handleMsgSetPauseStatus(ctx, k, msg)
|
||||||
|
default:
|
||||||
|
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", types.ModuleName, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMsgIssueTokens(ctx sdk.Context, k keeper.Keeper, msg types.MsgIssueTokens) (*sdk.Result, error) {
|
||||||
|
err := k.IssueTokens(ctx, msg.Tokens, msg.Sender, msg.Receiver)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMsgRedeemTokens(ctx sdk.Context, k keeper.Keeper, msg types.MsgRedeemTokens) (*sdk.Result, error) {
|
||||||
|
err := k.RedeemTokens(ctx, msg.Tokens, msg.Sender)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMsgBlockAddress(ctx sdk.Context, k keeper.Keeper, msg types.MsgBlockAddress) (*sdk.Result, error) {
|
||||||
|
err := k.BlockAddress(ctx, msg.Denom, msg.Sender, msg.Address)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMsgUnblockAddress(ctx sdk.Context, k keeper.Keeper, msg types.MsgUnblockAddress) (*sdk.Result, error) {
|
||||||
|
err := k.UnblockAddress(ctx, msg.Denom, msg.Sender, msg.Address)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMsgSetPauseStatus(ctx sdk.Context, k keeper.Keeper, msg types.MsgSetPauseStatus) (*sdk.Result, error) {
|
||||||
|
err := k.SetPauseStatus(ctx, msg.Sender, msg.Denom, msg.Status)
|
||||||
|
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
|
||||||
|
}
|
206
x/issuance/keeper/issuance.go
Normal file
206
x/issuance/keeper/issuance.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IssueTokens mints new tokens and sends them to the receiver address
|
||||||
|
func (k Keeper) IssueTokens(ctx sdk.Context, tokens sdk.Coin, owner, receiver sdk.AccAddress) error {
|
||||||
|
asset, found := k.GetAsset(ctx, tokens.Denom)
|
||||||
|
if !found {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", tokens.Denom)
|
||||||
|
}
|
||||||
|
if !owner.Equals(asset.Owner) {
|
||||||
|
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
|
||||||
|
}
|
||||||
|
if asset.Paused {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAssetPaused, "denom: %s", tokens.Denom)
|
||||||
|
}
|
||||||
|
blocked, _ := k.checkBlockedAddress(ctx, asset, receiver)
|
||||||
|
if blocked {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAccountBlocked, "address: %s", receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
acc := k.accountKeeper.GetAccount(ctx, receiver)
|
||||||
|
_, ok := acc.(supplyexported.ModuleAccountI)
|
||||||
|
if ok {
|
||||||
|
return sdkerrors.Wrapf(types.ErrIssueToModuleAccount, "address: %s", receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mint new tokens
|
||||||
|
err := k.supplyKeeper.MintCoins(ctx, types.ModuleAccountName, sdk.NewCoins(tokens))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// send to receiver
|
||||||
|
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, receiver, sdk.NewCoins(tokens))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeIssue,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyIssueAmount, tokens.String()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedeemTokens sends tokens from the owner address to the module account and burns them
|
||||||
|
func (k Keeper) RedeemTokens(ctx sdk.Context, tokens sdk.Coin, owner sdk.AccAddress) error {
|
||||||
|
asset, found := k.GetAsset(ctx, tokens.Denom)
|
||||||
|
if !found {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", tokens.Denom)
|
||||||
|
}
|
||||||
|
if !owner.Equals(asset.Owner) {
|
||||||
|
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
|
||||||
|
}
|
||||||
|
if asset.Paused {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAssetPaused, "denom: %s", tokens.Denom)
|
||||||
|
}
|
||||||
|
coins := sdk.NewCoins(tokens)
|
||||||
|
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleAccountName, coins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = k.supplyKeeper.BurnCoins(ctx, types.ModuleAccountName, coins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeRedeem,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyRedeemAmount, tokens.String()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockAddress adds an address to the blocked list
|
||||||
|
func (k Keeper) BlockAddress(ctx sdk.Context, denom string, owner, blockedAddress sdk.AccAddress) error {
|
||||||
|
asset, found := k.GetAsset(ctx, denom)
|
||||||
|
if !found {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", denom)
|
||||||
|
}
|
||||||
|
if !owner.Equals(asset.Owner) {
|
||||||
|
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
|
||||||
|
}
|
||||||
|
blocked, _ := k.checkBlockedAddress(ctx, asset, blockedAddress)
|
||||||
|
if blocked {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAccountAlreadyBlocked, "address: %s", blockedAddress)
|
||||||
|
}
|
||||||
|
asset.BlockedAddresses = append(asset.BlockedAddresses, blockedAddress)
|
||||||
|
k.SetAsset(ctx, asset)
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeBlock,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyBlock, blockedAddress.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyDenom, asset.Denom),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnblockAddress removes an address from the blocked list
|
||||||
|
func (k Keeper) UnblockAddress(ctx sdk.Context, denom string, owner, addr sdk.AccAddress) error {
|
||||||
|
asset, found := k.GetAsset(ctx, denom)
|
||||||
|
if !found {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", denom)
|
||||||
|
}
|
||||||
|
if !owner.Equals(asset.Owner) {
|
||||||
|
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
|
||||||
|
}
|
||||||
|
blocked, i := k.checkBlockedAddress(ctx, asset, addr)
|
||||||
|
if !blocked {
|
||||||
|
if blocked {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAccountAlreadyUnblocked, "address: %s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedAddrs := k.removeBlockedAddress(ctx, asset.BlockedAddresses, i)
|
||||||
|
asset.BlockedAddresses = blockedAddrs
|
||||||
|
k.SetAsset(ctx, asset)
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeUnblock,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyUnblock, addr.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyDenom, asset.Denom),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPauseStatus pauses/un-pauses an asset
|
||||||
|
func (k Keeper) SetPauseStatus(ctx sdk.Context, owner sdk.AccAddress, denom string, status bool) error {
|
||||||
|
asset, found := k.GetAsset(ctx, denom)
|
||||||
|
if !found {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", denom)
|
||||||
|
}
|
||||||
|
if !owner.Equals(asset.Owner) {
|
||||||
|
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
|
||||||
|
}
|
||||||
|
if asset.Paused == status {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
asset.Paused = !asset.Paused
|
||||||
|
k.SetAsset(ctx, asset)
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypePause,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyPauseStatus, fmt.Sprintf("%t", status)),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyDenom, asset.Denom),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeizeCoinsFromBlockedAddresses checks blocked addresses for coins of the input denom and transfers them to the owner account
|
||||||
|
func (k Keeper) SeizeCoinsFromBlockedAddresses(ctx sdk.Context, denom string) error {
|
||||||
|
asset, found := k.GetAsset(ctx, denom)
|
||||||
|
if !found {
|
||||||
|
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", denom)
|
||||||
|
}
|
||||||
|
for _, address := range asset.BlockedAddresses {
|
||||||
|
account := k.accountKeeper.GetAccount(ctx, address)
|
||||||
|
coinsAmount := account.GetCoins().AmountOf(denom)
|
||||||
|
if !coinsAmount.IsPositive() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
coins := sdk.NewCoins(sdk.NewCoin(denom, coinsAmount))
|
||||||
|
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, address, types.ModuleAccountName, coins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, asset.Owner, coins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeSeize,
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyAmount, coins.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyAddress, address.String()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) checkBlockedAddress(ctx sdk.Context, asset types.Asset, checkAddress sdk.AccAddress) (bool, int) {
|
||||||
|
for i, address := range asset.BlockedAddresses {
|
||||||
|
if address.Equals(checkAddress) {
|
||||||
|
return true, i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) removeBlockedAddress(ctx sdk.Context, blockedAddrs []sdk.AccAddress, i int) []sdk.AccAddress {
|
||||||
|
blockedAddrs[len(blockedAddrs)-1], blockedAddrs[i] = blockedAddrs[i], blockedAddrs[len(blockedAddrs)-1]
|
||||||
|
return blockedAddrs[:len(blockedAddrs)-1]
|
||||||
|
}
|
671
x/issuance/keeper/issuance_test.go
Normal file
671
x/issuance/keeper/issuance_test.go
Normal file
@ -0,0 +1,671 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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/issuance/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test suite used for all keeper tests
|
||||||
|
type KeeperTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
keeper keeper.Keeper
|
||||||
|
app app.TestApp
|
||||||
|
ctx sdk.Context
|
||||||
|
addrs []sdk.AccAddress
|
||||||
|
modAccount 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(5)
|
||||||
|
keeper := tApp.GetIssuanceKeeper()
|
||||||
|
modAccount, err := sdk.AccAddressFromBech32("kava1cj7njkw2g9fqx4e768zc75dp9sks8u9znxrf0w")
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.app = tApp
|
||||||
|
suite.ctx = ctx
|
||||||
|
suite.keeper = keeper
|
||||||
|
suite.addrs = addrs
|
||||||
|
suite.modAccount = modAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
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) TestGetSetParams() {
|
||||||
|
params := suite.keeper.GetParams(suite.ctx)
|
||||||
|
suite.Require().Equal(types.Params{Assets: types.Assets(nil)}, params)
|
||||||
|
asset := types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false)
|
||||||
|
params = types.NewParams(types.Assets{asset})
|
||||||
|
suite.keeper.SetParams(suite.ctx, params)
|
||||||
|
newParams := suite.keeper.GetParams(suite.ctx)
|
||||||
|
suite.Require().Equal(params, newParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestIssueTokens() {
|
||||||
|
type args struct {
|
||||||
|
assets types.Assets
|
||||||
|
sender sdk.AccAddress
|
||||||
|
tokens sdk.Coin
|
||||||
|
receiver sdk.AccAddress
|
||||||
|
}
|
||||||
|
type errArgs struct {
|
||||||
|
expectPass bool
|
||||||
|
contains string
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
errArgs errArgs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid issuance",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
receiver: suite.addrs[2],
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non-owner issuance",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[2],
|
||||||
|
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
receiver: suite.addrs[3],
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "account not authorized",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid denom",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
tokens: sdk.NewCoin("othertoken", sdk.NewInt(100000)),
|
||||||
|
receiver: suite.addrs[2],
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "no asset with input denom found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issue to blocked address",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
receiver: suite.addrs[1],
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "account is blocked",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issue to module account",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
receiver: suite.modAccount,
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "cannot issue tokens to module account",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"paused issuance",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, true),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
receiver: suite.addrs[1],
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "asset is paused",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
params := types.NewParams(tc.args.assets)
|
||||||
|
suite.keeper.SetParams(suite.ctx, params)
|
||||||
|
err := suite.keeper.IssueTokens(suite.ctx, tc.args.tokens, tc.args.sender, tc.args.receiver)
|
||||||
|
if tc.errArgs.expectPass {
|
||||||
|
suite.Require().NoError(err, tc.name)
|
||||||
|
receiverAccount := suite.getAccount(tc.args.receiver)
|
||||||
|
suite.Require().Equal(sdk.NewCoins(tc.args.tokens), receiverAccount.GetCoins())
|
||||||
|
} else {
|
||||||
|
suite.Require().Error(err, tc.name)
|
||||||
|
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestRedeemTokens() {
|
||||||
|
type args struct {
|
||||||
|
assets types.Assets
|
||||||
|
sender sdk.AccAddress
|
||||||
|
initialTokens sdk.Coin
|
||||||
|
redeemTokens sdk.Coin
|
||||||
|
}
|
||||||
|
type errArgs struct {
|
||||||
|
expectPass bool
|
||||||
|
contains string
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
errArgs errArgs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid redemption",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
redeemTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid denom redemption",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
redeemTokens: sdk.NewCoin("othertoken", sdk.NewInt(100000)),
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non-owner redemption",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[2],
|
||||||
|
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
redeemTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "account not authorized",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"paused redemption",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, true),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
redeemTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "asset is paused",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"redeem amount greater than balance",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
|
||||||
|
redeemTokens: sdk.NewCoin("usdtoken", sdk.NewInt(200000)),
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "insufficient funds",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
params := types.NewParams(tc.args.assets)
|
||||||
|
suite.keeper.SetParams(suite.ctx, params)
|
||||||
|
sk := suite.app.GetSupplyKeeper()
|
||||||
|
err := sk.MintCoins(suite.ctx, types.ModuleAccountName, sdk.NewCoins(tc.args.initialTokens))
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
err = sk.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleAccountName, tc.args.sender, sdk.NewCoins(tc.args.initialTokens))
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
err = suite.keeper.RedeemTokens(suite.ctx, tc.args.redeemTokens, tc.args.sender)
|
||||||
|
|
||||||
|
if tc.errArgs.expectPass {
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
initialSupply := sdk.NewCoins(tc.args.redeemTokens)
|
||||||
|
moduleAccount := suite.getModuleAccount(types.ModuleAccountName)
|
||||||
|
suite.Require().Equal(initialSupply.Sub(sdk.NewCoins(tc.args.redeemTokens)), moduleAccount.GetCoins())
|
||||||
|
} else {
|
||||||
|
suite.Require().Error(err)
|
||||||
|
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestBlockAddress() {
|
||||||
|
type args struct {
|
||||||
|
assets types.Assets
|
||||||
|
sender sdk.AccAddress
|
||||||
|
blockedAddr sdk.AccAddress
|
||||||
|
denom string
|
||||||
|
}
|
||||||
|
type errArgs struct {
|
||||||
|
expectPass bool
|
||||||
|
contains string
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
errArgs errArgs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid block",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
blockedAddr: suite.addrs[1],
|
||||||
|
denom: "usdtoken",
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non-owner block",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[2],
|
||||||
|
blockedAddr: suite.addrs[1],
|
||||||
|
denom: "usdtoken",
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "account not authorized",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid denom block",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
blockedAddr: suite.addrs[1],
|
||||||
|
denom: "othertoken",
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "no asset with input denom found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
params := types.NewParams(tc.args.assets)
|
||||||
|
suite.keeper.SetParams(suite.ctx, params)
|
||||||
|
|
||||||
|
err := suite.keeper.BlockAddress(suite.ctx, tc.args.denom, tc.args.sender, tc.args.blockedAddr)
|
||||||
|
if tc.errArgs.expectPass {
|
||||||
|
suite.Require().NoError(err, tc.name)
|
||||||
|
asset, found := suite.keeper.GetAsset(suite.ctx, tc.args.denom)
|
||||||
|
blocked := false
|
||||||
|
suite.Require().True(found)
|
||||||
|
for _, blockedAddr := range asset.BlockedAddresses {
|
||||||
|
if blockedAddr.Equals(tc.args.blockedAddr) {
|
||||||
|
blocked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suite.Require().True(blocked)
|
||||||
|
} else {
|
||||||
|
suite.Require().Error(err, tc.name)
|
||||||
|
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestUnblockAddress() {
|
||||||
|
type args struct {
|
||||||
|
assets types.Assets
|
||||||
|
sender sdk.AccAddress
|
||||||
|
blockedAddr sdk.AccAddress
|
||||||
|
denom string
|
||||||
|
}
|
||||||
|
type errArgs struct {
|
||||||
|
expectPass bool
|
||||||
|
contains string
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
errArgs errArgs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid unblock",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
blockedAddr: suite.addrs[1],
|
||||||
|
denom: "usdtoken",
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non-owner unblock",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[2],
|
||||||
|
blockedAddr: suite.addrs[1],
|
||||||
|
denom: "usdtoken",
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "account not authorized",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid denom block",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
blockedAddr: suite.addrs[1],
|
||||||
|
denom: "othertoken",
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "no asset with input denom found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
params := types.NewParams(tc.args.assets)
|
||||||
|
suite.keeper.SetParams(suite.ctx, params)
|
||||||
|
|
||||||
|
err := suite.keeper.UnblockAddress(suite.ctx, tc.args.denom, tc.args.sender, tc.args.blockedAddr)
|
||||||
|
if tc.errArgs.expectPass {
|
||||||
|
suite.Require().NoError(err, tc.name)
|
||||||
|
asset, found := suite.keeper.GetAsset(suite.ctx, tc.args.denom)
|
||||||
|
blocked := false
|
||||||
|
suite.Require().True(found)
|
||||||
|
for _, blockedAddr := range asset.BlockedAddresses {
|
||||||
|
if blockedAddr.Equals(tc.args.blockedAddr) {
|
||||||
|
blocked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suite.Require().False(blocked)
|
||||||
|
} else {
|
||||||
|
suite.Require().Error(err, tc.name)
|
||||||
|
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestChangePauseStatus() {
|
||||||
|
type args struct {
|
||||||
|
assets types.Assets
|
||||||
|
sender sdk.AccAddress
|
||||||
|
startStatus bool
|
||||||
|
endStatus bool
|
||||||
|
denom string
|
||||||
|
}
|
||||||
|
type errArgs struct {
|
||||||
|
expectPass bool
|
||||||
|
contains string
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
errArgs errArgs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid pause",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
startStatus: false,
|
||||||
|
endStatus: true,
|
||||||
|
denom: "usdtoken",
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid unpause",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, true),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
startStatus: true,
|
||||||
|
endStatus: false,
|
||||||
|
denom: "usdtoken",
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non-owner pause",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[2],
|
||||||
|
startStatus: false,
|
||||||
|
endStatus: true,
|
||||||
|
denom: "usdtoken",
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "account not authorized",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid denom pause",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
|
||||||
|
},
|
||||||
|
sender: suite.addrs[0],
|
||||||
|
startStatus: true,
|
||||||
|
endStatus: false,
|
||||||
|
denom: "othertoken",
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "no asset with input denom found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
params := types.NewParams(tc.args.assets)
|
||||||
|
suite.keeper.SetParams(suite.ctx, params)
|
||||||
|
|
||||||
|
err := suite.keeper.SetPauseStatus(suite.ctx, tc.args.sender, tc.args.denom, tc.args.endStatus)
|
||||||
|
if tc.errArgs.expectPass {
|
||||||
|
suite.Require().NoError(err, tc.name)
|
||||||
|
asset, found := suite.keeper.GetAsset(suite.ctx, tc.args.denom)
|
||||||
|
suite.Require().True(found)
|
||||||
|
suite.Require().Equal(tc.args.endStatus, asset.Paused)
|
||||||
|
} else {
|
||||||
|
suite.Require().Error(err, tc.name)
|
||||||
|
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestSeizeCoinsFromBlockedAddress() {
|
||||||
|
type args struct {
|
||||||
|
assets types.Assets
|
||||||
|
initialCoins sdk.Coin
|
||||||
|
blockedAddrs []sdk.AccAddress
|
||||||
|
denom string
|
||||||
|
}
|
||||||
|
type errArgs struct {
|
||||||
|
expectPass bool
|
||||||
|
contains string
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
errArgs errArgs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid seize",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
|
||||||
|
},
|
||||||
|
initialCoins: sdk.NewCoin("usdtoken", sdk.NewInt(100000000)),
|
||||||
|
denom: "usdtoken",
|
||||||
|
blockedAddrs: []sdk.AccAddress{suite.addrs[1], suite.addrs[2]},
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid denom seize",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
|
||||||
|
},
|
||||||
|
initialCoins: sdk.NewCoin("usdtoken", sdk.NewInt(100000000)),
|
||||||
|
denom: "othertoken",
|
||||||
|
blockedAddrs: []sdk.AccAddress{suite.addrs[1], suite.addrs[2]},
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "no asset with input denom found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
assetsWithBlockedAddrs := types.Assets{}
|
||||||
|
for _, asset := range tc.args.assets {
|
||||||
|
asset.BlockedAddresses = tc.args.blockedAddrs
|
||||||
|
assetsWithBlockedAddrs = append(assetsWithBlockedAddrs, asset)
|
||||||
|
}
|
||||||
|
params := types.NewParams(assetsWithBlockedAddrs)
|
||||||
|
suite.keeper.SetParams(suite.ctx, params)
|
||||||
|
sk := suite.app.GetSupplyKeeper()
|
||||||
|
for _, addr := range tc.args.blockedAddrs {
|
||||||
|
err := sk.MintCoins(suite.ctx, types.ModuleAccountName, sdk.NewCoins(tc.args.initialCoins))
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
err = sk.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleAccountName, addr, sdk.NewCoins(tc.args.initialCoins))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := suite.keeper.SeizeCoinsFromBlockedAddresses(suite.ctx, tc.args.denom)
|
||||||
|
if tc.errArgs.expectPass {
|
||||||
|
suite.Require().NoError(err, tc.name)
|
||||||
|
asset, found := suite.keeper.GetAsset(suite.ctx, tc.args.denom)
|
||||||
|
suite.Require().True(found)
|
||||||
|
ownerAccount := suite.getAccount(asset.Owner)
|
||||||
|
ownerCoinAmount := tc.args.initialCoins.Amount.Mul(sdk.NewInt(int64(len(tc.args.blockedAddrs))))
|
||||||
|
suite.Require().Equal(sdk.NewCoins(sdk.NewCoin(tc.args.denom, ownerCoinAmount)), ownerAccount.GetCoins())
|
||||||
|
} else {
|
||||||
|
suite.Require().Error(err, tc.name)
|
||||||
|
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeeperTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(KeeperTestSuite))
|
||||||
|
}
|
33
x/issuance/keeper/keeper.go
Normal file
33
x/issuance/keeper/keeper.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/params/subspace"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keeper keeper for the issuance module
|
||||||
|
type Keeper struct {
|
||||||
|
key sdk.StoreKey
|
||||||
|
cdc *codec.Codec
|
||||||
|
paramSubspace subspace.Subspace
|
||||||
|
accountKeeper types.AccountKeeper
|
||||||
|
supplyKeeper types.SupplyKeeper
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeeper returns a new keeper
|
||||||
|
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, ak types.AccountKeeper, sk types.SupplyKeeper) Keeper {
|
||||||
|
if !paramstore.HasKeyTable() {
|
||||||
|
paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
|
||||||
|
}
|
||||||
|
|
||||||
|
return Keeper{
|
||||||
|
key: key,
|
||||||
|
cdc: cdc,
|
||||||
|
paramSubspace: paramstore,
|
||||||
|
accountKeeper: ak,
|
||||||
|
supplyKeeper: sk,
|
||||||
|
}
|
||||||
|
}
|
41
x/issuance/keeper/params.go
Normal file
41
x/issuance/keeper/params.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAsset returns an asset from the params and a boolean for if it was found
|
||||||
|
func (k Keeper) GetAsset(ctx sdk.Context, denom string) (types.Asset, bool) {
|
||||||
|
params := k.GetParams(ctx)
|
||||||
|
for _, asset := range params.Assets {
|
||||||
|
if asset.Denom == denom {
|
||||||
|
return asset, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.Asset{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAsset sets an asset in the params
|
||||||
|
func (k Keeper) SetAsset(ctx sdk.Context, asset types.Asset) {
|
||||||
|
params := k.GetParams(ctx)
|
||||||
|
for i := range params.Assets {
|
||||||
|
if params.Assets[i].Denom == asset.Denom {
|
||||||
|
params.Assets[i] = asset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k.SetParams(ctx, params)
|
||||||
|
}
|
36
x/issuance/keeper/querier.go
Normal file
36
x/issuance/keeper/querier.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/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)
|
||||||
|
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
|
||||||
|
}
|
19
x/issuance/keeper/querier_test.go
Normal file
19
x/issuance/keeper/querier_test.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestQuerierGetParams() {
|
||||||
|
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))
|
||||||
|
suite.Require().Equal(types.Params{Assets: types.Assets(nil)}, p)
|
||||||
|
}
|
170
x/issuance/module.go
Normal file
170
x/issuance/module.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package issuance
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/client/cli"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/client/rest"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/simulation"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/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 REST routes for the issuance module.
|
||||||
|
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
|
||||||
|
rest.RegisterRoutes(ctx, rtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTxCmd returns the root tx command for the issuance module.
|
||||||
|
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return cli.GetTxCmd(cdc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueryCmd returns no root query command for the issuance module.
|
||||||
|
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return cli.GetQueryCmd(StoreKey, cdc)
|
||||||
|
}
|
||||||
|
|
||||||
|
//____________________________________________________________________________
|
||||||
|
|
||||||
|
// AppModule app module type
|
||||||
|
type AppModule struct {
|
||||||
|
AppModuleBasic
|
||||||
|
|
||||||
|
keeper Keeper
|
||||||
|
accountKeeper types.AccountKeeper
|
||||||
|
supplyKeeper types.SupplyKeeper
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppModule creates a new AppModule object
|
||||||
|
func NewAppModule(keeper Keeper, accountKeeper types.AccountKeeper, supplyKeeper types.SupplyKeeper) AppModule {
|
||||||
|
return AppModule{
|
||||||
|
AppModuleBasic: AppModuleBasic{},
|
||||||
|
keeper: keeper,
|
||||||
|
accountKeeper: accountKeeper,
|
||||||
|
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 (am 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) {}
|
||||||
|
|
||||||
|
// EndBlock module end-block
|
||||||
|
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
|
||||||
|
return []abci.ValidatorUpdate{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//____________________________________________________________________________
|
||||||
|
|
||||||
|
// GenerateGenesisState creates a randomized GenState of the issuance 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 issuance has no params.
|
||||||
|
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
|
||||||
|
return simulation.ParamChanges(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterStoreDecoder registers a decoder for issuance module's types
|
||||||
|
func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
|
||||||
|
sdr[StoreKey] = simulation.DecodeStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeightedOperations returns the all the issuance 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)
|
||||||
|
}
|
16
x/issuance/simulation/decoder.go
Normal file
16
x/issuance/simulation/decoder.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
|
||||||
|
"github.com/tendermint/tendermint/libs/kv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodeStore the issuance module has no store keys -- all state is stored in params
|
||||||
|
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
|
||||||
|
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
|
||||||
|
}
|
58
x/issuance/simulation/genesis.go
Normal file
58
x/issuance/simulation/genesis.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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/simulation"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
accs []simulation.Account
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomizedGenState generates a random GenesisState for the module
|
||||||
|
func RandomizedGenState(simState *module.SimulationState) {
|
||||||
|
accs = simState.Accounts
|
||||||
|
params := randomizedParams(simState.Rand)
|
||||||
|
gs := types.NewGenesisState(params)
|
||||||
|
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, gs))
|
||||||
|
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(gs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomizedParams(r *rand.Rand) types.Params {
|
||||||
|
assets := randomizedAssets(r)
|
||||||
|
return types.NewParams(assets)
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomizedAssets(r *rand.Rand) types.Assets {
|
||||||
|
randomAssets := types.Assets{}
|
||||||
|
numAssets := Max(1, r.Intn(5))
|
||||||
|
for i := 0; i < numAssets; i++ {
|
||||||
|
denom := strings.ToLower(simulation.RandStringOfLength(r, (r.Intn(3) + 3)))
|
||||||
|
owner := randomOwner(r)
|
||||||
|
paused := r.Intn(2) == 0
|
||||||
|
randomAsset := types.NewAsset(owner.Address, denom, []sdk.AccAddress{}, paused)
|
||||||
|
randomAssets = append(randomAssets, randomAsset)
|
||||||
|
}
|
||||||
|
return randomAssets
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomOwner(r *rand.Rand) simulation.Account {
|
||||||
|
acc, _ := simulation.RandomAcc(r, accs)
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max return max of two ints
|
||||||
|
func Max(x, y int) int {
|
||||||
|
if x > y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
305
x/issuance/simulation/operations.go
Normal file
305
x/issuance/simulation/operations.go
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
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/simulation"
|
||||||
|
simappparams "github.com/kava-labs/kava/app/params"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Simulation operation weights constants
|
||||||
|
const (
|
||||||
|
OpWeightMsgIssue = "op_weight_msg_issue"
|
||||||
|
OpWeightMsgRedeem = "op_weight_msg_redeem"
|
||||||
|
OpWeightMsgBlock = "op_weight_msg_block"
|
||||||
|
OpWeightMsgPause = "op_weight_msg_pause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WeightedOperations returns all the operations from the module with their respective weights
|
||||||
|
func WeightedOperations(
|
||||||
|
appParams simulation.AppParams, cdc *codec.Codec, ak types.AccountKeeper, k keeper.Keeper,
|
||||||
|
) simulation.WeightedOperations {
|
||||||
|
var (
|
||||||
|
weightMsgIssue int
|
||||||
|
weightMsgReedem int
|
||||||
|
weightMsgBlock int
|
||||||
|
weightMsgPause int
|
||||||
|
)
|
||||||
|
|
||||||
|
appParams.GetOrGenerate(cdc, OpWeightMsgIssue, &weightMsgIssue, nil,
|
||||||
|
func(_ *rand.Rand) {
|
||||||
|
weightMsgIssue = simappparams.DefaultWeightMsgIssue
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
appParams.GetOrGenerate(cdc, OpWeightMsgRedeem, &weightMsgReedem, nil,
|
||||||
|
func(_ *rand.Rand) {
|
||||||
|
weightMsgReedem = simappparams.DefaultWeightMsgRedeem
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
appParams.GetOrGenerate(cdc, OpWeightMsgBlock, &weightMsgBlock, nil,
|
||||||
|
func(_ *rand.Rand) {
|
||||||
|
weightMsgBlock = simappparams.DefaultWeightMsgBlock
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
appParams.GetOrGenerate(cdc, OpWeightMsgPause, &weightMsgPause, nil,
|
||||||
|
func(_ *rand.Rand) {
|
||||||
|
weightMsgPause = simappparams.DefaultWeightMsgPause
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return simulation.WeightedOperations{
|
||||||
|
simulation.NewWeightedOperation(
|
||||||
|
weightMsgIssue,
|
||||||
|
SimulateMsgIssueTokens(ak, k),
|
||||||
|
),
|
||||||
|
simulation.NewWeightedOperation(
|
||||||
|
weightMsgReedem,
|
||||||
|
SimulateMsgRedeemTokens(ak, k),
|
||||||
|
),
|
||||||
|
simulation.NewWeightedOperation(
|
||||||
|
weightMsgBlock,
|
||||||
|
SimulateMsgBlockAddress(ak, k),
|
||||||
|
),
|
||||||
|
simulation.NewWeightedOperation(
|
||||||
|
weightMsgPause,
|
||||||
|
SimulateMsgPause(ak, k),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimulateMsgIssueTokens generates a MsgIssueTokens with random values
|
||||||
|
func SimulateMsgIssueTokens(ak types.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) {
|
||||||
|
// shuffle the assets and get a random one
|
||||||
|
assets := k.GetParams(ctx).Assets
|
||||||
|
r.Shuffle(len(assets), func(i, j int) {
|
||||||
|
assets[i], assets[j] = assets[j], assets[i]
|
||||||
|
})
|
||||||
|
asset := assets[0]
|
||||||
|
|
||||||
|
if asset.Paused {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure owner account exists
|
||||||
|
ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
|
||||||
|
if !found {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// issue new tokens to the owner 50% of the time so we have funds to redeem
|
||||||
|
ownerAcc := ak.GetAccount(ctx, asset.Owner)
|
||||||
|
recipient := ownerAcc
|
||||||
|
if r.Intn(2) == 0 {
|
||||||
|
simAccount, _ := simulation.RandomAcc(r, accs)
|
||||||
|
recipient = ak.GetAccount(ctx, simAccount.Address)
|
||||||
|
}
|
||||||
|
if recipient == nil {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, nil
|
||||||
|
}
|
||||||
|
for _, blockedAddr := range asset.BlockedAddresses {
|
||||||
|
if recipient.GetAddress().Equals(blockedAddr) {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
randomAmount := simulation.RandIntBetween(r, 10000000, 1000000000000)
|
||||||
|
msg := types.NewMsgIssueTokens(asset.Owner, sdk.NewCoin(asset.Denom, sdk.NewInt(int64(randomAmount))), recipient.GetAddress())
|
||||||
|
spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime())
|
||||||
|
fees, err := simulation.RandomFees(r, ctx, spendableCoins)
|
||||||
|
if err != nil {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, err
|
||||||
|
}
|
||||||
|
tx := helpers.GenTx(
|
||||||
|
[]sdk.Msg{msg},
|
||||||
|
fees,
|
||||||
|
helpers.DefaultGenTxGas,
|
||||||
|
chainID,
|
||||||
|
[]uint64{ownerAcc.GetAccountNumber()},
|
||||||
|
[]uint64{ownerAcc.GetSequence()},
|
||||||
|
ownerSimAcc.PrivKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, _, err = app.Deliver(tx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error on issue %s\n%s\n", msg, asset)
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, err
|
||||||
|
}
|
||||||
|
return simulation.NewOperationMsg(msg, true, ""), nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimulateMsgRedeemTokens generates a MsgRedeemTokens with random values
|
||||||
|
func SimulateMsgRedeemTokens(ak types.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) {
|
||||||
|
// shuffle the assets and get a random one
|
||||||
|
assets := k.GetParams(ctx).Assets
|
||||||
|
r.Shuffle(len(assets), func(i, j int) {
|
||||||
|
assets[i], assets[j] = assets[j], assets[i]
|
||||||
|
})
|
||||||
|
asset := assets[0]
|
||||||
|
if asset.Paused {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure owner account exists
|
||||||
|
ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
|
||||||
|
if !found {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerAcc := ak.GetAccount(ctx, asset.Owner)
|
||||||
|
|
||||||
|
spendableCoinAmount := ownerAcc.SpendableCoins(ctx.BlockTime()).AmountOf(asset.Denom)
|
||||||
|
if spendableCoinAmount.IsZero() {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, nil
|
||||||
|
}
|
||||||
|
var redeemAmount sdk.Int
|
||||||
|
if spendableCoinAmount.Equal(sdk.OneInt()) {
|
||||||
|
redeemAmount = sdk.OneInt()
|
||||||
|
} else {
|
||||||
|
redeemAmount = sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(spendableCoinAmount.Int64()))))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgRedeemTokens(asset.Owner, sdk.NewCoin(asset.Denom, redeemAmount))
|
||||||
|
spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime()).Sub(sdk.NewCoins(sdk.NewCoin(asset.Denom, redeemAmount)))
|
||||||
|
fees, err := simulation.RandomFees(r, ctx, spendableCoins)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s\n", msg)
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, err
|
||||||
|
}
|
||||||
|
tx := helpers.GenTx(
|
||||||
|
[]sdk.Msg{msg},
|
||||||
|
fees,
|
||||||
|
helpers.DefaultGenTxGas,
|
||||||
|
chainID,
|
||||||
|
[]uint64{ownerAcc.GetAccountNumber()},
|
||||||
|
[]uint64{ownerAcc.GetSequence()},
|
||||||
|
ownerSimAcc.PrivKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, _, err = app.Deliver(tx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error on redeem %s\n%s\n", msg, asset)
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, err
|
||||||
|
}
|
||||||
|
return simulation.NewOperationMsg(msg, true, ""), nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimulateMsgBlockAddress generates a MsgBlockAddress with random values
|
||||||
|
func SimulateMsgBlockAddress(ak types.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) {
|
||||||
|
// shuffle the assets and get a random one
|
||||||
|
assets := k.GetParams(ctx).Assets
|
||||||
|
r.Shuffle(len(assets), func(i, j int) {
|
||||||
|
assets[i], assets[j] = assets[j], assets[i]
|
||||||
|
})
|
||||||
|
asset := assets[0]
|
||||||
|
|
||||||
|
// make sure owner account exists
|
||||||
|
ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
|
||||||
|
if !found {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
|
||||||
|
}
|
||||||
|
ownerAcc := ak.GetAccount(ctx, asset.Owner)
|
||||||
|
|
||||||
|
// find an account to block
|
||||||
|
simAccount, _ := simulation.RandomAcc(r, accs)
|
||||||
|
blockedAccount := ak.GetAccount(ctx, simAccount.Address)
|
||||||
|
if blockedAccount.GetAddress().Equals(asset.Owner) {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, nil
|
||||||
|
}
|
||||||
|
for _, blockedAddr := range asset.BlockedAddresses {
|
||||||
|
if blockedAccount.GetAddress().Equals(blockedAddr) {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgBlockAddress(asset.Owner, asset.Denom, blockedAccount.GetAddress())
|
||||||
|
spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime())
|
||||||
|
fees, err := simulation.RandomFees(r, ctx, spendableCoins)
|
||||||
|
if err != nil {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, err
|
||||||
|
}
|
||||||
|
tx := helpers.GenTx(
|
||||||
|
[]sdk.Msg{msg},
|
||||||
|
fees,
|
||||||
|
helpers.DefaultGenTxGas*2,
|
||||||
|
chainID,
|
||||||
|
[]uint64{ownerAcc.GetAccountNumber()},
|
||||||
|
[]uint64{ownerAcc.GetSequence()},
|
||||||
|
ownerSimAcc.PrivKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, _, err = app.Deliver(tx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error on block %s\n%s\n", msg, asset)
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, err
|
||||||
|
}
|
||||||
|
return simulation.NewOperationMsg(msg, true, ""), nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimulateMsgPause generates a MsgChangePauseStatus with random values
|
||||||
|
func SimulateMsgPause(ak types.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) {
|
||||||
|
// shuffle the assets and get a random one
|
||||||
|
assets := k.GetParams(ctx).Assets
|
||||||
|
r.Shuffle(len(assets), func(i, j int) {
|
||||||
|
assets[i], assets[j] = assets[j], assets[i]
|
||||||
|
})
|
||||||
|
asset := assets[0]
|
||||||
|
|
||||||
|
// make sure owner account exists
|
||||||
|
ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
|
||||||
|
if !found {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
|
||||||
|
}
|
||||||
|
ownerAcc := ak.GetAccount(ctx, asset.Owner)
|
||||||
|
|
||||||
|
// set status to paused/un-paused
|
||||||
|
status := r.Intn(2) == 0
|
||||||
|
|
||||||
|
msg := types.NewMsgSetPauseStatus(asset.Owner, asset.Denom, status)
|
||||||
|
spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime())
|
||||||
|
fees, err := simulation.RandomFees(r, ctx, spendableCoins)
|
||||||
|
if err != nil {
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, err
|
||||||
|
}
|
||||||
|
tx := helpers.GenTx(
|
||||||
|
[]sdk.Msg{msg},
|
||||||
|
fees,
|
||||||
|
helpers.DefaultGenTxGas*2,
|
||||||
|
chainID,
|
||||||
|
[]uint64{ownerAcc.GetAccountNumber()},
|
||||||
|
[]uint64{ownerAcc.GetSequence()},
|
||||||
|
ownerSimAcc.PrivKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, _, err = app.Deliver(tx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error on pause %s\n%s\n", msg, asset)
|
||||||
|
return simulation.NoOpMsg(types.ModuleName), nil, err
|
||||||
|
}
|
||||||
|
return simulation.NewOperationMsg(msg, true, ""), nil, nil
|
||||||
|
}
|
||||||
|
}
|
25
x/issuance/simulation/params.go
Normal file
25
x/issuance/simulation/params.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/simulation"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyAssets = "Assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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, keyAssets,
|
||||||
|
func(r *rand.Rand) string {
|
||||||
|
return fmt.Sprintf("\"%s\"", randomizedAssets(r))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
7
x/issuance/spec/01_concepts.md
Normal file
7
x/issuance/spec/01_concepts.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<!--
|
||||||
|
order: 1
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Concepts
|
||||||
|
|
||||||
|
The issuance mechanism in this module is designed to allow a trusted party to issue an asset on to the Kava blockchain. The issuer has sole discretion over the minting and redemption (burning) of the asset, as well as restricting access to the asset via asset seizure. The functionality of this module is similar to that of ERC-20 contracts for stablecoins that have a single issuer.
|
31
x/issuance/spec/02_state.md
Normal file
31
x/issuance/spec/02_state.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<!--
|
||||||
|
order: 2
|
||||||
|
-->
|
||||||
|
|
||||||
|
# State
|
||||||
|
|
||||||
|
## Parameters and Genesis State
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
// Asset type for assets in the issuance module
|
||||||
|
type Asset struct {
|
||||||
|
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
BlockedAddresses []sdk.AccAddress `json:"blocked_addresses" yaml:"blocked_addresses"`
|
||||||
|
Paused bool `json:"paused" yaml:"paused"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assets array of Asset
|
||||||
|
type Assets []Asset
|
||||||
|
|
||||||
|
// Params governance parameters for the issuance module
|
||||||
|
type Params struct {
|
||||||
|
Assets Assets `json:"assets" yaml:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenesisState state that must be provided at genesis
|
||||||
|
type GenesisState struct {
|
||||||
|
Assets Assets `json:"assets" yaml:"assets"`
|
||||||
|
}
|
||||||
|
```
|
68
x/issuance/spec/03_messages.md
Normal file
68
x/issuance/spec/03_messages.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<!--
|
||||||
|
order: 3
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Messages
|
||||||
|
|
||||||
|
The issuer can issue new tokens using a `MsgIssueTokens`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// MsgIssueTokens message type used to issue tokens
|
||||||
|
type MsgIssueTokens struct {
|
||||||
|
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||||
|
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
|
||||||
|
Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Modifications
|
||||||
|
|
||||||
|
* New tokens are minted from the issuance module account
|
||||||
|
* New tokens are transferred from the module account to the receiver
|
||||||
|
|
||||||
|
The issuer can redeem (burn) tokens using `MsgRedeemTokens`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// MsgRedeemTokens message type used to redeem (burn) tokens
|
||||||
|
type MsgRedeemTokens struct {
|
||||||
|
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||||
|
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Modifications
|
||||||
|
|
||||||
|
* Tokens are transferred from the owner address to the issuer module account
|
||||||
|
* Tokens are burned
|
||||||
|
|
||||||
|
Addresses can be added to the blocked list using `MsgBlockAddress`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// MsgBlockAddress message type used by the issuer to block an address from holding or transferring tokens
|
||||||
|
type MsgBlockAddress struct {
|
||||||
|
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
BlockedAddress sdk.AccAddress `json:"blocked_address" yaml:"blocked_address"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Modifications
|
||||||
|
|
||||||
|
* The address is added to the block list, which prevents the account from holding coins of that denom
|
||||||
|
* Tokens are sent back to the issuer
|
||||||
|
|
||||||
|
The issuer can pause or un-pause the contract using `MsgChangePauseStatus`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// MsgChangePauseStatus message type used by the issuer to issue new tokens
|
||||||
|
type MsgChangePauseStatus struct {
|
||||||
|
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
Status bool `json:"status" yaml:"status"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Modifications
|
||||||
|
|
||||||
|
* The `Paused` value of the correspond asset is updated to `Status`.
|
||||||
|
* Issuance and redemption are paused if `Paused` is false
|
18
x/issuance/spec/04_events.md
Normal file
18
x/issuance/spec/04_events.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!--
|
||||||
|
order: 4
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Events
|
||||||
|
|
||||||
|
The `x/issuance` module emits the following events:
|
||||||
|
|
||||||
|
## BeginBlock
|
||||||
|
|
||||||
|
| Type | Attribute Key | Attribute Value |
|
||||||
|
|----------------------|---------------------|-----------------|
|
||||||
|
| issue_tokens | amount_issued | `{amount}` |
|
||||||
|
| redeem_tokens | amount_redeemed | `{amount}` |
|
||||||
|
| block_address | address_blocked | `{address}` |
|
||||||
|
| block_address | denom | `{denom}` |
|
||||||
|
| change_pause_status | pause_status | `{bool}` |
|
||||||
|
| change_pause_status | denom | `{denom}` |
|
21
x/issuance/spec/05_params.md
Normal file
21
x/issuance/spec/05_params.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
order: 5
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Parameters
|
||||||
|
|
||||||
|
The issuance module has the following parameters:
|
||||||
|
|
||||||
|
| Key | Type | Example | Description |
|
||||||
|
|------------|----------------|-----------------|---------------------------------------------|
|
||||||
|
| Assets | array (Asset) | `[{see below}]` | array of assets created via issuance module |
|
||||||
|
|
||||||
|
|
||||||
|
Each `Asset` has the following parameters
|
||||||
|
|
||||||
|
| Key | Type | Example | Description |
|
||||||
|
|-------------------|------------------------|-------------------------------------------------|-------------------------------------------------------|
|
||||||
|
| Owner | sdk.AccAddress | "kava1cd8z53n7gh2hvz0lmmkzxkysfp5pghufat3h4a" | the address that controls the issuance of the asset |
|
||||||
|
| Denom | string | "usdtoken" | the denomination or exchange symbol of the asset |
|
||||||
|
| BlockedAccounts | array (sdk.AccAddress) | ["kava1tp9u8t8ang53a8tjh2mhqvvwdngqzjvmp3mamc"] | addresses which are blocked from holding the asset |
|
||||||
|
| Paused | boolean | false | boolean for if issuance and redemption are paused |
|
16
x/issuance/spec/06_begin_block.md
Normal file
16
x/issuance/spec/06_begin_block.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!--
|
||||||
|
order: 6
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Begin Block
|
||||||
|
|
||||||
|
At the start of each block, coins held by blocked addresses are redeemed
|
||||||
|
|
||||||
|
```go
|
||||||
|
func BeginBlocker(ctx sdk.Context, k Keeper) {
|
||||||
|
err := k.RedeemTokensFromBlockedAddresses(ctx, k)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
20
x/issuance/spec/README.md
Normal file
20
x/issuance/spec/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!--
|
||||||
|
order: 0
|
||||||
|
title: "Issuance Overview"
|
||||||
|
parent:
|
||||||
|
title: "issuance"
|
||||||
|
-->
|
||||||
|
|
||||||
|
# `issuance`
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
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/issuance` is an implementation of a Cosmos SDK Module that allows for an issuer to control the minting and burning of an asset. The issuer is a white-listed address, which may be a multisig address, that can mint and burn coins of a particular denomination. The issuer is the only entity that can mint or burn tokens of that asset type (i.e., there is no outside inflation or deflation). Additionally, the issuer can place transfer-restrictions on addresses, which prevents addresses from transferring and holding tokens with the issued denomination. Coins are minted and burned at the discretion of the issuer, using `MsgIssueTokens` and `MsgRedeemTokens` transaction types, respectively.
|
23
x/issuance/types/codec.go
Normal file
23
x/issuance/types/codec.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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 issuance module
|
||||||
|
func RegisterCodec(cdc *codec.Codec) {
|
||||||
|
cdc.RegisterConcrete(MsgIssueTokens{}, "issuance/MsgIssueTokens", nil)
|
||||||
|
cdc.RegisterConcrete(MsgRedeemTokens{}, "issuance/MsgRedeemTokens", nil)
|
||||||
|
cdc.RegisterConcrete(MsgBlockAddress{}, "issuance/MsgBlockAddress", nil)
|
||||||
|
cdc.RegisterConcrete(MsgUnblockAddress{}, "issuance/MsgUnblockAddress", nil)
|
||||||
|
cdc.RegisterConcrete(MsgSetPauseStatus{}, "issuance/MsgChangePauseStatus", nil)
|
||||||
|
cdc.RegisterConcrete(Asset{}, "issuance/Asset", nil)
|
||||||
|
}
|
18
x/issuance/types/errors.go
Normal file
18
x/issuance/types/errors.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DONTCOVER
|
||||||
|
|
||||||
|
// Errors used by the issuance module
|
||||||
|
var (
|
||||||
|
ErrAssetNotFound = sdkerrors.Register(ModuleName, 2, "no asset with input denom found")
|
||||||
|
ErrNotAuthorized = sdkerrors.Register(ModuleName, 3, "account not authorized")
|
||||||
|
ErrAssetPaused = sdkerrors.Register(ModuleName, 4, "asset is paused")
|
||||||
|
ErrAccountBlocked = sdkerrors.Register(ModuleName, 5, "account is blocked")
|
||||||
|
ErrAccountAlreadyBlocked = sdkerrors.Register(ModuleName, 6, "account is already blocked")
|
||||||
|
ErrAccountAlreadyUnblocked = sdkerrors.Register(ModuleName, 7, "account is already unblocked")
|
||||||
|
ErrIssueToModuleAccount = sdkerrors.Register(ModuleName, 8, "cannot issue tokens to module account")
|
||||||
|
)
|
19
x/issuance/types/events.go
Normal file
19
x/issuance/types/events.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// Events emitted by the issuance module
|
||||||
|
const (
|
||||||
|
EventTypeIssue = "issue_tokens"
|
||||||
|
EventTypeRedeem = "redeem_tokens"
|
||||||
|
EventTypeBlock = "block_address"
|
||||||
|
EventTypeUnblock = "unblock_address"
|
||||||
|
EventTypePause = "change_pause_status"
|
||||||
|
EventTypeSeize = "seize_coins_from_blocked_address"
|
||||||
|
AttributeValueCategory = ModuleName
|
||||||
|
AttributeKeyDenom = "denom"
|
||||||
|
AttributeKeyIssueAmount = "amount_issued"
|
||||||
|
AttributeKeyRedeemAmount = "amount_redeemed"
|
||||||
|
AttributeKeyBlock = "address_blocked"
|
||||||
|
AttributeKeyUnblock = "address_unblocked"
|
||||||
|
AttributeKeyAddress = "address"
|
||||||
|
AttributeKeyPauseStatus = "pause_status"
|
||||||
|
)
|
21
x/issuance/types/expected_keepers.go
Normal file
21
x/issuance/types/expected_keepers.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SupplyKeeper defines the expected supply keeper for module accounts (noalias)
|
||||||
|
type SupplyKeeper interface {
|
||||||
|
GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI
|
||||||
|
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
|
||||||
|
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
|
||||||
|
BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error
|
||||||
|
MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountKeeper expected interface for the account keeper (noalias)
|
||||||
|
type AccountKeeper interface {
|
||||||
|
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
|
||||||
|
}
|
40
x/issuance/types/genesis.go
Normal file
40
x/issuance/types/genesis.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
// GenesisState is the state that must be provided at genesis for the issuance module
|
||||||
|
type GenesisState struct {
|
||||||
|
Params Params `json:"params" yaml:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGenesisState returns a new GenesisState
|
||||||
|
func NewGenesisState(params Params) GenesisState {
|
||||||
|
return GenesisState{
|
||||||
|
Params: params,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultGenesisState returns the default GenesisState for the issuance module
|
||||||
|
func DefaultGenesisState() GenesisState {
|
||||||
|
return GenesisState{
|
||||||
|
Params: DefaultParams(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation of genesis data returning an
|
||||||
|
// error for any failed validation criteria.
|
||||||
|
func (gs GenesisState) Validate() error {
|
||||||
|
return gs.Params.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{})
|
||||||
|
}
|
155
x/issuance/types/genesis_test.go
Normal file
155
x/issuance/types/genesis_test.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/issuance/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GenesisTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
addrs []sdk.AccAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *GenesisTestSuite) SetupTest() {
|
||||||
|
config := sdk.GetConfig()
|
||||||
|
app.SetBech32AddressPrefixes(config)
|
||||||
|
|
||||||
|
_, addrs := app.GeneratePrivKeyAddressPairs(2)
|
||||||
|
suite.addrs = addrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *GenesisTestSuite) TestValidate() {
|
||||||
|
type args struct {
|
||||||
|
assets types.Assets
|
||||||
|
}
|
||||||
|
type errArgs struct {
|
||||||
|
expectPass bool
|
||||||
|
contains string
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
errArgs errArgs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"default",
|
||||||
|
args{
|
||||||
|
assets: types.DefaultAssets,
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"with asset",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blocked owner",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[0]}, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "asset owner cannot be blocked",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty owner",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(sdk.AccAddress{}, "usdtoken", []sdk.AccAddress{suite.addrs[0]}, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "owner must not be empty",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty blocked address",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{sdk.AccAddress{}}, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "blocked address must not be empty",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid denom",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "USD2T ", []sdk.AccAddress{}, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "invalid denom",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duplicate denom",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
types.NewAsset(suite.addrs[1], "usdtoken", []sdk.AccAddress{}, true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "duplicate asset denoms",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duplicate asset",
|
||||||
|
args{
|
||||||
|
assets: types.Assets{
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "duplicate asset denoms",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
gs := types.NewGenesisState(types.NewParams(tc.args.assets))
|
||||||
|
err := gs.Validate()
|
||||||
|
if tc.errArgs.expectPass {
|
||||||
|
suite.Require().NoError(err, tc.name)
|
||||||
|
} else {
|
||||||
|
suite.Require().Error(err, tc.name)
|
||||||
|
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenesisTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(GenesisTestSuite))
|
||||||
|
}
|
18
x/issuance/types/keys.go
Normal file
18
x/issuance/types/keys.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ModuleName The name that will be used throughout the module
|
||||||
|
ModuleName = "issuance"
|
||||||
|
|
||||||
|
// 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
|
||||||
|
)
|
282
x/issuance/types/msg.go
Normal file
282
x/issuance/types/msg.go
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
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
|
||||||
|
var _ sdk.Msg = &MsgIssueTokens{}
|
||||||
|
var _ sdk.Msg = &MsgRedeemTokens{}
|
||||||
|
var _ sdk.Msg = &MsgBlockAddress{}
|
||||||
|
var _ sdk.Msg = &MsgUnblockAddress{}
|
||||||
|
var _ sdk.Msg = &MsgSetPauseStatus{}
|
||||||
|
|
||||||
|
// MsgIssueTokens message type used by the issuer to issue new tokens
|
||||||
|
type MsgIssueTokens struct {
|
||||||
|
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||||
|
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
|
||||||
|
Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsgIssueTokens returns a new MsgIssueTokens
|
||||||
|
func NewMsgIssueTokens(sender sdk.AccAddress, tokens sdk.Coin, receiver sdk.AccAddress) MsgIssueTokens {
|
||||||
|
return MsgIssueTokens{
|
||||||
|
Sender: sender,
|
||||||
|
Tokens: tokens,
|
||||||
|
Receiver: receiver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route return the message type used for routing the message.
|
||||||
|
func (msg MsgIssueTokens) Route() string { return RouterKey }
|
||||||
|
|
||||||
|
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||||
|
func (msg MsgIssueTokens) Type() string { return "issue_tokens" }
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that doesn't require access to state.
|
||||||
|
func (msg MsgIssueTokens) ValidateBasic() error {
|
||||||
|
if msg.Sender.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
|
||||||
|
}
|
||||||
|
if msg.Tokens.IsZero() || !msg.Tokens.IsValid() {
|
||||||
|
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "invalid tokens %s", msg.Tokens)
|
||||||
|
}
|
||||||
|
if msg.Receiver.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignBytes gets the canonical byte representation of the Msg
|
||||||
|
func (msg MsgIssueTokens) GetSignBytes() []byte {
|
||||||
|
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigners returns the addresses of signers that must sign
|
||||||
|
func (msg MsgIssueTokens) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Sender}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer
|
||||||
|
func (msg MsgIssueTokens) String() string {
|
||||||
|
return fmt.Sprintf(`Issue Tokens:
|
||||||
|
Sender %s
|
||||||
|
Tokens %s
|
||||||
|
Receiver %s
|
||||||
|
`, msg.Sender, msg.Tokens, msg.Receiver,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MsgRedeemTokens message type used by the issuer to redeem (burn) tokens
|
||||||
|
type MsgRedeemTokens struct {
|
||||||
|
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||||
|
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsgRedeemTokens returns a new MsgRedeemTokens
|
||||||
|
func NewMsgRedeemTokens(sender sdk.AccAddress, tokens sdk.Coin) MsgRedeemTokens {
|
||||||
|
return MsgRedeemTokens{
|
||||||
|
Sender: sender,
|
||||||
|
Tokens: tokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route return the message type used for routing the message.
|
||||||
|
func (msg MsgRedeemTokens) Route() string { return RouterKey }
|
||||||
|
|
||||||
|
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||||
|
func (msg MsgRedeemTokens) Type() string { return "redeem_tokens" }
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that doesn't require access to state.
|
||||||
|
func (msg MsgRedeemTokens) ValidateBasic() error {
|
||||||
|
if msg.Sender.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
|
||||||
|
}
|
||||||
|
if msg.Tokens.IsZero() || !msg.Tokens.IsValid() {
|
||||||
|
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "invalid tokens %s", msg.Tokens)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignBytes gets the canonical byte representation of the Msg
|
||||||
|
func (msg MsgRedeemTokens) GetSignBytes() []byte {
|
||||||
|
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigners returns the addresses of signers that must sign
|
||||||
|
func (msg MsgRedeemTokens) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Sender}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer
|
||||||
|
func (msg MsgRedeemTokens) String() string {
|
||||||
|
return fmt.Sprintf(`Redeem Tokens:
|
||||||
|
Sender %s
|
||||||
|
Tokens %s
|
||||||
|
`, msg.Sender, msg.Tokens,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MsgBlockAddress message type used by the issuer to block an address from holding or transferring tokens
|
||||||
|
type MsgBlockAddress struct {
|
||||||
|
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
Address sdk.AccAddress `json:"blocked_address" yaml:"blocked_address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsgBlockAddress returns a new MsgBlockAddress
|
||||||
|
func NewMsgBlockAddress(sender sdk.AccAddress, denom string, addr sdk.AccAddress) MsgBlockAddress {
|
||||||
|
return MsgBlockAddress{
|
||||||
|
Sender: sender,
|
||||||
|
Denom: denom,
|
||||||
|
Address: addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route return the message type used for routing the message.
|
||||||
|
func (msg MsgBlockAddress) Route() string { return RouterKey }
|
||||||
|
|
||||||
|
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||||
|
func (msg MsgBlockAddress) Type() string { return "block_address" }
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that doesn't require access to state.
|
||||||
|
func (msg MsgBlockAddress) ValidateBasic() error {
|
||||||
|
if msg.Sender.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
|
||||||
|
}
|
||||||
|
if msg.Address.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "blocked address cannot be empty")
|
||||||
|
}
|
||||||
|
return sdk.ValidateDenom(msg.Denom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignBytes gets the canonical byte representation of the Msg
|
||||||
|
func (msg MsgBlockAddress) GetSignBytes() []byte {
|
||||||
|
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigners returns the addresses of signers that must sign
|
||||||
|
func (msg MsgBlockAddress) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Sender}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer
|
||||||
|
func (msg MsgBlockAddress) String() string {
|
||||||
|
return fmt.Sprintf(`Block Address:
|
||||||
|
Sender %s
|
||||||
|
Denom %s
|
||||||
|
Address %s
|
||||||
|
`, msg.Sender, msg.Denom, msg.Address,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MsgUnblockAddress message type used by the issuer to unblock an address from holding or transferring tokens
|
||||||
|
type MsgUnblockAddress struct {
|
||||||
|
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
Address sdk.AccAddress `json:"address" yaml:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsgUnblockAddress returns a new MsgUnblockAddress
|
||||||
|
func NewMsgUnblockAddress(sender sdk.AccAddress, denom string, addr sdk.AccAddress) MsgUnblockAddress {
|
||||||
|
return MsgUnblockAddress{
|
||||||
|
Sender: sender,
|
||||||
|
Denom: denom,
|
||||||
|
Address: addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route return the message type used for routing the message.
|
||||||
|
func (msg MsgUnblockAddress) Route() string { return RouterKey }
|
||||||
|
|
||||||
|
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||||
|
func (msg MsgUnblockAddress) Type() string { return "unblock_address" }
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that doesn't require access to state.
|
||||||
|
func (msg MsgUnblockAddress) ValidateBasic() error {
|
||||||
|
if msg.Sender.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
|
||||||
|
}
|
||||||
|
if msg.Address.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "blocked address cannot be empty")
|
||||||
|
}
|
||||||
|
return sdk.ValidateDenom(msg.Denom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignBytes gets the canonical byte representation of the Msg
|
||||||
|
func (msg MsgUnblockAddress) GetSignBytes() []byte {
|
||||||
|
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigners returns the addresses of signers that must sign
|
||||||
|
func (msg MsgUnblockAddress) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Sender}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer
|
||||||
|
func (msg MsgUnblockAddress) String() string {
|
||||||
|
return fmt.Sprintf(`Unblock Address:
|
||||||
|
Sender %s
|
||||||
|
Denom %s
|
||||||
|
Address %s
|
||||||
|
`, msg.Sender, msg.Denom, msg.Address,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MsgSetPauseStatus message type used by the issuer to issue new tokens
|
||||||
|
type MsgSetPauseStatus struct {
|
||||||
|
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
Status bool `json:"status" yaml:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsgSetPauseStatus returns a new MsgSetPauseStatus
|
||||||
|
func NewMsgSetPauseStatus(sender sdk.AccAddress, denom string, status bool) MsgSetPauseStatus {
|
||||||
|
return MsgSetPauseStatus{
|
||||||
|
Sender: sender,
|
||||||
|
Denom: denom,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route return the message type used for routing the message.
|
||||||
|
func (msg MsgSetPauseStatus) Route() string { return RouterKey }
|
||||||
|
|
||||||
|
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||||
|
func (msg MsgSetPauseStatus) Type() string { return "change_pause_status" }
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that doesn't require access to state.
|
||||||
|
func (msg MsgSetPauseStatus) ValidateBasic() error {
|
||||||
|
if msg.Sender.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
|
||||||
|
}
|
||||||
|
return sdk.ValidateDenom(msg.Denom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignBytes gets the canonical byte representation of the Msg
|
||||||
|
func (msg MsgSetPauseStatus) GetSignBytes() []byte {
|
||||||
|
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigners returns the addresses of signers that must sign
|
||||||
|
func (msg MsgSetPauseStatus) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Sender}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer
|
||||||
|
func (msg MsgSetPauseStatus) String() string {
|
||||||
|
return fmt.Sprintf(`Set Pause Status:
|
||||||
|
Sender %s
|
||||||
|
Denom %s
|
||||||
|
Status %t
|
||||||
|
`, msg.Sender, msg.Denom, msg.Status,
|
||||||
|
)
|
||||||
|
}
|
133
x/issuance/types/params.go
Normal file
133
x/issuance/types/params.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parameter keys and default values
|
||||||
|
var (
|
||||||
|
KeyAssets = []byte("Assets")
|
||||||
|
DefaultAssets = Assets{}
|
||||||
|
ModuleAccountName = ModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
// Params governance parameters for the issuance module
|
||||||
|
type Params struct {
|
||||||
|
Assets Assets `json:"assets" yaml:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParams returns a new params object
|
||||||
|
func NewParams(assets Assets) Params {
|
||||||
|
return Params{Assets: assets}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultParams returns default params for issuance module
|
||||||
|
func DefaultParams() Params {
|
||||||
|
return NewParams(DefaultAssets)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(KeyAssets, &p.Assets, validateAssetsParam),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks that the parameters have valid values.
|
||||||
|
func (p Params) Validate() error {
|
||||||
|
return validateAssetsParam(p.Assets)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAssetsParam(i interface{}) error {
|
||||||
|
assets, ok := i.(Assets)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid parameter type: %T", i)
|
||||||
|
}
|
||||||
|
return assets.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer
|
||||||
|
func (p Params) String() string {
|
||||||
|
return fmt.Sprintf(`Params:
|
||||||
|
Assets: %s
|
||||||
|
`, p.Assets)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asset type for assets in the issuance module
|
||||||
|
type Asset struct {
|
||||||
|
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
BlockedAddresses []sdk.AccAddress `json:"blocked_addresses" yaml:"blocked_addresses"`
|
||||||
|
Paused bool `json:"paused" yaml:"paused"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAsset returns a new Asset
|
||||||
|
func NewAsset(owner sdk.AccAddress, denom string, blockedAddresses []sdk.AccAddress, paused bool) Asset {
|
||||||
|
return Asset{
|
||||||
|
Owner: owner,
|
||||||
|
Denom: denom,
|
||||||
|
BlockedAddresses: blockedAddresses,
|
||||||
|
Paused: paused,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs a basic check of asset fields
|
||||||
|
func (a Asset) Validate() error {
|
||||||
|
if a.Owner.Empty() {
|
||||||
|
return fmt.Errorf("owner must not be empty")
|
||||||
|
}
|
||||||
|
for _, address := range a.BlockedAddresses {
|
||||||
|
if address.Empty() {
|
||||||
|
return fmt.Errorf("blocked address must not be empty")
|
||||||
|
}
|
||||||
|
if a.Owner.Equals(address) {
|
||||||
|
return fmt.Errorf("asset owner cannot be blocked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sdk.ValidateDenom(a.Denom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer
|
||||||
|
func (a Asset) String() string {
|
||||||
|
return fmt.Sprintf(`Asset:
|
||||||
|
Owner: %s
|
||||||
|
Paused: %t
|
||||||
|
Denom: %s
|
||||||
|
Blocked Addresses: %s`,
|
||||||
|
a.Owner, a.Paused, a.Denom, a.BlockedAddresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assets array of Asset
|
||||||
|
type Assets []Asset
|
||||||
|
|
||||||
|
// Validate checks if all assets are valid and there are no duplicate entries
|
||||||
|
func (as Assets) Validate() error {
|
||||||
|
assetDenoms := make(map[string]bool)
|
||||||
|
for _, a := range as {
|
||||||
|
if assetDenoms[a.Denom] {
|
||||||
|
return fmt.Errorf("cannot have duplicate asset denoms: %s", a.Denom)
|
||||||
|
}
|
||||||
|
if err := a.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
assetDenoms[a.Denom] = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer
|
||||||
|
func (as Assets) String() string {
|
||||||
|
out := ""
|
||||||
|
for _, a := range as {
|
||||||
|
out += a.String()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
12
x/issuance/types/querier.go
Normal file
12
x/issuance/types/querier.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// Querier routes for the issuance module
|
||||||
|
const (
|
||||||
|
QueryGetParams = "parameters"
|
||||||
|
QueryGetAsset = "asset"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QueryAssetParams params for querying an asset by denom
|
||||||
|
type QueryAssetParams struct {
|
||||||
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user