diff --git a/app/ante_handler.go b/app/ante_handler.go index 5bca6498..3e372faf 100644 --- a/app/ante_handler.go +++ b/app/ante_handler.go @@ -2,6 +2,8 @@ package app import ( errorsmod "cosmossdk.io/errors" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" globalfeeante "github.com/OmniFlix/omniflixhub/v2/x/globalfee/ante" globalfeekeeper "github.com/OmniFlix/omniflixhub/v2/x/globalfee/keeper" "github.com/cosmos/cosmos-sdk/codec" @@ -26,6 +28,7 @@ type HandlerOptions struct { GovKeeper govkeeper.Keeper IBCKeeper *ibckeeper.Keeper TxCounterStoreKey storetypes.StoreKey + WasmConfig wasmtypes.WasmConfig Codec codec.BinaryCodec BypassMinFeeMsgTypes []string @@ -52,6 +55,8 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { anteDecorators := []sdk.AnteDecorator{ ante.NewSetUpContextDecorator(), // Outermost AnteDecorator, SetUpContext must be called first + wasmkeeper.NewLimitSimulationGasDecorator(options.WasmConfig.SimulationGasLimit), + wasmkeeper.NewCountTXDecorator(options.TxCounterStoreKey), ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker), ante.NewValidateBasicDecorator(), ante.NewTxTimeoutHeightDecorator(), diff --git a/app/app.go b/app/app.go index 6d91081f..5dff03df 100644 --- a/app/app.go +++ b/app/app.go @@ -7,9 +7,12 @@ import ( "os" "path/filepath" + "github.com/CosmWasm/wasmd/x/wasm" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1" - + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" "github.com/OmniFlix/omniflixhub/v2/app/openapiconsole" appparams "github.com/OmniFlix/omniflixhub/v2/app/params" "github.com/OmniFlix/omniflixhub/v2/docs" @@ -57,6 +60,18 @@ import ( const Name = "omniflixhub" +var ( + // ProposalsEnabled If EnabledSpecificProposals is "", and this is "true", then enable all x/wasm proposals. + // If EnabledSpecificProposals is "", and this is not "true", then disable all x/wasm proposals. + ProposalsEnabled = "true" + // EnableSpecificProposals If set to non-empty string it must be comma-separated list of values that are all a subset + // of "EnableAllProposals" (takes precedence over ProposalsEnabled) + // https://github.com/CosmWasm/wasmd/blob/02a54d33ff2c064f3539ae12d75d027d9c665f05/x/wasm/internal/types/proposal.go#L28-L34 + EnableSpecificProposals = "" + + EmptyWasmOpts []wasm.Option +) + func getGovProposalHandlers() []govclient.ProposalHandler { var govProposalHandlers []govclient.ProposalHandler govProposalHandlers = append(govProposalHandlers, @@ -120,6 +135,7 @@ func NewOmniFlixApp( invCheckPeriod uint, encodingConfig appparams.EncodingConfig, appOpts servertypes.AppOptions, + wasmOpts []wasmkeeper.Option, baseAppOptions ...func(*baseapp.BaseApp), ) *OmniFlixApp { appCodec := encodingConfig.Marshaler @@ -154,6 +170,7 @@ func NewOmniFlixApp( invCheckPeriod, logger, appOpts, + wasmOpts, ) /**** Module Options ****/ @@ -205,6 +222,11 @@ func NewOmniFlixApp( } reflectionv1.RegisterReflectionServiceServer(app.GRPCQueryRouter(), reflectionSvc) + wasmConfig, err := wasm.ReadWasmConfig(appOpts) + if err != nil { + panic("error while reading wasm config: " + err.Error()) + } + anteHandler, err := NewAnteHandler( HandlerOptions{ HandlerOptions: ante.HandlerOptions{ @@ -214,9 +236,11 @@ func NewOmniFlixApp( SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), SigGasConsumer: ante.DefaultSigVerificationGasConsumer, }, - GovKeeper: app.GovKeeper, - IBCKeeper: app.IBCKeeper, - Codec: appCodec, + GovKeeper: app.GovKeeper, + IBCKeeper: app.IBCKeeper, + Codec: appCodec, + WasmConfig: wasmConfig, + TxCounterStoreKey: app.AppKeepers.GetKey(wasmtypes.StoreKey), BypassMinFeeMsgTypes: GetDefaultBypassFeeMessages(), GlobalFeeKeeper: app.GlobalFeeKeeper, diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index b027b7f6..ed0c48e3 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -1,6 +1,12 @@ package keepers import ( + "fmt" + "path/filepath" + + "github.com/CosmWasm/wasmd/x/wasm" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" "github.com/OmniFlix/omniflixhub/v2/x/ics721nft" nfttransfer "github.com/bianjieai/nft-transfer" "github.com/cometbft/cometbft/libs/log" @@ -10,7 +16,6 @@ import ( servertypes "github.com/cosmos/cosmos-sdk/server/types" "github.com/cosmos/cosmos-sdk/store/streaming" storetypes "github.com/cosmos/cosmos-sdk/store/types" - icq "github.com/cosmos/ibc-apps/modules/async-icq/v7" icqkeeper "github.com/cosmos/ibc-apps/modules/async-icq/v7/keeper" icqtypes "github.com/cosmos/ibc-apps/modules/async-icq/v7/types" @@ -107,6 +112,8 @@ import ( ibcnfttransferkeeper "github.com/bianjieai/nft-transfer/keeper" ibcnfttransfertypes "github.com/bianjieai/nft-transfer/types" + + tfbindings "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/bindings" ) var tokenFactoryCapabilities = []string{ @@ -146,6 +153,7 @@ type AppKeepers struct { GroupKeeper groupkeeper.Keeper TokenFactoryKeeper tokenfactorykeeper.Keeper IBCNFTTransferKeeper ibcnfttransferkeeper.Keeper + WasmKeeper wasmkeeper.Keeper // make scoped keepers public for test purposes ScopedIBCKeeper capabilitykeeper.ScopedKeeper @@ -153,6 +161,7 @@ type AppKeepers struct { ScopedICAHostKeeper capabilitykeeper.ScopedKeeper ScopedICQKeeper capabilitykeeper.ScopedKeeper ScopedNFTTransferKeeper capabilitykeeper.ScopedKeeper + ScopedWasmKeeper capabilitykeeper.ScopedKeeper AllocKeeper allockeeper.Keeper ONFTKeeper onftkeeper.Keeper @@ -173,6 +182,7 @@ func NewAppKeeper( invCheckPeriod uint, logger log.Logger, appOpts servertypes.AppOptions, + wasmOpts []wasmkeeper.Option, ) AppKeepers { appKeepers := AppKeepers{} @@ -213,7 +223,7 @@ func NewAppKeeper( appKeepers.ScopedICAHostKeeper = appKeepers.CapabilityKeeper.ScopeToModule(icahosttypes.SubModuleName) appKeepers.ScopedICQKeeper = appKeepers.CapabilityKeeper.ScopeToModule(icqtypes.ModuleName) appKeepers.ScopedNFTTransferKeeper = appKeepers.CapabilityKeeper.ScopeToModule(ibcnfttransfertypes.ModuleName) - + appKeepers.ScopedWasmKeeper = appKeepers.CapabilityKeeper.ScopeToModule(wasmtypes.ModuleName) appKeepers.CapabilityKeeper.Seal() appKeepers.CrisisKeeper = crisiskeeper.NewKeeper( @@ -519,6 +529,50 @@ func NewAppKeeper( appKeepers.IBCKeeper.SetRouter(ibcRouter) + // wasm configuration + + wasmDir := filepath.Join(homePath, "wasm") + wasmConfig, err := wasm.ReadWasmConfig(appOpts) + if err != nil { + panic(fmt.Sprintf("error while reading wasm config: %s", err)) + } + + // custom tokenfactory messages + tfOpts := tfbindings.RegisterCustomPlugins(appKeepers.BankKeeper, &appKeepers.TokenFactoryKeeper) + wasmOpts = append(wasmOpts, tfOpts...) + + querierOpts := wasmkeeper.WithQueryPlugins( + &wasmkeeper.QueryPlugins{ + Stargate: wasmkeeper.AcceptListStargateQuerier( + AcceptedStargateQueries(), + bApp.GRPCQueryRouter(), + appCodec, + ), + }) + + wasmOpts = append(wasmOpts, querierOpts) + + appKeepers.WasmKeeper = wasmkeeper.NewKeeper( + appCodec, + keys[wasmtypes.StoreKey], + appKeepers.AccountKeeper, + appKeepers.BankKeeper, + appKeepers.StakingKeeper, + distrkeeper.NewQuerier(appKeepers.DistrKeeper), + appKeepers.IBCKeeper.ChannelKeeper, + appKeepers.IBCKeeper.ChannelKeeper, + &appKeepers.IBCKeeper.PortKeeper, + appKeepers.ScopedWasmKeeper, + appKeepers.TransferKeeper, + bApp.MsgServiceRouter(), + bApp.GRPCQueryRouter(), + wasmDir, + wasmConfig, + GetWasmCapabilities(), + govModAddress, + wasmOpts..., + ) + return appKeepers } @@ -547,6 +601,7 @@ func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino paramsKeeper.Subspace(packetforwardtypes.ModuleName) paramsKeeper.Subspace(globalfee.ModuleName) paramsKeeper.Subspace(tokenfactorytypes.ModuleName) + paramsKeeper.Subspace(wasmtypes.ModuleName) paramsKeeper.Subspace(alloctypes.ModuleName) paramsKeeper.Subspace(onfttypes.ModuleName) paramsKeeper.Subspace(marketplacetypes.ModuleName) diff --git a/app/keepers/keys.go b/app/keepers/keys.go index 7ad21306..ab56232b 100644 --- a/app/keepers/keys.go +++ b/app/keepers/keys.go @@ -1,6 +1,7 @@ package keepers import ( + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" alloctypes "github.com/OmniFlix/omniflixhub/v2/x/alloc/types" globalfeetypes "github.com/OmniFlix/omniflixhub/v2/x/globalfee/types" itctypes "github.com/OmniFlix/omniflixhub/v2/x/itc/types" @@ -56,6 +57,7 @@ func (appKeepers *AppKeepers) GenerateKeys() { capabilitytypes.StoreKey, crisistypes.StoreKey, feegrant.StoreKey, + wasmtypes.StoreKey, globalfeetypes.StoreKey, group.StoreKey, tokenfactorytypes.StoreKey, diff --git a/app/keepers/wasm.go b/app/keepers/wasm.go new file mode 100644 index 00000000..255529b0 --- /dev/null +++ b/app/keepers/wasm.go @@ -0,0 +1,97 @@ +package keepers + +import ( + "strings" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + itctypes "github.com/OmniFlix/omniflixhub/v2/x/itc/types" + marketplacetypes "github.com/OmniFlix/omniflixhub/v2/x/marketplace/types" + onfttypes "github.com/OmniFlix/omniflixhub/v2/x/onft/types" + tokenfactorytypes "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/types" + streampaytypes "github.com/OmniFlix/streampay/v2/x/streampay/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + ibcconnectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" +) + +// AllCapabilities returns all capabilities available with the current wasmvm +// See https://github.com/CosmWasm/cosmwasm/blob/main/docs/CAPABILITIES-BUILT-IN.md +// This functionality is going to be moved upstream: https://github.com/CosmWasm/wasmvm/issues/425 +var wasmCapabilities = []string{ + "iterator", + "staking", + "stargate", + "cosmwasm_1_1", + "cosmwasm_1_2", + "cosmwasm_1_3", + "cosmwasm_1_4", + "cosmwasm_1_5", + "token_factory", +} + +func AcceptedStargateQueries() wasmkeeper.AcceptedStargateQueries { + return wasmkeeper.AcceptedStargateQueries{ + // ibc + "/ibc.core.client.v1.Query/ClientState": &ibcclienttypes.QueryClientStateResponse{}, + "/ibc.core.client.v1.Query/ConsensusState": &ibcclienttypes.QueryConsensusStateResponse{}, + "/ibc.core.connection.v1.Query/Connection": &ibcconnectiontypes.QueryConnectionResponse{}, + + // governance + "/cosmos.gov.v1beta1.Query/Vote": &govv1.QueryVoteResponse{}, + + // distribution + "/cosmos.distribution.v1beta1.Query/DelegationRewards": &distrtypes.QueryDelegationRewardsResponse{}, + + // staking + "/cosmos.staking.v1beta1.Query/Delegation": &stakingtypes.QueryDelegationResponse{}, + "/cosmos.staking.v1beta1.Query/Redelegations": &stakingtypes.QueryRedelegationsResponse{}, + "/cosmos.staking.v1beta1.Query/UnbondingDelegation": &stakingtypes.QueryUnbondingDelegationResponse{}, + "/cosmos.staking.v1beta1.Query/Validator": &stakingtypes.QueryValidatorResponse{}, + "/cosmos.staking.v1beta1.Query/Params": &stakingtypes.QueryParamsResponse{}, + "/cosmos.staking.v1beta1.Query/Pool": &stakingtypes.QueryPoolResponse{}, + + // onft + "/OmniFlix.onft.v1beta1.Query/Denoms": &onfttypes.QueryDenomsResponse{}, + "/OmniFlix.onft.v1beta1.Query/Denom": &onfttypes.QueryDenomResponse{}, + "/OmniFlix.onft.v1beta1.Query/IBCDenom": &onfttypes.QueryDenomResponse{}, + "/OmniFlix.onft.v1beta1.Query/Collection": &onfttypes.QueryCollectionResponse{}, + "/OmniFlix.onft.v1beta1.Query/IBCCollection": &onfttypes.QueryCollectionResponse{}, + "/OmniFlix.onft.v1beta1.Query/OwnerONFTs": &onfttypes.QueryOwnerONFTsResponse{}, + "/OmniFlix.onft.v1beta1.Query/ONFT": &onfttypes.QueryONFTResponse{}, + "/OmniFlix.onft.v1beta1.Query/Supply": &onfttypes.QuerySupplyResponse{}, + "/OmniFlix.onft.v1beta1.Query/Params": &onfttypes.QueryParamsResponse{}, + + // marketplace + "/OmniFlix.marketplace.v1beta1.Query/Listings": &marketplacetypes.QueryListingsResponse{}, + "/OmniFlix.marketplace.v1beta1.Query/Listing": &marketplacetypes.QueryListingResponse{}, + "/OmniFlix.marketplace.v1beta1.Query/ListingsByOwner": &marketplacetypes.QueryListingsByOwnerResponse{}, + "/OmniFlix.marketplace.v1beta1.Query/Auctions": &marketplacetypes.QueryAuctionsResponse{}, + "/OmniFlix.marketplace.v1beta1.Query/Auction": &marketplacetypes.QueryAuctionResponse{}, + "/OmniFlix.marketplace.v1beta1.Query/AuctionsByOwner": &marketplacetypes.QueryAuctionsResponse{}, + "/OmniFlix.marketplace.v1beta1.Query/Bids": &marketplacetypes.QueryBidsResponse{}, + "/OmniFlix.marketplace.v1beta1.Query/Bid": &marketplacetypes.QueryBidResponse{}, + "/OmniFlix.marketplace.v1beta1.Query/Params": &marketplacetypes.QueryParamsResponse{}, + + // itc + "/OmniFlix.itc.v1.Query/Campaigns": &itctypes.QueryCampaignsResponse{}, + "/OmniFlix.itc.v1.Query/Campaign": &itctypes.QueryCampaignResponse{}, + "/OmniFlix.itc.v1.Query/Claims": &itctypes.QueryClaimsResponse{}, + "/OmniFlix.itc.v1.Query/Params": &itctypes.QueryParamsResponse{}, + + // streampay + "/OmniFlix.streampay.v1.Query/StreamPayments": &streampaytypes.QueryStreamPaymentsResponse{}, + "/OmniFlix.streampay.v1.Query/StreamPayment": &streampaytypes.QueryStreamPaymentResponse{}, + "/OmniFlix.streampay.v1.Query/Params": &streampaytypes.QueryParamsResponse{}, + + // tokenfactory queries + "/osmosis.tokenfactory.v1beta1.Query/Params": &tokenfactorytypes.QueryParamsResponse{}, + "/osmosis.tokenfactory.v1beta1.Query/DenomAuthorityMetadata": &tokenfactorytypes.QueryDenomAuthorityMetadataResponse{}, + "/osmosis.tokenfactory.v1beta1.Query/DenomsFromCreator": &tokenfactorytypes.QueryDenomsFromCreatorResponse{}, + } +} + +func GetWasmCapabilities() string { + return strings.Join(wasmCapabilities, ",") +} diff --git a/app/modules.go b/app/modules.go index 1f6946c8..196ca006 100644 --- a/app/modules.go +++ b/app/modules.go @@ -1,6 +1,8 @@ package app import ( + "github.com/CosmWasm/wasmd/x/wasm" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" appparams "github.com/OmniFlix/omniflixhub/v2/app/params" "github.com/OmniFlix/omniflixhub/v2/x/globalfee" nfttransfer "github.com/bianjieai/nft-transfer" @@ -108,6 +110,7 @@ var ( mint.AppModuleBasic{}, distr.AppModuleBasic{}, gov.NewAppModuleBasic(getGovProposalHandlers()), + wasm.AppModuleBasic{}, groupmodule.AppModuleBasic{}, params.AppModuleBasic{}, consensus.AppModuleBasic{}, @@ -149,6 +152,7 @@ var ( icatypes.ModuleName: nil, tokenfactorytypes.ModuleName: {authtypes.Minter, authtypes.Burner}, globalfee.ModuleName: nil, + wasmtypes.ModuleName: {authtypes.Burner}, alloctypes.ModuleName: {authtypes.Minter, authtypes.Burner, authtypes.Staking}, nft.ModuleName: nil, onfttypes.ModuleName: nil, @@ -226,6 +230,15 @@ func appModules( params.NewAppModule(app.ParamsKeeper), consensus.NewAppModule(appCodec, app.ConsensusParamsKeeper), transfer.NewAppModule(app.TransferKeeper), + wasm.NewAppModule( + appCodec, + &app.WasmKeeper, + app.StakingKeeper, + app.AccountKeeper, + app.BankKeeper, + app.MsgServiceRouter(), + app.GetSubspace(wasmtypes.ModuleName), + ), ica.NewAppModule(nil, &app.ICAHostKeeper), icq.NewAppModule(app.ICQKeeper, app.GetSubspace(icqtypes.ModuleName)), nfttransfer.NewAppModule(app.IBCNFTTransferKeeper), @@ -304,6 +317,7 @@ func orderBeginBlockers() []string { govtypes.ModuleName, paramstypes.ModuleName, consensusparamtypes.ModuleName, + wasmtypes.ModuleName, ibctransfertypes.ModuleName, icatypes.ModuleName, icqtypes.ModuleName, @@ -338,6 +352,7 @@ func orderEndBlockers() []string { vestingtypes.ModuleName, paramstypes.ModuleName, consensusparamtypes.ModuleName, + wasmtypes.ModuleName, ibctransfertypes.ModuleName, icatypes.ModuleName, icqtypes.ModuleName, @@ -388,6 +403,7 @@ func orderInitGenesis() []string { upgradetypes.ModuleName, vestingtypes.ModuleName, feegrant.ModuleName, + wasmtypes.ModuleName, globalfee.ModuleName, group.ModuleName, tokenfactorytypes.ModuleName, diff --git a/app/prefix.go b/app/prefix.go index ed545e5d..258fd8cb 100644 --- a/app/prefix.go +++ b/app/prefix.go @@ -1,6 +1,7 @@ package app import ( + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -21,5 +22,6 @@ func SetConfig() { config.SetBech32PrefixForAccount(AccountAddressPrefix, AccountPubKeyPrefix) config.SetBech32PrefixForValidator(ValidatorAddressPrefix, ValidatorPubKeyPrefix) config.SetBech32PrefixForConsensusNode(ConsNodeAddressPrefix, ConsNodePubKeyPrefix) + config.SetAddressVerifier(wasmtypes.VerifyAddressLen()) config.Seal() } diff --git a/app/test_helpers.go b/app/test_helpers.go index 3d68c1a5..1642db14 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -6,6 +6,8 @@ import ( "testing" "time" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/snapshots" @@ -133,7 +135,7 @@ func SetupWithGenesisValSet( return omniflixTestApp } -func setup(t *testing.T, withGenesis bool) (*OmniFlixApp, GenesisState) { +func setup(t *testing.T, withGenesis bool, opts ...wasmkeeper.Option) (*OmniFlixApp, GenesisState) { t.Helper() db := dbm.NewMemDB() @@ -147,7 +149,7 @@ func setup(t *testing.T, withGenesis bool) (*OmniFlixApp, GenesisState) { snapshotStore, err := snapshots.NewStore(snapshotDB, snapshotDir) require.NoError(t, err) - appOptions := make(simtestutil.AppOptionsMap, 0) + appOptions := make(simtestutil.AppOptionsMap) appOptions[flags.FlagHome] = nodeHome // ensure unique folder app := NewOmniFlixApp( @@ -160,6 +162,7 @@ func setup(t *testing.T, withGenesis bool) (*OmniFlixApp, GenesisState) { 0, encCdc, appOptions, + opts, baseApp.SetChainID(SimAppChainID), baseApp.SetSnapshot(snapshotStore, snapshottypes.SnapshotOptions{KeepRecent: 2}), ) diff --git a/cmd/omniflixhubd/cmd/root.go b/cmd/omniflixhubd/cmd/root.go index 603dba9c..9606369d 100644 --- a/cmd/omniflixhubd/cmd/root.go +++ b/cmd/omniflixhubd/cmd/root.go @@ -9,10 +9,13 @@ import ( "github.com/cosmos/cosmos-sdk/client/config" "github.com/cosmos/cosmos-sdk/client/debug" "github.com/cosmos/cosmos-sdk/client/pruning" + "github.com/prometheus/client_golang/prometheus" + "github.com/OmniFlix/omniflixhub/v2/app" "github.com/OmniFlix/omniflixhub/v2/app/params" - "github.com/OmniFlix/omniflixhub/v2/app" + "github.com/CosmWasm/wasmd/x/wasm" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" dbm "github.com/cometbft/cometbft-db" tmcli "github.com/cometbft/cometbft/libs/cli" "github.com/cometbft/cometbft/libs/log" @@ -118,6 +121,7 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { func addModuleInitFlags(startCmd *cobra.Command) { crisis.AddModuleInitFlags(startCmd) + wasm.AddModuleInitFlags(startCmd) } func genesisCommand(encodingConfig params.EncodingConfig, cmds ...*cobra.Command) *cobra.Command { @@ -192,6 +196,11 @@ func (a appCreator) newApp(logger log.Logger, db dbm.DB, traceStore io.Writer, a skipUpgradeHeights[int64(h)] = true } + var wasmOpts []wasmkeeper.Option + if cast.ToBool(appOpts.Get("telemetry.enabled")) { + wasmOpts = append(wasmOpts, wasmkeeper.WithVMCacheMetrics(prometheus.DefaultRegisterer)) + } + baseappOptions := server.DefaultBaseappOptions(appOpts) return app.NewOmniFlixApp( @@ -204,6 +213,7 @@ func (a appCreator) newApp(logger log.Logger, db dbm.DB, traceStore io.Writer, a cast.ToUint(appOpts.Get(server.FlagInvCheckPeriod)), a.encCfg, appOpts, + wasmOpts, baseappOptions..., ) } @@ -226,6 +236,7 @@ func (a appCreator) appExport( return servertypes.ExportedApp{}, errors.New("application home not set") } + var emptyWasmOpts []wasmkeeper.Option if height != -1 { anApp = app.NewOmniFlixApp( logger, @@ -237,6 +248,7 @@ func (a appCreator) appExport( uint(1), a.encCfg, appOpts, + emptyWasmOpts, ) if err := anApp.LoadHeight(height); err != nil { @@ -253,6 +265,7 @@ func (a appCreator) appExport( uint(1), a.encCfg, appOpts, + emptyWasmOpts, ) } diff --git a/go.mod b/go.mod index 4f055ec5..d3d13e92 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/OmniFlix/omniflixhub/v2 go 1.21 require ( + github.com/CosmWasm/wasmd v0.45.0 + github.com/CosmWasm/wasmvm v1.5.0 github.com/OmniFlix/streampay/v2 v2.3.0 github.com/bianjieai/nft-transfer v1.1.3-ibc-v7.3.0 github.com/cometbft/cometbft v0.37.2 @@ -27,6 +29,12 @@ require ( google.golang.org/protobuf v1.31.0 ) +require ( + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect +) + require ( cloud.google.com/go v0.110.4 // indirect cloud.google.com/go/compute v1.20.1 // indirect @@ -63,7 +71,7 @@ require ( github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect - github.com/cosmos/iavl v0.20.0 // indirect + github.com/cosmos/iavl v0.20.1 // indirect github.com/cosmos/ics23/go v0.10.0 // indirect github.com/cosmos/ledger-cosmos-go v0.12.4 // indirect github.com/cosmos/rosetta-sdk-go v0.10.0 // indirect @@ -137,10 +145,10 @@ require ( github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_golang v1.16.0 github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect github.com/rakyll/statik v0.1.7 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect diff --git a/go.sum b/go.sum index 60150933..396f31ea 100644 --- a/go.sum +++ b/go.sum @@ -222,6 +222,10 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= +github.com/CosmWasm/wasmd v0.45.0 h1:9zBqrturKJwC2kVsfHvbrA++EN0PS7UTXCffCGbg6JI= +github.com/CosmWasm/wasmd v0.45.0/go.mod h1:RnSAiqbNIZu4QhO+0pd7qGZgnYAMBPGmXpzTADag944= +github.com/CosmWasm/wasmvm v1.5.0 h1:3hKeT9SfwfLhxTGKH3vXaKFzBz1yuvP8SlfwfQXbQfw= +github.com/CosmWasm/wasmvm v1.5.0/go.mod h1:fXB+m2gyh4v9839zlIXdMZGeLAxqUdYdFQqYsTha2hc= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= @@ -409,8 +413,8 @@ github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ github.com/cosmos/gogoproto v1.4.2/go.mod h1:cLxOsn1ljAHSV527CHOtaIP91kK6cCrZETRBrkzItWU= github.com/cosmos/gogoproto v1.4.10 h1:QH/yT8X+c0F4ZDacDv3z+xE3WU1P1Z3wQoLMBRJoKuI= github.com/cosmos/gogoproto v1.4.10/go.mod h1:3aAZzeRWpAwr+SS/LLkICX2/kDFyaYVzckBDzygIxek= -github.com/cosmos/iavl v0.20.0 h1:fTVznVlepH0KK8NyKq8w+U7c2L6jofa27aFX6YGlm38= -github.com/cosmos/iavl v0.20.0/go.mod h1:WO7FyvaZJoH65+HFOsDir7xU9FWk2w9cHXNW1XHcl7A= +github.com/cosmos/iavl v0.20.1 h1:rM1kqeG3/HBT85vsZdoSNsehciqUQPWrR4BYmqE2+zg= +github.com/cosmos/iavl v0.20.1/go.mod h1:WO7FyvaZJoH65+HFOsDir7xU9FWk2w9cHXNW1XHcl7A= github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v7 v7.1.1 h1:PqIK9vTr6zxCdQmrDZwxwL4KMAqg/GRGsiMEiaMP4wA= github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v7 v7.1.1/go.mod h1:UvDmcGIWJPIytq+Q78/ff5NTOsuX/7IrNgEugTW5i0s= github.com/cosmos/ibc-apps/modules/async-icq/v7 v7.1.1 h1:02RCbih5lQ8aGdDMSvxhTnk5JDLEDitn17ytEE1Qhko= @@ -466,6 +470,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= @@ -1044,8 +1050,8 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -1070,8 +1076,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= diff --git a/x/onft/types/constants.go b/x/onft/types/constants.go index 4ece88de..b15bf520 100644 --- a/x/onft/types/constants.go +++ b/x/onft/types/constants.go @@ -3,7 +3,7 @@ package types const ( MinDenomLen = 3 MaxDenomLen = 128 - MinIDLen = 3 + MinIDLen = 1 MaxIDLen = 128 MaxNameLen = 256 MaxDescriptionLen = 4096 diff --git a/x/onft/types/validations.go b/x/onft/types/validations.go index b9f6ba47..0cd40e16 100644 --- a/x/onft/types/validations.go +++ b/x/onft/types/validations.go @@ -14,10 +14,10 @@ func ValidateONFTID(onftId string) error { ErrInvalidONFTID, "invalid onftId %s, length must be between [%d, %d]", onftId, MinIDLen, MaxIDLen) } - if !IsBeginWithAlpha(onftId) || !IsAlphaNumeric(onftId) { + if !IsAlphaNumeric(onftId) { return errorsmod.Wrapf( ErrInvalidONFTID, - "invalid onftId %s, only accepts alphanumeric characters and begin with an english letter", onftId) + "invalid onftId %s, only accepts alphanumeric characters", onftId) } return nil } diff --git a/x/tokenfactory/bindings/custom_msg_test.go b/x/tokenfactory/bindings/custom_msg_test.go new file mode 100644 index 00000000..838e007a --- /dev/null +++ b/x/tokenfactory/bindings/custom_msg_test.go @@ -0,0 +1,328 @@ +package bindings_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/OmniFlix/omniflixhub/v2/app" + bindings "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/bindings/types" + "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/types" +) + +func TestCreateDenomMsg(t *testing.T) { + creator := RandomAccountAddress() + customApp, ctx := SetupCustomApp(t, creator) + + lucky := RandomAccountAddress() + reflect := instantiateReflectContract(t, ctx, customApp, lucky) + require.NotEmpty(t, reflect) + + // Fund reflect contract with 100 base denom creation fees + reflectAmount := sdk.NewCoins(sdk.NewCoin(types.DefaultParams().DenomCreationFee[0].Denom, types.DefaultParams().DenomCreationFee[0].Amount.MulRaw(100))) + fundAccount(t, ctx, customApp, reflect, reflectAmount) + + msg := bindings.TokenFactoryMsg{CreateDenom: &bindings.CreateDenom{ + Subdenom: "SUN", + }} + err := executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + + // query the denom and see if it matches + query := bindings.TokenFactoryQuery{ + FullDenom: &bindings.FullDenom{ + CreatorAddr: reflect.String(), + Subdenom: "SUN", + }, + } + resp := bindings.FullDenomResponse{} + queryCustom(t, ctx, customApp, reflect, query, &resp) + + require.Equal(t, resp.Denom, fmt.Sprintf("factory/%s/SUN", reflect.String())) +} + +func TestMintMsg(t *testing.T) { + creator := RandomAccountAddress() + customApp, ctx := SetupCustomApp(t, creator) + + lucky := RandomAccountAddress() + reflect := instantiateReflectContract(t, ctx, customApp, lucky) + require.NotEmpty(t, reflect) + + // Fund reflect contract with 100 base denom creation fees + reflectAmount := sdk.NewCoins(sdk.NewCoin(types.DefaultParams().DenomCreationFee[0].Denom, types.DefaultParams().DenomCreationFee[0].Amount.MulRaw(100))) + fundAccount(t, ctx, customApp, reflect, reflectAmount) + + // lucky was broke + balances := customApp.AppKeepers.BankKeeper.GetAllBalances(ctx, lucky) + require.Empty(t, balances) + + // Create denom for minting + msg := bindings.TokenFactoryMsg{CreateDenom: &bindings.CreateDenom{ + Subdenom: "SUN", + }} + err := executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + sunDenom := fmt.Sprintf("factory/%s/%s", reflect.String(), msg.CreateDenom.Subdenom) + + amount, ok := sdk.NewIntFromString("808010808") + require.True(t, ok) + msg = bindings.TokenFactoryMsg{MintTokens: &bindings.MintTokens{ + Denom: sunDenom, + Amount: amount, + MintToAddress: lucky.String(), + }} + err = executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + + balances = customApp.AppKeepers.BankKeeper.GetAllBalances(ctx, lucky) + require.Len(t, balances, 1) + coin := balances[0] + require.Equal(t, amount, coin.Amount) + require.Contains(t, coin.Denom, "factory/") + + // query the denom and see if it matches + query := bindings.TokenFactoryQuery{ + FullDenom: &bindings.FullDenom{ + CreatorAddr: reflect.String(), + Subdenom: "SUN", + }, + } + resp := bindings.FullDenomResponse{} + queryCustom(t, ctx, customApp, reflect, query, &resp) + + require.Equal(t, resp.Denom, coin.Denom) + + // mint the same denom again + err = executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + + balances = customApp.AppKeepers.BankKeeper.GetAllBalances(ctx, lucky) + require.Len(t, balances, 1) + coin = balances[0] + require.Equal(t, amount.MulRaw(2), coin.Amount) + require.Contains(t, coin.Denom, "factory/") + + // query the denom and see if it matches + query = bindings.TokenFactoryQuery{ + FullDenom: &bindings.FullDenom{ + CreatorAddr: reflect.String(), + Subdenom: "SUN", + }, + } + resp = bindings.FullDenomResponse{} + queryCustom(t, ctx, customApp, reflect, query, &resp) + + require.Equal(t, resp.Denom, coin.Denom) + + // now mint another amount / denom + // create it first + msg = bindings.TokenFactoryMsg{CreateDenom: &bindings.CreateDenom{ + Subdenom: "MOON", + }} + err = executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + moonDenom := fmt.Sprintf("factory/%s/%s", reflect.String(), msg.CreateDenom.Subdenom) + + amount = amount.SubRaw(1) + msg = bindings.TokenFactoryMsg{MintTokens: &bindings.MintTokens{ + Denom: moonDenom, + Amount: amount, + MintToAddress: lucky.String(), + }} + err = executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + + balances = customApp.AppKeepers.BankKeeper.GetAllBalances(ctx, lucky) + require.Len(t, balances, 2) + coin = balances[0] + require.Equal(t, amount, coin.Amount) + require.Contains(t, coin.Denom, "factory/") + + // query the denom and see if it matches + query = bindings.TokenFactoryQuery{ + FullDenom: &bindings.FullDenom{ + CreatorAddr: reflect.String(), + Subdenom: "MOON", + }, + } + resp = bindings.FullDenomResponse{} + queryCustom(t, ctx, customApp, reflect, query, &resp) + + require.Equal(t, resp.Denom, coin.Denom) + + // and check the first denom is unchanged + coin = balances[1] + require.Equal(t, amount.AddRaw(1).MulRaw(2), coin.Amount) + require.Contains(t, coin.Denom, "factory/") + + // query the denom and see if it matches + query = bindings.TokenFactoryQuery{ + FullDenom: &bindings.FullDenom{ + CreatorAddr: reflect.String(), + Subdenom: "SUN", + }, + } + resp = bindings.FullDenomResponse{} + queryCustom(t, ctx, customApp, reflect, query, &resp) + + require.Equal(t, resp.Denom, coin.Denom) +} + +func TestForceTransfer(t *testing.T) { + creator := RandomAccountAddress() + customApp, ctx := SetupCustomApp(t, creator) + + lucky := RandomAccountAddress() + rcpt := RandomAccountAddress() + reflect := instantiateReflectContract(t, ctx, customApp, lucky) + require.NotEmpty(t, reflect) + + // Fund reflect contract with 100 base denom creation fees + reflectAmount := sdk.NewCoins(sdk.NewCoin(types.DefaultParams().DenomCreationFee[0].Denom, types.DefaultParams().DenomCreationFee[0].Amount.MulRaw(100))) + fundAccount(t, ctx, customApp, reflect, reflectAmount) + + // lucky was broke + balances := customApp.AppKeepers.BankKeeper.GetAllBalances(ctx, lucky) + require.Empty(t, balances) + + // Create denom for minting + msg := bindings.TokenFactoryMsg{CreateDenom: &bindings.CreateDenom{ + Subdenom: "SUN", + }} + err := executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + sunDenom := fmt.Sprintf("factory/%s/%s", reflect.String(), msg.CreateDenom.Subdenom) + + amount, ok := sdk.NewIntFromString("808010808") + require.True(t, ok) + + // Mint new tokens to lucky + msg = bindings.TokenFactoryMsg{MintTokens: &bindings.MintTokens{ + Denom: sunDenom, + Amount: amount, + MintToAddress: lucky.String(), + }} + err = executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + + // Force move 100 tokens from lucky to rcpt + msg = bindings.TokenFactoryMsg{ForceTransfer: &bindings.ForceTransfer{ + Denom: sunDenom, + Amount: sdk.NewInt(100), + FromAddress: lucky.String(), + ToAddress: rcpt.String(), + }} + err = executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + + // check the balance of rcpt + balances = customApp.AppKeepers.BankKeeper.GetAllBalances(ctx, rcpt) + require.Len(t, balances, 1) + coin := balances[0] + require.Equal(t, sdk.NewInt(100), coin.Amount) +} + +func TestBurnMsg(t *testing.T) { + creator := RandomAccountAddress() + customApp, ctx := SetupCustomApp(t, creator) + + lucky := RandomAccountAddress() + reflect := instantiateReflectContract(t, ctx, customApp, lucky) + require.NotEmpty(t, reflect) + + // Fund reflect contract with 100 base denom creation fees + reflectAmount := sdk.NewCoins(sdk.NewCoin(types.DefaultParams().DenomCreationFee[0].Denom, types.DefaultParams().DenomCreationFee[0].Amount.MulRaw(100))) + fundAccount(t, ctx, customApp, reflect, reflectAmount) + + // lucky was broke + balances := customApp.AppKeepers.BankKeeper.GetAllBalances(ctx, lucky) + require.Empty(t, balances) + + // Create denom for minting + msg := bindings.TokenFactoryMsg{CreateDenom: &bindings.CreateDenom{ + Subdenom: "SUN", + }} + err := executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + sunDenom := fmt.Sprintf("factory/%s/%s", reflect.String(), msg.CreateDenom.Subdenom) + + amount, ok := sdk.NewIntFromString("808010809") + require.True(t, ok) + + msg = bindings.TokenFactoryMsg{MintTokens: &bindings.MintTokens{ + Denom: sunDenom, + Amount: amount, + MintToAddress: lucky.String(), + }} + err = executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + + // can burn from different address with burnFrom + amt, ok := sdk.NewIntFromString("1") + require.True(t, ok) + msg = bindings.TokenFactoryMsg{BurnTokens: &bindings.BurnTokens{ + Denom: sunDenom, + Amount: amt, + BurnFromAddress: lucky.String(), + }} + err = executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) + + // lucky needs to send balance to reflect contract to burn it + luckyBalance := customApp.AppKeepers.BankKeeper.GetAllBalances(ctx, lucky) + err = customApp.AppKeepers.BankKeeper.SendCoins(ctx, lucky, reflect, luckyBalance) + require.NoError(t, err) + + msg = bindings.TokenFactoryMsg{BurnTokens: &bindings.BurnTokens{ + Denom: sunDenom, + Amount: amount.Abs().Sub(sdk.NewInt(1)), + BurnFromAddress: reflect.String(), + }} + err = executeCustom(t, ctx, customApp, reflect, lucky, msg, sdk.Coin{}) + require.NoError(t, err) +} + +type ReflectExec struct { + ReflectMsg *ReflectMsgs `json:"reflect_msg,omitempty"` + ReflectSubMsg *ReflectSubMsgs `json:"reflect_sub_msg,omitempty"` +} + +type ReflectMsgs struct { + Msgs []wasmvmtypes.CosmosMsg `json:"msgs"` +} + +type ReflectSubMsgs struct { + Msgs []wasmvmtypes.SubMsg `json:"msgs"` +} + +func executeCustom(t *testing.T, ctx sdk.Context, customApp *app.OmniFlixApp, contract sdk.AccAddress, sender sdk.AccAddress, msg bindings.TokenFactoryMsg, funds sdk.Coin) error { //nolint:unparam // funds is always nil but could change in the future. + customBz, err := json.Marshal(msg) + require.NoError(t, err) + + reflectMsg := ReflectExec{ + ReflectMsg: &ReflectMsgs{ + Msgs: []wasmvmtypes.CosmosMsg{{ + Custom: customBz, + }}, + }, + } + reflectBz, err := json.Marshal(reflectMsg) + require.NoError(t, err) + + // no funds sent if amount is 0 + var coins sdk.Coins + if !funds.Amount.IsNil() { + coins = sdk.Coins{funds} + } + + contractKeeper := keeper.NewDefaultPermissionKeeper(customApp.AppKeepers.WasmKeeper) + _, err = contractKeeper.Execute(ctx, contract, sender, reflectBz, coins) + return err +} diff --git a/x/tokenfactory/bindings/custom_query_test.go b/x/tokenfactory/bindings/custom_query_test.go new file mode 100644 index 00000000..62563201 --- /dev/null +++ b/x/tokenfactory/bindings/custom_query_test.go @@ -0,0 +1,71 @@ +package bindings_test + +import ( + "encoding/json" + "fmt" + "testing" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/OmniFlix/omniflixhub/v2/app" + bindings "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/bindings/types" +) + +func TestQueryFullDenom(t *testing.T) { + actor := RandomAccountAddress() + customApp, ctx := SetupCustomApp(t, actor) + + reflect := instantiateReflectContract(t, ctx, customApp, actor) + require.NotEmpty(t, reflect) + + // query full denom + query := bindings.TokenFactoryQuery{ + FullDenom: &bindings.FullDenom{ + CreatorAddr: reflect.String(), + Subdenom: "ustart", + }, + } + resp := bindings.FullDenomResponse{} + queryCustom(t, ctx, customApp, reflect, query, &resp) + + expected := fmt.Sprintf("factory/%s/ustart", reflect.String()) + require.EqualValues(t, expected, resp.Denom) +} + +type ReflectQuery struct { + Chain *ChainRequest `json:"chain,omitempty"` +} + +type ChainRequest struct { + Request wasmvmtypes.QueryRequest `json:"request"` +} + +type ChainResponse struct { + Data []byte `json:"data"` +} + +func queryCustom(t *testing.T, ctx sdk.Context, customApp *app.OmniFlixApp, contract sdk.AccAddress, request bindings.TokenFactoryQuery, response interface{}) { + msgBz, err := json.Marshal(request) + require.NoError(t, err) + fmt.Println("queryCustom1", string(msgBz)) + + query := ReflectQuery{ + Chain: &ChainRequest{ + Request: wasmvmtypes.QueryRequest{Custom: msgBz}, + }, + } + queryBz, err := json.Marshal(query) + require.NoError(t, err) + fmt.Println("queryCustom2", string(queryBz)) + + resBz, err := customApp.AppKeepers.WasmKeeper.QuerySmart(ctx, contract, queryBz) + require.NoError(t, err) + var resp ChainResponse + err = json.Unmarshal(resBz, &resp) + require.NoError(t, err) + err = json.Unmarshal(resp.Data, response) + require.NoError(t, err) +} diff --git a/x/tokenfactory/bindings/helpers_test.go b/x/tokenfactory/bindings/helpers_test.go new file mode 100644 index 00000000..8ace8daf --- /dev/null +++ b/x/tokenfactory/bindings/helpers_test.go @@ -0,0 +1,92 @@ +package bindings_test + +import ( + "os" + "testing" + "time" + + "github.com/CosmWasm/wasmd/x/wasm/keeper" + "github.com/stretchr/testify/require" + + "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/crypto/ed25519" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktestutil "github.com/cosmos/cosmos-sdk/x/bank/testutil" + + "github.com/OmniFlix/omniflixhub/v2/app" +) + +func CreateTestInput(t *testing.T) (*app.OmniFlixApp, sdk.Context) { + omniflix := app.Setup(t) + ctx := omniflix.BaseApp.NewContext(false, tmproto.Header{Height: 1, ChainID: "testing", Time: time.Now().UTC()}) + return omniflix, ctx +} + +func FundAccount(t *testing.T, ctx sdk.Context, customApp *app.OmniFlixApp, acct sdk.AccAddress) { + err := banktestutil.FundAccount(customApp.AppKeepers.BankKeeper, ctx, acct, sdk.NewCoins( + sdk.NewCoin("uflix", sdk.NewInt(10000000000)), + )) + require.NoError(t, err) +} + +// we need to make this deterministic (same every test run), as content might affect gas costs +func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.AccAddress) { + key := ed25519.GenPrivKey() + pub := key.PubKey() + addr := sdk.AccAddress(pub.Address()) + return key, pub, addr +} + +func RandomAccountAddress() sdk.AccAddress { + _, _, addr := keyPubAddr() + return addr +} + +func RandomBech32AccountAddress() string { + return RandomAccountAddress().String() +} + +func storeReflectCode(t *testing.T, ctx sdk.Context, customApp *app.OmniFlixApp, addr sdk.AccAddress) uint64 { + wasmCode, err := os.ReadFile("./testdata/token_reflect.wasm") + require.NoError(t, err) + + contractKeeper := keeper.NewDefaultPermissionKeeper(customApp.AppKeepers.WasmKeeper) + codeID, _, err := contractKeeper.Create(ctx, addr, wasmCode, nil) + require.NoError(t, err) + + return codeID +} + +func instantiateReflectContract(t *testing.T, ctx sdk.Context, customApp *app.OmniFlixApp, funder sdk.AccAddress) sdk.AccAddress { + initMsgBz := []byte("{}") + contractKeeper := keeper.NewDefaultPermissionKeeper(customApp.AppKeepers.WasmKeeper) + codeID := uint64(1) + addr, _, err := contractKeeper.Instantiate(ctx, codeID, funder, funder, initMsgBz, "demo contract", nil) + require.NoError(t, err) + + return addr +} + +func fundAccount(t *testing.T, ctx sdk.Context, customApp *app.OmniFlixApp, addr sdk.AccAddress, coins sdk.Coins) { + err := banktestutil.FundAccount( + customApp.AppKeepers.BankKeeper, + ctx, + addr, + coins, + ) + require.NoError(t, err) +} + +func SetupCustomApp(t *testing.T, addr sdk.AccAddress) (*app.OmniFlixApp, sdk.Context) { + customApp, ctx := CreateTestInput(t) + wasmKeeper := customApp.AppKeepers.WasmKeeper + + storeReflectCode(t, ctx, customApp, addr) + + cInfo := wasmKeeper.GetCodeInfo(ctx, 1) + require.NotNil(t, cInfo) + + return customApp, ctx +} diff --git a/x/tokenfactory/bindings/message_plugin.go b/x/tokenfactory/bindings/message_plugin.go new file mode 100644 index 00000000..b6a237fc --- /dev/null +++ b/x/tokenfactory/bindings/message_plugin.go @@ -0,0 +1,368 @@ +package bindings + +import ( + "encoding/json" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + bindingstypes "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/bindings/types" + tokenfactorykeeper "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/keeper" + tokenfactorytypes "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/types" +) + +// CustomMessageDecorator returns decorator for custom CosmWasm bindings messages +func CustomMessageDecorator(bank bankkeeper.Keeper, tokenFactory *tokenfactorykeeper.Keeper) func(wasmkeeper.Messenger) wasmkeeper.Messenger { + return func(old wasmkeeper.Messenger) wasmkeeper.Messenger { + return &CustomMessenger{ + wrapped: old, + bank: bank, + tokenFactory: tokenFactory, + } + } +} + +type CustomMessenger struct { + wrapped wasmkeeper.Messenger + bank bankkeeper.Keeper + tokenFactory *tokenfactorykeeper.Keeper +} + +var _ wasmkeeper.Messenger = (*CustomMessenger)(nil) + +// DispatchMsg executes on the contractMsg. +func (m *CustomMessenger) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Event, [][]byte, error) { + if msg.Custom != nil { + // only handle the happy path where this is really creating / minting / swapping ... + // leave everything else for the wrapped version + var contractMsg bindingstypes.TokenFactoryMsg + if err := json.Unmarshal(msg.Custom, &contractMsg); err != nil { + return nil, nil, errorsmod.Wrap(err, "token factory msg") + } + + if contractMsg.CreateDenom != nil { + return m.createDenom(ctx, contractAddr, contractMsg.CreateDenom) + } + if contractMsg.MintTokens != nil { + return m.mintTokens(ctx, contractAddr, contractMsg.MintTokens) + } + if contractMsg.ChangeAdmin != nil { + return m.changeAdmin(ctx, contractAddr, contractMsg.ChangeAdmin) + } + if contractMsg.BurnTokens != nil { + return m.burnTokens(ctx, contractAddr, contractMsg.BurnTokens) + } + if contractMsg.SetMetadata != nil { + return m.setMetadata(ctx, contractAddr, contractMsg.SetMetadata) + } + if contractMsg.ForceTransfer != nil { + return m.forceTransfer(ctx, contractAddr, contractMsg.ForceTransfer) + } + } + return m.wrapped.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg) +} + +// createDenom creates a new token denom +func (m *CustomMessenger) createDenom(ctx sdk.Context, contractAddr sdk.AccAddress, createDenom *bindingstypes.CreateDenom) ([]sdk.Event, [][]byte, error) { + bz, err := PerformCreateDenom(m.tokenFactory, m.bank, ctx, contractAddr, createDenom) + if err != nil { + return nil, nil, errorsmod.Wrap(err, "perform create denom") + } + // TODO: double check how this is all encoded to the contract + return nil, [][]byte{bz}, nil +} + +// PerformCreateDenom is used with createDenom to create a token denom; validates the msgCreateDenom. +func PerformCreateDenom(f *tokenfactorykeeper.Keeper, b bankkeeper.Keeper, ctx sdk.Context, contractAddr sdk.AccAddress, createDenom *bindingstypes.CreateDenom) ([]byte, error) { + if createDenom == nil { + return nil, wasmvmtypes.InvalidRequest{Err: "create denom null create denom"} + } + + msgServer := tokenfactorykeeper.NewMsgServerImpl(*f) + + msgCreateDenom := tokenfactorytypes.NewMsgCreateDenom(contractAddr.String(), createDenom.Subdenom) + + if err := msgCreateDenom.ValidateBasic(); err != nil { + return nil, errorsmod.Wrap(err, "failed validating MsgCreateDenom") + } + + // Create denom + resp, err := msgServer.CreateDenom( + sdk.WrapSDKContext(ctx), + msgCreateDenom, + ) + if err != nil { + return nil, errorsmod.Wrap(err, "creating denom") + } + + if createDenom.Metadata != nil { + newDenom := resp.NewTokenDenom + err := PerformSetMetadata(f, b, ctx, contractAddr, newDenom, *createDenom.Metadata) + if err != nil { + return nil, errorsmod.Wrap(err, "setting metadata") + } + } + + return resp.Marshal() +} + +// mintTokens mints tokens of a specified denom to an address. +func (m *CustomMessenger) mintTokens(ctx sdk.Context, contractAddr sdk.AccAddress, mint *bindingstypes.MintTokens) ([]sdk.Event, [][]byte, error) { + err := PerformMint(m.tokenFactory, m.bank, ctx, contractAddr, mint) + if err != nil { + return nil, nil, errorsmod.Wrap(err, "perform mint") + } + return nil, nil, nil +} + +// PerformMint used with mintTokens to validate the mint message and mint through token factory. +func PerformMint(f *tokenfactorykeeper.Keeper, b bankkeeper.Keeper, ctx sdk.Context, contractAddr sdk.AccAddress, mint *bindingstypes.MintTokens) error { + if mint == nil { + return wasmvmtypes.InvalidRequest{Err: "mint token null mint"} + } + rcpt, err := parseAddress(mint.MintToAddress) + if err != nil { + return err + } + + coin := sdk.Coin{Denom: mint.Denom, Amount: mint.Amount} + sdkMsg := tokenfactorytypes.NewMsgMint(contractAddr.String(), coin) + + if err = sdkMsg.ValidateBasic(); err != nil { + return err + } + + // Mint through token factory / message server + msgServer := tokenfactorykeeper.NewMsgServerImpl(*f) + _, err = msgServer.Mint(sdk.WrapSDKContext(ctx), sdkMsg) + if err != nil { + return errorsmod.Wrap(err, "minting coins from message") + } + + if b.BlockedAddr(rcpt) { + return errorsmod.Wrapf(err, "minting coins to blocked address %s", rcpt.String()) + } + + err = b.SendCoins(ctx, contractAddr, rcpt, sdk.NewCoins(coin)) + if err != nil { + return errorsmod.Wrap(err, "sending newly minted coins from message") + } + return nil +} + +// changeAdmin changes the admin. +func (m *CustomMessenger) changeAdmin(ctx sdk.Context, contractAddr sdk.AccAddress, changeAdmin *bindingstypes.ChangeAdmin) ([]sdk.Event, [][]byte, error) { + err := ChangeAdmin(m.tokenFactory, ctx, contractAddr, changeAdmin) + if err != nil { + return nil, nil, errorsmod.Wrap(err, "failed to change admin") + } + return nil, nil, nil +} + +// ChangeAdmin is used with changeAdmin to validate changeAdmin messages and to dispatch. +func ChangeAdmin(f *tokenfactorykeeper.Keeper, ctx sdk.Context, contractAddr sdk.AccAddress, changeAdmin *bindingstypes.ChangeAdmin) error { + if changeAdmin == nil { + return wasmvmtypes.InvalidRequest{Err: "changeAdmin is nil"} + } + newAdminAddr, err := parseAddress(changeAdmin.NewAdminAddress) + if err != nil { + return err + } + + changeAdminMsg := tokenfactorytypes.NewMsgChangeAdmin(contractAddr.String(), changeAdmin.Denom, newAdminAddr.String()) + if err := changeAdminMsg.ValidateBasic(); err != nil { + return err + } + + msgServer := tokenfactorykeeper.NewMsgServerImpl(*f) + _, err = msgServer.ChangeAdmin(sdk.WrapSDKContext(ctx), changeAdminMsg) + if err != nil { + return errorsmod.Wrap(err, "failed changing admin from message") + } + return nil +} + +// burnTokens burns tokens. +func (m *CustomMessenger) burnTokens(ctx sdk.Context, contractAddr sdk.AccAddress, burn *bindingstypes.BurnTokens) ([]sdk.Event, [][]byte, error) { + err := PerformBurn(m.tokenFactory, ctx, contractAddr, burn) + if err != nil { + return nil, nil, errorsmod.Wrap(err, "perform burn") + } + return nil, nil, nil +} + +// PerformBurn performs token burning after validating tokenBurn message. +func PerformBurn(f *tokenfactorykeeper.Keeper, ctx sdk.Context, contractAddr sdk.AccAddress, burn *bindingstypes.BurnTokens) error { + if burn == nil { + return wasmvmtypes.InvalidRequest{Err: "burn token null mint"} + } + + coin := sdk.Coin{Denom: burn.Denom, Amount: burn.Amount} + sdkMsg := tokenfactorytypes.NewMsgBurn(contractAddr.String(), coin) + if burn.BurnFromAddress != "" { + sdkMsg = tokenfactorytypes.NewMsgBurnFrom(contractAddr.String(), coin, burn.BurnFromAddress) + } + + if err := sdkMsg.ValidateBasic(); err != nil { + return err + } + + // Burn through token factory / message server + msgServer := tokenfactorykeeper.NewMsgServerImpl(*f) + _, err := msgServer.Burn(sdk.WrapSDKContext(ctx), sdkMsg) + if err != nil { + return errorsmod.Wrap(err, "burning coins from message") + } + return nil +} + +// forceTransfer moves tokens. +func (m *CustomMessenger) forceTransfer(ctx sdk.Context, contractAddr sdk.AccAddress, forcetransfer *bindingstypes.ForceTransfer) ([]sdk.Event, [][]byte, error) { + err := PerformForceTransfer(m.tokenFactory, ctx, contractAddr, forcetransfer) + if err != nil { + return nil, nil, errorsmod.Wrap(err, "perform force transfer") + } + return nil, nil, nil +} + +// PerformForceTransfer performs token moving after validating tokenForceTransfer message. +func PerformForceTransfer(f *tokenfactorykeeper.Keeper, ctx sdk.Context, contractAddr sdk.AccAddress, forcetransfer *bindingstypes.ForceTransfer) error { + if forcetransfer == nil { + return wasmvmtypes.InvalidRequest{Err: "force transfer null"} + } + + _, err := parseAddress(forcetransfer.FromAddress) + if err != nil { + return err + } + + _, err = parseAddress(forcetransfer.ToAddress) + if err != nil { + return err + } + + coin := sdk.Coin{Denom: forcetransfer.Denom, Amount: forcetransfer.Amount} + sdkMsg := tokenfactorytypes.NewMsgForceTransfer(contractAddr.String(), coin, forcetransfer.FromAddress, forcetransfer.ToAddress) + + if err := sdkMsg.ValidateBasic(); err != nil { + return err + } + + // Transfer through token factory / message server + msgServer := tokenfactorykeeper.NewMsgServerImpl(*f) + _, err = msgServer.ForceTransfer(sdk.WrapSDKContext(ctx), sdkMsg) + if err != nil { + return errorsmod.Wrap(err, "force transferring from message") + } + return nil +} + +// createDenom creates a new token denom +func (m *CustomMessenger) setMetadata(ctx sdk.Context, contractAddr sdk.AccAddress, setMetadata *bindingstypes.SetMetadata) ([]sdk.Event, [][]byte, error) { + err := PerformSetMetadata(m.tokenFactory, m.bank, ctx, contractAddr, setMetadata.Denom, setMetadata.Metadata) + if err != nil { + return nil, nil, errorsmod.Wrap(err, "perform create denom") + } + return nil, nil, nil +} + +// PerformSetMetadata is used with setMetadata to add new metadata +// It also is called inside CreateDenom if optional metadata field is set +func PerformSetMetadata(f *tokenfactorykeeper.Keeper, b bankkeeper.Keeper, ctx sdk.Context, contractAddr sdk.AccAddress, denom string, metadata bindingstypes.Metadata) error { + // ensure contract address is admin of denom + auth, err := f.GetAuthorityMetadata(ctx, denom) + if err != nil { + return err + } + if auth.Admin != contractAddr.String() { + return wasmvmtypes.InvalidRequest{Err: "only admin can set metadata"} + } + + // ensure we are setting proper denom metadata (bank uses Base field, fill it if missing) + if metadata.Base == "" { + metadata.Base = denom + } else if metadata.Base != denom { + // this is the key that we set + return wasmvmtypes.InvalidRequest{Err: "Base must be the same as denom"} + } + + // Create and validate the metadata + bankMetadata := WasmMetadataToSdk(metadata) + if err := bankMetadata.Validate(); err != nil { + return err + } + + b.SetDenomMetaData(ctx, bankMetadata) + return nil +} + +// GetFullDenom is a function, not method, so the message_plugin can use it +func GetFullDenom(contract string, subDenom string) (string, error) { + // Address validation + if _, err := parseAddress(contract); err != nil { + return "", err + } + fullDenom, err := tokenfactorytypes.GetTokenDenom(contract, subDenom) + if err != nil { + return "", errorsmod.Wrap(err, "validate sub-denom") + } + + return fullDenom, nil +} + +// parseAddress parses address from bech32 string and verifies its format. +func parseAddress(addr string) (sdk.AccAddress, error) { + parsed, err := sdk.AccAddressFromBech32(addr) + if err != nil { + return nil, errorsmod.Wrap(err, "address from bech32") + } + err = sdk.VerifyAddressFormat(parsed) + if err != nil { + return nil, errorsmod.Wrap(err, "verify address format") + } + return parsed, nil +} + +func WasmMetadataToSdk(metadata bindingstypes.Metadata) banktypes.Metadata { + denoms := []*banktypes.DenomUnit{} + for _, unit := range metadata.DenomUnits { + denoms = append(denoms, &banktypes.DenomUnit{ + Denom: unit.Denom, + Exponent: unit.Exponent, + Aliases: unit.Aliases, + }) + } + return banktypes.Metadata{ + Description: metadata.Description, + Display: metadata.Display, + Base: metadata.Base, + Name: metadata.Name, + Symbol: metadata.Symbol, + DenomUnits: denoms, + } +} + +func SdkMetadataToWasm(metadata banktypes.Metadata) *bindingstypes.Metadata { + denoms := []bindingstypes.DenomUnit{} + for _, unit := range metadata.DenomUnits { + denoms = append(denoms, bindingstypes.DenomUnit{ + Denom: unit.Denom, + Exponent: unit.Exponent, + Aliases: unit.Aliases, + }) + } + return &bindingstypes.Metadata{ + Description: metadata.Description, + Display: metadata.Display, + Base: metadata.Base, + Name: metadata.Name, + Symbol: metadata.Symbol, + DenomUnits: denoms, + } +} diff --git a/x/tokenfactory/bindings/queries.go b/x/tokenfactory/bindings/queries.go new file mode 100644 index 00000000..cf30c8d3 --- /dev/null +++ b/x/tokenfactory/bindings/queries.go @@ -0,0 +1,57 @@ +package bindings + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + + bindingstypes "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/bindings/types" + tokenfactorykeeper "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/keeper" +) + +type QueryPlugin struct { + bankKeeper bankkeeper.Keeper + tokenFactoryKeeper *tokenfactorykeeper.Keeper +} + +// NewQueryPlugin returns a reference to a new QueryPlugin. +func NewQueryPlugin(b bankkeeper.Keeper, tfk *tokenfactorykeeper.Keeper) *QueryPlugin { + return &QueryPlugin{ + bankKeeper: b, + tokenFactoryKeeper: tfk, + } +} + +// GetDenomAdmin is a query to get denom admin. +func (qp QueryPlugin) GetDenomAdmin(ctx sdk.Context, denom string) (*bindingstypes.AdminResponse, error) { + metadata, err := qp.tokenFactoryKeeper.GetAuthorityMetadata(ctx, denom) + if err != nil { + return nil, fmt.Errorf("failed to get admin for denom: %s", denom) + } + return &bindingstypes.AdminResponse{Admin: metadata.Admin}, nil +} + +func (qp QueryPlugin) GetDenomsByCreator(ctx sdk.Context, creator string) (*bindingstypes.DenomsByCreatorResponse, error) { + // TODO: validate creator address + denoms := qp.tokenFactoryKeeper.GetDenomsFromCreator(ctx, creator) + return &bindingstypes.DenomsByCreatorResponse{Denoms: denoms}, nil +} + +func (qp QueryPlugin) GetMetadata(ctx sdk.Context, denom string) (*bindingstypes.MetadataResponse, error) { + metadata, found := qp.bankKeeper.GetDenomMetaData(ctx, denom) + var parsed *bindingstypes.Metadata + if found { + parsed = SdkMetadataToWasm(metadata) + } + return &bindingstypes.MetadataResponse{Metadata: parsed}, nil +} + +func (qp QueryPlugin) GetParams(ctx sdk.Context) (*bindingstypes.ParamsResponse, error) { + params := qp.tokenFactoryKeeper.GetParams(ctx) + return &bindingstypes.ParamsResponse{ + Params: bindingstypes.Params{ + DenomCreationFee: ConvertSdkCoinsToWasmCoins(params.DenomCreationFee), + }, + }, nil +} diff --git a/x/tokenfactory/bindings/query_plugin.go b/x/tokenfactory/bindings/query_plugin.go new file mode 100644 index 00000000..a72fdb51 --- /dev/null +++ b/x/tokenfactory/bindings/query_plugin.go @@ -0,0 +1,117 @@ +package bindings + +import ( + "encoding/json" + "fmt" + + errorsmod "cosmossdk.io/errors" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + bindingstypes "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/bindings/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// CustomQuerier dispatches custom CosmWasm bindings queries. +func CustomQuerier(qp *QueryPlugin) func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { + return func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { + var contractQuery bindingstypes.TokenFactoryQuery + if err := json.Unmarshal(request, &contractQuery); err != nil { + return nil, errorsmod.Wrap(err, "osmosis query") + } + + switch { + case contractQuery.FullDenom != nil: + creator := contractQuery.FullDenom.CreatorAddr + subdenom := contractQuery.FullDenom.Subdenom + + fullDenom, err := GetFullDenom(creator, subdenom) + if err != nil { + return nil, errorsmod.Wrap(err, "osmo full denom query") + } + + res := bindingstypes.FullDenomResponse{ + Denom: fullDenom, + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to marshal FullDenomResponse") + } + + return bz, nil + + case contractQuery.Admin != nil: + res, err := qp.GetDenomAdmin(ctx, contractQuery.Admin.Denom) + if err != nil { + return nil, err + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("failed to JSON marshal AdminResponse: %w", err) + } + + return bz, nil + + case contractQuery.Metadata != nil: + res, err := qp.GetMetadata(ctx, contractQuery.Metadata.Denom) + if err != nil { + return nil, err + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("failed to JSON marshal MetadataResponse: %w", err) + } + + return bz, nil + + case contractQuery.DenomsByCreator != nil: + res, err := qp.GetDenomsByCreator(ctx, contractQuery.DenomsByCreator.Creator) + if err != nil { + return nil, err + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("failed to JSON marshal DenomsByCreatorResponse: %w", err) + } + + return bz, nil + + case contractQuery.Params != nil: + res, err := qp.GetParams(ctx) + if err != nil { + return nil, err + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("failed to JSON marshal ParamsResponse: %w", err) + } + + return bz, nil + + default: + return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown token query variant"} + } + } +} + +// ConvertSdkCoinsToWasmCoins converts sdk type coins to wasm vm type coins +func ConvertSdkCoinsToWasmCoins(coins []sdk.Coin) wasmvmtypes.Coins { + var toSend wasmvmtypes.Coins + for _, coin := range coins { + c := ConvertSdkCoinToWasmCoin(coin) + toSend = append(toSend, c) + } + return toSend +} + +// ConvertSdkCoinToWasmCoin converts a sdk type coin to a wasm vm type coin +func ConvertSdkCoinToWasmCoin(coin sdk.Coin) wasmvmtypes.Coin { + return wasmvmtypes.Coin{ + Denom: coin.Denom, + // Note: tokenfactory tokens have 18 decimal places, so 10^22 is common, no longer in u64 range + Amount: coin.Amount.String(), + } +} diff --git a/x/tokenfactory/bindings/testdata/README.md b/x/tokenfactory/bindings/testdata/README.md new file mode 100644 index 00000000..221c6518 --- /dev/null +++ b/x/tokenfactory/bindings/testdata/README.md @@ -0,0 +1,5 @@ +# token-reflect-contract + + + +Commit: 834bb36573fb21c74f8e78207308d9001df127a2 diff --git a/x/tokenfactory/bindings/testdata/token_reflect.wasm b/x/tokenfactory/bindings/testdata/token_reflect.wasm new file mode 100755 index 00000000..0526f174 Binary files /dev/null and b/x/tokenfactory/bindings/testdata/token_reflect.wasm differ diff --git a/x/tokenfactory/bindings/types/msg.go b/x/tokenfactory/bindings/types/msg.go new file mode 100644 index 00000000..81d9a68f --- /dev/null +++ b/x/tokenfactory/bindings/types/msg.go @@ -0,0 +1,64 @@ +package types + +import "cosmossdk.io/math" + +type TokenFactoryMsg struct { + /// Contracts can create denoms, namespaced under the contract's address. + /// A contract may create any number of independent sub-denoms. + CreateDenom *CreateDenom `json:"create_denom,omitempty"` + /// Contracts can change the admin of a denom that they are the admin of. + ChangeAdmin *ChangeAdmin `json:"change_admin,omitempty"` + /// Contracts can mint native tokens for an existing factory denom + /// that they are the admin of. + MintTokens *MintTokens `json:"mint_tokens,omitempty"` + /// Contracts can burn native tokens for an existing factory denom + /// that they are the admin of. + /// Currently, the burn from address must be the admin contract. + BurnTokens *BurnTokens `json:"burn_tokens,omitempty"` + /// Sets the metadata on a denom which the contract controls + SetMetadata *SetMetadata `json:"set_metadata,omitempty"` + /// Forces a transfer of tokens from one address to another. + ForceTransfer *ForceTransfer `json:"force_transfer,omitempty"` +} + +// CreateDenom creates a new factory denom, of denomination: +// factory/{creating contract address}/{Subdenom} +// Subdenom can be of length at most 44 characters, in [0-9a-zA-Z./] +// The (creating contract address, subdenom) pair must be unique. +// The created denom's admin is the creating contract address, +// but this admin can be changed using the ChangeAdmin binding. +type CreateDenom struct { + Subdenom string `json:"subdenom"` + Metadata *Metadata `json:"metadata,omitempty"` +} + +// ChangeAdmin changes the admin for a factory denom. +// If the NewAdminAddress is empty, the denom has no admin. +type ChangeAdmin struct { + Denom string `json:"denom"` + NewAdminAddress string `json:"new_admin_address"` +} + +type MintTokens struct { + Denom string `json:"denom"` + Amount math.Int `json:"amount"` + MintToAddress string `json:"mint_to_address"` +} + +type BurnTokens struct { + Denom string `json:"denom"` + Amount math.Int `json:"amount"` + BurnFromAddress string `json:"burn_from_address"` +} + +type SetMetadata struct { + Denom string `json:"denom"` + Metadata Metadata `json:"metadata"` +} + +type ForceTransfer struct { + Denom string `json:"denom"` + Amount math.Int `json:"amount"` + FromAddress string `json:"from_address"` + ToAddress string `json:"to_address"` +} diff --git a/x/tokenfactory/bindings/types/query.go b/x/tokenfactory/bindings/types/query.go new file mode 100644 index 00000000..60f0ac3e --- /dev/null +++ b/x/tokenfactory/bindings/types/query.go @@ -0,0 +1,55 @@ +package types + +// See https://github.com/CosmWasm/token-bindings/blob/main/packages/bindings/src/query.rs +type TokenFactoryQuery struct { + /// Given a subdenom minted by a contract via `OsmosisMsg::MintTokens`, + /// returns the full denom as used by `BankMsg::Send`. + FullDenom *FullDenom `json:"full_denom,omitempty"` + Admin *DenomAdmin `json:"admin,omitempty"` + Metadata *GetMetadata `json:"metadata,omitempty"` + DenomsByCreator *DenomsByCreator `json:"denoms_by_creator,omitempty"` + Params *GetParams `json:"params,omitempty"` +} + +// query types + +type FullDenom struct { + CreatorAddr string `json:"creator_addr"` + Subdenom string `json:"subdenom"` +} + +type GetMetadata struct { + Denom string `json:"denom"` +} + +type DenomAdmin struct { + Denom string `json:"denom"` +} + +type DenomsByCreator struct { + Creator string `json:"creator"` +} + +type GetParams struct{} + +// responses + +type FullDenomResponse struct { + Denom string `json:"denom"` +} + +type AdminResponse struct { + Admin string `json:"admin"` +} + +type MetadataResponse struct { + Metadata *Metadata `json:"metadata,omitempty"` +} + +type DenomsByCreatorResponse struct { + Denoms []string `json:"denoms"` +} + +type ParamsResponse struct { + Params Params `json:"params"` +} diff --git a/x/tokenfactory/bindings/types/types.go b/x/tokenfactory/bindings/types/types.go new file mode 100644 index 00000000..2c75feeb --- /dev/null +++ b/x/tokenfactory/bindings/types/types.go @@ -0,0 +1,37 @@ +package types + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/types" +) + +type Metadata struct { + Description string `json:"description"` + // DenomUnits represents the list of DenomUnit's for a given coin + DenomUnits []DenomUnit `json:"denom_units"` + // Base represents the base denom (should be the DenomUnit with exponent = 0). + Base string `json:"base"` + // Display indicates the suggested denom that should be displayed in clients. + Display string `json:"display"` + // Name defines the name of the token (eg: Cosmos Atom) + Name string `json:"name"` + // Symbol is the token symbol usually shown on exchanges (eg: ATOM). + // This can be the same as the display. + Symbol string `json:"symbol"` +} + +type DenomUnit struct { + // Denom represents the string name of the given denom unit (e.g uatom). + Denom string `json:"denom"` + // Exponent represents power of 10 exponent that one must + // raise the base_denom to in order to equal the given DenomUnit's denom + // 1 denom = 1^exponent base_denom + // (e.g. with a base_denom of uatom, one can create a DenomUnit of 'atom' with + // exponent = 6, thus: 1 atom = 10^6 uatom). + Exponent uint32 `json:"exponent"` + // Aliases is a list of string aliases for the given denom + Aliases []string `json:"aliases"` +} + +type Params struct { + DenomCreationFee []wasmvmtypes.Coin `json:"denom_creation_fee"` +} diff --git a/x/tokenfactory/bindings/validate_msg_test.go b/x/tokenfactory/bindings/validate_msg_test.go new file mode 100644 index 00000000..ef5f5186 --- /dev/null +++ b/x/tokenfactory/bindings/validate_msg_test.go @@ -0,0 +1,415 @@ +package bindings_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + wasmbinding "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/bindings" + bindings "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/bindings/types" + "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/types" +) + +func TestCreateDenom(t *testing.T) { + actor := RandomAccountAddress() + app, ctx := SetupCustomApp(t, actor) + + // Fund actor with 100 base denom creation fees + actorAmount := sdk.NewCoins(sdk.NewCoin(types.DefaultParams().DenomCreationFee[0].Denom, types.DefaultParams().DenomCreationFee[0].Amount.MulRaw(100))) + fundAccount(t, ctx, app, actor, actorAmount) + + specs := map[string]struct { + createDenom *bindings.CreateDenom + expErr bool + }{ + "valid sub-denom": { + createDenom: &bindings.CreateDenom{ + Subdenom: "MOON", + }, + }, + "empty sub-denom": { + createDenom: &bindings.CreateDenom{ + Subdenom: "", + }, + expErr: false, + }, + "invalid sub-denom": { + createDenom: &bindings.CreateDenom{ + Subdenom: "sub-denom_2", + }, + expErr: false, + }, + "null create denom": { + createDenom: nil, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // when + _, gotErr := wasmbinding.PerformCreateDenom(&app.AppKeepers.TokenFactoryKeeper, app.AppKeepers.BankKeeper, ctx, actor, spec.createDenom) + // then + if spec.expErr { + t.Logf("validate_msg_test got error: %v", gotErr) + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestChangeAdmin(t *testing.T) { + const validDenom = "validdenom" + + tokenCreator := RandomAccountAddress() + + specs := map[string]struct { + actor sdk.AccAddress + changeAdmin *bindings.ChangeAdmin + + expErrMsg string + }{ + "valid": { + changeAdmin: &bindings.ChangeAdmin{ + Denom: fmt.Sprintf("factory/%s/%s", tokenCreator.String(), validDenom), + NewAdminAddress: RandomBech32AccountAddress(), + }, + actor: tokenCreator, + }, + "typo in factory in denom name": { + changeAdmin: &bindings.ChangeAdmin{ + Denom: fmt.Sprintf("facory/%s/%s", tokenCreator.String(), validDenom), + NewAdminAddress: RandomBech32AccountAddress(), + }, + actor: tokenCreator, + expErrMsg: "denom prefix is incorrect. Is: facory. Should be: factory: invalid denom", + }, + "invalid address in denom": { + changeAdmin: &bindings.ChangeAdmin{ + Denom: fmt.Sprintf("factory/%s/%s", RandomBech32AccountAddress(), validDenom), + NewAdminAddress: RandomBech32AccountAddress(), + }, + actor: tokenCreator, + expErrMsg: "failed changing admin from message: unauthorized account", + }, + "other denom name in 3 part name": { + changeAdmin: &bindings.ChangeAdmin{ + Denom: fmt.Sprintf("factory/%s/%s", tokenCreator.String(), "invalid denom"), + NewAdminAddress: RandomBech32AccountAddress(), + }, + actor: tokenCreator, + expErrMsg: fmt.Sprintf("invalid denom: factory/%s/invalid denom", tokenCreator.String()), + }, + "empty denom": { + changeAdmin: &bindings.ChangeAdmin{ + Denom: "", + NewAdminAddress: RandomBech32AccountAddress(), + }, + actor: tokenCreator, + expErrMsg: "invalid denom: ", + }, + "empty address": { + changeAdmin: &bindings.ChangeAdmin{ + Denom: fmt.Sprintf("factory/%s/%s", tokenCreator.String(), validDenom), + NewAdminAddress: "", + }, + actor: tokenCreator, + expErrMsg: "address from bech32: empty address string is not allowed", + }, + "creator is a different address": { + changeAdmin: &bindings.ChangeAdmin{ + Denom: fmt.Sprintf("factory/%s/%s", tokenCreator.String(), validDenom), + NewAdminAddress: RandomBech32AccountAddress(), + }, + actor: RandomAccountAddress(), + expErrMsg: "failed changing admin from message: unauthorized account", + }, + "change to the same address": { + changeAdmin: &bindings.ChangeAdmin{ + Denom: fmt.Sprintf("factory/%s/%s", tokenCreator.String(), validDenom), + NewAdminAddress: tokenCreator.String(), + }, + actor: tokenCreator, + }, + "nil binding": { + actor: tokenCreator, + expErrMsg: "invalid request: changeAdmin is nil - original request: ", + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // Setup + app, ctx := SetupCustomApp(t, tokenCreator) + + // Fund actor with 100 base denom creation fees + actorAmount := sdk.NewCoins(sdk.NewCoin(types.DefaultParams().DenomCreationFee[0].Denom, types.DefaultParams().DenomCreationFee[0].Amount.MulRaw(100))) + fundAccount(t, ctx, app, tokenCreator, actorAmount) + + _, err := wasmbinding.PerformCreateDenom(&app.AppKeepers.TokenFactoryKeeper, app.AppKeepers.BankKeeper, ctx, tokenCreator, &bindings.CreateDenom{ + Subdenom: validDenom, + }) + require.NoError(t, err) + + err = wasmbinding.ChangeAdmin(&app.AppKeepers.TokenFactoryKeeper, ctx, spec.actor, spec.changeAdmin) + if len(spec.expErrMsg) > 0 { + require.Error(t, err) + actualErrMsg := err.Error() + require.Equal(t, spec.expErrMsg, actualErrMsg) + return + } + require.NoError(t, err) + }) + } +} + +func TestMint(t *testing.T) { + creator := RandomAccountAddress() + app, ctx := SetupCustomApp(t, creator) + + // Fund actor with 100 base denom creation fees + tokenCreationFeeAmt := sdk.NewCoins(sdk.NewCoin(types.DefaultParams().DenomCreationFee[0].Denom, types.DefaultParams().DenomCreationFee[0].Amount.MulRaw(100))) + fundAccount(t, ctx, app, creator, tokenCreationFeeAmt) + + // Create denoms for valid mint tests + validDenom := bindings.CreateDenom{ + Subdenom: "MOON", + } + _, err := wasmbinding.PerformCreateDenom(&app.AppKeepers.TokenFactoryKeeper, app.AppKeepers.BankKeeper, ctx, creator, &validDenom) + require.NoError(t, err) + + emptyDenom := bindings.CreateDenom{ + Subdenom: "", + } + _, err = wasmbinding.PerformCreateDenom(&app.AppKeepers.TokenFactoryKeeper, app.AppKeepers.BankKeeper, ctx, creator, &emptyDenom) + require.NoError(t, err) + + validDenomStr := fmt.Sprintf("factory/%s/%s", creator.String(), validDenom.Subdenom) + emptyDenomStr := fmt.Sprintf("factory/%s/%s", creator.String(), emptyDenom.Subdenom) + + lucky := RandomAccountAddress() + + // lucky was broke + balances := app.AppKeepers.BankKeeper.GetAllBalances(ctx, lucky) + require.Empty(t, balances) + + amount, ok := sdk.NewIntFromString("8080") + require.True(t, ok) + + specs := map[string]struct { + mint *bindings.MintTokens + expErr bool + }{ + "valid mint": { + mint: &bindings.MintTokens{ + Denom: validDenomStr, + Amount: amount, + MintToAddress: lucky.String(), + }, + }, + "empty sub-denom": { + mint: &bindings.MintTokens{ + Denom: emptyDenomStr, + Amount: amount, + MintToAddress: lucky.String(), + }, + expErr: false, + }, + "nonexistent sub-denom": { + mint: &bindings.MintTokens{ + Denom: fmt.Sprintf("factory/%s/%s", creator.String(), "SUN"), + Amount: amount, + MintToAddress: lucky.String(), + }, + expErr: true, + }, + "invalid sub-denom": { + mint: &bindings.MintTokens{ + Denom: "sub-denom_2", + Amount: amount, + MintToAddress: lucky.String(), + }, + expErr: true, + }, + "zero amount": { + mint: &bindings.MintTokens{ + Denom: validDenomStr, + Amount: sdk.ZeroInt(), + MintToAddress: lucky.String(), + }, + expErr: true, + }, + "negative amount": { + mint: &bindings.MintTokens{ + Denom: validDenomStr, + Amount: amount.Neg(), + MintToAddress: lucky.String(), + }, + expErr: true, + }, + "empty recipient": { + mint: &bindings.MintTokens{ + Denom: validDenomStr, + Amount: amount, + MintToAddress: "", + }, + expErr: true, + }, + "invalid recipient": { + mint: &bindings.MintTokens{ + Denom: validDenomStr, + Amount: amount, + MintToAddress: "invalid", + }, + expErr: true, + }, + "null mint": { + mint: nil, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // when + gotErr := wasmbinding.PerformMint(&app.AppKeepers.TokenFactoryKeeper, app.AppKeepers.BankKeeper, ctx, creator, spec.mint) + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestBurn(t *testing.T) { + creator := RandomAccountAddress() + app, ctx := SetupCustomApp(t, creator) + + // Fund actor with 100 base denom creation fees + tokenCreationFeeAmt := sdk.NewCoins(sdk.NewCoin(types.DefaultParams().DenomCreationFee[0].Denom, types.DefaultParams().DenomCreationFee[0].Amount.MulRaw(100))) + fundAccount(t, ctx, app, creator, tokenCreationFeeAmt) + + // Create denoms for valid burn tests + validDenom := bindings.CreateDenom{ + Subdenom: "MOON", + } + _, err := wasmbinding.PerformCreateDenom(&app.AppKeepers.TokenFactoryKeeper, app.AppKeepers.BankKeeper, ctx, creator, &validDenom) + require.NoError(t, err) + + emptyDenom := bindings.CreateDenom{ + Subdenom: "", + } + _, err = wasmbinding.PerformCreateDenom(&app.AppKeepers.TokenFactoryKeeper, app.AppKeepers.BankKeeper, ctx, creator, &emptyDenom) + require.NoError(t, err) + + lucky := RandomAccountAddress() + + // lucky was broke + balances := app.AppKeepers.BankKeeper.GetAllBalances(ctx, lucky) + require.Empty(t, balances) + + validDenomStr := fmt.Sprintf("factory/%s/%s", creator.String(), validDenom.Subdenom) + emptyDenomStr := fmt.Sprintf("factory/%s/%s", creator.String(), emptyDenom.Subdenom) + mintAmount, ok := sdk.NewIntFromString("8080") + require.True(t, ok) + + specs := map[string]struct { + burn *bindings.BurnTokens + expErr bool + }{ + "valid burn": { + burn: &bindings.BurnTokens{ + Denom: validDenomStr, + Amount: mintAmount, + BurnFromAddress: creator.String(), + }, + expErr: false, + }, + "non admin address": { + burn: &bindings.BurnTokens{ + Denom: validDenomStr, + Amount: mintAmount, + BurnFromAddress: lucky.String(), + }, + expErr: true, + }, + "empty sub-denom": { + burn: &bindings.BurnTokens{ + Denom: emptyDenomStr, + Amount: mintAmount, + BurnFromAddress: creator.String(), + }, + expErr: false, + }, + "invalid sub-denom": { + burn: &bindings.BurnTokens{ + Denom: "sub-denom_2", + Amount: mintAmount, + BurnFromAddress: creator.String(), + }, + expErr: true, + }, + "non-minted denom": { + burn: &bindings.BurnTokens{ + Denom: fmt.Sprintf("factory/%s/%s", creator.String(), "SUN"), + Amount: mintAmount, + BurnFromAddress: creator.String(), + }, + expErr: true, + }, + "zero amount": { + burn: &bindings.BurnTokens{ + Denom: validDenomStr, + Amount: sdk.ZeroInt(), + BurnFromAddress: creator.String(), + }, + expErr: true, + }, + "negative amount": { + burn: nil, + expErr: true, + }, + "null burn": { + burn: &bindings.BurnTokens{ + Denom: validDenomStr, + Amount: mintAmount.Neg(), + BurnFromAddress: creator.String(), + }, + expErr: true, + }, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // Mint valid denom str and empty denom string for burn test + mintBinding := &bindings.MintTokens{ + Denom: validDenomStr, + Amount: mintAmount, + MintToAddress: creator.String(), + } + err := wasmbinding.PerformMint(&app.AppKeepers.TokenFactoryKeeper, app.AppKeepers.BankKeeper, ctx, creator, mintBinding) + require.NoError(t, err) + + emptyDenomMintBinding := &bindings.MintTokens{ + Denom: emptyDenomStr, + Amount: mintAmount, + MintToAddress: creator.String(), + } + err = wasmbinding.PerformMint(&app.AppKeepers.TokenFactoryKeeper, app.AppKeepers.BankKeeper, ctx, creator, emptyDenomMintBinding) + require.NoError(t, err) + + // when + gotErr := wasmbinding.PerformBurn(&app.AppKeepers.TokenFactoryKeeper, ctx, creator, spec.burn) + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} diff --git a/x/tokenfactory/bindings/validate_queries_test.go b/x/tokenfactory/bindings/validate_queries_test.go new file mode 100644 index 00000000..5d5e3aaf --- /dev/null +++ b/x/tokenfactory/bindings/validate_queries_test.go @@ -0,0 +1,115 @@ +package bindings_test + +import ( + "fmt" + "testing" + + wasmbinding "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/bindings" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFullDenom(t *testing.T) { + actor := RandomAccountAddress() + + specs := map[string]struct { + addr string + subdenom string + expFullDenom string + expErr bool + }{ + "valid address": { + addr: actor.String(), + subdenom: "subDenom1", + expFullDenom: fmt.Sprintf("factory/%s/subDenom1", actor.String()), + }, + "empty address": { + addr: "", + subdenom: "subDenom1", + expErr: true, + }, + "invalid address": { + addr: "invalid", + subdenom: "subDenom1", + expErr: true, + }, + "empty sub-denom": { + addr: actor.String(), + subdenom: "", + expFullDenom: fmt.Sprintf("factory/%s/", actor.String()), + }, + "valid sub-denom (contains underscore)": { + addr: actor.String(), + subdenom: "sub_denom", + expFullDenom: fmt.Sprintf("factory/%s/sub_denom", actor.String()), + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // when + gotFullDenom, gotErr := wasmbinding.GetFullDenom(spec.addr, spec.subdenom) + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expFullDenom, gotFullDenom, "exp %s but got %s", spec.expFullDenom, gotFullDenom) + }) + } +} + +func TestDenomAdmin(t *testing.T) { + addr := RandomAccountAddress() + app, ctx := SetupCustomApp(t, addr) + + // set token creation fee to zero to make testing easier + tfParams := app.AppKeepers.TokenFactoryKeeper.GetParams(ctx) + tfParams.DenomCreationFee = sdk.NewCoins() + if err := app.AppKeepers.TokenFactoryKeeper.SetParams(ctx, tfParams); err != nil { + t.Fatal(err) + } + + // create a subdenom via the token factory + admin := sdk.AccAddress([]byte("addr1_______________")) + tfDenom, err := app.AppKeepers.TokenFactoryKeeper.CreateDenom(ctx, admin.String(), "subdenom") + require.NoError(t, err) + require.NotEmpty(t, tfDenom) + + queryPlugin := wasmbinding.NewQueryPlugin(app.AppKeepers.BankKeeper, &app.AppKeepers.TokenFactoryKeeper) + + testCases := []struct { + name string + denom string + expectErr bool + expectAdmin string + }{ + { + name: "valid token factory denom", + denom: tfDenom, + expectAdmin: admin.String(), + }, + { + name: "invalid token factory denom", + denom: "uosmo", + expectErr: false, + expectAdmin: "", + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + resp, err := queryPlugin.GetDenomAdmin(ctx, tc.denom) + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, tc.expectAdmin, resp.Admin) + } + }) + } +} diff --git a/x/tokenfactory/bindings/wasm.go b/x/tokenfactory/bindings/wasm.go new file mode 100644 index 00000000..fbfbc28f --- /dev/null +++ b/x/tokenfactory/bindings/wasm.go @@ -0,0 +1,26 @@ +package bindings + +import ( + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + tokenfactorykeeper "github.com/OmniFlix/omniflixhub/v2/x/tokenfactory/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" +) + +func RegisterCustomPlugins( + bank bankkeeper.Keeper, + tokenFactory *tokenfactorykeeper.Keeper, +) []wasmkeeper.Option { + wasmQueryPlugin := NewQueryPlugin(bank, tokenFactory) + + queryPluginOpt := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{ + Custom: CustomQuerier(wasmQueryPlugin), + }) + messengerDecoratorOpt := wasmkeeper.WithMessageHandlerDecorator( + CustomMessageDecorator(bank, tokenFactory), + ) + + return []wasmkeeper.Option{ + queryPluginOpt, + messengerDecoratorOpt, + } +}