feat: Port x/wasm module #92
109
proto/wasm/v1/authz.proto
Normal file
@ -0,0 +1,109 @@
|
||||
syntax = "proto3";
|
||||
package cosmwasm.wasm.v1;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
import "cosmos_proto/cosmos.proto";
|
||||
import "cosmos/base/v1beta1/coin.proto";
|
||||
import "google/protobuf/any.proto";
|
||||
|
||||
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
|
||||
option (gogoproto.goproto_getters_all) = false;
|
||||
|
||||
// ContractExecutionAuthorization defines authorization for wasm execute.
|
||||
// Since: wasmd 0.30
|
||||
message ContractExecutionAuthorization {
|
||||
option (cosmos_proto.implements_interface) = "Authorization";
|
||||
|
||||
// Grants for contract executions
|
||||
repeated ContractGrant grants = 1 [ (gogoproto.nullable) = false ];
|
||||
}
|
||||
|
||||
// ContractMigrationAuthorization defines authorization for wasm contract
|
||||
// migration. Since: wasmd 0.30
|
||||
message ContractMigrationAuthorization {
|
||||
option (cosmos_proto.implements_interface) = "Authorization";
|
||||
|
||||
// Grants for contract migrations
|
||||
repeated ContractGrant grants = 1 [ (gogoproto.nullable) = false ];
|
||||
}
|
||||
|
||||
// ContractGrant a granted permission for a single contract
|
||||
// Since: wasmd 0.30
|
||||
message ContractGrant {
|
||||
// Contract is the bech32 address of the smart contract
|
||||
string contract = 1;
|
||||
|
||||
// Limit defines execution limits that are enforced and updated when the grant
|
||||
// is applied. When the limit lapsed the grant is removed.
|
||||
google.protobuf.Any limit = 2
|
||||
[ (cosmos_proto.accepts_interface) = "ContractAuthzLimitX" ];
|
||||
|
||||
// Filter define more fine-grained control on the message payload passed
|
||||
// to the contract in the operation. When no filter applies on execution, the
|
||||
// operation is prohibited.
|
||||
google.protobuf.Any filter = 3
|
||||
[ (cosmos_proto.accepts_interface) = "ContractAuthzFilterX" ];
|
||||
}
|
||||
|
||||
// MaxCallsLimit limited number of calls to the contract. No funds transferable.
|
||||
// Since: wasmd 0.30
|
||||
message MaxCallsLimit {
|
||||
option (cosmos_proto.implements_interface) = "ContractAuthzLimitX";
|
||||
|
||||
// Remaining number that is decremented on each execution
|
||||
uint64 remaining = 1;
|
||||
}
|
||||
|
||||
// MaxFundsLimit defines the maximal amounts that can be sent to the contract.
|
||||
// Since: wasmd 0.30
|
||||
message MaxFundsLimit {
|
||||
option (cosmos_proto.implements_interface) = "ContractAuthzLimitX";
|
||||
|
||||
// Amounts is the maximal amount of tokens transferable to the contract.
|
||||
repeated cosmos.base.v1beta1.Coin amounts = 1 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
|
||||
];
|
||||
}
|
||||
|
||||
// CombinedLimit defines the maximal amounts that can be sent to a contract and
|
||||
// the maximal number of calls executable. Both need to remain >0 to be valid.
|
||||
// Since: wasmd 0.30
|
||||
message CombinedLimit {
|
||||
option (cosmos_proto.implements_interface) = "ContractAuthzLimitX";
|
||||
|
||||
// Remaining number that is decremented on each execution
|
||||
uint64 calls_remaining = 1;
|
||||
// Amounts is the maximal amount of tokens transferable to the contract.
|
||||
repeated cosmos.base.v1beta1.Coin amounts = 2 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
|
||||
];
|
||||
}
|
||||
|
||||
// AllowAllMessagesFilter is a wildcard to allow any type of contract payload
|
||||
// message.
|
||||
// Since: wasmd 0.30
|
||||
message AllowAllMessagesFilter {
|
||||
option (cosmos_proto.implements_interface) = "ContractAuthzFilterX";
|
||||
}
|
||||
|
||||
// AcceptedMessageKeysFilter accept only the specific contract message keys in
|
||||
// the json object to be executed.
|
||||
// Since: wasmd 0.30
|
||||
message AcceptedMessageKeysFilter {
|
||||
option (cosmos_proto.implements_interface) = "ContractAuthzFilterX";
|
||||
|
||||
// Messages is the list of unique keys
|
||||
repeated string keys = 1;
|
||||
}
|
||||
|
||||
// AcceptedMessagesFilter accept only the specific raw contract messages to be
|
||||
// executed.
|
||||
// Since: wasmd 0.30
|
||||
message AcceptedMessagesFilter {
|
||||
option (cosmos_proto.implements_interface) = "ContractAuthzFilterX";
|
||||
|
||||
// Messages is the list of raw contract messages
|
||||
repeated bytes messages = 1 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
}
|
46
proto/wasm/v1/genesis.proto
Normal file
@ -0,0 +1,46 @@
|
||||
syntax = "proto3";
|
||||
package cosmwasm.wasm.v1;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
import "wasm/v1/types.proto";
|
||||
|
||||
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
|
||||
|
||||
// GenesisState - genesis state of x/wasm
|
||||
message GenesisState {
|
||||
Params params = 1 [ (gogoproto.nullable) = false ];
|
||||
repeated Code codes = 2
|
||||
[ (gogoproto.nullable) = false, (gogoproto.jsontag) = "codes,omitempty" ];
|
||||
repeated Contract contracts = 3 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.jsontag) = "contracts,omitempty"
|
||||
];
|
||||
repeated Sequence sequences = 4 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.jsontag) = "sequences,omitempty"
|
||||
];
|
||||
}
|
||||
|
||||
// Code struct encompasses CodeInfo and CodeBytes
|
||||
message Code {
|
||||
uint64 code_id = 1 [ (gogoproto.customname) = "CodeID" ];
|
||||
CodeInfo code_info = 2 [ (gogoproto.nullable) = false ];
|
||||
bytes code_bytes = 3;
|
||||
// Pinned to wasmvm cache
|
||||
bool pinned = 4;
|
||||
}
|
||||
|
||||
// Contract struct encompasses ContractAddress, ContractInfo, and ContractState
|
||||
message Contract {
|
||||
string contract_address = 1;
|
||||
ContractInfo contract_info = 2 [ (gogoproto.nullable) = false ];
|
||||
repeated Model contract_state = 3 [ (gogoproto.nullable) = false ];
|
||||
repeated ContractCodeHistoryEntry contract_code_history = 4
|
||||
[ (gogoproto.nullable) = false ];
|
||||
}
|
||||
|
||||
// Sequence key and value of an id generation counter
|
||||
message Sequence {
|
||||
bytes id_key = 1 [ (gogoproto.customname) = "IDKey" ];
|
||||
uint64 value = 2;
|
||||
}
|
31
proto/wasm/v1/ibc.proto
Normal file
@ -0,0 +1,31 @@
|
||||
syntax = "proto3";
|
||||
package cosmwasm.wasm.v1;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
|
||||
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
|
||||
option (gogoproto.goproto_getters_all) = false;
|
||||
|
||||
// MsgIBCSend
|
||||
message MsgIBCSend {
|
||||
// the channel by which the packet will be sent
|
||||
string channel = 2 [ (gogoproto.moretags) = "yaml:\"source_channel\"" ];
|
||||
|
||||
// Timeout height relative to the current block height.
|
||||
// The timeout is disabled when set to 0.
|
||||
uint64 timeout_height = 4
|
||||
[ (gogoproto.moretags) = "yaml:\"timeout_height\"" ];
|
||||
// Timeout timestamp (in nanoseconds) relative to the current block timestamp.
|
||||
// The timeout is disabled when set to 0.
|
||||
uint64 timeout_timestamp = 5
|
||||
[ (gogoproto.moretags) = "yaml:\"timeout_timestamp\"" ];
|
||||
|
||||
// Data is the payload to transfer. We must not make assumption what format or
|
||||
// content is in here.
|
||||
bytes data = 6;
|
||||
}
|
||||
|
||||
// MsgIBCCloseChannel port and channel need to be owned by the contract
|
||||
message MsgIBCCloseChannel {
|
||||
string channel = 2 [ (gogoproto.moretags) = "yaml:\"source_channel\"" ];
|
||||
}
|
272
proto/wasm/v1/proposal.proto
Normal file
@ -0,0 +1,272 @@
|
||||
syntax = "proto3";
|
||||
package cosmwasm.wasm.v1;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
import "cosmos_proto/cosmos.proto";
|
||||
import "cosmos/base/v1beta1/coin.proto";
|
||||
import "wasm/v1/types.proto";
|
||||
|
||||
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
|
||||
option (gogoproto.goproto_stringer_all) = false;
|
||||
option (gogoproto.goproto_getters_all) = false;
|
||||
option (gogoproto.equal_all) = true;
|
||||
|
||||
// StoreCodeProposal gov proposal content type to submit WASM code to the system
|
||||
message StoreCodeProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1;
|
||||
// Description is a human readable text
|
||||
string description = 2;
|
||||
// RunAs is the address that is passed to the contract's environment as sender
|
||||
string run_as = 3;
|
||||
// WASMByteCode can be raw or gzip compressed
|
||||
bytes wasm_byte_code = 4 [ (gogoproto.customname) = "WASMByteCode" ];
|
||||
// Used in v1beta1
|
||||
reserved 5, 6;
|
||||
// InstantiatePermission to apply on contract creation, optional
|
||||
AccessConfig instantiate_permission = 7;
|
||||
// UnpinCode code on upload, optional
|
||||
bool unpin_code = 8;
|
||||
// Source is the URL where the code is hosted
|
||||
string source = 9;
|
||||
// Builder is the docker image used to build the code deterministically, used
|
||||
// for smart contract verification
|
||||
string builder = 10;
|
||||
// CodeHash is the SHA256 sum of the code outputted by builder, used for smart
|
||||
// contract verification
|
||||
bytes code_hash = 11;
|
||||
}
|
||||
|
||||
// InstantiateContractProposal gov proposal content type to instantiate a
|
||||
// contract.
|
||||
message InstantiateContractProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1;
|
||||
// Description is a human readable text
|
||||
string description = 2;
|
||||
// RunAs is the address that is passed to the contract's environment as sender
|
||||
string run_as = 3;
|
||||
// Admin is an optional address that can execute migrations
|
||||
string admin = 4;
|
||||
// CodeID is the reference to the stored WASM code
|
||||
uint64 code_id = 5 [ (gogoproto.customname) = "CodeID" ];
|
||||
// Label is optional metadata to be stored with a constract instance.
|
||||
string label = 6;
|
||||
// Msg json encoded message to be passed to the contract on instantiation
|
||||
bytes msg = 7 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
// Funds coins that are transferred to the contract on instantiation
|
||||
repeated cosmos.base.v1beta1.Coin funds = 8 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
|
||||
];
|
||||
}
|
||||
|
||||
// InstantiateContract2Proposal gov proposal content type to instantiate
|
||||
// contract 2
|
||||
message InstantiateContract2Proposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1;
|
||||
// Description is a human readable text
|
||||
string description = 2;
|
||||
// RunAs is the address that is passed to the contract's enviroment as sender
|
||||
string run_as = 3;
|
||||
// Admin is an optional address that can execute migrations
|
||||
string admin = 4;
|
||||
// CodeID is the reference to the stored WASM code
|
||||
uint64 code_id = 5 [ (gogoproto.customname) = "CodeID" ];
|
||||
// Label is optional metadata to be stored with a constract instance.
|
||||
string label = 6;
|
||||
// Msg json encode message to be passed to the contract on instantiation
|
||||
bytes msg = 7 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
// Funds coins that are transferred to the contract on instantiation
|
||||
repeated cosmos.base.v1beta1.Coin funds = 8 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
|
||||
];
|
||||
// Salt is an arbitrary value provided by the sender. Size can be 1 to 64.
|
||||
bytes salt = 9;
|
||||
// FixMsg include the msg value into the hash for the predictable address.
|
||||
// Default is false
|
||||
bool fix_msg = 10;
|
||||
}
|
||||
|
||||
// MigrateContractProposal gov proposal content type to migrate a contract.
|
||||
message MigrateContractProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1;
|
||||
// Description is a human readable text
|
||||
string description = 2;
|
||||
// Note: skipping 3 as this was previously used for unneeded run_as
|
||||
|
||||
// Contract is the address of the smart contract
|
||||
string contract = 4;
|
||||
// CodeID references the new WASM code
|
||||
uint64 code_id = 5 [ (gogoproto.customname) = "CodeID" ];
|
||||
// Msg json encoded message to be passed to the contract on migration
|
||||
bytes msg = 6 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
}
|
||||
|
||||
// SudoContractProposal gov proposal content type to call sudo on a contract.
|
||||
message SudoContractProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1;
|
||||
// Description is a human readable text
|
||||
string description = 2;
|
||||
// Contract is the address of the smart contract
|
||||
string contract = 3;
|
||||
// Msg json encoded message to be passed to the contract as sudo
|
||||
bytes msg = 4 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
}
|
||||
|
||||
// ExecuteContractProposal gov proposal content type to call execute on a
|
||||
// contract.
|
||||
message ExecuteContractProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1;
|
||||
// Description is a human readable text
|
||||
string description = 2;
|
||||
// RunAs is the address that is passed to the contract's environment as sender
|
||||
string run_as = 3;
|
||||
// Contract is the address of the smart contract
|
||||
string contract = 4;
|
||||
// Msg json encoded message to be passed to the contract as execute
|
||||
bytes msg = 5 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
// Funds coins that are transferred to the contract on instantiation
|
||||
repeated cosmos.base.v1beta1.Coin funds = 6 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
|
||||
];
|
||||
}
|
||||
|
||||
// UpdateAdminProposal gov proposal content type to set an admin for a contract.
|
||||
message UpdateAdminProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1;
|
||||
// Description is a human readable text
|
||||
string description = 2;
|
||||
// NewAdmin address to be set
|
||||
string new_admin = 3 [ (gogoproto.moretags) = "yaml:\"new_admin\"" ];
|
||||
// Contract is the address of the smart contract
|
||||
string contract = 4;
|
||||
}
|
||||
|
||||
// ClearAdminProposal gov proposal content type to clear the admin of a
|
||||
// contract.
|
||||
message ClearAdminProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1;
|
||||
// Description is a human readable text
|
||||
string description = 2;
|
||||
// Contract is the address of the smart contract
|
||||
string contract = 3;
|
||||
}
|
||||
|
||||
// PinCodesProposal gov proposal content type to pin a set of code ids in the
|
||||
// wasmvm cache.
|
||||
message PinCodesProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1 [ (gogoproto.moretags) = "yaml:\"title\"" ];
|
||||
// Description is a human readable text
|
||||
string description = 2 [ (gogoproto.moretags) = "yaml:\"description\"" ];
|
||||
// CodeIDs references the new WASM codes
|
||||
repeated uint64 code_ids = 3 [
|
||||
(gogoproto.customname) = "CodeIDs",
|
||||
(gogoproto.moretags) = "yaml:\"code_ids\""
|
||||
];
|
||||
}
|
||||
|
||||
// UnpinCodesProposal gov proposal content type to unpin a set of code ids in
|
||||
// the wasmvm cache.
|
||||
message UnpinCodesProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1 [ (gogoproto.moretags) = "yaml:\"title\"" ];
|
||||
// Description is a human readable text
|
||||
string description = 2 [ (gogoproto.moretags) = "yaml:\"description\"" ];
|
||||
// CodeIDs references the WASM codes
|
||||
repeated uint64 code_ids = 3 [
|
||||
(gogoproto.customname) = "CodeIDs",
|
||||
(gogoproto.moretags) = "yaml:\"code_ids\""
|
||||
];
|
||||
}
|
||||
|
||||
// AccessConfigUpdate contains the code id and the access config to be
|
||||
// applied.
|
||||
message AccessConfigUpdate {
|
||||
// CodeID is the reference to the stored WASM code to be updated
|
||||
uint64 code_id = 1 [ (gogoproto.customname) = "CodeID" ];
|
||||
// InstantiatePermission to apply to the set of code ids
|
||||
AccessConfig instantiate_permission = 2 [ (gogoproto.nullable) = false ];
|
||||
}
|
||||
|
||||
// UpdateInstantiateConfigProposal gov proposal content type to update
|
||||
// instantiate config to a set of code ids.
|
||||
message UpdateInstantiateConfigProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1 [ (gogoproto.moretags) = "yaml:\"title\"" ];
|
||||
// Description is a human readable text
|
||||
string description = 2 [ (gogoproto.moretags) = "yaml:\"description\"" ];
|
||||
// AccessConfigUpdate contains the list of code ids and the access config
|
||||
// to be applied.
|
||||
repeated AccessConfigUpdate access_config_updates = 3
|
||||
[ (gogoproto.nullable) = false ];
|
||||
}
|
||||
|
||||
// StoreAndInstantiateContractProposal gov proposal content type to store
|
||||
// and instantiate the contract.
|
||||
message StoreAndInstantiateContractProposal {
|
||||
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
|
||||
|
||||
// Title is a short summary
|
||||
string title = 1;
|
||||
// Description is a human readable text
|
||||
string description = 2;
|
||||
// RunAs is the address that is passed to the contract's environment as sender
|
||||
string run_as = 3;
|
||||
// WASMByteCode can be raw or gzip compressed
|
||||
bytes wasm_byte_code = 4 [ (gogoproto.customname) = "WASMByteCode" ];
|
||||
// InstantiatePermission to apply on contract creation, optional
|
||||
AccessConfig instantiate_permission = 5;
|
||||
// UnpinCode code on upload, optional
|
||||
bool unpin_code = 6;
|
||||
// Admin is an optional address that can execute migrations
|
||||
string admin = 7;
|
||||
// Label is optional metadata to be stored with a constract instance.
|
||||
string label = 8;
|
||||
// Msg json encoded message to be passed to the contract on instantiation
|
||||
bytes msg = 9 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
// Funds coins that are transferred to the contract on instantiation
|
||||
repeated cosmos.base.v1beta1.Coin funds = 10 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
|
||||
];
|
||||
// Source is the URL where the code is hosted
|
||||
string source = 11;
|
||||
// Builder is the docker image used to build the code deterministically, used
|
||||
// for smart contract verification
|
||||
string builder = 12;
|
||||
// CodeHash is the SHA256 sum of the code outputted by builder, used for smart
|
||||
// contract verification
|
||||
bytes code_hash = 13;
|
||||
}
|
263
proto/wasm/v1/query.proto
Normal file
@ -0,0 +1,263 @@
|
||||
syntax = "proto3";
|
||||
package cosmwasm.wasm.v1;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
import "wasm/v1/types.proto";
|
||||
import "google/api/annotations.proto";
|
||||
import "cosmos/base/query/v1beta1/pagination.proto";
|
||||
|
||||
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
|
||||
option (gogoproto.goproto_getters_all) = false;
|
||||
option (gogoproto.equal_all) = false;
|
||||
|
||||
// Query provides defines the gRPC querier service
|
||||
service Query {
|
||||
// ContractInfo gets the contract meta data
|
||||
rpc ContractInfo(QueryContractInfoRequest)
|
||||
returns (QueryContractInfoResponse) {
|
||||
option (google.api.http).get = "/cosmwasm/wasm/v1/contract/{address}";
|
||||
}
|
||||
// ContractHistory gets the contract code history
|
||||
rpc ContractHistory(QueryContractHistoryRequest)
|
||||
returns (QueryContractHistoryResponse) {
|
||||
option (google.api.http).get =
|
||||
"/cosmwasm/wasm/v1/contract/{address}/history";
|
||||
}
|
||||
// ContractsByCode lists all smart contracts for a code id
|
||||
rpc ContractsByCode(QueryContractsByCodeRequest)
|
||||
returns (QueryContractsByCodeResponse) {
|
||||
option (google.api.http).get = "/cosmwasm/wasm/v1/code/{code_id}/contracts";
|
||||
}
|
||||
// AllContractState gets all raw store data for a single contract
|
||||
rpc AllContractState(QueryAllContractStateRequest)
|
||||
returns (QueryAllContractStateResponse) {
|
||||
option (google.api.http).get = "/cosmwasm/wasm/v1/contract/{address}/state";
|
||||
}
|
||||
// RawContractState gets single key from the raw store data of a contract
|
||||
rpc RawContractState(QueryRawContractStateRequest)
|
||||
returns (QueryRawContractStateResponse) {
|
||||
option (google.api.http).get =
|
||||
"/cosmwasm/wasm/v1/contract/{address}/raw/{query_data}";
|
||||
}
|
||||
// SmartContractState get smart query result from the contract
|
||||
rpc SmartContractState(QuerySmartContractStateRequest)
|
||||
returns (QuerySmartContractStateResponse) {
|
||||
option (google.api.http).get =
|
||||
"/cosmwasm/wasm/v1/contract/{address}/smart/{query_data}";
|
||||
}
|
||||
// Code gets the binary code and metadata for a singe wasm code
|
||||
rpc Code(QueryCodeRequest) returns (QueryCodeResponse) {
|
||||
option (google.api.http).get = "/cosmwasm/wasm/v1/code/{code_id}";
|
||||
}
|
||||
// Codes gets the metadata for all stored wasm codes
|
||||
rpc Codes(QueryCodesRequest) returns (QueryCodesResponse) {
|
||||
option (google.api.http).get = "/cosmwasm/wasm/v1/code";
|
||||
}
|
||||
|
||||
// PinnedCodes gets the pinned code ids
|
||||
rpc PinnedCodes(QueryPinnedCodesRequest) returns (QueryPinnedCodesResponse) {
|
||||
option (google.api.http).get = "/cosmwasm/wasm/v1/codes/pinned";
|
||||
}
|
||||
|
||||
// Params gets the module params
|
||||
rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
|
||||
option (google.api.http).get = "/cosmwasm/wasm/v1/codes/params";
|
||||
}
|
||||
|
||||
// ContractsByCreator gets the contracts by creator
|
||||
rpc ContractsByCreator(QueryContractsByCreatorRequest)
|
||||
returns (QueryContractsByCreatorResponse) {
|
||||
option (google.api.http).get =
|
||||
"/cosmwasm/wasm/v1/contracts/creator/{creator_address}";
|
||||
}
|
||||
}
|
||||
|
||||
// QueryContractInfoRequest is the request type for the Query/ContractInfo RPC
|
||||
// method
|
||||
message QueryContractInfoRequest {
|
||||
// address is the address of the contract to query
|
||||
string address = 1;
|
||||
}
|
||||
// QueryContractInfoResponse is the response type for the Query/ContractInfo RPC
|
||||
// method
|
||||
message QueryContractInfoResponse {
|
||||
option (gogoproto.equal) = true;
|
||||
|
||||
// address is the address of the contract
|
||||
string address = 1;
|
||||
ContractInfo contract_info = 2 [
|
||||
(gogoproto.embed) = true,
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.jsontag) = ""
|
||||
];
|
||||
}
|
||||
|
||||
// QueryContractHistoryRequest is the request type for the Query/ContractHistory
|
||||
// RPC method
|
||||
message QueryContractHistoryRequest {
|
||||
// address is the address of the contract to query
|
||||
string address = 1;
|
||||
// pagination defines an optional pagination for the request.
|
||||
cosmos.base.query.v1beta1.PageRequest pagination = 2;
|
||||
}
|
||||
|
||||
// QueryContractHistoryResponse is the response type for the
|
||||
// Query/ContractHistory RPC method
|
||||
message QueryContractHistoryResponse {
|
||||
repeated ContractCodeHistoryEntry entries = 1
|
||||
[ (gogoproto.nullable) = false ];
|
||||
// pagination defines the pagination in the response.
|
||||
cosmos.base.query.v1beta1.PageResponse pagination = 2;
|
||||
}
|
||||
|
||||
// QueryContractsByCodeRequest is the request type for the Query/ContractsByCode
|
||||
// RPC method
|
||||
message QueryContractsByCodeRequest {
|
||||
uint64 code_id = 1; // grpc-gateway_out does not support Go style CodID
|
||||
// pagination defines an optional pagination for the request.
|
||||
cosmos.base.query.v1beta1.PageRequest pagination = 2;
|
||||
}
|
||||
|
||||
// QueryContractsByCodeResponse is the response type for the
|
||||
// Query/ContractsByCode RPC method
|
||||
message QueryContractsByCodeResponse {
|
||||
// contracts are a set of contract addresses
|
||||
repeated string contracts = 1;
|
||||
|
||||
// pagination defines the pagination in the response.
|
||||
cosmos.base.query.v1beta1.PageResponse pagination = 2;
|
||||
}
|
||||
|
||||
// QueryAllContractStateRequest is the request type for the
|
||||
// Query/AllContractState RPC method
|
||||
message QueryAllContractStateRequest {
|
||||
// address is the address of the contract
|
||||
string address = 1;
|
||||
// pagination defines an optional pagination for the request.
|
||||
cosmos.base.query.v1beta1.PageRequest pagination = 2;
|
||||
}
|
||||
|
||||
// QueryAllContractStateResponse is the response type for the
|
||||
// Query/AllContractState RPC method
|
||||
message QueryAllContractStateResponse {
|
||||
repeated Model models = 1 [ (gogoproto.nullable) = false ];
|
||||
// pagination defines the pagination in the response.
|
||||
cosmos.base.query.v1beta1.PageResponse pagination = 2;
|
||||
}
|
||||
|
||||
// QueryRawContractStateRequest is the request type for the
|
||||
// Query/RawContractState RPC method
|
||||
message QueryRawContractStateRequest {
|
||||
// address is the address of the contract
|
||||
string address = 1;
|
||||
bytes query_data = 2;
|
||||
}
|
||||
|
||||
// QueryRawContractStateResponse is the response type for the
|
||||
// Query/RawContractState RPC method
|
||||
message QueryRawContractStateResponse {
|
||||
// Data contains the raw store data
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
// QuerySmartContractStateRequest is the request type for the
|
||||
// Query/SmartContractState RPC method
|
||||
message QuerySmartContractStateRequest {
|
||||
// address is the address of the contract
|
||||
string address = 1;
|
||||
// QueryData contains the query data passed to the contract
|
||||
bytes query_data = 2 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
}
|
||||
|
||||
// QuerySmartContractStateResponse is the response type for the
|
||||
// Query/SmartContractState RPC method
|
||||
message QuerySmartContractStateResponse {
|
||||
// Data contains the json data returned from the smart contract
|
||||
bytes data = 1 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
}
|
||||
|
||||
// QueryCodeRequest is the request type for the Query/Code RPC method
|
||||
message QueryCodeRequest {
|
||||
uint64 code_id = 1; // grpc-gateway_out does not support Go style CodID
|
||||
}
|
||||
|
||||
// CodeInfoResponse contains code meta data from CodeInfo
|
||||
message CodeInfoResponse {
|
||||
option (gogoproto.equal) = true;
|
||||
|
||||
uint64 code_id = 1 [
|
||||
(gogoproto.customname) = "CodeID",
|
||||
(gogoproto.jsontag) = "id"
|
||||
]; // id for legacy support
|
||||
string creator = 2;
|
||||
bytes data_hash = 3
|
||||
[ (gogoproto.casttype) =
|
||||
"github.com/tendermint/tendermint/libs/bytes.HexBytes" ];
|
||||
// Used in v1beta1
|
||||
reserved 4, 5;
|
||||
AccessConfig instantiate_permission = 6 [ (gogoproto.nullable) = false ];
|
||||
}
|
||||
|
||||
// QueryCodeResponse is the response type for the Query/Code RPC method
|
||||
message QueryCodeResponse {
|
||||
option (gogoproto.equal) = true;
|
||||
CodeInfoResponse code_info = 1
|
||||
[ (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
|
||||
bytes data = 2 [ (gogoproto.jsontag) = "data" ];
|
||||
}
|
||||
|
||||
// QueryCodesRequest is the request type for the Query/Codes RPC method
|
||||
message QueryCodesRequest {
|
||||
// pagination defines an optional pagination for the request.
|
||||
cosmos.base.query.v1beta1.PageRequest pagination = 1;
|
||||
}
|
||||
|
||||
// QueryCodesResponse is the response type for the Query/Codes RPC method
|
||||
message QueryCodesResponse {
|
||||
repeated CodeInfoResponse code_infos = 1 [ (gogoproto.nullable) = false ];
|
||||
// pagination defines the pagination in the response.
|
||||
cosmos.base.query.v1beta1.PageResponse pagination = 2;
|
||||
}
|
||||
|
||||
// QueryPinnedCodesRequest is the request type for the Query/PinnedCodes
|
||||
// RPC method
|
||||
message QueryPinnedCodesRequest {
|
||||
// pagination defines an optional pagination for the request.
|
||||
cosmos.base.query.v1beta1.PageRequest pagination = 2;
|
||||
}
|
||||
|
||||
// QueryPinnedCodesResponse is the response type for the
|
||||
// Query/PinnedCodes RPC method
|
||||
message QueryPinnedCodesResponse {
|
||||
repeated uint64 code_ids = 1
|
||||
[ (gogoproto.nullable) = false, (gogoproto.customname) = "CodeIDs" ];
|
||||
// pagination defines the pagination in the response.
|
||||
cosmos.base.query.v1beta1.PageResponse pagination = 2;
|
||||
}
|
||||
|
||||
// QueryParamsRequest is the request type for the Query/Params RPC method.
|
||||
message QueryParamsRequest {}
|
||||
|
||||
// QueryParamsResponse is the response type for the Query/Params RPC method.
|
||||
message QueryParamsResponse {
|
||||
// params defines the parameters of the module.
|
||||
Params params = 1 [ (gogoproto.nullable) = false ];
|
||||
}
|
||||
|
||||
// QueryContractsByCreatorRequest is the request type for the
|
||||
// Query/ContractsByCreator RPC method.
|
||||
message QueryContractsByCreatorRequest {
|
||||
// CreatorAddress is the address of contract creator
|
||||
string creator_address = 1;
|
||||
// Pagination defines an optional pagination for the request.
|
||||
cosmos.base.query.v1beta1.PageRequest pagination = 2;
|
||||
}
|
||||
|
||||
// QueryContractsByCreatorResponse is the response type for the
|
||||
// Query/ContractsByCreator RPC method.
|
||||
message QueryContractsByCreatorResponse {
|
||||
// ContractAddresses result set
|
||||
repeated string contract_addresses = 1;
|
||||
// Pagination defines the pagination in the response.
|
||||
cosmos.base.query.v1beta1.PageResponse pagination = 2;
|
||||
}
|
192
proto/wasm/v1/tx.proto
Normal file
@ -0,0 +1,192 @@
|
||||
syntax = "proto3";
|
||||
package cosmwasm.wasm.v1;
|
||||
|
||||
import "cosmos/base/v1beta1/coin.proto";
|
||||
import "gogoproto/gogo.proto";
|
||||
import "wasm/v1/types.proto";
|
||||
|
||||
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
|
||||
option (gogoproto.goproto_getters_all) = false;
|
||||
|
||||
// Msg defines the wasm Msg service.
|
||||
service Msg {
|
||||
// StoreCode to submit Wasm code to the system
|
||||
rpc StoreCode(MsgStoreCode) returns (MsgStoreCodeResponse);
|
||||
// InstantiateContract creates a new smart contract instance for the given
|
||||
// code id.
|
||||
rpc InstantiateContract(MsgInstantiateContract)
|
||||
returns (MsgInstantiateContractResponse);
|
||||
// InstantiateContract2 creates a new smart contract instance for the given
|
||||
// code id with a predictable address
|
||||
rpc InstantiateContract2(MsgInstantiateContract2)
|
||||
returns (MsgInstantiateContract2Response);
|
||||
// Execute submits the given message data to a smart contract
|
||||
rpc ExecuteContract(MsgExecuteContract) returns (MsgExecuteContractResponse);
|
||||
// Migrate runs a code upgrade/ downgrade for a smart contract
|
||||
rpc MigrateContract(MsgMigrateContract) returns (MsgMigrateContractResponse);
|
||||
// UpdateAdmin sets a new admin for a smart contract
|
||||
rpc UpdateAdmin(MsgUpdateAdmin) returns (MsgUpdateAdminResponse);
|
||||
// ClearAdmin removes any admin stored for a smart contract
|
||||
rpc ClearAdmin(MsgClearAdmin) returns (MsgClearAdminResponse);
|
||||
// UpdateInstantiateConfig updates instantiate config for a smart contract
|
||||
rpc UpdateInstantiateConfig(MsgUpdateInstantiateConfig)
|
||||
returns (MsgUpdateInstantiateConfigResponse);
|
||||
}
|
||||
|
||||
// MsgStoreCode submit Wasm code to the system
|
||||
message MsgStoreCode {
|
||||
// Sender is the that actor that signed the messages
|
||||
string sender = 1;
|
||||
// WASMByteCode can be raw or gzip compressed
|
||||
bytes wasm_byte_code = 2 [ (gogoproto.customname) = "WASMByteCode" ];
|
||||
// Used in v1beta1
|
||||
reserved 3, 4;
|
||||
// InstantiatePermission access control to apply on contract creation,
|
||||
// optional
|
||||
AccessConfig instantiate_permission = 5;
|
||||
}
|
||||
// MsgStoreCodeResponse returns store result data.
|
||||
message MsgStoreCodeResponse {
|
||||
// CodeID is the reference to the stored WASM code
|
||||
uint64 code_id = 1 [ (gogoproto.customname) = "CodeID" ];
|
||||
// Checksum is the sha256 hash of the stored code
|
||||
bytes checksum = 2;
|
||||
}
|
||||
|
||||
// MsgInstantiateContract create a new smart contract instance for the given
|
||||
// code id.
|
||||
message MsgInstantiateContract {
|
||||
// Sender is the that actor that signed the messages
|
||||
string sender = 1;
|
||||
// Admin is an optional address that can execute migrations
|
||||
string admin = 2;
|
||||
// CodeID is the reference to the stored WASM code
|
||||
uint64 code_id = 3 [ (gogoproto.customname) = "CodeID" ];
|
||||
// Label is optional metadata to be stored with a contract instance.
|
||||
string label = 4;
|
||||
// Msg json encoded message to be passed to the contract on instantiation
|
||||
bytes msg = 5 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
// Funds coins that are transferred to the contract on instantiation
|
||||
repeated cosmos.base.v1beta1.Coin funds = 6 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
|
||||
];
|
||||
}
|
||||
|
||||
// MsgInstantiateContract2 create a new smart contract instance for the given
|
||||
// code id with a predicable address.
|
||||
message MsgInstantiateContract2 {
|
||||
// Sender is the that actor that signed the messages
|
||||
string sender = 1;
|
||||
// Admin is an optional address that can execute migrations
|
||||
string admin = 2;
|
||||
// CodeID is the reference to the stored WASM code
|
||||
uint64 code_id = 3 [ (gogoproto.customname) = "CodeID" ];
|
||||
// Label is optional metadata to be stored with a contract instance.
|
||||
string label = 4;
|
||||
// Msg json encoded message to be passed to the contract on instantiation
|
||||
bytes msg = 5 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
// Funds coins that are transferred to the contract on instantiation
|
||||
repeated cosmos.base.v1beta1.Coin funds = 6 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
|
||||
];
|
||||
// Salt is an arbitrary value provided by the sender. Size can be 1 to 64.
|
||||
bytes salt = 7;
|
||||
// FixMsg include the msg value into the hash for the predictable address.
|
||||
// Default is false
|
||||
bool fix_msg = 8;
|
||||
}
|
||||
|
||||
// MsgInstantiateContractResponse return instantiation result data
|
||||
message MsgInstantiateContractResponse {
|
||||
// Address is the bech32 address of the new contract instance.
|
||||
string address = 1;
|
||||
// Data contains bytes to returned from the contract
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
// MsgInstantiateContract2Response return instantiation result data
|
||||
message MsgInstantiateContract2Response {
|
||||
// Address is the bech32 address of the new contract instance.
|
||||
string address = 1;
|
||||
// Data contains bytes to returned from the contract
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
// MsgExecuteContract submits the given message data to a smart contract
|
||||
message MsgExecuteContract {
|
||||
// Sender is the that actor that signed the messages
|
||||
string sender = 1;
|
||||
// Contract is the address of the smart contract
|
||||
string contract = 2;
|
||||
// Msg json encoded message to be passed to the contract
|
||||
bytes msg = 3 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
// Funds coins that are transferred to the contract on execution
|
||||
repeated cosmos.base.v1beta1.Coin funds = 5 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
|
||||
];
|
||||
}
|
||||
|
||||
// MsgExecuteContractResponse returns execution result data.
|
||||
message MsgExecuteContractResponse {
|
||||
// Data contains bytes to returned from the contract
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
// MsgMigrateContract runs a code upgrade/ downgrade for a smart contract
|
||||
message MsgMigrateContract {
|
||||
// Sender is the that actor that signed the messages
|
||||
string sender = 1;
|
||||
// Contract is the address of the smart contract
|
||||
string contract = 2;
|
||||
// CodeID references the new WASM code
|
||||
uint64 code_id = 3 [ (gogoproto.customname) = "CodeID" ];
|
||||
// Msg json encoded message to be passed to the contract on migration
|
||||
bytes msg = 4 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
}
|
||||
|
||||
// MsgMigrateContractResponse returns contract migration result data.
|
||||
message MsgMigrateContractResponse {
|
||||
// Data contains same raw bytes returned as data from the wasm contract.
|
||||
// (May be empty)
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
// MsgUpdateAdmin sets a new admin for a smart contract
|
||||
message MsgUpdateAdmin {
|
||||
// Sender is the that actor that signed the messages
|
||||
string sender = 1;
|
||||
// NewAdmin address to be set
|
||||
string new_admin = 2;
|
||||
// Contract is the address of the smart contract
|
||||
string contract = 3;
|
||||
}
|
||||
|
||||
// MsgUpdateAdminResponse returns empty data
|
||||
message MsgUpdateAdminResponse {}
|
||||
|
||||
// MsgClearAdmin removes any admin stored for a smart contract
|
||||
message MsgClearAdmin {
|
||||
// Sender is the actor that signed the messages
|
||||
string sender = 1;
|
||||
// Contract is the address of the smart contract
|
||||
string contract = 3;
|
||||
}
|
||||
|
||||
// MsgClearAdminResponse returns empty data
|
||||
message MsgClearAdminResponse {}
|
||||
|
||||
// MsgUpdateInstantiateConfig updates instantiate config for a smart contract
|
||||
message MsgUpdateInstantiateConfig {
|
||||
// Sender is the that actor that signed the messages
|
||||
string sender = 1;
|
||||
// CodeID references the stored WASM code
|
||||
uint64 code_id = 2 [ (gogoproto.customname) = "CodeID" ];
|
||||
// NewInstantiatePermission is the new access control
|
||||
AccessConfig new_instantiate_permission = 3;
|
||||
}
|
||||
|
||||
// MsgUpdateInstantiateConfigResponse returns empty data
|
||||
message MsgUpdateInstantiateConfigResponse {}
|
144
proto/wasm/v1/types.proto
Normal file
@ -0,0 +1,144 @@
|
||||
syntax = "proto3";
|
||||
package cosmwasm.wasm.v1;
|
||||
|
||||
import "cosmos_proto/cosmos.proto";
|
||||
import "gogoproto/gogo.proto";
|
||||
import "google/protobuf/any.proto";
|
||||
|
||||
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
|
||||
option (gogoproto.goproto_getters_all) = false;
|
||||
option (gogoproto.equal_all) = true;
|
||||
|
||||
// AccessType permission types
|
||||
enum AccessType {
|
||||
option (gogoproto.goproto_enum_prefix) = false;
|
||||
option (gogoproto.goproto_enum_stringer) = false;
|
||||
// AccessTypeUnspecified placeholder for empty value
|
||||
ACCESS_TYPE_UNSPECIFIED = 0
|
||||
[ (gogoproto.enumvalue_customname) = "AccessTypeUnspecified" ];
|
||||
// AccessTypeNobody forbidden
|
||||
ACCESS_TYPE_NOBODY = 1
|
||||
[ (gogoproto.enumvalue_customname) = "AccessTypeNobody" ];
|
||||
// AccessTypeOnlyAddress restricted to a single address
|
||||
// Deprecated: use AccessTypeAnyOfAddresses instead
|
||||
ACCESS_TYPE_ONLY_ADDRESS = 2
|
||||
[ (gogoproto.enumvalue_customname) = "AccessTypeOnlyAddress" ];
|
||||
// AccessTypeEverybody unrestricted
|
||||
ACCESS_TYPE_EVERYBODY = 3
|
||||
[ (gogoproto.enumvalue_customname) = "AccessTypeEverybody" ];
|
||||
// AccessTypeAnyOfAddresses allow any of the addresses
|
||||
ACCESS_TYPE_ANY_OF_ADDRESSES = 4
|
||||
[ (gogoproto.enumvalue_customname) = "AccessTypeAnyOfAddresses" ];
|
||||
}
|
||||
|
||||
// AccessTypeParam
|
||||
message AccessTypeParam {
|
||||
option (gogoproto.goproto_stringer) = true;
|
||||
AccessType value = 1 [ (gogoproto.moretags) = "yaml:\"value\"" ];
|
||||
}
|
||||
|
||||
// AccessConfig access control type.
|
||||
message AccessConfig {
|
||||
option (gogoproto.goproto_stringer) = true;
|
||||
AccessType permission = 1 [ (gogoproto.moretags) = "yaml:\"permission\"" ];
|
||||
|
||||
// Address
|
||||
// Deprecated: replaced by addresses
|
||||
string address = 2 [ (gogoproto.moretags) = "yaml:\"address\"" ];
|
||||
repeated string addresses = 3 [ (gogoproto.moretags) = "yaml:\"addresses\"" ];
|
||||
}
|
||||
|
||||
// Params defines the set of wasm parameters.
|
||||
message Params {
|
||||
option (gogoproto.goproto_stringer) = false;
|
||||
AccessConfig code_upload_access = 1 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.moretags) = "yaml:\"code_upload_access\""
|
||||
];
|
||||
AccessType instantiate_default_permission = 2
|
||||
[ (gogoproto.moretags) = "yaml:\"instantiate_default_permission\"" ];
|
||||
}
|
||||
|
||||
// CodeInfo is data for the uploaded contract WASM code
|
||||
message CodeInfo {
|
||||
// CodeHash is the unique identifier created by wasmvm
|
||||
bytes code_hash = 1;
|
||||
// Creator address who initially stored the code
|
||||
string creator = 2;
|
||||
// Used in v1beta1
|
||||
reserved 3, 4;
|
||||
// InstantiateConfig access control to apply on contract creation, optional
|
||||
AccessConfig instantiate_config = 5 [ (gogoproto.nullable) = false ];
|
||||
}
|
||||
|
||||
// ContractInfo stores a WASM contract instance
|
||||
message ContractInfo {
|
||||
option (gogoproto.equal) = true;
|
||||
|
||||
// CodeID is the reference to the stored Wasm code
|
||||
uint64 code_id = 1 [ (gogoproto.customname) = "CodeID" ];
|
||||
// Creator address who initially instantiated the contract
|
||||
string creator = 2;
|
||||
// Admin is an optional address that can execute migrations
|
||||
string admin = 3;
|
||||
// Label is optional metadata to be stored with a contract instance.
|
||||
string label = 4;
|
||||
// Created Tx position when the contract was instantiated.
|
||||
AbsoluteTxPosition created = 5;
|
||||
string ibc_port_id = 6 [ (gogoproto.customname) = "IBCPortID" ];
|
||||
|
||||
// Extension is an extension point to store custom metadata within the
|
||||
// persistence model.
|
||||
google.protobuf.Any extension = 7
|
||||
[ (cosmos_proto.accepts_interface) = "ContractInfoExtension" ];
|
||||
}
|
||||
|
||||
// ContractCodeHistoryOperationType actions that caused a code change
|
||||
enum ContractCodeHistoryOperationType {
|
||||
option (gogoproto.goproto_enum_prefix) = false;
|
||||
// ContractCodeHistoryOperationTypeUnspecified placeholder for empty value
|
||||
CONTRACT_CODE_HISTORY_OPERATION_TYPE_UNSPECIFIED = 0
|
||||
[ (gogoproto.enumvalue_customname) =
|
||||
"ContractCodeHistoryOperationTypeUnspecified" ];
|
||||
// ContractCodeHistoryOperationTypeInit on chain contract instantiation
|
||||
CONTRACT_CODE_HISTORY_OPERATION_TYPE_INIT = 1
|
||||
[ (gogoproto.enumvalue_customname) =
|
||||
"ContractCodeHistoryOperationTypeInit" ];
|
||||
// ContractCodeHistoryOperationTypeMigrate code migration
|
||||
CONTRACT_CODE_HISTORY_OPERATION_TYPE_MIGRATE = 2
|
||||
[ (gogoproto.enumvalue_customname) =
|
||||
"ContractCodeHistoryOperationTypeMigrate" ];
|
||||
// ContractCodeHistoryOperationTypeGenesis based on genesis data
|
||||
CONTRACT_CODE_HISTORY_OPERATION_TYPE_GENESIS = 3
|
||||
[ (gogoproto.enumvalue_customname) =
|
||||
"ContractCodeHistoryOperationTypeGenesis" ];
|
||||
}
|
||||
|
||||
// ContractCodeHistoryEntry metadata to a contract.
|
||||
message ContractCodeHistoryEntry {
|
||||
ContractCodeHistoryOperationType operation = 1;
|
||||
// CodeID is the reference to the stored WASM code
|
||||
uint64 code_id = 2 [ (gogoproto.customname) = "CodeID" ];
|
||||
// Updated Tx position when the operation was executed.
|
||||
AbsoluteTxPosition updated = 3;
|
||||
bytes msg = 4 [ (gogoproto.casttype) = "RawContractMessage" ];
|
||||
}
|
||||
|
||||
// AbsoluteTxPosition is a unique transaction position that allows for global
|
||||
// ordering of transactions.
|
||||
message AbsoluteTxPosition {
|
||||
// BlockHeight is the block the contract was created at
|
||||
uint64 block_height = 1;
|
||||
// TxIndex is a monotonic counter within the block (actual transaction index,
|
||||
// or gas consumed)
|
||||
uint64 tx_index = 2;
|
||||
}
|
||||
|
||||
// Model is a struct that holds a KV pair
|
||||
message Model {
|
||||
// hex-encode key to read it better (this is often ascii)
|
||||
bytes key = 1 [ (gogoproto.casttype) =
|
||||
"github.com/tendermint/tendermint/libs/bytes.HexBytes" ];
|
||||
// base64-encode raw value
|
||||
bytes value = 2;
|
||||
}
|
205
x/wasm/Governance.md
Normal file
@ -0,0 +1,205 @@
|
||||
# Governance
|
||||
|
||||
This document gives an overview of how the various governance
|
||||
proposals interact with the CosmWasm contract lifecycle. It is
|
||||
a high-level, technical introduction meant to provide context before
|
||||
looking into the code, or constructing proposals.
|
||||
|
||||
## Proposal Types
|
||||
We have added 9 new wasm specific proposal types that cover the contract's live cycle and authorization:
|
||||
|
||||
* `StoreCodeProposal` - upload a wasm binary
|
||||
* `InstantiateContractProposal` - instantiate a wasm contract
|
||||
* `MigrateContractProposal` - migrate a wasm contract to a new code version
|
||||
* `SudoContractProposal` - call into the protected `sudo` entry point of a contract
|
||||
* `ExecuteContractProposal` - execute a wasm contract as an arbitrary user
|
||||
* `UpdateAdminProposal` - set a new admin for a contract
|
||||
* `ClearAdminProposal` - clear admin for a contract to prevent further migrations
|
||||
* `PinCodes` - pin the given code ids in cache. This trades memory for reduced startup time and lowers gas cost
|
||||
* `UnpinCodes` - unpin the given code ids from the cache. This frees up memory and returns to standard speed and gas cost
|
||||
* `UpdateInstantiateConfigProposal` - update instantiate permissions to a list of given code ids.
|
||||
* `StoreAndInstantiateContractProposal` - upload and instantiate a wasm contract.
|
||||
|
||||
For details see the proposal type [implementation](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/proposal.go)
|
||||
|
||||
### Unit tests
|
||||
[Proposal type validations](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/proposal_test.go)
|
||||
|
||||
## Proposal Handler
|
||||
The [wasmd proposal_handler](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/keeper/proposal_handler.go) implements the `gov.Handler` function
|
||||
and executes the wasmd proposal types after a successful tally.
|
||||
|
||||
The proposal handler uses a [`GovAuthorizationPolicy`](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/keeper/authz_policy.go#L29) to bypass the existing contract's authorization policy.
|
||||
|
||||
### Tests
|
||||
* [Integration: Submit and execute proposal](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/keeper/proposal_integration_test.go)
|
||||
|
||||
## Gov Integration
|
||||
The wasmd proposal handler can be added to the gov router in the [abci app](https://github.com/CosmWasm/wasmd/blob/master/app/app.go#L306)
|
||||
to receive proposal execution calls.
|
||||
```go
|
||||
govRouter.AddRoute(wasm.RouterKey, wasm.NewWasmProposalHandler(app.wasmKeeper, enabledProposals))
|
||||
```
|
||||
|
||||
## Wasmd Authorization Settings
|
||||
|
||||
Settings via sdk `params` module:
|
||||
- `code_upload_access` - who can upload a wasm binary: `Nobody`, `Everybody`, `OnlyAddress`
|
||||
- `instantiate_default_permission` - platform default, who can instantiate a wasm binary when the code owner has not set it
|
||||
|
||||
See [params.go](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/params.go)
|
||||
|
||||
### Init Params Via Genesis
|
||||
|
||||
```json
|
||||
"wasm": {
|
||||
"params": {
|
||||
"code_upload_access": {
|
||||
"permission": "Everybody"
|
||||
},
|
||||
"instantiate_default_permission": "Everybody"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
The values can be updated via gov proposal implemented in the `params` module.
|
||||
|
||||
### Update Params Via [ParamChangeProposal](https://github.com/cosmos/cosmos-sdk/blob/v0.45.3/proto/cosmos/params/v1beta1/params.proto#L10)
|
||||
Example to submit a parameter change gov proposal:
|
||||
```sh
|
||||
wasmd tx gov submit-proposal param-change <proposal-json-file> --from validator --chain-id=testing -b block
|
||||
```
|
||||
#### Content examples
|
||||
* Disable wasm code uploads
|
||||
```json
|
||||
{
|
||||
"title": "Foo",
|
||||
"description": "Bar",
|
||||
"changes": [
|
||||
{
|
||||
"subspace": "wasm",
|
||||
"key": "uploadAccess",
|
||||
"value": {
|
||||
"permission": "Nobody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"deposit": ""
|
||||
}
|
||||
```
|
||||
* Allow wasm code uploads for everybody
|
||||
```json
|
||||
{
|
||||
"title": "Foo",
|
||||
"description": "Bar",
|
||||
"changes": [
|
||||
{
|
||||
"subspace": "wasm",
|
||||
"key": "uploadAccess",
|
||||
"value": {
|
||||
"permission": "Everybody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"deposit": ""
|
||||
}
|
||||
```
|
||||
|
||||
* Restrict code uploads to a single address
|
||||
```json
|
||||
{
|
||||
"title": "Foo",
|
||||
"description": "Bar",
|
||||
"changes": [
|
||||
{
|
||||
"subspace": "wasm",
|
||||
"key": "uploadAccess",
|
||||
"value": {
|
||||
"permission": "OnlyAddress",
|
||||
"address": "cosmos1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0fr2sh"
|
||||
}
|
||||
}
|
||||
],
|
||||
"deposit": ""
|
||||
}
|
||||
```
|
||||
* Set chain **default** instantiation settings to nobody
|
||||
```json
|
||||
{
|
||||
"title": "Foo",
|
||||
"description": "Bar",
|
||||
"changes": [
|
||||
{
|
||||
"subspace": "wasm",
|
||||
"key": "instantiateAccess",
|
||||
"value": "Nobody"
|
||||
}
|
||||
],
|
||||
"deposit": ""
|
||||
}
|
||||
```
|
||||
* Set chain **default** instantiation settings to everybody
|
||||
```json
|
||||
{
|
||||
"title": "Foo",
|
||||
"description": "Bar",
|
||||
"changes": [
|
||||
{
|
||||
"subspace": "wasm",
|
||||
"key": "instantiateAccess",
|
||||
"value": "Everybody"
|
||||
}
|
||||
],
|
||||
"deposit": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Enable gov proposals at **compile time**.
|
||||
As gov proposals bypass the existing authorization policy they are disabled and require to be enabled at compile time.
|
||||
```
|
||||
-X github.com/CosmWasm/wasmd/app.ProposalsEnabled=true - enable all x/wasm governance proposals (default false)
|
||||
-X github.com/CosmWasm/wasmd/app.EnableSpecificProposals=MigrateContract,UpdateAdmin,ClearAdmin - enable a subset of the x/wasm governance proposal types (overrides ProposalsEnabled)
|
||||
```
|
||||
|
||||
The `ParamChangeProposal` is always enabled.
|
||||
|
||||
### Tests
|
||||
* [params validation unit tests](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/params_test.go)
|
||||
* [genesis validation tests](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/genesis_test.go)
|
||||
* [policy integration tests](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/keeper/keeper_test.go)
|
||||
|
||||
## CLI
|
||||
|
||||
```shell script
|
||||
wasmd tx gov submit-proposal [command]
|
||||
|
||||
Available Commands:
|
||||
wasm-store Submit a wasm binary proposal
|
||||
instantiate-contract Submit an instantiate wasm contract proposal
|
||||
migrate-contract Submit a migrate wasm contract to a new code version proposal
|
||||
set-contract-admin Submit a new admin for a contract proposal
|
||||
clear-contract-admin Submit a clear admin for a contract to prevent further migrations proposal
|
||||
...
|
||||
```
|
||||
## Rest
|
||||
New [`ProposalHandlers`](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/client/proposal_handler.go)
|
||||
|
||||
* Integration
|
||||
```shell script
|
||||
gov.NewAppModuleBasic(append(wasmclient.ProposalHandlers, paramsclient.ProposalHandler, distr.ProposalHandler, upgradeclient.ProposalHandler)...),
|
||||
```
|
||||
In [abci app](https://github.com/CosmWasm/wasmd/blob/master/app/app.go#L109)
|
||||
|
||||
### Tests
|
||||
* [Rest Unit tests](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/client/proposal_handler_test.go)
|
||||
* [Rest smoke LCD test](https://github.com/CosmWasm/wasmd/blob/master/lcd_test/wasm_test.go)
|
||||
|
||||
|
||||
|
||||
## Pull requests
|
||||
* https://github.com/CosmWasm/wasmd/pull/190
|
||||
* https://github.com/CosmWasm/wasmd/pull/186
|
||||
* https://github.com/CosmWasm/wasmd/pull/183
|
||||
* https://github.com/CosmWasm/wasmd/pull/180
|
||||
* https://github.com/CosmWasm/wasmd/pull/179
|
||||
* https://github.com/CosmWasm/wasmd/pull/173
|
137
x/wasm/IBC.md
Normal file
@ -0,0 +1,137 @@
|
||||
# IBC specification
|
||||
|
||||
This documents how CosmWasm contracts are expected to interact with IBC.
|
||||
|
||||
## General Concepts
|
||||
|
||||
**IBC Enabled** - when instantiating a contract, we detect if it supports IBC messages.
|
||||
We require "feature flags" in the contract/vm handshake to ensure compatibility
|
||||
for features like staking or chain-specific extensions. IBC functionality will require
|
||||
another "feature flag", and the list of "enabled features" can be returned to the `x/wasm`
|
||||
module to control conditional IBC behavior.
|
||||
|
||||
If this feature is enabled, it is considered "IBC Enabled", and that info will
|
||||
be stored in the ContractInfo. (For mock, we assume all contracts are IBC enabled)
|
||||
|
||||
Also, please read the [IBC Docs](https://docs.cosmos.network/master/ibc/overview.html)
|
||||
for detailed descriptions of the terms *Port*, *Client*, *Connection*,
|
||||
and *Channel*
|
||||
|
||||
## Overview
|
||||
|
||||
We use "One Port per Contract", which is the most straight-forward mapping, treating each contract
|
||||
like a module. It does lead to very long portIDs however. Pay special attention to both the Channel establishment
|
||||
(which should be compatible with standard ICS20 modules without changes on their part), as well
|
||||
as how contracts can properly identify their counterparty.
|
||||
|
||||
(We considered on port for the `x/wasm` module and multiplexing on it, but [dismissed that idea](#rejected-ideas))
|
||||
|
||||
* Upon `Instantiate`, if a contract is *IBC Enabled*, we dynamically
|
||||
bind a port for this contract. The port name is `wasm.<contract address>`,
|
||||
eg. `wasm.cosmos1hmdudppzceg27qsuq707tjg8rkgj7g5hnvnw29`
|
||||
* If a *Channel* is being established with a registered `wasm.xyz` port,
|
||||
the `x/wasm.Keeper` will handle this and call into the appropriate
|
||||
contract to determine supported protocol versions during the
|
||||
[`ChanOpenTry` and `ChanOpenAck` phases](https://docs.cosmos.network/master/ibc/overview.html#channels).
|
||||
(See [Channel Handshake Version Negotiation](https://docs.cosmos.network/master/ibc/custom.html#channel-handshake-version-negotiation))
|
||||
* Both the *Port* and the *Channel* are fully owned by one contract.
|
||||
* `x/wasm` will allow both *ORDERED* and *UNORDERED* channels and pass that mode
|
||||
down to the contract in `OnChanOpenTry`, so the contract can decide if it accepts
|
||||
the mode. We will recommend the contract developers stick with *ORDERED* channels
|
||||
for custom protocols unless they can reason about async packet timing.
|
||||
* When sending a packet, the CosmWasm contract must specify the local *ChannelID*.
|
||||
As there is a unique *PortID* per contract, that is filled in by `x/wasm`
|
||||
to produce the globally unique `(PortID, ChannelID)`
|
||||
* When receiving a Packet (or Ack or Timeout), the contracts receives the local
|
||||
*ChannelID* it came from, as well as the packet that was sent by the counterparty.
|
||||
* When receiving an Ack or Timeout packet, the contract also receives the
|
||||
original packet that it sent earlier.
|
||||
* We do not support multihop packets in this model (they are rejected by `x/wasm`).
|
||||
They are currently not fully specified nor implemented in IBC 1.0, so let us
|
||||
simplify our model until this is well established
|
||||
|
||||
## Workflow
|
||||
|
||||
Establishing *Clients* and *Connections* is out of the scope of this
|
||||
module and must be created by the same means as for `ibc-transfer`
|
||||
(via the [go cli](https://github.com/cosmos/relayer) or better [ts-relayer](https://github.com/confio/ts-relayer)).
|
||||
`x/wasm` will bind a unique *Port* for each "IBC Enabled" contract.
|
||||
|
||||
For mocks, all the Packet Handling and Channel Lifecycle Hooks are routed
|
||||
to some Golang stub handler, but containing the contract address, so we
|
||||
can perform contract-specific actions for each packet. In a real setting,
|
||||
we route to the contract that owns the port/channel and call one of it's various
|
||||
entry points.
|
||||
|
||||
Please refer to the CosmWasm repo for all
|
||||
[details on the IBC API from the point of view of a CosmWasm contract](https://github.com/CosmWasm/cosmwasm/blob/main/IBC.md).
|
||||
|
||||
## Future Ideas
|
||||
|
||||
Here are some ideas we may add in the future
|
||||
|
||||
### Dynamic Ports and Channels
|
||||
|
||||
* multiple ports per contract
|
||||
* elastic ports that can be assigned to different contracts
|
||||
* transfer of channels to another contract
|
||||
|
||||
This is inspired by the Agoric design, but also adds considerable complexity to both the `x/wasm`
|
||||
implementation as well as the correctness reasoning of any given contract. This will not be
|
||||
available in the first version of our "IBC Enabled contracts", but we can consider it for later,
|
||||
if there are concrete user cases that would significantly benefit from this added complexity.
|
||||
|
||||
### Add multihop support
|
||||
|
||||
Once the ICS and IBC specs fully establish how multihop packets work, we should add support for that.
|
||||
Both on setting up the routes with OpenChannel, as well as acting as an intermediate relayer (if that is possible)
|
||||
|
||||
## Rejected Ideas
|
||||
|
||||
### One Port per Module
|
||||
|
||||
We decided on "one port per contract", especially after the IBC team raised
|
||||
the max length on port names to allow `wasm-<bech32 address>` to be a valid port.
|
||||
Here are the arguments for "one port for x/wasm" vs "one port per contract". Here
|
||||
was an alternate proposal:
|
||||
|
||||
In this approach, the `x/wasm` module just binds one port to handle all
|
||||
modules. This can be well defined name like `wasm`. Since we always
|
||||
have `(ChannelID, PortID)` for routing messages, we can reuse one port
|
||||
for all contracts as long as we have a clear way to map the `ChannelID`
|
||||
to a specific contract when it is being established.
|
||||
|
||||
|
||||
* On genesis we bind the port `wasm` for all communication with the `x/wasm`
|
||||
module.
|
||||
* The *Port* is fully owned by `x/wasm`
|
||||
* Each *Channel* is fully owned by one contract.
|
||||
* `x/wasm` only accepts *ORDERED Channels* for simplicity of contract
|
||||
correctness.
|
||||
|
||||
To clarify:
|
||||
|
||||
* When a *Channel* is being established with port `wasm`, the
|
||||
`x/wasm.Keeper` must be able to identify for which contract this
|
||||
is destined. **how to do so**??
|
||||
* One idea: the channel name must be the contract address. This means
|
||||
(`wasm`, `cosmos13d...`) will map to the given contract in the wasm module.
|
||||
The problem with this is that if two contracts from chainA want to
|
||||
connect to the same contracts on chainB, they will want to claim the
|
||||
same *ChannelID* and *PortID*. Not sure how to differentiate multiple
|
||||
parties in this way.
|
||||
* Other ideas: have a special field we send on `OnChanOpenInit` that
|
||||
specifies the destination contract, and allow any *ChannelID*.
|
||||
However, looking at [`OnChanOpenInit` function signature](https://docs.cosmos.network/master/ibc/custom.html#implement-ibcmodule-interface-and-callbacks),
|
||||
I don't see a place to put this extra info, without abusing the version field,
|
||||
which is a [specified field](https://docs.cosmos.network/master/ibc/custom.html#channel-handshake-version-negotiation):
|
||||
```
|
||||
Versions must be strings but can implement any versioning structure.
|
||||
If your application plans to have linear releases then semantic versioning is recommended.
|
||||
...
|
||||
Valid version selection includes selecting a compatible version identifier with a subset
|
||||
of features supported by your application for that version.
|
||||
...
|
||||
ICS20 currently implements basic string matching with a
|
||||
single supported version.
|
||||
```
|
219
x/wasm/README.md
Normal file
@ -0,0 +1,219 @@
|
||||
# Wasm Module
|
||||
|
||||
This should be a brief overview of the functionality
|
||||
|
||||
## Configuration
|
||||
|
||||
You can add the following section to `config/app.toml`:
|
||||
|
||||
```toml
|
||||
[wasm]
|
||||
# This is the maximum sdk gas (wasm and storage) that we allow for any x/wasm "smart" queries
|
||||
query_gas_limit = 300000
|
||||
# This defines the memory size for Wasm modules that we can keep cached to speed-up instantiation
|
||||
# The value is in MiB not bytes
|
||||
memory_cache_size = 300
|
||||
```
|
||||
|
||||
The values can also be set via CLI flags on with the `start` command:
|
||||
```shell script
|
||||
--wasm.memory_cache_size uint32 Sets the size in MiB (NOT bytes) of an in-memory cache for wasm modules. Set to 0 to disable. (default 100)
|
||||
--wasm.query_gas_limit uint Set the max gas that can be spent on executing a query with a Wasm contract (default 3000000)
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
A number of events are returned to allow good indexing of the transactions from smart contracts.
|
||||
|
||||
Every call to Instantiate or Execute will be tagged with the info on the contract that was executed and who executed it.
|
||||
It should look something like this (with different addresses). The module is always `wasm`, and `code_id` is only present
|
||||
when Instantiating a contract, so you can subscribe to new instances, it is omitted on Execute. There is also an `action` tag
|
||||
which is auto-added by the Cosmos SDK and has a value of either `store-code`, `instantiate` or `execute` depending on which message
|
||||
was sent:
|
||||
|
||||
```json
|
||||
{
|
||||
"Type": "message",
|
||||
"Attr": [
|
||||
{
|
||||
"key": "module",
|
||||
"value": "wasm"
|
||||
},
|
||||
{
|
||||
"key": "action",
|
||||
"value": "instantiate"
|
||||
},
|
||||
{
|
||||
"key": "signer",
|
||||
"value": "cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x"
|
||||
},
|
||||
{
|
||||
"key": "code_id",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"key": "_contract_address",
|
||||
"value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If any funds were transferred to the contract as part of the message, or if the contract released funds as part of it's executions,
|
||||
it will receive the typical events associated with sending tokens from bank. In this case, we instantiate the contract and
|
||||
provide a initial balance in the same `MsgInstantiateContract`. We see the following events in addition to the above one:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Type": "transfer",
|
||||
"Attr": [
|
||||
{
|
||||
"key": "recipient",
|
||||
"value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"
|
||||
},
|
||||
{
|
||||
"key": "sender",
|
||||
"value": "cosmos1ffnqn02ft2psvyv4dyr56nnv6plllf9pm2kpmv"
|
||||
},
|
||||
{
|
||||
"key": "amount",
|
||||
"value": "100000denom"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Finally, the contract itself can emit a "custom event" on Execute only (not on Init).
|
||||
There is one event per contract, so if one contract calls a second contract, you may receive
|
||||
one event for the original contract and one for the re-invoked contract. All attributes from the contract are passed through verbatim,
|
||||
and we add a `_contract_address` attribute that contains the actual contract that emitted that event.
|
||||
Here is an example from the escrow contract successfully releasing funds to the destination address:
|
||||
|
||||
```json
|
||||
{
|
||||
"Type": "wasm",
|
||||
"Attr": [
|
||||
{
|
||||
"key": "_contract_address",
|
||||
"value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"
|
||||
},
|
||||
{
|
||||
"key": "action",
|
||||
"value": "release"
|
||||
},
|
||||
{
|
||||
"key": "destination",
|
||||
"value": "cosmos14k7v7ms4jxkk2etmg9gljxjm4ru3qjdugfsflq"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Pulling this all together
|
||||
|
||||
We will invoke an escrow contract to release to the designated beneficiary.
|
||||
The escrow was previously loaded with `100000denom` (from the above example).
|
||||
In this transaction, we send `5000denom` along with the `MsgExecuteContract`
|
||||
and the contract releases the entire funds (`105000denom`) to the beneficiary.
|
||||
|
||||
We will see all the following events, where you should be able to reconstruct the actions
|
||||
(remember there are two events for each transfer). We see (1) the initial transfer of funds
|
||||
to the contract, (2) the contract custom event that it released funds (3) the transfer of funds
|
||||
from the contract to the beneficiary and (4) the generic x/wasm event stating that the contract
|
||||
was executed (which always appears, while 2 is optional and has information as reliable as the contract):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Type": "transfer",
|
||||
"Attr": [
|
||||
{
|
||||
"key": "recipient",
|
||||
"value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"
|
||||
},
|
||||
{
|
||||
"key": "sender",
|
||||
"value": "cosmos1zm074khx32hqy20hlshlsd423n07pwlu9cpt37"
|
||||
},
|
||||
{
|
||||
"key": "amount",
|
||||
"value": "5000denom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "wasm",
|
||||
"Attr": [
|
||||
{
|
||||
"key": "_contract_address",
|
||||
"value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"
|
||||
},
|
||||
{
|
||||
"key": "action",
|
||||
"value": "release"
|
||||
},
|
||||
{
|
||||
"key": "destination",
|
||||
"value": "cosmos14k7v7ms4jxkk2etmg9gljxjm4ru3qjdugfsflq"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "transfer",
|
||||
"Attr": [
|
||||
{
|
||||
"key": "recipient",
|
||||
"value": "cosmos14k7v7ms4jxkk2etmg9gljxjm4ru3qjdugfsflq"
|
||||
},
|
||||
{
|
||||
"key": "sender",
|
||||
"value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"
|
||||
},
|
||||
{
|
||||
"key": "amount",
|
||||
"value": "105000denom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "message",
|
||||
"Attr": [
|
||||
{
|
||||
"key": "module",
|
||||
"value": "wasm"
|
||||
},
|
||||
{
|
||||
"key": "action",
|
||||
"value": "execute"
|
||||
},
|
||||
{
|
||||
"key": "signer",
|
||||
"value": "cosmos1zm074khx32hqy20hlshlsd423n07pwlu9cpt37"
|
||||
},
|
||||
{
|
||||
"key": "_contract_address",
|
||||
"value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
A note on this format. This is what we return from our module. However, it seems to me that many events with the same `Type`
|
||||
get merged together somewhere along the stack, so in this case, you *may* end up with one "transfer" event with the info for
|
||||
both transfers. Double check when evaluating the event logs, I will document better with more experience, especially when I
|
||||
find out the entire path for the events.
|
||||
|
||||
## Messages
|
||||
|
||||
TODO
|
||||
|
||||
## CLI
|
||||
|
||||
TODO - working, but not the nicest interface (json + bash = bleh). Use to upload, but I suggest to focus on frontend / js tooling
|
||||
|
||||
## Rest
|
||||
|
||||
TODO - main supported interface, under rapid change
|
133
x/wasm/alias.go
Normal file
@ -0,0 +1,133 @@
|
||||
// nolint
|
||||
// autogenerated code using github.com/rigelrozanski/multitool
|
||||
// aliases generated for the following subdirectories:
|
||||
// ALIASGEN: github.com/cerc-io/laconicd/x/wasm/types
|
||||
// ALIASGEN: github.com/cerc-io/laconicd/x/wasm/keeper
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
const (
|
||||
firstCodeID = 1
|
||||
ModuleName = types.ModuleName
|
||||
StoreKey = types.StoreKey
|
||||
TStoreKey = types.TStoreKey
|
||||
QuerierRoute = types.QuerierRoute
|
||||
RouterKey = types.RouterKey
|
||||
WasmModuleEventType = types.WasmModuleEventType
|
||||
AttributeKeyContractAddr = types.AttributeKeyContractAddr
|
||||
ProposalTypeStoreCode = types.ProposalTypeStoreCode
|
||||
ProposalTypeInstantiateContract = types.ProposalTypeInstantiateContract
|
||||
ProposalTypeMigrateContract = types.ProposalTypeMigrateContract
|
||||
ProposalTypeUpdateAdmin = types.ProposalTypeUpdateAdmin
|
||||
ProposalTypeClearAdmin = types.ProposalTypeClearAdmin
|
||||
QueryListContractByCode = keeper.QueryListContractByCode
|
||||
QueryGetContract = keeper.QueryGetContract
|
||||
QueryGetContractState = keeper.QueryGetContractState
|
||||
QueryGetCode = keeper.QueryGetCode
|
||||
QueryListCode = keeper.QueryListCode
|
||||
QueryMethodContractStateSmart = keeper.QueryMethodContractStateSmart
|
||||
QueryMethodContractStateAll = keeper.QueryMethodContractStateAll
|
||||
QueryMethodContractStateRaw = keeper.QueryMethodContractStateRaw
|
||||
)
|
||||
|
||||
var (
|
||||
// functions aliases
|
||||
RegisterCodec = types.RegisterLegacyAminoCodec
|
||||
RegisterInterfaces = types.RegisterInterfaces
|
||||
ValidateGenesis = types.ValidateGenesis
|
||||
ConvertToProposals = types.ConvertToProposals
|
||||
GetCodeKey = types.GetCodeKey
|
||||
GetContractAddressKey = types.GetContractAddressKey
|
||||
GetContractStorePrefixKey = types.GetContractStorePrefix
|
||||
NewCodeInfo = types.NewCodeInfo
|
||||
NewAbsoluteTxPosition = types.NewAbsoluteTxPosition
|
||||
NewContractInfo = types.NewContractInfo
|
||||
NewEnv = types.NewEnv
|
||||
NewWasmCoins = types.NewWasmCoins
|
||||
DefaultWasmConfig = types.DefaultWasmConfig
|
||||
DefaultParams = types.DefaultParams
|
||||
InitGenesis = keeper.InitGenesis
|
||||
ExportGenesis = keeper.ExportGenesis
|
||||
NewMessageHandler = keeper.NewDefaultMessageHandler
|
||||
DefaultEncoders = keeper.DefaultEncoders
|
||||
EncodeBankMsg = keeper.EncodeBankMsg
|
||||
NoCustomMsg = keeper.NoCustomMsg
|
||||
EncodeStakingMsg = keeper.EncodeStakingMsg
|
||||
EncodeWasmMsg = keeper.EncodeWasmMsg
|
||||
NewKeeper = keeper.NewKeeper
|
||||
DefaultQueryPlugins = keeper.DefaultQueryPlugins
|
||||
BankQuerier = keeper.BankQuerier
|
||||
NoCustomQuerier = keeper.NoCustomQuerier
|
||||
StakingQuerier = keeper.StakingQuerier
|
||||
WasmQuerier = keeper.WasmQuerier
|
||||
CreateTestInput = keeper.CreateTestInput
|
||||
TestHandler = keeper.TestHandler
|
||||
NewWasmProposalHandler = keeper.NewWasmProposalHandler
|
||||
NewQuerier = keeper.Querier
|
||||
ContractFromPortID = keeper.ContractFromPortID
|
||||
WithWasmEngine = keeper.WithWasmEngine
|
||||
NewCountTXDecorator = keeper.NewCountTXDecorator
|
||||
|
||||
// variable aliases
|
||||
ModuleCdc = types.ModuleCdc
|
||||
DefaultCodespace = types.DefaultCodespace
|
||||
ErrCreateFailed = types.ErrCreateFailed
|
||||
ErrAccountExists = types.ErrAccountExists
|
||||
ErrInstantiateFailed = types.ErrInstantiateFailed
|
||||
ErrExecuteFailed = types.ErrExecuteFailed
|
||||
ErrGasLimit = types.ErrGasLimit
|
||||
ErrInvalidGenesis = types.ErrInvalidGenesis
|
||||
ErrNotFound = types.ErrNotFound
|
||||
ErrQueryFailed = types.ErrQueryFailed
|
||||
ErrInvalidMsg = types.ErrInvalidMsg
|
||||
KeyLastCodeID = types.KeyLastCodeID
|
||||
KeyLastInstanceID = types.KeyLastInstanceID
|
||||
CodeKeyPrefix = types.CodeKeyPrefix
|
||||
ContractKeyPrefix = types.ContractKeyPrefix
|
||||
ContractStorePrefix = types.ContractStorePrefix
|
||||
EnableAllProposals = types.EnableAllProposals
|
||||
DisableAllProposals = types.DisableAllProposals
|
||||
)
|
||||
|
||||
type (
|
||||
ProposalType = types.ProposalType
|
||||
GenesisState = types.GenesisState
|
||||
Code = types.Code
|
||||
Contract = types.Contract
|
||||
MsgStoreCode = types.MsgStoreCode
|
||||
MsgStoreCodeResponse = types.MsgStoreCodeResponse
|
||||
MsgInstantiateContract = types.MsgInstantiateContract
|
||||
MsgInstantiateContract2 = types.MsgInstantiateContract2
|
||||
MsgInstantiateContractResponse = types.MsgInstantiateContractResponse
|
||||
MsgExecuteContract = types.MsgExecuteContract
|
||||
MsgExecuteContractResponse = types.MsgExecuteContractResponse
|
||||
MsgMigrateContract = types.MsgMigrateContract
|
||||
MsgMigrateContractResponse = types.MsgMigrateContractResponse
|
||||
MsgUpdateAdmin = types.MsgUpdateAdmin
|
||||
MsgUpdateAdminResponse = types.MsgUpdateAdminResponse
|
||||
MsgClearAdmin = types.MsgClearAdmin
|
||||
MsgWasmIBCCall = types.MsgIBCSend
|
||||
MsgClearAdminResponse = types.MsgClearAdminResponse
|
||||
MsgServer = types.MsgServer
|
||||
Model = types.Model
|
||||
CodeInfo = types.CodeInfo
|
||||
ContractInfo = types.ContractInfo
|
||||
CreatedAt = types.AbsoluteTxPosition
|
||||
Config = types.WasmConfig
|
||||
CodeInfoResponse = types.CodeInfoResponse
|
||||
MessageHandler = keeper.SDKMessageHandler
|
||||
BankEncoder = keeper.BankEncoder
|
||||
CustomEncoder = keeper.CustomEncoder
|
||||
StakingEncoder = keeper.StakingEncoder
|
||||
WasmEncoder = keeper.WasmEncoder
|
||||
MessageEncoders = keeper.MessageEncoders
|
||||
Keeper = keeper.Keeper
|
||||
QueryHandler = keeper.QueryHandler
|
||||
CustomQuerier = keeper.CustomQuerier
|
||||
QueryPlugins = keeper.QueryPlugins
|
||||
Option = keeper.Option
|
||||
)
|
834
x/wasm/client/cli/gov_tx.go
Normal file
@ -0,0 +1,834 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/version"
|
||||
"github.com/cosmos/cosmos-sdk/x/gov/client/cli"
|
||||
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func ProposalStoreCodeCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "wasm-store [wasm file] --title [text] --description [text] --run-as [address] --unpin-code [unpin_code] --source [source] --builder [builder] --code-hash [code_hash]",
|
||||
Short: "Submit a wasm binary proposal",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := parseStoreCodeArgs(args[0], clientCtx.FromAddress, cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runAs, err := cmd.Flags().GetString(flagRunAs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("run-as: %s", err)
|
||||
}
|
||||
if len(runAs) == 0 {
|
||||
return errors.New("run-as address is required")
|
||||
}
|
||||
|
||||
unpinCode, err := cmd.Flags().GetBool(flagUnpinCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source, builder, codeHash, err := parseVerificationFlags(src.WASMByteCode, cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := types.StoreCodeProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
RunAs: runAs,
|
||||
WASMByteCode: src.WASMByteCode,
|
||||
InstantiatePermission: src.InstantiatePermission,
|
||||
UnpinCode: unpinCode,
|
||||
Source: source,
|
||||
Builder: builder,
|
||||
CodeHash: codeHash,
|
||||
}
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
cmd.Flags().String(flagRunAs, "", "The address that is stored as code creator")
|
||||
cmd.Flags().Bool(flagUnpinCode, false, "Unpin code on upload, optional")
|
||||
cmd.Flags().String(flagSource, "", "Code Source URL is a valid absolute HTTPS URI to the contract's source code,")
|
||||
cmd.Flags().String(flagBuilder, "", "Builder is a valid docker image name with tag, such as \"cosmwasm/workspace-optimizer:0.12.9\"")
|
||||
cmd.Flags().BytesHex(flagCodeHash, nil, "CodeHash is the sha256 hash of the wasm code")
|
||||
addInstantiatePermissionFlags(cmd)
|
||||
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parseVerificationFlags(wasm []byte, flags *flag.FlagSet) (string, string, []byte, error) {
|
||||
source, err := flags.GetString(flagSource)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("source: %s", err)
|
||||
}
|
||||
builder, err := flags.GetString(flagBuilder)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("builder: %s", err)
|
||||
}
|
||||
codeHash, err := flags.GetBytesHex(flagCodeHash)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("codeHash: %s", err)
|
||||
}
|
||||
|
||||
// if any set require others to be set
|
||||
if len(source) != 0 || len(builder) != 0 || len(codeHash) != 0 {
|
||||
if source == "" {
|
||||
return "", "", nil, fmt.Errorf("source is required")
|
||||
}
|
||||
if _, err = url.ParseRequestURI(source); err != nil {
|
||||
return "", "", nil, fmt.Errorf("source: %s", err)
|
||||
}
|
||||
if builder == "" {
|
||||
return "", "", nil, fmt.Errorf("builder is required")
|
||||
}
|
||||
if _, err := reference.ParseDockerRef(builder); err != nil {
|
||||
return "", "", nil, fmt.Errorf("builder: %s", err)
|
||||
}
|
||||
if len(codeHash) == 0 {
|
||||
return "", "", nil, fmt.Errorf("code hash is required")
|
||||
}
|
||||
// wasm is unzipped in parseStoreCodeArgs
|
||||
// checksum generation will be decoupled here
|
||||
// reference https://github.com/CosmWasm/wasmvm/issues/359
|
||||
checksum := sha256.Sum256(wasm)
|
||||
if !bytes.Equal(checksum[:], codeHash) {
|
||||
return "", "", nil, fmt.Errorf("code-hash mismatch: %X, checksum: %X", codeHash, checksum)
|
||||
}
|
||||
}
|
||||
return source, builder, codeHash, nil
|
||||
}
|
||||
|
||||
func ProposalInstantiateContractCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "instantiate-contract [code_id_int64] [json_encoded_init_args] --label [text] --title [text] --description [text] --run-as [address] --admin [address,optional] --amount [coins,optional]",
|
||||
Short: "Submit an instantiate wasm contract proposal",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := parseInstantiateArgs(args[0], args[1], clientCtx.Keyring, clientCtx.FromAddress, cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runAs, err := cmd.Flags().GetString(flagRunAs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("run-as: %s", err)
|
||||
}
|
||||
if len(runAs) == 0 {
|
||||
return errors.New("run-as address is required")
|
||||
}
|
||||
|
||||
content := types.InstantiateContractProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
RunAs: runAs,
|
||||
Admin: src.Admin,
|
||||
CodeID: src.CodeID,
|
||||
Label: src.Label,
|
||||
Msg: src.Msg,
|
||||
Funds: src.Funds,
|
||||
}
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation")
|
||||
cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists")
|
||||
cmd.Flags().String(flagAdmin, "", "Address or key name of an admin")
|
||||
cmd.Flags().String(flagRunAs, "", "The address that pays the init funds. It is the creator of the contract and passed to the contract as sender on proposal execution")
|
||||
cmd.Flags().Bool(flagNoAdmin, false, "You must set this explicitly if you don't want an admin")
|
||||
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ProposalInstantiateContract2Cmd() *cobra.Command {
|
||||
decoder := newArgDecoder(hex.DecodeString)
|
||||
cmd := &cobra.Command{
|
||||
Use: "instantiate-contract-2 [code_id_int64] [json_encoded_init_args] [salt] --label [text] --title [text] --description [text] --run-as [address] --admin [address,optional] --amount [coins,optional] --fix-msg [bool,optional]",
|
||||
Short: "Submit an instantiate wasm contract proposal with predictable address",
|
||||
Args: cobra.ExactArgs(3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := parseInstantiateArgs(args[0], args[1], clientCtx.Keyring, clientCtx.FromAddress, cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runAs, err := cmd.Flags().GetString(flagRunAs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("run-as: %s", err)
|
||||
}
|
||||
if len(runAs) == 0 {
|
||||
return errors.New("run-as address is required")
|
||||
}
|
||||
|
||||
salt, err := decoder.DecodeString(args[2])
|
||||
if err != nil {
|
||||
return fmt.Errorf("salt: %w", err)
|
||||
}
|
||||
|
||||
fixMsg, err := cmd.Flags().GetBool(flagFixMsg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fix msg: %w", err)
|
||||
}
|
||||
|
||||
content := types.NewInstantiateContract2Proposal(proposalTitle, proposalDescr, runAs, src.Admin, src.CodeID, src.Label, src.Msg, src.Funds, salt, fixMsg)
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation")
|
||||
cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists")
|
||||
cmd.Flags().String(flagAdmin, "", "Address of an admin")
|
||||
cmd.Flags().String(flagRunAs, "", "The address that pays the init funds. It is the creator of the contract and passed to the contract as sender on proposal execution")
|
||||
cmd.Flags().Bool(flagNoAdmin, false, "You must set this explicitly if you don't want an admin")
|
||||
cmd.Flags().Bool(flagFixMsg, false, "An optional flag to include the json_encoded_init_args for the predictable address generation mode")
|
||||
decoder.RegisterFlags(cmd.PersistentFlags(), "salt")
|
||||
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ProposalStoreAndInstantiateContractCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "store-instantiate [wasm file] [json_encoded_init_args] --label [text] --title [text] --description [text] --run-as [address]" +
|
||||
"--unpin-code [unpin_code,optional] --source [source,optional] --builder [builder,optional] --code-hash [code_hash,optional] --admin [address,optional] --amount [coins,optional]",
|
||||
Short: "Submit and instantiate a wasm contract proposal",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := parseStoreCodeArgs(args[0], clientCtx.FromAddress, cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runAs, err := cmd.Flags().GetString(flagRunAs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("run-as: %s", err)
|
||||
}
|
||||
if len(runAs) == 0 {
|
||||
return errors.New("run-as address is required")
|
||||
}
|
||||
|
||||
unpinCode, err := cmd.Flags().GetBool(flagUnpinCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source, builder, codeHash, err := parseVerificationFlags(src.WASMByteCode, cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amountStr, err := cmd.Flags().GetString(flagAmount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("amount: %s", err)
|
||||
}
|
||||
amount, err := sdk.ParseCoinsNormalized(amountStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("amount: %s", err)
|
||||
}
|
||||
label, err := cmd.Flags().GetString(flagLabel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("label: %s", err)
|
||||
}
|
||||
if label == "" {
|
||||
return errors.New("label is required on all contracts")
|
||||
}
|
||||
adminStr, err := cmd.Flags().GetString(flagAdmin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("admin: %s", err)
|
||||
}
|
||||
noAdmin, err := cmd.Flags().GetBool(flagNoAdmin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("no-admin: %s", err)
|
||||
}
|
||||
|
||||
// ensure sensible admin is set (or explicitly immutable)
|
||||
if adminStr == "" && !noAdmin {
|
||||
return fmt.Errorf("you must set an admin or explicitly pass --no-admin to make it immutible (wasmd issue #719)")
|
||||
}
|
||||
if adminStr != "" && noAdmin {
|
||||
return fmt.Errorf("you set an admin and passed --no-admin, those cannot both be true")
|
||||
}
|
||||
|
||||
if adminStr != "" {
|
||||
addr, err := sdk.AccAddressFromBech32(adminStr)
|
||||
if err != nil {
|
||||
info, err := clientCtx.Keyring.Key(adminStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("admin %s", err)
|
||||
}
|
||||
adminStr = info.GetAddress().String()
|
||||
} else {
|
||||
adminStr = addr.String()
|
||||
}
|
||||
}
|
||||
|
||||
content := types.StoreAndInstantiateContractProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
RunAs: runAs,
|
||||
WASMByteCode: src.WASMByteCode,
|
||||
InstantiatePermission: src.InstantiatePermission,
|
||||
UnpinCode: unpinCode,
|
||||
Source: source,
|
||||
Builder: builder,
|
||||
CodeHash: codeHash,
|
||||
Admin: adminStr,
|
||||
Label: label,
|
||||
Msg: []byte(args[1]),
|
||||
Funds: amount,
|
||||
}
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
cmd.Flags().String(flagRunAs, "", "The address that is stored as code creator. It is the creator of the contract and passed to the contract as sender on proposal execution")
|
||||
cmd.Flags().Bool(flagUnpinCode, false, "Unpin code on upload, optional")
|
||||
cmd.Flags().String(flagSource, "", "Code Source URL is a valid absolute HTTPS URI to the contract's source code,")
|
||||
cmd.Flags().String(flagBuilder, "", "Builder is a valid docker image name with tag, such as \"cosmwasm/workspace-optimizer:0.12.9\"")
|
||||
cmd.Flags().BytesHex(flagCodeHash, nil, "CodeHash is the sha256 hash of the wasm code")
|
||||
cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation")
|
||||
cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists")
|
||||
cmd.Flags().String(flagAdmin, "", "Address or key name of an admin")
|
||||
cmd.Flags().Bool(flagNoAdmin, false, "You must set this explicitly if you don't want an admin")
|
||||
addInstantiatePermissionFlags(cmd)
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ProposalMigrateContractCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "migrate-contract [contract_addr_bech32] [new_code_id_int64] [json_encoded_migration_args]",
|
||||
Short: "Submit a migrate wasm contract to a new code version proposal",
|
||||
Args: cobra.ExactArgs(3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := parseMigrateContractArgs(args, clientCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := types.MigrateContractProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
Contract: src.Contract,
|
||||
CodeID: src.CodeID,
|
||||
Msg: src.Msg,
|
||||
}
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ProposalExecuteContractCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "execute-contract [contract_addr_bech32] [json_encoded_migration_args]",
|
||||
Short: "Submit a execute wasm contract proposal (run by any address)",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contract := args[0]
|
||||
execMsg := []byte(args[1])
|
||||
amountStr, err := cmd.Flags().GetString(flagAmount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("amount: %s", err)
|
||||
}
|
||||
funds, err := sdk.ParseCoinsNormalized(amountStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("amount: %s", err)
|
||||
}
|
||||
runAs, err := cmd.Flags().GetString(flagRunAs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("run-as: %s", err)
|
||||
}
|
||||
|
||||
if len(runAs) == 0 {
|
||||
return errors.New("run-as address is required")
|
||||
}
|
||||
|
||||
content := types.ExecuteContractProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
Contract: contract,
|
||||
Msg: execMsg,
|
||||
RunAs: runAs,
|
||||
Funds: funds,
|
||||
}
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
cmd.Flags().String(flagRunAs, "", "The address that is passed as sender to the contract on proposal execution")
|
||||
cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation")
|
||||
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ProposalSudoContractCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "sudo-contract [contract_addr_bech32] [json_encoded_migration_args]",
|
||||
Short: "Submit a sudo wasm contract proposal (to call privileged commands)",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contract := args[0]
|
||||
sudoMsg := []byte(args[1])
|
||||
|
||||
content := types.SudoContractProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
Contract: contract,
|
||||
Msg: sudoMsg,
|
||||
}
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
// proposal flagsExecute
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ProposalUpdateContractAdminCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "set-contract-admin [contract_addr_bech32] [new_admin_addr_bech32]",
|
||||
Short: "Submit a new admin for a contract proposal",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := parseUpdateContractAdminArgs(args, clientCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := types.UpdateAdminProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
Contract: src.Contract,
|
||||
NewAdmin: src.NewAdmin,
|
||||
}
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ProposalClearContractAdminCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "clear-contract-admin [contract_addr_bech32]",
|
||||
Short: "Submit a clear admin for a contract to prevent further migrations proposal",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := types.ClearAdminProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
Contract: args[0],
|
||||
}
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ProposalPinCodesCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "pin-codes [code-ids]",
|
||||
Short: "Submit a pin code proposal for pinning a code to cache",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
codeIds, err := parsePinCodesArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := types.PinCodesProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
CodeIDs: codeIds,
|
||||
}
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parsePinCodesArgs(args []string) ([]uint64, error) {
|
||||
codeIDs := make([]uint64, len(args))
|
||||
for i, c := range args {
|
||||
codeID, err := strconv.ParseUint(c, 10, 64)
|
||||
if err != nil {
|
||||
return codeIDs, fmt.Errorf("code IDs: %s", err)
|
||||
}
|
||||
codeIDs[i] = codeID
|
||||
}
|
||||
return codeIDs, nil
|
||||
}
|
||||
|
||||
func ProposalUnpinCodesCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "unpin-codes [code-ids]",
|
||||
Short: "Submit a unpin code proposal for unpinning a code to cache",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
codeIds, err := parsePinCodesArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := types.UnpinCodesProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
CodeIDs: codeIds,
|
||||
}
|
||||
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parseAccessConfig(raw string) (c types.AccessConfig, err error) {
|
||||
switch raw {
|
||||
case "nobody":
|
||||
return types.AllowNobody, nil
|
||||
case "everybody":
|
||||
return types.AllowEverybody, nil
|
||||
default:
|
||||
parts := strings.Split(raw, ",")
|
||||
addrs := make([]sdk.AccAddress, len(parts))
|
||||
for i, v := range parts {
|
||||
addr, err := sdk.AccAddressFromBech32(v)
|
||||
if err != nil {
|
||||
return types.AccessConfig{}, fmt.Errorf("unable to parse address %q: %s", v, err)
|
||||
}
|
||||
addrs[i] = addr
|
||||
}
|
||||
defer func() { // convert panic in ".With" to error for better output
|
||||
if r := recover(); r != nil {
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
cfg := types.AccessTypeAnyOfAddresses.With(addrs...)
|
||||
return cfg, cfg.ValidateBasic()
|
||||
}
|
||||
}
|
||||
|
||||
func parseAccessConfigUpdates(args []string) ([]types.AccessConfigUpdate, error) {
|
||||
updates := make([]types.AccessConfigUpdate, len(args))
|
||||
for i, c := range args {
|
||||
// format: code_id:access_config
|
||||
// access_config: nobody|everybody|address(es)
|
||||
parts := strings.Split(c, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid format")
|
||||
}
|
||||
|
||||
codeID, err := strconv.ParseUint(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid code ID: %s", err)
|
||||
}
|
||||
|
||||
accessConfig, err := parseAccessConfig(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updates[i] = types.AccessConfigUpdate{
|
||||
CodeID: codeID,
|
||||
InstantiatePermission: accessConfig,
|
||||
}
|
||||
}
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
func ProposalUpdateInstantiateConfigCmd() *cobra.Command {
|
||||
bech32Prefix := sdk.GetConfig().GetBech32AccountAddrPrefix()
|
||||
cmd := &cobra.Command{
|
||||
Use: "update-instantiate-config [code-id:permission]...",
|
||||
Short: "Submit an update instantiate config proposal.",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Long: strings.TrimSpace(
|
||||
fmt.Sprintf(`Submit an update instantiate config proposal for multiple code ids.
|
||||
|
||||
Example:
|
||||
$ %s tx gov submit-proposal update-instantiate-config 1:nobody 2:everybody 3:%s1l2rsakp388kuv9k8qzq6lrm9taddae7fpx59wm,%s1vx8knpllrj7n963p9ttd80w47kpacrhuts497x
|
||||
`, version.AppName, bech32Prefix, bech32Prefix)),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updates, err := parseAccessConfigUpdates(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := types.UpdateInstantiateConfigProposal{
|
||||
Title: proposalTitle,
|
||||
Description: proposalDescr,
|
||||
AccessConfigUpdates: updates,
|
||||
}
|
||||
msg, err := govv1beta1.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
// proposal flags
|
||||
cmd.Flags().String(cli.FlagTitle, "", "Title of proposal")
|
||||
cmd.Flags().String(cli.FlagDescription, "", "Description of proposal")
|
||||
cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func getProposalInfo(cmd *cobra.Command) (client.Context, string, string, sdk.Coins, error) {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return client.Context{}, "", "", nil, err
|
||||
}
|
||||
|
||||
proposalTitle, err := cmd.Flags().GetString(cli.FlagTitle)
|
||||
if err != nil {
|
||||
return clientCtx, proposalTitle, "", nil, err
|
||||
}
|
||||
|
||||
|
||||
proposalDescr, err := cmd.Flags().GetString(cli.FlagDescription)
|
||||
if err != nil {
|
||||
return client.Context{}, proposalTitle, proposalDescr, nil, err
|
||||
}
|
||||
![]() Variable
|
||||
|
||||
depositArg, err := cmd.Flags().GetString(cli.FlagDeposit)
|
||||
if err != nil {
|
||||
return client.Context{}, proposalTitle, proposalDescr, nil, err
|
||||
}
|
||||
|
||||
deposit, err := sdk.ParseCoinsNormalized(depositArg)
|
||||
if err != nil {
|
||||
return client.Context{}, proposalTitle, proposalDescr, deposit, err
|
||||
}
|
||||
![]() Variable
|
||||
|
||||
return clientCtx, proposalTitle, proposalDescr, deposit, nil
|
||||
}
|
158
x/wasm/client/cli/gov_tx_test.go
Normal file
@ -0,0 +1,158 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestParseAccessConfigUpdates(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
src []string
|
||||
exp []types.AccessConfigUpdate
|
||||
expErr bool
|
||||
}{
|
||||
"nobody": {
|
||||
src: []string{"1:nobody"},
|
||||
exp: []types.AccessConfigUpdate{{
|
||||
CodeID: 1,
|
||||
InstantiatePermission: types.AccessConfig{Permission: types.AccessTypeNobody},
|
||||
}},
|
||||
},
|
||||
"everybody": {
|
||||
src: []string{"1:everybody"},
|
||||
exp: []types.AccessConfigUpdate{{
|
||||
CodeID: 1,
|
||||
InstantiatePermission: types.AccessConfig{Permission: types.AccessTypeEverybody},
|
||||
}},
|
||||
},
|
||||
"any of addresses - single": {
|
||||
src: []string{"1:cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x"},
|
||||
exp: []types.AccessConfigUpdate{
|
||||
{
|
||||
CodeID: 1,
|
||||
InstantiatePermission: types.AccessConfig{
|
||||
Permission: types.AccessTypeAnyOfAddresses,
|
||||
Addresses: []string{"cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"any of addresses - multiple": {
|
||||
src: []string{"1:cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x,cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"},
|
||||
exp: []types.AccessConfigUpdate{
|
||||
{
|
||||
CodeID: 1,
|
||||
InstantiatePermission: types.AccessConfig{
|
||||
Permission: types.AccessTypeAnyOfAddresses,
|
||||
Addresses: []string{"cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x", "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"multiple code ids with different permissions": {
|
||||
src: []string{"1:cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x,cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", "2:nobody"},
|
||||
exp: []types.AccessConfigUpdate{
|
||||
{
|
||||
CodeID: 1,
|
||||
InstantiatePermission: types.AccessConfig{
|
||||
Permission: types.AccessTypeAnyOfAddresses,
|
||||
Addresses: []string{"cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x", "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"},
|
||||
},
|
||||
}, {
|
||||
CodeID: 2,
|
||||
InstantiatePermission: types.AccessConfig{
|
||||
Permission: types.AccessTypeNobody,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"any of addresses - empty list": {
|
||||
src: []string{"1:"},
|
||||
expErr: true,
|
||||
},
|
||||
"any of addresses - invalid address": {
|
||||
src: []string{"1:foo"},
|
||||
expErr: true,
|
||||
},
|
||||
"any of addresses - duplicate address": {
|
||||
src: []string{"1:cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x,cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x"},
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, gotErr := parseAccessConfigUpdates(spec.src)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
assert.Equal(t, spec.exp, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCodeInfoFlags(t *testing.T) {
|
||||
correctSource := "https://github.com/CosmWasm/wasmd/blob/main/x/wasm/keeper/testdata/hackatom.wasm"
|
||||
correctBuilderRef := "cosmwasm/workspace-optimizer:0.12.9"
|
||||
|
||||
wasmBin, err := os.ReadFile("../../keeper/testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
checksumStr := "beb3de5e9b93b52e514c74ce87ccddb594b9bcd33b7f1af1bb6da63fc883917b"
|
||||
|
||||
specs := map[string]struct {
|
||||
args []string
|
||||
expErr bool
|
||||
}{
|
||||
"source missing": {
|
||||
args: []string{"--builder=" + correctBuilderRef, "--code-hash=" + checksumStr},
|
||||
expErr: true,
|
||||
},
|
||||
"builder missing": {
|
||||
args: []string{"--code-source-url=" + correctSource, "--code-hash=" + checksumStr},
|
||||
expErr: true,
|
||||
},
|
||||
"code hash missing": {
|
||||
args: []string{"--code-source-url=" + correctSource, "--builder=" + correctBuilderRef},
|
||||
expErr: true,
|
||||
},
|
||||
"source format wrong": {
|
||||
args: []string{"--code-source-url=" + "format_wrong", "--builder=" + correctBuilderRef, "--code-hash=" + checksumStr},
|
||||
expErr: true,
|
||||
},
|
||||
"builder format wrong": {
|
||||
args: []string{"--code-source-url=" + correctSource, "--builder=" + "format//", "--code-hash=" + checksumStr},
|
||||
expErr: true,
|
||||
},
|
||||
"code hash wrong": {
|
||||
args: []string{"--code-source-url=" + correctSource, "--builder=" + correctBuilderRef, "--code-hash=" + "AA"},
|
||||
expErr: true,
|
||||
},
|
||||
"happy path, none set": {
|
||||
args: []string{},
|
||||
expErr: false,
|
||||
},
|
||||
"happy path all set": {
|
||||
args: []string{"--code-source-url=" + correctSource, "--builder=" + correctBuilderRef, "--code-hash=" + checksumStr},
|
||||
expErr: false,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
flags := ProposalStoreCodeCmd().Flags()
|
||||
require.NoError(t, flags.Parse(spec.args))
|
||||
_, _, _, gotErr := parseVerificationFlags(wasmBin, flags)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
})
|
||||
}
|
||||
}
|
163
x/wasm/client/cli/new_tx.go
Normal file
@ -0,0 +1,163 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// MigrateContractCmd will migrate a contract to a new code version
|
||||
func MigrateContractCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "migrate [contract_addr_bech32] [new_code_id_int64] [json_encoded_migration_args]",
|
||||
Short: "Migrate a wasm contract to a new code version",
|
||||
Aliases: []string{"update", "mig", "m"},
|
||||
Args: cobra.ExactArgs(3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := parseMigrateContractArgs(args, clientCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddTxFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parseMigrateContractArgs(args []string, cliCtx client.Context) (types.MsgMigrateContract, error) {
|
||||
// get the id of the code to instantiate
|
||||
codeID, err := strconv.ParseUint(args[1], 10, 64)
|
||||
if err != nil {
|
||||
return types.MsgMigrateContract{}, sdkerrors.Wrap(err, "code id")
|
||||
}
|
||||
|
||||
migrateMsg := args[2]
|
||||
|
||||
msg := types.MsgMigrateContract{
|
||||
Sender: cliCtx.GetFromAddress().String(),
|
||||
Contract: args[0],
|
||||
CodeID: codeID,
|
||||
Msg: []byte(migrateMsg),
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// UpdateContractAdminCmd sets an new admin for a contract
|
||||
func UpdateContractAdminCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "set-contract-admin [contract_addr_bech32] [new_admin_addr_bech32]",
|
||||
Short: "Set new admin for a contract",
|
||||
Aliases: []string{"new-admin", "admin", "set-adm", "sa"},
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := parseUpdateContractAdminArgs(args, clientCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddTxFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parseUpdateContractAdminArgs(args []string, cliCtx client.Context) (types.MsgUpdateAdmin, error) {
|
||||
msg := types.MsgUpdateAdmin{
|
||||
Sender: cliCtx.GetFromAddress().String(),
|
||||
Contract: args[0],
|
||||
NewAdmin: args[1],
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// ClearContractAdminCmd clears an admin for a contract
|
||||
func ClearContractAdminCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "clear-contract-admin [contract_addr_bech32]",
|
||||
Short: "Clears admin for a contract to prevent further migrations",
|
||||
Aliases: []string{"clear-admin", "clr-adm"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := types.MsgClearAdmin{
|
||||
Sender: clientCtx.GetFromAddress().String(),
|
||||
Contract: args[0],
|
||||
}
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddTxFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// UpdateInstantiateConfigCmd updates instantiate config for a smart contract.
|
||||
func UpdateInstantiateConfigCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update-instantiate-config [code_id_int64]",
|
||||
Short: "Update instantiate config for a codeID",
|
||||
Aliases: []string{"update-instantiate-config"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
codeID, err := strconv.ParseUint(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
perm, err := parseAccessConfigFlags(cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := types.MsgUpdateInstantiateConfig{
|
||||
Sender: string(clientCtx.GetFromAddress()),
|
||||
CodeID: codeID,
|
||||
NewInstantiatePermission: perm,
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
addInstantiatePermissionFlags(cmd)
|
||||
flags.AddTxFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
674
x/wasm/client/cli/query.go
Normal file
@ -0,0 +1,674 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
wasmvm "github.com/CosmWasm/wasmvm"
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/spf13/cobra"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func GetQueryCmd() *cobra.Command {
|
||||
queryCmd := &cobra.Command{
|
||||
Use: types.ModuleName,
|
||||
Short: "Querying commands for the wasm module",
|
||||
DisableFlagParsing: true,
|
||||
SuggestionsMinimumDistance: 2,
|
||||
RunE: client.ValidateCmd,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
queryCmd.AddCommand(
|
||||
GetCmdListCode(),
|
||||
GetCmdListContractByCode(),
|
||||
GetCmdQueryCode(),
|
||||
GetCmdQueryCodeInfo(),
|
||||
GetCmdGetContractInfo(),
|
||||
GetCmdGetContractHistory(),
|
||||
GetCmdGetContractState(),
|
||||
GetCmdListPinnedCode(),
|
||||
GetCmdLibVersion(),
|
||||
GetCmdQueryParams(),
|
||||
GetCmdBuildAddress(),
|
||||
GetCmdListContractsByCreator(),
|
||||
)
|
||||
return queryCmd
|
||||
}
|
||||
|
||||
// GetCmdLibVersion gets current libwasmvm version.
|
||||
func GetCmdLibVersion() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "libwasmvm-version",
|
||||
Short: "Get libwasmvm version",
|
||||
Long: "Get libwasmvm version",
|
||||
Aliases: []string{"lib-version"},
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
version, err := wasmvm.LibwasmvmVersion()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error retrieving libwasmvm version: %w", err)
|
||||
}
|
||||
fmt.Println(version)
|
||||
return nil
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdBuildAddress build a contract address
|
||||
func GetCmdBuildAddress() *cobra.Command {
|
||||
decoder := newArgDecoder(hex.DecodeString)
|
||||
cmd := &cobra.Command{
|
||||
Use: "build-address [code-hash] [creator-address] [salt-hex-encoded] [json_encoded_init_args (required when set as fixed)]",
|
||||
Short: "build contract address",
|
||||
Aliases: []string{"address"},
|
||||
Args: cobra.RangeArgs(3, 4),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
codeHash, err := hex.DecodeString(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("code-hash: %s", err)
|
||||
}
|
||||
creator, err := sdk.AccAddressFromBech32(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("creator: %s", err)
|
||||
}
|
||||
salt, err := hex.DecodeString(args[2])
|
||||
switch {
|
||||
case err != nil:
|
||||
return fmt.Errorf("salt: %s", err)
|
||||
case len(salt) == 0:
|
||||
return errors.New("empty salt")
|
||||
}
|
||||
|
||||
if len(args) == 3 {
|
||||
cmd.Println(keeper.BuildContractAddressPredictable(codeHash, creator, salt, []byte{}).String())
|
||||
return nil
|
||||
}
|
||||
msg := types.RawContractMessage(args[3])
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return fmt.Errorf("init message: %s", err)
|
||||
}
|
||||
cmd.Println(keeper.BuildContractAddressPredictable(codeHash, creator, salt, msg).String())
|
||||
return nil
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
decoder.RegisterFlags(cmd.PersistentFlags(), "salt")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdListCode lists all wasm code uploaded
|
||||
func GetCmdListCode() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list-code",
|
||||
Short: "List all wasm bytecode on the chain",
|
||||
Long: "List all wasm bytecode on the chain",
|
||||
Aliases: []string{"list-codes", "codes", "lco"},
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.Codes(
|
||||
context.Background(),
|
||||
&types.QueryCodesRequest{
|
||||
Pagination: pageReq,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clientCtx.PrintProto(res)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
flags.AddPaginationFlagsToCmd(cmd, "list codes")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdListContractByCode lists all wasm code uploaded for given code id
|
||||
func GetCmdListContractByCode() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list-contract-by-code [code_id]",
|
||||
Short: "List wasm all bytecode on the chain for given code id",
|
||||
Long: "List wasm all bytecode on the chain for given code id",
|
||||
Aliases: []string{"list-contracts-by-code", "list-contracts", "contracts", "lca"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
codeID, err := strconv.ParseUint(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if codeID == 0 {
|
||||
return errors.New("empty code id")
|
||||
}
|
||||
|
||||
pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.ContractsByCode(
|
||||
context.Background(),
|
||||
&types.QueryContractsByCodeRequest{
|
||||
CodeId: codeID,
|
||||
Pagination: pageReq,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clientCtx.PrintProto(res)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
flags.AddPaginationFlagsToCmd(cmd, "list contracts by code")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdQueryCode returns the bytecode for a given contract
|
||||
func GetCmdQueryCode() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "code [code_id] [output filename]",
|
||||
Short: "Downloads wasm bytecode for given code id",
|
||||
Long: "Downloads wasm bytecode for given code id",
|
||||
Aliases: []string{"source-code", "source"},
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
codeID, err := strconv.ParseUint(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.Code(
|
||||
context.Background(),
|
||||
&types.QueryCodeRequest{
|
||||
CodeId: codeID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(res.Data) == 0 {
|
||||
return fmt.Errorf("contract not found")
|
||||
}
|
||||
|
||||
fmt.Printf("Downloading wasm code to %s\n", args[1])
|
||||
return os.WriteFile(args[1], res.Data, 0o600)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdQueryCodeInfo returns the code info for a given code id
|
||||
func GetCmdQueryCodeInfo() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "code-info [code_id]",
|
||||
Short: "Prints out metadata of a code id",
|
||||
Long: "Prints out metadata of a code id",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
codeID, err := strconv.ParseUint(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.Code(
|
||||
context.Background(),
|
||||
&types.QueryCodeRequest{
|
||||
CodeId: codeID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.CodeInfoResponse == nil {
|
||||
return fmt.Errorf("contract not found")
|
||||
}
|
||||
|
||||
return clientCtx.PrintProto(res.CodeInfoResponse)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdGetContractInfo gets details about a given contract
|
||||
func GetCmdGetContractInfo() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "contract [bech32_address]",
|
||||
Short: "Prints out metadata of a contract given its address",
|
||||
Long: "Prints out metadata of a contract given its address",
|
||||
Aliases: []string{"meta", "c"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sdk.AccAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.ContractInfo(
|
||||
context.Background(),
|
||||
&types.QueryContractInfoRequest{
|
||||
Address: args[0],
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clientCtx.PrintProto(res)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdGetContractState dumps full internal state of a given contract
|
||||
func GetCmdGetContractState() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "contract-state",
|
||||
Short: "Querying commands for the wasm module",
|
||||
Aliases: []string{"state", "cs", "s"},
|
||||
DisableFlagParsing: true,
|
||||
SuggestionsMinimumDistance: 2,
|
||||
RunE: client.ValidateCmd,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
GetCmdGetContractStateAll(),
|
||||
GetCmdGetContractStateRaw(),
|
||||
GetCmdGetContractStateSmart(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func GetCmdGetContractStateAll() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "all [bech32_address]",
|
||||
Short: "Prints out all internal state of a contract given its address",
|
||||
Long: "Prints out all internal state of a contract given its address",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sdk.AccAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.AllContractState(
|
||||
context.Background(),
|
||||
&types.QueryAllContractStateRequest{
|
||||
Address: args[0],
|
||||
Pagination: pageReq,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clientCtx.PrintProto(res)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
flags.AddPaginationFlagsToCmd(cmd, "contract state")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func GetCmdGetContractStateRaw() *cobra.Command {
|
||||
decoder := newArgDecoder(hex.DecodeString)
|
||||
cmd := &cobra.Command{
|
||||
Use: "raw [bech32_address] [key]",
|
||||
Short: "Prints out internal state for key of a contract given its address",
|
||||
Long: "Prints out internal state for of a contract given its address",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sdk.AccAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryData, err := decoder.DecodeString(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.RawContractState(
|
||||
context.Background(),
|
||||
&types.QueryRawContractStateRequest{
|
||||
Address: args[0],
|
||||
QueryData: queryData,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clientCtx.PrintProto(res)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
decoder.RegisterFlags(cmd.PersistentFlags(), "key argument")
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func GetCmdGetContractStateSmart() *cobra.Command {
|
||||
decoder := newArgDecoder(asciiDecodeString)
|
||||
cmd := &cobra.Command{
|
||||
Use: "smart [bech32_address] [query]",
|
||||
Short: "Calls contract with given address with query data and prints the returned result",
|
||||
Long: "Calls contract with given address with query data and prints the returned result",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sdk.AccAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if args[1] == "" {
|
||||
return errors.New("query data must not be empty")
|
||||
}
|
||||
|
||||
queryData, err := decoder.DecodeString(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode query: %s", err)
|
||||
}
|
||||
if !json.Valid(queryData) {
|
||||
return errors.New("query data must be json")
|
||||
}
|
||||
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.SmartContractState(
|
||||
context.Background(),
|
||||
&types.QuerySmartContractStateRequest{
|
||||
Address: args[0],
|
||||
QueryData: queryData,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clientCtx.PrintProto(res)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
decoder.RegisterFlags(cmd.PersistentFlags(), "query argument")
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdGetContractHistory prints the code history for a given contract
|
||||
func GetCmdGetContractHistory() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "contract-history [bech32_address]",
|
||||
Short: "Prints out the code history for a contract given its address",
|
||||
Long: "Prints out the code history for a contract given its address",
|
||||
Aliases: []string{"history", "hist", "ch"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sdk.AccAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.ContractHistory(
|
||||
context.Background(),
|
||||
&types.QueryContractHistoryRequest{
|
||||
Address: args[0],
|
||||
Pagination: pageReq,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return clientCtx.PrintProto(res)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
flags.AddPaginationFlagsToCmd(cmd, "contract history")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdListPinnedCode lists all wasm code ids that are pinned
|
||||
func GetCmdListPinnedCode() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "pinned",
|
||||
Short: "List all pinned code ids",
|
||||
Long: "List all pinned code ids",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.PinnedCodes(
|
||||
context.Background(),
|
||||
&types.QueryPinnedCodesRequest{
|
||||
Pagination: pageReq,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clientCtx.PrintProto(res)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
flags.AddPaginationFlagsToCmd(cmd, "list codes")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdListContractsByCreator lists all contracts by creator
|
||||
func GetCmdListContractsByCreator() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list-contracts-by-creator [creator]",
|
||||
Short: "List all contracts by creator",
|
||||
Long: "List all contracts by creator",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = sdk.AccAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
res, err := queryClient.ContractsByCreator(
|
||||
context.Background(),
|
||||
&types.QueryContractsByCreatorRequest{
|
||||
CreatorAddress: args[0],
|
||||
Pagination: pageReq,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clientCtx.PrintProto(res)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type argumentDecoder struct {
|
||||
// dec is the default decoder
|
||||
dec func(string) ([]byte, error)
|
||||
asciiF, hexF, b64F bool
|
||||
}
|
||||
|
||||
func newArgDecoder(def func(string) ([]byte, error)) *argumentDecoder {
|
||||
return &argumentDecoder{dec: def}
|
||||
}
|
||||
|
||||
func (a *argumentDecoder) RegisterFlags(f *flag.FlagSet, argName string) {
|
||||
f.BoolVar(&a.asciiF, "ascii", false, "ascii encoded "+argName)
|
||||
f.BoolVar(&a.hexF, "hex", false, "hex encoded "+argName)
|
||||
f.BoolVar(&a.b64F, "b64", false, "base64 encoded "+argName)
|
||||
}
|
||||
|
||||
func (a *argumentDecoder) DecodeString(s string) ([]byte, error) {
|
||||
found := -1
|
||||
for i, v := range []*bool{&a.asciiF, &a.hexF, &a.b64F} {
|
||||
if !*v {
|
||||
continue
|
||||
}
|
||||
if found != -1 {
|
||||
return nil, errors.New("multiple decoding flags used")
|
||||
}
|
||||
found = i
|
||||
}
|
||||
switch found {
|
||||
case 0:
|
||||
return asciiDecodeString(s)
|
||||
case 1:
|
||||
return hex.DecodeString(s)
|
||||
case 2:
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
default:
|
||||
return a.dec(s)
|
||||
}
|
||||
}
|
||||
|
||||
func asciiDecodeString(s string) ([]byte, error) {
|
||||
return []byte(s), nil
|
||||
}
|
||||
|
||||
// sdk ReadPageRequest expects binary but we encoded to base64 in our marshaller
|
||||
func withPageKeyDecoded(flagSet *flag.FlagSet) *flag.FlagSet {
|
||||
encoded, err := flagSet.GetString(flags.FlagPageKey)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
err = flagSet.Set(flags.FlagPageKey, string(raw))
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return flagSet
|
||||
}
|
||||
|
||||
// GetCmdQueryParams implements a command to return the current wasm
|
||||
// parameters.
|
||||
func GetCmdQueryParams() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "params",
|
||||
Short: "Query the current wasm parameters",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
|
||||
params := &types.QueryParamsRequest{}
|
||||
res, err := queryClient.Params(cmd.Context(), params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return clientCtx.PrintProto(&res.Params)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
544
x/wasm/client/cli/tx.go
Normal file
@ -0,0 +1,544 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keyring"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/version"
|
||||
"github.com/cosmos/cosmos-sdk/x/authz"
|
||||
"github.com/spf13/cobra"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/ioutils"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
const (
|
||||
flagAmount = "amount"
|
||||
flagLabel = "label"
|
||||
flagSource = "code-source-url"
|
||||
flagBuilder = "builder"
|
||||
flagCodeHash = "code-hash"
|
||||
flagAdmin = "admin"
|
||||
flagNoAdmin = "no-admin"
|
||||
flagFixMsg = "fix-msg"
|
||||
flagRunAs = "run-as"
|
||||
flagInstantiateByEverybody = "instantiate-everybody"
|
||||
flagInstantiateNobody = "instantiate-nobody"
|
||||
flagInstantiateByAddress = "instantiate-only-address"
|
||||
flagInstantiateByAnyOfAddress = "instantiate-anyof-addresses"
|
||||
flagUnpinCode = "unpin-code"
|
||||
flagAllowedMsgKeys = "allow-msg-keys"
|
||||
flagAllowedRawMsgs = "allow-raw-msgs"
|
||||
flagExpiration = "expiration"
|
||||
flagMaxCalls = "max-calls"
|
||||
flagMaxFunds = "max-funds"
|
||||
flagAllowAllMsgs = "allow-all-messages"
|
||||
flagNoTokenTransfer = "no-token-transfer" //nolint:gosec
|
||||
)
|
||||
|
||||
// GetTxCmd returns the transaction commands for this module
|
||||
func GetTxCmd() *cobra.Command {
|
||||
txCmd := &cobra.Command{
|
||||
Use: types.ModuleName,
|
||||
Short: "Wasm transaction subcommands",
|
||||
DisableFlagParsing: true,
|
||||
SuggestionsMinimumDistance: 2,
|
||||
RunE: client.ValidateCmd,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
txCmd.AddCommand(
|
||||
StoreCodeCmd(),
|
||||
InstantiateContractCmd(),
|
||||
InstantiateContract2Cmd(),
|
||||
ExecuteContractCmd(),
|
||||
MigrateContractCmd(),
|
||||
UpdateContractAdminCmd(),
|
||||
ClearContractAdminCmd(),
|
||||
GrantAuthorizationCmd(),
|
||||
UpdateInstantiateConfigCmd(),
|
||||
)
|
||||
return txCmd
|
||||
}
|
||||
|
||||
// StoreCodeCmd will upload code to be reused.
|
||||
func StoreCodeCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "store [wasm file]",
|
||||
Short: "Upload a wasm binary",
|
||||
Aliases: []string{"upload", "st", "s"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg, err := parseStoreCodeArgs(args[0], clientCtx.GetFromAddress(), cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
addInstantiatePermissionFlags(cmd)
|
||||
flags.AddTxFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parseStoreCodeArgs(file string, sender sdk.AccAddress, flags *flag.FlagSet) (types.MsgStoreCode, error) {
|
||||
wasm, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return types.MsgStoreCode{}, err
|
||||
}
|
||||
|
||||
// gzip the wasm file
|
||||
if ioutils.IsWasm(wasm) {
|
||||
wasm, err = ioutils.GzipIt(wasm)
|
||||
|
||||
if err != nil {
|
||||
return types.MsgStoreCode{}, err
|
||||
}
|
||||
} else if !ioutils.IsGzip(wasm) {
|
||||
return types.MsgStoreCode{}, fmt.Errorf("invalid input file. Use wasm binary or gzip")
|
||||
}
|
||||
|
||||
perm, err := parseAccessConfigFlags(flags)
|
||||
if err != nil {
|
||||
return types.MsgStoreCode{}, err
|
||||
}
|
||||
|
||||
msg := types.MsgStoreCode{
|
||||
Sender: sender.String(),
|
||||
WASMByteCode: wasm,
|
||||
InstantiatePermission: perm,
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func parseAccessConfigFlags(flags *flag.FlagSet) (*types.AccessConfig, error) {
|
||||
addrs, err := flags.GetStringSlice(flagInstantiateByAnyOfAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("flag any of: %s", err)
|
||||
}
|
||||
if len(addrs) != 0 {
|
||||
acceptedAddrs := make([]sdk.AccAddress, len(addrs))
|
||||
for i, v := range addrs {
|
||||
acceptedAddrs[i], err = sdk.AccAddressFromBech32(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse %q: %w", v, err)
|
||||
}
|
||||
}
|
||||
x := types.AccessTypeAnyOfAddresses.With(acceptedAddrs...)
|
||||
return &x, nil
|
||||
}
|
||||
|
||||
onlyAddrStr, err := flags.GetString(flagInstantiateByAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("instantiate by address: %s", err)
|
||||
}
|
||||
if onlyAddrStr != "" {
|
||||
return nil, fmt.Errorf("not supported anymore. Use: %s", flagInstantiateByAnyOfAddress)
|
||||
}
|
||||
everybodyStr, err := flags.GetString(flagInstantiateByEverybody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("instantiate by everybody: %s", err)
|
||||
}
|
||||
if everybodyStr != "" {
|
||||
ok, err := strconv.ParseBool(everybodyStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("boolean value expected for instantiate by everybody: %s", err)
|
||||
}
|
||||
if ok {
|
||||
return &types.AllowEverybody, nil
|
||||
}
|
||||
}
|
||||
|
||||
nobodyStr, err := flags.GetString(flagInstantiateNobody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("instantiate by nobody: %s", err)
|
||||
}
|
||||
if nobodyStr != "" {
|
||||
ok, err := strconv.ParseBool(nobodyStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("boolean value expected for instantiate by nobody: %s", err)
|
||||
}
|
||||
if ok {
|
||||
return &types.AllowNobody, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func addInstantiatePermissionFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().String(flagInstantiateByEverybody, "", "Everybody can instantiate a contract from the code, optional")
|
||||
cmd.Flags().String(flagInstantiateNobody, "", "Nobody except the governance process can instantiate a contract from the code, optional")
|
||||
cmd.Flags().String(flagInstantiateByAddress, "", fmt.Sprintf("Removed: use %s instead", flagInstantiateByAnyOfAddress))
|
||||
cmd.Flags().StringSlice(flagInstantiateByAnyOfAddress, []string{}, "Any of the addresses can instantiate a contract from the code, optional")
|
||||
}
|
||||
|
||||
// InstantiateContractCmd will instantiate a contract from previously uploaded code.
|
||||
func InstantiateContractCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "instantiate [code_id_int64] [json_encoded_init_args] --label [text] --admin [address,optional] --amount [coins,optional] ",
|
||||
Short: "Instantiate a wasm contract",
|
||||
Long: fmt.Sprintf(`Creates a new instance of an uploaded wasm code with the given 'constructor' message.
|
||||
Each contract instance has a unique address assigned.
|
||||
Example:
|
||||
$ %s tx wasm instantiate 1 '{"foo":"bar"}' --admin="$(%s keys show mykey -a)" \
|
||||
--from mykey --amount="100ustake" --label "local0.1.0"
|
||||
`, version.AppName, version.AppName),
|
||||
Aliases: []string{"start", "init", "inst", "i"},
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg, err := parseInstantiateArgs(args[0], args[1], clientCtx.Keyring, clientCtx.GetFromAddress(), cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation")
|
||||
cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists")
|
||||
cmd.Flags().String(flagAdmin, "", "Address or key name of an admin")
|
||||
cmd.Flags().Bool(flagNoAdmin, false, "You must set this explicitly if you don't want an admin")
|
||||
flags.AddTxFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// InstantiateContract2Cmd will instantiate a contract from previously uploaded code with predicable address generated
|
||||
func InstantiateContract2Cmd() *cobra.Command {
|
||||
decoder := newArgDecoder(hex.DecodeString)
|
||||
cmd := &cobra.Command{
|
||||
Use: "instantiate2 [code_id_int64] [json_encoded_init_args] [salt] --label [text] --admin [address,optional] --amount [coins,optional] " +
|
||||
"--fix-msg [bool,optional]",
|
||||
Short: "Instantiate a wasm contract with predictable address",
|
||||
Long: fmt.Sprintf(`Creates a new instance of an uploaded wasm code with the given 'constructor' message.
|
||||
Each contract instance has a unique address assigned. They are assigned automatically but in order to have predictable addresses
|
||||
for special use cases, the given 'salt' argument and '--fix-msg' parameters can be used to generate a custom address.
|
||||
|
||||
Predictable address example (also see '%s query wasm build-address -h'):
|
||||
$ %s tx wasm instantiate2 1 '{"foo":"bar"}' $(echo -n "testing" | xxd -ps) --admin="$(%s keys show mykey -a)" \
|
||||
--from mykey --amount="100ustake" --label "local0.1.0" \
|
||||
--fix-msg
|
||||
`, version.AppName, version.AppName, version.AppName),
|
||||
Aliases: []string{"start", "init", "inst", "i"},
|
||||
Args: cobra.ExactArgs(3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
salt, err := decoder.DecodeString(args[2])
|
||||
if err != nil {
|
||||
return fmt.Errorf("salt: %w", err)
|
||||
}
|
||||
fixMsg, err := cmd.Flags().GetBool(flagFixMsg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fix msg: %w", err)
|
||||
}
|
||||
data, err := parseInstantiateArgs(args[0], args[1], clientCtx.Keyring, clientCtx.GetFromAddress(), cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg := &types.MsgInstantiateContract2{
|
||||
Sender: data.Sender,
|
||||
Admin: data.Admin,
|
||||
CodeID: data.CodeID,
|
||||
Label: data.Label,
|
||||
Msg: data.Msg,
|
||||
Funds: data.Funds,
|
||||
Salt: salt,
|
||||
FixMsg: fixMsg,
|
||||
}
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation")
|
||||
cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists")
|
||||
cmd.Flags().String(flagAdmin, "", "Address or key name of an admin")
|
||||
cmd.Flags().Bool(flagNoAdmin, false, "You must set this explicitly if you don't want an admin")
|
||||
cmd.Flags().Bool(flagFixMsg, false, "An optional flag to include the json_encoded_init_args for the predictable address generation mode")
|
||||
decoder.RegisterFlags(cmd.PersistentFlags(), "salt")
|
||||
flags.AddTxFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parseInstantiateArgs(rawCodeID, initMsg string, kr keyring.Keyring, sender sdk.AccAddress, flags *flag.FlagSet) (*types.MsgInstantiateContract, error) {
|
||||
// get the id of the code to instantiate
|
||||
codeID, err := strconv.ParseUint(rawCodeID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amountStr, err := flags.GetString(flagAmount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("amount: %s", err)
|
||||
}
|
||||
amount, err := sdk.ParseCoinsNormalized(amountStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("amount: %s", err)
|
||||
}
|
||||
label, err := flags.GetString(flagLabel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("label: %s", err)
|
||||
}
|
||||
if label == "" {
|
||||
return nil, errors.New("label is required on all contracts")
|
||||
}
|
||||
adminStr, err := flags.GetString(flagAdmin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("admin: %s", err)
|
||||
}
|
||||
|
||||
noAdmin, err := flags.GetBool(flagNoAdmin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no-admin: %s", err)
|
||||
}
|
||||
|
||||
// ensure sensible admin is set (or explicitly immutable)
|
||||
if adminStr == "" && !noAdmin {
|
||||
return nil, fmt.Errorf("you must set an admin or explicitly pass --no-admin to make it immutible (wasmd issue #719)")
|
||||
}
|
||||
if adminStr != "" && noAdmin {
|
||||
return nil, fmt.Errorf("you set an admin and passed --no-admin, those cannot both be true")
|
||||
}
|
||||
|
||||
if adminStr != "" {
|
||||
addr, err := sdk.AccAddressFromBech32(adminStr)
|
||||
if err != nil {
|
||||
info, err := kr.Key(adminStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("admin %s", err)
|
||||
}
|
||||
adminStr = info.GetAddress().String()
|
||||
} else {
|
||||
adminStr = addr.String()
|
||||
}
|
||||
}
|
||||
|
||||
// build and sign the transaction, then broadcast to Tendermint
|
||||
msg := types.MsgInstantiateContract{
|
||||
Sender: sender.String(),
|
||||
CodeID: codeID,
|
||||
Label: label,
|
||||
Funds: amount,
|
||||
Msg: []byte(initMsg),
|
||||
Admin: adminStr,
|
||||
}
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// ExecuteContractCmd will instantiate a contract from previously uploaded code.
|
||||
func ExecuteContractCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "execute [contract_addr_bech32] [json_encoded_send_args] --amount [coins,optional]",
|
||||
Short: "Execute a command on a wasm contract",
|
||||
Aliases: []string{"run", "call", "exec", "ex", "e"},
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := parseExecuteArgs(args[0], args[1], clientCtx.GetFromAddress(), cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
cmd.Flags().String(flagAmount, "", "Coins to send to the contract along with command")
|
||||
flags.AddTxFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parseExecuteArgs(contractAddr string, execMsg string, sender sdk.AccAddress, flags *flag.FlagSet) (types.MsgExecuteContract, error) {
|
||||
amountStr, err := flags.GetString(flagAmount)
|
||||
if err != nil {
|
||||
return types.MsgExecuteContract{}, fmt.Errorf("amount: %s", err)
|
||||
}
|
||||
|
||||
amount, err := sdk.ParseCoinsNormalized(amountStr)
|
||||
if err != nil {
|
||||
return types.MsgExecuteContract{}, err
|
||||
}
|
||||
|
||||
return types.MsgExecuteContract{
|
||||
Sender: sender.String(),
|
||||
Contract: contractAddr,
|
||||
Funds: amount,
|
||||
Msg: []byte(execMsg),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GrantAuthorizationCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "grant [grantee] [message_type=\"execution\"|\"migration\"] [contract_addr_bech32] --allow-raw-msgs [msg1,msg2,...] --allow-msg-keys [key1,key2,...] --allow-all-messages",
|
||||
Short: "Grant authorization to an address",
|
||||
Long: fmt.Sprintf(`Grant authorization to an address.
|
||||
Examples:
|
||||
$ %s tx grant <grantee_addr> execution <contract_addr> --allow-all-messages --max-calls 1 --no-token-transfer --expiration 1667979596
|
||||
|
||||
$ %s tx grant <grantee_addr> execution <contract_addr> --allow-all-messages --max-funds 100000uwasm --expiration 1667979596
|
||||
|
||||
$ %s tx grant <grantee_addr> execution <contract_addr> --allow-all-messages --max-calls 5 --max-funds 100000uwasm --expiration 1667979596
|
||||
`, version.AppName, version.AppName, version.AppName),
|
||||
Args: cobra.ExactArgs(3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
grantee, err := sdk.AccAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contract, err := sdk.AccAddressFromBech32(args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgKeys, err := cmd.Flags().GetStringSlice(flagAllowedMsgKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawMsgs, err := cmd.Flags().GetStringSlice(flagAllowedRawMsgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maxFundsStr, err := cmd.Flags().GetString(flagMaxFunds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("max funds: %s", err)
|
||||
}
|
||||
|
||||
maxCalls, err := cmd.Flags().GetUint64(flagMaxCalls)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exp, err := cmd.Flags().GetInt64(flagExpiration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exp == 0 {
|
||||
return errors.New("expiration must be set")
|
||||
}
|
||||
|
||||
allowAllMsgs, err := cmd.Flags().GetBool(flagAllowAllMsgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
noTokenTransfer, err := cmd.Flags().GetBool(flagNoTokenTransfer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var limit types.ContractAuthzLimitX
|
||||
switch {
|
||||
case maxFundsStr != "" && maxCalls != 0 && !noTokenTransfer:
|
||||
maxFunds, err := sdk.ParseCoinsNormalized(maxFundsStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("max funds: %s", err)
|
||||
}
|
||||
limit = types.NewCombinedLimit(maxCalls, maxFunds...)
|
||||
case maxFundsStr != "" && maxCalls == 0 && !noTokenTransfer:
|
||||
maxFunds, err := sdk.ParseCoinsNormalized(maxFundsStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("max funds: %s", err)
|
||||
}
|
||||
limit = types.NewMaxFundsLimit(maxFunds...)
|
||||
case maxCalls != 0 && noTokenTransfer && maxFundsStr == "":
|
||||
limit = types.NewMaxCallsLimit(maxCalls)
|
||||
default:
|
||||
return errors.New("invalid limit setup")
|
||||
}
|
||||
|
||||
var filter types.ContractAuthzFilterX
|
||||
switch {
|
||||
case allowAllMsgs && len(msgKeys) != 0 || allowAllMsgs && len(rawMsgs) != 0 || len(msgKeys) != 0 && len(rawMsgs) != 0:
|
||||
return errors.New("cannot set more than one filter within one grant")
|
||||
case allowAllMsgs:
|
||||
filter = types.NewAllowAllMessagesFilter()
|
||||
case len(msgKeys) != 0:
|
||||
filter = types.NewAcceptedMessageKeysFilter(msgKeys...)
|
||||
case len(rawMsgs) != 0:
|
||||
msgs := make([]types.RawContractMessage, len(rawMsgs))
|
||||
for i, msg := range rawMsgs {
|
||||
msgs[i] = types.RawContractMessage(msg)
|
||||
}
|
||||
filter = types.NewAcceptedMessagesFilter(msgs...)
|
||||
default:
|
||||
return errors.New("invalid filter setup")
|
||||
}
|
||||
|
||||
grant, err := types.NewContractGrant(contract, limit, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var authorization authz.Authorization
|
||||
switch args[1] {
|
||||
case "execution":
|
||||
authorization = types.NewContractExecutionAuthorization(*grant)
|
||||
case "migration":
|
||||
authorization = types.NewContractMigrationAuthorization(*grant)
|
||||
default:
|
||||
return fmt.Errorf("%s authorization type not supported", args[1])
|
||||
}
|
||||
|
||||
grantMsg, err := authz.NewMsgGrant(clientCtx.GetFromAddress(), grantee, authorization, time.Unix(0, exp))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), grantMsg)
|
||||
},
|
||||
}
|
||||
flags.AddTxFlagsToCmd(cmd)
|
||||
cmd.Flags().StringSlice(flagAllowedMsgKeys, []string{}, "Allowed msg keys")
|
||||
cmd.Flags().StringSlice(flagAllowedRawMsgs, []string{}, "Allowed raw msgs")
|
||||
cmd.Flags().Uint64(flagMaxCalls, 0, "Maximal number of calls to the contract")
|
||||
cmd.Flags().String(flagMaxFunds, "", "Maximal amount of tokens transferable to the contract.")
|
||||
cmd.Flags().Int64(flagExpiration, 0, "The Unix timestamp.")
|
||||
cmd.Flags().Bool(flagAllowAllMsgs, false, "Allow all messages")
|
||||
cmd.Flags().Bool(flagNoTokenTransfer, false, "Don't allow token transfer")
|
||||
return cmd
|
||||
}
|
59
x/wasm/client/cli/tx_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestParseAccessConfigFlags(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
args []string
|
||||
expCfg *types.AccessConfig
|
||||
expErr bool
|
||||
}{
|
||||
"nobody": {
|
||||
args: []string{"--instantiate-nobody=true"},
|
||||
expCfg: &types.AccessConfig{Permission: types.AccessTypeNobody},
|
||||
},
|
||||
"everybody": {
|
||||
args: []string{"--instantiate-everybody=true"},
|
||||
expCfg: &types.AccessConfig{Permission: types.AccessTypeEverybody},
|
||||
},
|
||||
"only address": {
|
||||
args: []string{"--instantiate-only-address=cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x"},
|
||||
expErr: true,
|
||||
},
|
||||
"only address - invalid": {
|
||||
args: []string{"--instantiate-only-address=foo"},
|
||||
expErr: true,
|
||||
},
|
||||
"any of address": {
|
||||
args: []string{"--instantiate-anyof-addresses=cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x,cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"},
|
||||
expCfg: &types.AccessConfig{Permission: types.AccessTypeAnyOfAddresses, Addresses: []string{"cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x", "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"}},
|
||||
},
|
||||
"any of address - invalid": {
|
||||
args: []string{"--instantiate-anyof-addresses=cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x,foo"},
|
||||
expErr: true,
|
||||
},
|
||||
"not set": {
|
||||
args: []string{},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
flags := StoreCodeCmd().Flags()
|
||||
require.NoError(t, flags.Parse(spec.args))
|
||||
gotCfg, gotErr := parseAccessConfigFlags(flags)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
assert.Equal(t, spec.expCfg, gotCfg)
|
||||
})
|
||||
}
|
||||
}
|
25
x/wasm/client/proposal_handler.go
Normal file
@ -0,0 +1,25 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
govclient "github.com/cosmos/cosmos-sdk/x/gov/client"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/client/cli"
|
||||
"github.com/cerc-io/laconicd/x/wasm/client/rest" //nolint:staticcheck
|
||||
)
|
||||
|
||||
// ProposalHandlers define the wasm cli proposal types and rest handler.
|
||||
// Deprecated: the rest package will be removed. You can use the GRPC gateway instead
|
||||
var ProposalHandlers = []govclient.ProposalHandler{
|
||||
govclient.NewProposalHandler(cli.ProposalStoreCodeCmd, rest.StoreCodeProposalHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalInstantiateContractCmd, rest.InstantiateProposalHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalMigrateContractCmd, rest.MigrateProposalHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalExecuteContractCmd, rest.ExecuteProposalHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalSudoContractCmd, rest.SudoProposalHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalUpdateContractAdminCmd, rest.UpdateContractAdminProposalHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalClearContractAdminCmd, rest.ClearContractAdminProposalHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalPinCodesCmd, rest.PinCodeProposalHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalUnpinCodesCmd, rest.UnpinCodeProposalHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalUpdateInstantiateConfigCmd, rest.UpdateInstantiateConfigProposalHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalStoreAndInstantiateContractCmd, rest.EmptyRestHandler),
|
||||
govclient.NewProposalHandler(cli.ProposalInstantiateContract2Cmd, rest.EmptyRestHandler),
|
||||
}
|
381
x/wasm/client/proposal_handler_test.go
Normal file
@ -0,0 +1,381 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
)
|
||||
|
||||
func TestGovRestHandlers(t *testing.T) {
|
||||
type dict map[string]interface{}
|
||||
var (
|
||||
anyAddress = "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz"
|
||||
aBaseReq = dict{
|
||||
"from": anyAddress,
|
||||
"memo": "rest test",
|
||||
"chain_id": "testing",
|
||||
"account_number": "1",
|
||||
"sequence": "1",
|
||||
"fees": []dict{{"denom": "ustake", "amount": "1000000"}},
|
||||
}
|
||||
)
|
||||
encodingConfig := keeper.MakeEncodingConfig(t)
|
||||
clientCtx := client.Context{}.
|
||||
WithCodec(encodingConfig.Marshaler).
|
||||
WithTxConfig(encodingConfig.TxConfig).
|
||||
WithLegacyAmino(encodingConfig.Amino).
|
||||
WithInput(os.Stdin).
|
||||
WithAccountRetriever(authtypes.AccountRetriever{}).
|
||||
WithBroadcastMode(flags.BroadcastBlock).
|
||||
WithChainID("testing")
|
||||
|
||||
// router setup as in gov/client/rest/tx.go
|
||||
propSubRtr := mux.NewRouter().PathPrefix("/gov/proposals").Subrouter()
|
||||
for _, ph := range ProposalHandlers {
|
||||
r := ph.RESTHandler(clientCtx)
|
||||
propSubRtr.HandleFunc(fmt.Sprintf("/%s", r.SubRoute), r.Handler).Methods("POST")
|
||||
}
|
||||
|
||||
specs := map[string]struct {
|
||||
srcBody dict
|
||||
srcPath string
|
||||
expCode int
|
||||
}{
|
||||
"store-code": {
|
||||
srcPath: "/gov/proposals/wasm_store_code",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "store-code",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"wasm_byte_code": []byte("valid wasm byte code"),
|
||||
"source": "https://example.com/",
|
||||
"builder": "cosmwasm/workspace-optimizer:v0.12.9",
|
||||
"code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D",
|
||||
"instantiate_permission": dict{
|
||||
"permission": "OnlyAddress",
|
||||
"address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusOK,
|
||||
},
|
||||
"store-code without verification info": {
|
||||
srcPath: "/gov/proposals/wasm_store_code",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "store-code",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"wasm_byte_code": []byte("valid wasm byte code"),
|
||||
"instantiate_permission": dict{
|
||||
"permission": "OnlyAddress",
|
||||
"address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusOK,
|
||||
},
|
||||
"store-code without permission": {
|
||||
srcPath: "/gov/proposals/wasm_store_code",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "store-code",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"wasm_byte_code": []byte("valid wasm byte code"),
|
||||
"source": "https://example.com/",
|
||||
"builder": "cosmwasm/workspace-optimizer:v0.12.9",
|
||||
"code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D",
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusOK,
|
||||
},
|
||||
"store-code invalid permission": {
|
||||
srcPath: "/gov/proposals/wasm_store_code",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "store-code",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"wasm_byte_code": []byte("valid wasm byte code"),
|
||||
"source": "https://example.com/",
|
||||
"builder": "cosmwasm/workspace-optimizer:v0.12.9",
|
||||
"code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D",
|
||||
"instantiate_permission": dict{
|
||||
"permission": "Nobody",
|
||||
"address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusBadRequest,
|
||||
},
|
||||
"store-code with incomplete proposal data: blank title": {
|
||||
srcPath: "/gov/proposals/wasm_store_code",
|
||||
srcBody: dict{
|
||||
"title": "",
|
||||
"description": "My proposal",
|
||||
"type": "store-code",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"wasm_byte_code": []byte("valid wasm byte code"),
|
||||
"source": "https://example.com/",
|
||||
"code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D",
|
||||
"builder": "cosmwasm/workspace-optimizer:v0.12.9",
|
||||
"instantiate_permission": dict{
|
||||
"permission": "OnlyAddress",
|
||||
"address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusBadRequest,
|
||||
},
|
||||
"store-code with incomplete content data: no wasm_byte_code": {
|
||||
srcPath: "/gov/proposals/wasm_store_code",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "store-code",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"wasm_byte_code": "",
|
||||
"builder": "cosmwasm/workspace-optimizer:v0.12.9",
|
||||
"source": "https://example.com/",
|
||||
"code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D",
|
||||
"instantiate_permission": dict{
|
||||
"permission": "OnlyAddress",
|
||||
"address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusBadRequest,
|
||||
},
|
||||
"store-code with incomplete content data: no builder": {
|
||||
srcPath: "/gov/proposals/wasm_store_code",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "store-code",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"wasm_byte_code": "",
|
||||
"source": "https://example.com/",
|
||||
"code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D",
|
||||
"instantiate_permission": dict{
|
||||
"permission": "OnlyAddress",
|
||||
"address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusBadRequest,
|
||||
},
|
||||
"store-code with incomplete content data: no code hash": {
|
||||
srcPath: "/gov/proposals/wasm_store_code",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "store-code",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"wasm_byte_code": "",
|
||||
"builder": "cosmwasm/workspace-optimizer:v0.12.9",
|
||||
"source": "https://example.com/",
|
||||
"instantiate_permission": dict{
|
||||
"permission": "OnlyAddress",
|
||||
"address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusBadRequest,
|
||||
},
|
||||
"store-code with incomplete content data: no source": {
|
||||
srcPath: "/gov/proposals/wasm_store_code",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "store-code",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"wasm_byte_code": "",
|
||||
"builder": "cosmwasm/workspace-optimizer:v0.12.9",
|
||||
"code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D",
|
||||
"instantiate_permission": dict{
|
||||
"permission": "OnlyAddress",
|
||||
"address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusBadRequest,
|
||||
},
|
||||
"instantiate contract": {
|
||||
srcPath: "/gov/proposals/wasm_instantiate",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "instantiate",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"admin": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"code_id": "1",
|
||||
"label": "https://example.com/",
|
||||
"msg": dict{"recipient": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz"},
|
||||
"funds": []dict{{"denom": "ustake", "amount": "100"}},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusOK,
|
||||
},
|
||||
"migrate contract": {
|
||||
srcPath: "/gov/proposals/wasm_migrate",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "migrate",
|
||||
"contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr",
|
||||
"code_id": "1",
|
||||
"msg": dict{"foo": "bar"},
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusOK,
|
||||
},
|
||||
"execute contract": {
|
||||
srcPath: "/gov/proposals/wasm_execute",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "migrate",
|
||||
"contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr",
|
||||
"msg": dict{"foo": "bar"},
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusOK,
|
||||
},
|
||||
"execute contract fails with no run_as": {
|
||||
srcPath: "/gov/proposals/wasm_execute",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "migrate",
|
||||
"contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr",
|
||||
"msg": dict{"foo": "bar"},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusBadRequest,
|
||||
},
|
||||
"execute contract fails with no message": {
|
||||
srcPath: "/gov/proposals/wasm_execute",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "migrate",
|
||||
"contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr",
|
||||
"run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusBadRequest,
|
||||
},
|
||||
"sudo contract": {
|
||||
srcPath: "/gov/proposals/wasm_sudo",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "migrate",
|
||||
"contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr",
|
||||
"msg": dict{"foo": "bar"},
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusOK,
|
||||
},
|
||||
"sudo contract fails with no message": {
|
||||
srcPath: "/gov/proposals/wasm_sudo",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "migrate",
|
||||
"contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr",
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusBadRequest,
|
||||
},
|
||||
"update contract admin": {
|
||||
srcPath: "/gov/proposals/wasm_update_admin",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "migrate",
|
||||
"contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr",
|
||||
"new_admin": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz",
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusOK,
|
||||
},
|
||||
"clear contract admin": {
|
||||
srcPath: "/gov/proposals/wasm_clear_admin",
|
||||
srcBody: dict{
|
||||
"title": "Test Proposal",
|
||||
"description": "My proposal",
|
||||
"type": "migrate",
|
||||
"contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr",
|
||||
"deposit": []dict{{"denom": "ustake", "amount": "10"}},
|
||||
"proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"base_req": aBaseReq,
|
||||
},
|
||||
expCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
src, err := json.Marshal(spec.srcBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
// when
|
||||
r := httptest.NewRequest("POST", spec.srcPath, bytes.NewReader(src))
|
||||
w := httptest.NewRecorder()
|
||||
propSubRtr.ServeHTTP(w, r)
|
||||
|
||||
// then
|
||||
require.Equal(t, spec.expCode, w.Code, w.Body.String())
|
||||
})
|
||||
}
|
||||
}
|
547
x/wasm/client/rest/gov.go
Normal file
@ -0,0 +1,547 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest"
|
||||
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
type StoreCodeProposalJSONReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Proposer string `json:"proposer" yaml:"proposer"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
|
||||
RunAs string `json:"run_as" yaml:"run_as"`
|
||||
// WASMByteCode can be raw or gzip compressed
|
||||
WASMByteCode []byte `json:"wasm_byte_code" yaml:"wasm_byte_code"`
|
||||
// InstantiatePermission to apply on contract creation, optional
|
||||
InstantiatePermission *types.AccessConfig `json:"instantiate_permission" yaml:"instantiate_permission"`
|
||||
|
||||
// UnpinCode indicates if the code should not be pinned as part of the proposal.
|
||||
UnpinCode bool `json:"unpin_code" yaml:"unpin_code"`
|
||||
|
||||
// Source is the URL where the code is hosted
|
||||
Source string `json:"source" yaml:"source"`
|
||||
// Builder is the docker image used to build the code deterministically, used for smart
|
||||
// contract verification
|
||||
Builder string `json:"builder" yaml:"builder"`
|
||||
// CodeHash is the SHA256 sum of the code outputted by optimizer, used for smart contract verification
|
||||
CodeHash []byte `json:"code_hash" yaml:"code_hash"`
|
||||
}
|
||||
|
||||
func (s StoreCodeProposalJSONReq) Content() govtypes.Content {
|
||||
return &types.StoreCodeProposal{
|
||||
Title: s.Title,
|
||||
Description: s.Description,
|
||||
RunAs: s.RunAs,
|
||||
WASMByteCode: s.WASMByteCode,
|
||||
InstantiatePermission: s.InstantiatePermission,
|
||||
UnpinCode: s.UnpinCode,
|
||||
Source: s.Source,
|
||||
Builder: s.Builder,
|
||||
CodeHash: s.CodeHash,
|
||||
}
|
||||
}
|
||||
|
||||
func (s StoreCodeProposalJSONReq) GetProposer() string {
|
||||
return s.Proposer
|
||||
}
|
||||
|
||||
func (s StoreCodeProposalJSONReq) GetDeposit() sdk.Coins {
|
||||
return s.Deposit
|
||||
}
|
||||
|
||||
func (s StoreCodeProposalJSONReq) GetBaseReq() rest.BaseReq {
|
||||
return s.BaseReq
|
||||
}
|
||||
|
||||
func StoreCodeProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "wasm_store_code",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
var req StoreCodeProposalJSONReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
toStdTxResponse(cliCtx, w, req)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type InstantiateProposalJSONReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
|
||||
Proposer string `json:"proposer" yaml:"proposer"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
|
||||
RunAs string `json:"run_as" yaml:"run_as"`
|
||||
// Admin is an optional address that can execute migrations
|
||||
Admin string `json:"admin,omitempty" yaml:"admin"`
|
||||
Code uint64 `json:"code_id" yaml:"code_id"`
|
||||
Label string `json:"label" yaml:"label"`
|
||||
Msg json.RawMessage `json:"msg" yaml:"msg"`
|
||||
Funds sdk.Coins `json:"funds" yaml:"funds"`
|
||||
}
|
||||
|
||||
func (s InstantiateProposalJSONReq) Content() govtypes.Content {
|
||||
return &types.InstantiateContractProposal{
|
||||
Title: s.Title,
|
||||
Description: s.Description,
|
||||
RunAs: s.RunAs,
|
||||
Admin: s.Admin,
|
||||
CodeID: s.Code,
|
||||
Label: s.Label,
|
||||
Msg: types.RawContractMessage(s.Msg),
|
||||
Funds: s.Funds,
|
||||
}
|
||||
}
|
||||
|
||||
func (s InstantiateProposalJSONReq) GetProposer() string {
|
||||
return s.Proposer
|
||||
}
|
||||
|
||||
func (s InstantiateProposalJSONReq) GetDeposit() sdk.Coins {
|
||||
return s.Deposit
|
||||
}
|
||||
|
||||
func (s InstantiateProposalJSONReq) GetBaseReq() rest.BaseReq {
|
||||
return s.BaseReq
|
||||
}
|
||||
|
||||
func InstantiateProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "wasm_instantiate",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
var req InstantiateProposalJSONReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
toStdTxResponse(cliCtx, w, req)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type MigrateProposalJSONReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
|
||||
Proposer string `json:"proposer" yaml:"proposer"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
|
||||
Contract string `json:"contract" yaml:"contract"`
|
||||
Code uint64 `json:"code_id" yaml:"code_id"`
|
||||
Msg json.RawMessage `json:"msg" yaml:"msg"`
|
||||
}
|
||||
|
||||
func (s MigrateProposalJSONReq) Content() govtypes.Content {
|
||||
return &types.MigrateContractProposal{
|
||||
Title: s.Title,
|
||||
Description: s.Description,
|
||||
Contract: s.Contract,
|
||||
CodeID: s.Code,
|
||||
Msg: types.RawContractMessage(s.Msg),
|
||||
}
|
||||
}
|
||||
|
||||
func (s MigrateProposalJSONReq) GetProposer() string {
|
||||
return s.Proposer
|
||||
}
|
||||
|
||||
func (s MigrateProposalJSONReq) GetDeposit() sdk.Coins {
|
||||
return s.Deposit
|
||||
}
|
||||
|
||||
func (s MigrateProposalJSONReq) GetBaseReq() rest.BaseReq {
|
||||
return s.BaseReq
|
||||
}
|
||||
|
||||
func MigrateProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "wasm_migrate",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
var req MigrateProposalJSONReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
toStdTxResponse(cliCtx, w, req)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ExecuteProposalJSONReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
|
||||
Proposer string `json:"proposer" yaml:"proposer"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
|
||||
Contract string `json:"contract" yaml:"contract"`
|
||||
Msg json.RawMessage `json:"msg" yaml:"msg"`
|
||||
// RunAs is the role that is passed to the contract's environment
|
||||
RunAs string `json:"run_as" yaml:"run_as"`
|
||||
Funds sdk.Coins `json:"funds" yaml:"funds"`
|
||||
}
|
||||
|
||||
func (s ExecuteProposalJSONReq) Content() govtypes.Content {
|
||||
return &types.ExecuteContractProposal{
|
||||
Title: s.Title,
|
||||
Description: s.Description,
|
||||
Contract: s.Contract,
|
||||
Msg: types.RawContractMessage(s.Msg),
|
||||
RunAs: s.RunAs,
|
||||
Funds: s.Funds,
|
||||
}
|
||||
}
|
||||
|
||||
func (s ExecuteProposalJSONReq) GetProposer() string {
|
||||
return s.Proposer
|
||||
}
|
||||
|
||||
func (s ExecuteProposalJSONReq) GetDeposit() sdk.Coins {
|
||||
return s.Deposit
|
||||
}
|
||||
|
||||
func (s ExecuteProposalJSONReq) GetBaseReq() rest.BaseReq {
|
||||
return s.BaseReq
|
||||
}
|
||||
|
||||
func ExecuteProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "wasm_execute",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
var req ExecuteProposalJSONReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
toStdTxResponse(cliCtx, w, req)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type SudoProposalJSONReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
|
||||
Proposer string `json:"proposer" yaml:"proposer"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
|
||||
Contract string `json:"contract" yaml:"contract"`
|
||||
Msg json.RawMessage `json:"msg" yaml:"msg"`
|
||||
}
|
||||
|
||||
func (s SudoProposalJSONReq) Content() govtypes.Content {
|
||||
return &types.SudoContractProposal{
|
||||
Title: s.Title,
|
||||
Description: s.Description,
|
||||
Contract: s.Contract,
|
||||
Msg: types.RawContractMessage(s.Msg),
|
||||
}
|
||||
}
|
||||
|
||||
func (s SudoProposalJSONReq) GetProposer() string {
|
||||
return s.Proposer
|
||||
}
|
||||
|
||||
func (s SudoProposalJSONReq) GetDeposit() sdk.Coins {
|
||||
return s.Deposit
|
||||
}
|
||||
|
||||
func (s SudoProposalJSONReq) GetBaseReq() rest.BaseReq {
|
||||
return s.BaseReq
|
||||
}
|
||||
|
||||
func SudoProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "wasm_sudo",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
var req SudoProposalJSONReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
toStdTxResponse(cliCtx, w, req)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateAdminJSONReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
|
||||
Proposer string `json:"proposer" yaml:"proposer"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
|
||||
NewAdmin string `json:"new_admin" yaml:"new_admin"`
|
||||
Contract string `json:"contract" yaml:"contract"`
|
||||
}
|
||||
|
||||
func (s UpdateAdminJSONReq) Content() govtypes.Content {
|
||||
return &types.UpdateAdminProposal{
|
||||
Title: s.Title,
|
||||
Description: s.Description,
|
||||
Contract: s.Contract,
|
||||
NewAdmin: s.NewAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
func (s UpdateAdminJSONReq) GetProposer() string {
|
||||
return s.Proposer
|
||||
}
|
||||
|
||||
func (s UpdateAdminJSONReq) GetDeposit() sdk.Coins {
|
||||
return s.Deposit
|
||||
}
|
||||
|
||||
func (s UpdateAdminJSONReq) GetBaseReq() rest.BaseReq {
|
||||
return s.BaseReq
|
||||
}
|
||||
|
||||
func UpdateContractAdminProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "wasm_update_admin",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
var req UpdateAdminJSONReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
toStdTxResponse(cliCtx, w, req)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ClearAdminJSONReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
|
||||
Proposer string `json:"proposer" yaml:"proposer"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
|
||||
Contract string `json:"contract" yaml:"contract"`
|
||||
}
|
||||
|
||||
func (s ClearAdminJSONReq) Content() govtypes.Content {
|
||||
return &types.ClearAdminProposal{
|
||||
Title: s.Title,
|
||||
Description: s.Description,
|
||||
Contract: s.Contract,
|
||||
}
|
||||
}
|
||||
|
||||
func (s ClearAdminJSONReq) GetProposer() string {
|
||||
return s.Proposer
|
||||
}
|
||||
|
||||
func (s ClearAdminJSONReq) GetDeposit() sdk.Coins {
|
||||
return s.Deposit
|
||||
}
|
||||
|
||||
func (s ClearAdminJSONReq) GetBaseReq() rest.BaseReq {
|
||||
return s.BaseReq
|
||||
}
|
||||
|
||||
func ClearContractAdminProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "wasm_clear_admin",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
var req ClearAdminJSONReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
toStdTxResponse(cliCtx, w, req)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type PinCodeJSONReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
|
||||
Proposer string `json:"proposer" yaml:"proposer"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
|
||||
CodeIDs []uint64 `json:"code_ids" yaml:"code_ids"`
|
||||
}
|
||||
|
||||
func (s PinCodeJSONReq) Content() govtypes.Content {
|
||||
return &types.PinCodesProposal{
|
||||
Title: s.Title,
|
||||
Description: s.Description,
|
||||
CodeIDs: s.CodeIDs,
|
||||
}
|
||||
}
|
||||
|
||||
func (s PinCodeJSONReq) GetProposer() string {
|
||||
return s.Proposer
|
||||
}
|
||||
|
||||
func (s PinCodeJSONReq) GetDeposit() sdk.Coins {
|
||||
return s.Deposit
|
||||
}
|
||||
|
||||
func (s PinCodeJSONReq) GetBaseReq() rest.BaseReq {
|
||||
return s.BaseReq
|
||||
}
|
||||
|
||||
func PinCodeProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "pin_code",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
var req PinCodeJSONReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
toStdTxResponse(cliCtx, w, req)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type UnpinCodeJSONReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
|
||||
Proposer string `json:"proposer" yaml:"proposer"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
|
||||
CodeIDs []uint64 `json:"code_ids" yaml:"code_ids"`
|
||||
}
|
||||
|
||||
func (s UnpinCodeJSONReq) Content() govtypes.Content {
|
||||
return &types.UnpinCodesProposal{
|
||||
Title: s.Title,
|
||||
Description: s.Description,
|
||||
CodeIDs: s.CodeIDs,
|
||||
}
|
||||
}
|
||||
|
||||
func (s UnpinCodeJSONReq) GetProposer() string {
|
||||
return s.Proposer
|
||||
}
|
||||
|
||||
func (s UnpinCodeJSONReq) GetDeposit() sdk.Coins {
|
||||
return s.Deposit
|
||||
}
|
||||
|
||||
func (s UnpinCodeJSONReq) GetBaseReq() rest.BaseReq {
|
||||
return s.BaseReq
|
||||
}
|
||||
|
||||
func UnpinCodeProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "unpin_code",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
var req UnpinCodeJSONReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
toStdTxResponse(cliCtx, w, req)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateInstantiateConfigProposalJSONReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Proposer string `json:"proposer" yaml:"proposer"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
AccessConfigUpdates []types.AccessConfigUpdate `json:"access_config_updates" yaml:"access_config_updates"`
|
||||
}
|
||||
|
||||
func (s UpdateInstantiateConfigProposalJSONReq) Content() govtypes.Content {
|
||||
return &types.UpdateInstantiateConfigProposal{
|
||||
Title: s.Title,
|
||||
Description: s.Description,
|
||||
AccessConfigUpdates: s.AccessConfigUpdates,
|
||||
}
|
||||
}
|
||||
|
||||
func (s UpdateInstantiateConfigProposalJSONReq) GetProposer() string {
|
||||
return s.Proposer
|
||||
}
|
||||
|
||||
func (s UpdateInstantiateConfigProposalJSONReq) GetDeposit() sdk.Coins {
|
||||
return s.Deposit
|
||||
}
|
||||
|
||||
func (s UpdateInstantiateConfigProposalJSONReq) GetBaseReq() rest.BaseReq {
|
||||
return s.BaseReq
|
||||
}
|
||||
|
||||
func UpdateInstantiateConfigProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "update_instantiate_config",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
var req UpdateInstantiateConfigProposalJSONReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
toStdTxResponse(cliCtx, w, req)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type wasmProposalData interface {
|
||||
Content() govtypes.Content
|
||||
GetProposer() string
|
||||
GetDeposit() sdk.Coins
|
||||
GetBaseReq() rest.BaseReq
|
||||
}
|
||||
|
||||
func toStdTxResponse(cliCtx client.Context, w http.ResponseWriter, data wasmProposalData) {
|
||||
proposerAddr, err := sdk.AccAddressFromBech32(data.GetProposer())
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
msg, err := govtypes.NewMsgSubmitProposal(data.Content(), data.GetDeposit(), proposerAddr)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
baseReq := data.GetBaseReq().Sanitize()
|
||||
if !baseReq.ValidateBasic(w) {
|
||||
return
|
||||
}
|
||||
tx.WriteGeneratedTxResponse(cliCtx, w, baseReq, msg)
|
||||
}
|
||||
|
||||
func EmptyRestHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
|
||||
return govrest.ProposalRESTHandler{
|
||||
SubRoute: "unsupported",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, "Legacy REST Routes are not supported for gov proposals")
|
||||
},
|
||||
}
|
||||
}
|
86
x/wasm/client/rest/new_tx.go
Normal file
@ -0,0 +1,86 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func registerNewTxRoutes(cliCtx client.Context, r *mux.Router) {
|
||||
r.HandleFunc("/wasm/contract/{contractAddr}/admin", setContractAdminHandlerFn(cliCtx)).Methods("PUT")
|
||||
r.HandleFunc("/wasm/contract/{contractAddr}/code", migrateContractHandlerFn(cliCtx)).Methods("PUT")
|
||||
}
|
||||
|
||||
type migrateContractReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
Admin string `json:"admin,omitempty" yaml:"admin"`
|
||||
CodeID uint64 `json:"code_id" yaml:"code_id"`
|
||||
Msg []byte `json:"msg,omitempty" yaml:"msg"`
|
||||
}
|
||||
|
||||
type updateContractAdministrateReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
Admin string `json:"admin,omitempty" yaml:"admin"`
|
||||
}
|
||||
|
||||
func setContractAdminHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req updateContractAdministrateReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
contractAddr := vars["contractAddr"]
|
||||
|
||||
req.BaseReq = req.BaseReq.Sanitize()
|
||||
if !req.BaseReq.ValidateBasic(w) {
|
||||
return
|
||||
}
|
||||
|
||||
msg := &types.MsgUpdateAdmin{
|
||||
Sender: req.BaseReq.From,
|
||||
NewAdmin: req.Admin,
|
||||
Contract: contractAddr,
|
||||
}
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func migrateContractHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req migrateContractReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
contractAddr := vars["contractAddr"]
|
||||
|
||||
req.BaseReq = req.BaseReq.Sanitize()
|
||||
if !req.BaseReq.ValidateBasic(w) {
|
||||
return
|
||||
}
|
||||
|
||||
msg := &types.MsgMigrateContract{
|
||||
Sender: req.BaseReq.From,
|
||||
Contract: contractAddr,
|
||||
CodeID: req.CodeID,
|
||||
Msg: req.Msg,
|
||||
}
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, msg)
|
||||
}
|
||||
}
|
270
x/wasm/client/rest/query.go
Normal file
@ -0,0 +1,270 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func registerQueryRoutes(cliCtx client.Context, r *mux.Router) {
|
||||
r.HandleFunc("/wasm/code", listCodesHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc("/wasm/code/{codeID}", queryCodeHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc("/wasm/code/{codeID}/contracts", listContractsByCodeHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc("/wasm/contract/{contractAddr}", queryContractHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc("/wasm/contract/{contractAddr}/state", queryContractStateAllHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc("/wasm/contract/{contractAddr}/history", queryContractHistoryFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc("/wasm/contract/{contractAddr}/smart/{query}", queryContractStateSmartHandlerFn(cliCtx)).Queries("encoding", "{encoding}").Methods("GET")
|
||||
r.HandleFunc("/wasm/contract/{contractAddr}/raw/{key}", queryContractStateRawHandlerFn(cliCtx)).Queries("encoding", "{encoding}").Methods("GET")
|
||||
}
|
||||
|
||||
func listCodesHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, keeper.QueryListCode)
|
||||
res, height, err := cliCtx.Query(route)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
rest.PostProcessResponse(w, cliCtx, json.RawMessage(res))
|
||||
}
|
||||
}
|
||||
|
||||
func queryCodeHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
codeID, err := strconv.ParseUint(mux.Vars(r)["codeID"], 10, 64)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s/%d", types.QuerierRoute, keeper.QueryGetCode, codeID)
|
||||
res, height, err := cliCtx.Query(route)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if len(res) == 0 {
|
||||
rest.WriteErrorResponse(w, http.StatusNotFound, "contract not found")
|
||||
return
|
||||
}
|
||||
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
rest.PostProcessResponse(w, cliCtx, json.RawMessage(res))
|
||||
}
|
||||
}
|
||||
|
||||
func listContractsByCodeHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
codeID, err := strconv.ParseUint(mux.Vars(r)["codeID"], 10, 64)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s/%d", types.QuerierRoute, keeper.QueryListContractByCode, codeID)
|
||||
res, height, err := cliCtx.Query(route)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
rest.PostProcessResponse(w, cliCtx, json.RawMessage(res))
|
||||
}
|
||||
}
|
||||
|
||||
func queryContractHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
addr, err := sdk.AccAddressFromBech32(mux.Vars(r)["contractAddr"])
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContract, addr.String())
|
||||
res, height, err := cliCtx.Query(route)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
rest.PostProcessResponse(w, cliCtx, json.RawMessage(res))
|
||||
}
|
||||
}
|
||||
|
||||
func queryContractStateAllHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
addr, err := sdk.AccAddressFromBech32(mux.Vars(r)["contractAddr"])
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateAll)
|
||||
res, height, err := cliCtx.Query(route)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// parse res
|
||||
var resultData []types.Model
|
||||
err = json.Unmarshal(res, &resultData)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
rest.PostProcessResponse(w, cliCtx, resultData)
|
||||
}
|
||||
}
|
||||
|
||||
func queryContractStateRawHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := newArgDecoder(hex.DecodeString)
|
||||
addr, err := sdk.AccAddressFromBech32(mux.Vars(r)["contractAddr"])
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
decoder.encoding = mux.Vars(r)["encoding"]
|
||||
queryData, err := decoder.DecodeString(mux.Vars(r)["key"])
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateRaw)
|
||||
res, height, err := cliCtx.QueryWithData(route, queryData)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
// ensure this is base64 encoded
|
||||
encoded := base64.StdEncoding.EncodeToString(res)
|
||||
rest.PostProcessResponse(w, cliCtx, encoded)
|
||||
}
|
||||
}
|
||||
|
||||
type smartResponse struct {
|
||||
Smart []byte `json:"smart"`
|
||||
}
|
||||
|
||||
func queryContractStateSmartHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := newArgDecoder(hex.DecodeString)
|
||||
addr, err := sdk.AccAddressFromBech32(mux.Vars(r)["contractAddr"])
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
decoder.encoding = mux.Vars(r)["encoding"]
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateSmart)
|
||||
|
||||
queryData, err := decoder.DecodeString(mux.Vars(r)["query"])
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
res, height, err := cliCtx.QueryWithData(route, queryData)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
// return as raw bytes (to be base64-encoded)
|
||||
responseData := smartResponse{Smart: res}
|
||||
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
rest.PostProcessResponse(w, cliCtx, responseData)
|
||||
}
|
||||
}
|
||||
|
||||
func queryContractHistoryFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
addr, err := sdk.AccAddressFromBech32(mux.Vars(r)["contractAddr"])
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s/%s", types.QuerierRoute, keeper.QueryContractHistory, addr.String())
|
||||
res, height, err := cliCtx.Query(route)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
rest.PostProcessResponse(w, cliCtx, json.RawMessage(res))
|
||||
}
|
||||
}
|
||||
|
||||
type argumentDecoder struct {
|
||||
// dec is the default decoder
|
||||
dec func(string) ([]byte, error)
|
||||
encoding string
|
||||
}
|
||||
|
||||
func newArgDecoder(def func(string) ([]byte, error)) *argumentDecoder {
|
||||
return &argumentDecoder{dec: def}
|
||||
}
|
||||
|
||||
func (a *argumentDecoder) DecodeString(s string) ([]byte, error) {
|
||||
switch a.encoding {
|
||||
case "hex":
|
||||
return hex.DecodeString(s)
|
||||
case "base64":
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
default:
|
||||
return a.dec(s)
|
||||
}
|
||||
}
|
15
x/wasm/client/rest/rest.go
Normal file
@ -0,0 +1,15 @@
|
||||
// Deprecated: the rest package will be removed. You can use the GRPC gateway instead
|
||||
package rest
|
||||
|
||||
import (
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// RegisterRoutes registers staking-related REST handlers to a router
|
||||
// Deprecated: the rest package will be removed. You can use the GRPC gateway instead
|
||||
func RegisterRoutes(cliCtx client.Context, r *mux.Router) {
|
||||
registerQueryRoutes(cliCtx, r)
|
||||
registerTxRoutes(cliCtx, r)
|
||||
registerNewTxRoutes(cliCtx, r)
|
||||
}
|
149
x/wasm/client/rest/tx.go
Normal file
@ -0,0 +1,149 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/ioutils"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func registerTxRoutes(cliCtx client.Context, r *mux.Router) {
|
||||
r.HandleFunc("/wasm/code", storeCodeHandlerFn(cliCtx)).Methods("POST")
|
||||
r.HandleFunc("/wasm/code/{codeId}", instantiateContractHandlerFn(cliCtx)).Methods("POST")
|
||||
r.HandleFunc("/wasm/contract/{contractAddr}", executeContractHandlerFn(cliCtx)).Methods("POST")
|
||||
}
|
||||
|
||||
type storeCodeReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
WasmBytes []byte `json:"wasm_bytes"`
|
||||
}
|
||||
|
||||
type instantiateContractReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
Label string `json:"label" yaml:"label"`
|
||||
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||
Admin string `json:"admin,omitempty" yaml:"admin"`
|
||||
Msg []byte `json:"msg" yaml:"msg"`
|
||||
}
|
||||
|
||||
type executeContractReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
ExecMsg []byte `json:"exec_msg" yaml:"exec_msg"`
|
||||
Amount sdk.Coins `json:"coins" yaml:"coins"`
|
||||
}
|
||||
|
||||
func storeCodeHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req storeCodeReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.BaseReq = req.BaseReq.Sanitize()
|
||||
if !req.BaseReq.ValidateBasic(w) {
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
wasm := req.WasmBytes
|
||||
|
||||
// gzip the wasm file
|
||||
if ioutils.IsWasm(wasm) {
|
||||
wasm, err = ioutils.GzipIt(wasm)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
} else if !ioutils.IsGzip(wasm) {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, "Invalid input file, use wasm binary or zip")
|
||||
return
|
||||
}
|
||||
|
||||
// build and sign the transaction, then broadcast to Tendermint
|
||||
msg := types.MsgStoreCode{
|
||||
Sender: req.BaseReq.From,
|
||||
WASMByteCode: wasm,
|
||||
}
|
||||
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, &msg)
|
||||
}
|
||||
}
|
||||
|
||||
func instantiateContractHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req instantiateContractReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
|
||||
req.BaseReq = req.BaseReq.Sanitize()
|
||||
if !req.BaseReq.ValidateBasic(w) {
|
||||
return
|
||||
}
|
||||
|
||||
// get the id of the code to instantiate
|
||||
codeID, err := strconv.ParseUint(vars["codeId"], 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg := types.MsgInstantiateContract{
|
||||
Sender: req.BaseReq.From,
|
||||
CodeID: codeID,
|
||||
Label: req.Label,
|
||||
Funds: req.Deposit,
|
||||
Msg: req.Msg,
|
||||
Admin: req.Admin,
|
||||
}
|
||||
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, &msg)
|
||||
}
|
||||
}
|
||||
|
||||
func executeContractHandlerFn(cliCtx client.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req executeContractReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
contractAddr := vars["contractAddr"]
|
||||
|
||||
req.BaseReq = req.BaseReq.Sanitize()
|
||||
if !req.BaseReq.ValidateBasic(w) {
|
||||
return
|
||||
}
|
||||
|
||||
msg := types.MsgExecuteContract{
|
||||
Sender: req.BaseReq.From,
|
||||
Contract: contractAddr,
|
||||
Msg: req.ExecMsg,
|
||||
Funds: req.Amount,
|
||||
}
|
||||
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, &msg)
|
||||
}
|
||||
}
|
34
x/wasm/common_test.go
Normal file
@ -0,0 +1,34 @@
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ensure store code returns the expected response
|
||||
func assertStoreCodeResponse(t *testing.T, data []byte, expected uint64) {
|
||||
var pStoreResp MsgStoreCodeResponse
|
||||
require.NoError(t, pStoreResp.Unmarshal(data))
|
||||
require.Equal(t, pStoreResp.CodeID, expected)
|
||||
}
|
||||
|
||||
// ensure execution returns the expected data
|
||||
func assertExecuteResponse(t *testing.T, data []byte, expected []byte) {
|
||||
var pExecResp MsgExecuteContractResponse
|
||||
require.NoError(t, pExecResp.Unmarshal(data))
|
||||
require.Equal(t, pExecResp.Data, expected)
|
||||
}
|
||||
|
||||
// ensures this returns a valid bech32 address and returns it
|
||||
func parseInitResponse(t *testing.T, data []byte) string {
|
||||
var pInstResp MsgInstantiateContractResponse
|
||||
require.NoError(t, pInstResp.Unmarshal(data))
|
||||
require.NotEmpty(t, pInstResp.Address)
|
||||
addr := pInstResp.Address
|
||||
// ensure this is a valid sdk address
|
||||
_, err := sdk.AccAddressFromBech32(addr)
|
||||
require.NoError(t, err)
|
||||
return addr
|
||||
}
|
96
x/wasm/genesis_test.go
Normal file
@ -0,0 +1,96 @@
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInitGenesis(t *testing.T) {
|
||||
data := setupTest(t)
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000))
|
||||
creator := data.faucet.NewFundedRandomAccount(data.ctx, deposit.Add(deposit...)...)
|
||||
fred := data.faucet.NewFundedRandomAccount(data.ctx, topUp...)
|
||||
|
||||
h := data.module.Route().Handler()
|
||||
q := data.module.LegacyQuerierHandler(nil)
|
||||
|
||||
msg := MsgStoreCode{
|
||||
Sender: creator.String(),
|
||||
WASMByteCode: testContract,
|
||||
}
|
||||
err := msg.ValidateBasic()
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := h(data.ctx, &msg)
|
||||
require.NoError(t, err)
|
||||
assertStoreCodeResponse(t, res.Data, 1)
|
||||
|
||||
_, _, bob := keyPubAddr()
|
||||
initMsg := initMsg{
|
||||
Verifier: fred,
|
||||
Beneficiary: bob,
|
||||
}
|
||||
initMsgBz, err := json.Marshal(initMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
initCmd := MsgInstantiateContract{
|
||||
Sender: creator.String(),
|
||||
CodeID: firstCodeID,
|
||||
Msg: initMsgBz,
|
||||
Funds: deposit,
|
||||
Label: "testing",
|
||||
}
|
||||
res, err = h(data.ctx, &initCmd)
|
||||
require.NoError(t, err)
|
||||
contractBech32Addr := parseInitResponse(t, res.Data)
|
||||
|
||||
execCmd := MsgExecuteContract{
|
||||
Sender: fred.String(),
|
||||
Contract: contractBech32Addr,
|
||||
Msg: []byte(`{"release":{}}`),
|
||||
Funds: topUp,
|
||||
}
|
||||
res, err = h(data.ctx, &execCmd)
|
||||
require.NoError(t, err)
|
||||
// from https://github.com/CosmWasm/cosmwasm/blob/master/contracts/hackatom/src/contract.rs#L167
|
||||
assertExecuteResponse(t, res.Data, []byte{0xf0, 0x0b, 0xaa})
|
||||
|
||||
// ensure all contract state is as after init
|
||||
assertCodeList(t, q, data.ctx, 1)
|
||||
assertCodeBytes(t, q, data.ctx, 1, testContract)
|
||||
|
||||
assertContractList(t, q, data.ctx, 1, []string{contractBech32Addr})
|
||||
assertContractInfo(t, q, data.ctx, contractBech32Addr, 1, creator)
|
||||
assertContractState(t, q, data.ctx, contractBech32Addr, state{
|
||||
Verifier: fred.String(),
|
||||
Beneficiary: bob.String(),
|
||||
Funder: creator.String(),
|
||||
})
|
||||
|
||||
// export into genstate
|
||||
genState := ExportGenesis(data.ctx, &data.keeper)
|
||||
|
||||
// create new app to import genstate into
|
||||
newData := setupTest(t)
|
||||
q2 := newData.module.LegacyQuerierHandler(nil)
|
||||
|
||||
// initialize new app with genstate
|
||||
InitGenesis(newData.ctx, &newData.keeper, *genState)
|
||||
|
||||
// run same checks again on newdata, to make sure it was reinitialized correctly
|
||||
assertCodeList(t, q2, newData.ctx, 1)
|
||||
assertCodeBytes(t, q2, newData.ctx, 1, testContract)
|
||||
|
||||
assertContractList(t, q2, newData.ctx, 1, []string{contractBech32Addr})
|
||||
assertContractInfo(t, q2, newData.ctx, contractBech32Addr, 1, creator)
|
||||
assertContractState(t, q2, newData.ctx, contractBech32Addr, state{
|
||||
Verifier: fred.String(),
|
||||
Beneficiary: bob.String(),
|
||||
Funder: creator.String(),
|
||||
})
|
||||
}
|
77
x/wasm/handler.go
Normal file
@ -0,0 +1,77 @@
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
)
|
||||
|
||||
// NewHandler returns a handler for "wasm" type messages.
|
||||
func NewHandler(k types.ContractOpsKeeper) sdk.Handler {
|
||||
msgServer := keeper.NewMsgServerImpl(k)
|
||||
|
||||
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
|
||||
ctx = ctx.WithEventManager(sdk.NewEventManager())
|
||||
|
||||
var (
|
||||
res proto.Message
|
||||
err error
|
||||
)
|
||||
switch msg := msg.(type) {
|
||||
case *MsgStoreCode: //nolint:typecheck
|
||||
res, err = msgServer.StoreCode(sdk.WrapSDKContext(ctx), msg)
|
||||
case *MsgInstantiateContract:
|
||||
res, err = msgServer.InstantiateContract(sdk.WrapSDKContext(ctx), msg)
|
||||
case *MsgInstantiateContract2:
|
||||
res, err = msgServer.InstantiateContract2(sdk.WrapSDKContext(ctx), msg)
|
||||
case *MsgExecuteContract:
|
||||
res, err = msgServer.ExecuteContract(sdk.WrapSDKContext(ctx), msg)
|
||||
case *MsgMigrateContract:
|
||||
res, err = msgServer.MigrateContract(sdk.WrapSDKContext(ctx), msg)
|
||||
case *MsgUpdateAdmin:
|
||||
res, err = msgServer.UpdateAdmin(sdk.WrapSDKContext(ctx), msg)
|
||||
case *MsgClearAdmin:
|
||||
res, err = msgServer.ClearAdmin(sdk.WrapSDKContext(ctx), msg)
|
||||
case *types.MsgUpdateInstantiateConfig:
|
||||
res, err = msgServer.UpdateInstantiateConfig(sdk.WrapSDKContext(ctx), msg)
|
||||
default:
|
||||
errMsg := fmt.Sprintf("unrecognized wasm message type: %T", msg)
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg)
|
||||
}
|
||||
|
||||
ctx = ctx.WithEventManager(filterMessageEvents(ctx))
|
||||
return sdk.WrapServiceResult(ctx, res, err)
|
||||
}
|
||||
}
|
||||
|
||||
// filterMessageEvents returns the same events with all of type == EventTypeMessage removed except
|
||||
// for wasm message types.
|
||||
// this is so only our top-level message event comes through
|
||||
func filterMessageEvents(ctx sdk.Context) *sdk.EventManager {
|
||||
m := sdk.NewEventManager()
|
||||
for _, e := range ctx.EventManager().Events() {
|
||||
if e.Type == sdk.EventTypeMessage &&
|
||||
!hasWasmModuleAttribute(e.Attributes) {
|
||||
continue
|
||||
}
|
||||
m.EmitEvent(e)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func hasWasmModuleAttribute(attrs []abci.EventAttribute) bool {
|
||||
for _, a := range attrs {
|
||||
if sdk.AttributeKeyModule == string(a.Key) &&
|
||||
types.ModuleName == string(a.Value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
357
x/wasm/ibc.go
Normal file
@ -0,0 +1,357 @@
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
porttypes "github.com/cosmos/ibc-go/v4/modules/core/05-port/types"
|
||||
host "github.com/cosmos/ibc-go/v4/modules/core/24-host"
|
||||
ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
var _ porttypes.IBCModule = IBCHandler{}
|
||||
|
||||
// internal interface that is implemented by ibc middleware
|
||||
type appVersionGetter interface {
|
||||
// GetAppVersion returns the application level version with all middleware data stripped out
|
||||
GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool)
|
||||
}
|
||||
|
||||
type IBCHandler struct {
|
||||
keeper types.IBCContractKeeper
|
||||
channelKeeper types.ChannelKeeper
|
||||
appVersionGetter appVersionGetter
|
||||
}
|
||||
|
||||
func NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper, vg appVersionGetter) IBCHandler {
|
||||
return IBCHandler{keeper: k, channelKeeper: ck, appVersionGetter: vg}
|
||||
}
|
||||
|
||||
// OnChanOpenInit implements the IBCModule interface
|
||||
func (i IBCHandler) OnChanOpenInit(
|
||||
ctx sdk.Context,
|
||||
order channeltypes.Order,
|
||||
connectionHops []string,
|
||||
portID string,
|
||||
channelID string,
|
||||
chanCap *capabilitytypes.Capability,
|
||||
counterParty channeltypes.Counterparty,
|
||||
version string,
|
||||
) (string, error) {
|
||||
// ensure port, version, capability
|
||||
if err := ValidateChannelParams(channelID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
contractAddr, err := ContractFromPortID(portID)
|
||||
if err != nil {
|
||||
return "", sdkerrors.Wrapf(err, "contract port id")
|
||||
}
|
||||
|
||||
msg := wasmvmtypes.IBCChannelOpenMsg{
|
||||
OpenInit: &wasmvmtypes.IBCOpenInit{
|
||||
Channel: wasmvmtypes.IBCChannel{
|
||||
Endpoint: wasmvmtypes.IBCEndpoint{PortID: portID, ChannelID: channelID},
|
||||
CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{PortID: counterParty.PortId, ChannelID: counterParty.ChannelId},
|
||||
Order: order.String(),
|
||||
// DESIGN V3: this may be "" ??
|
||||
Version: version,
|
||||
ConnectionID: connectionHops[0], // At the moment this list must be of length 1. In the future multi-hop channels may be supported.
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Allow contracts to return a version (or default to proposed version if unset)
|
||||
acceptedVersion, err := i.keeper.OnOpenChannel(ctx, contractAddr, msg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if acceptedVersion == "" {
|
||||
acceptedVersion = version
|
||||
}
|
||||
|
||||
// Claim channel capability passed back by IBC module
|
||||
if err := i.keeper.ClaimCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)); err != nil {
|
||||
return "", sdkerrors.Wrap(err, "claim capability")
|
||||
}
|
||||
return acceptedVersion, nil
|
||||
}
|
||||
|
||||
// OnChanOpenTry implements the IBCModule interface
|
||||
func (i IBCHandler) OnChanOpenTry(
|
||||
ctx sdk.Context,
|
||||
order channeltypes.Order,
|
||||
connectionHops []string,
|
||||
portID, channelID string,
|
||||
chanCap *capabilitytypes.Capability,
|
||||
counterParty channeltypes.Counterparty,
|
||||
counterpartyVersion string,
|
||||
) (string, error) {
|
||||
// ensure port, version, capability
|
||||
if err := ValidateChannelParams(channelID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
contractAddr, err := ContractFromPortID(portID)
|
||||
if err != nil {
|
||||
return "", sdkerrors.Wrapf(err, "contract port id")
|
||||
}
|
||||
|
||||
msg := wasmvmtypes.IBCChannelOpenMsg{
|
||||
OpenTry: &wasmvmtypes.IBCOpenTry{
|
||||
Channel: wasmvmtypes.IBCChannel{
|
||||
Endpoint: wasmvmtypes.IBCEndpoint{PortID: portID, ChannelID: channelID},
|
||||
CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{PortID: counterParty.PortId, ChannelID: counterParty.ChannelId},
|
||||
Order: order.String(),
|
||||
Version: counterpartyVersion,
|
||||
ConnectionID: connectionHops[0], // At the moment this list must be of length 1. In the future multi-hop channels may be supported.
|
||||
},
|
||||
CounterpartyVersion: counterpartyVersion,
|
||||
},
|
||||
}
|
||||
|
||||
// Allow contracts to return a version (or default to counterpartyVersion if unset)
|
||||
version, err := i.keeper.OnOpenChannel(ctx, contractAddr, msg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if version == "" {
|
||||
version = counterpartyVersion
|
||||
}
|
||||
|
||||
// Module may have already claimed capability in OnChanOpenInit in the case of crossing hellos
|
||||
// (ie chainA and chainB both call ChanOpenInit before one of them calls ChanOpenTry)
|
||||
// If module can already authenticate the capability then module already owns it, so we don't need to claim
|
||||
// Otherwise, module does not have channel capability, and we must claim it from IBC
|
||||
if !i.keeper.AuthenticateCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)) {
|
||||
// Only claim channel capability passed back by IBC module if we do not already own it
|
||||
if err := i.keeper.ClaimCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)); err != nil {
|
||||
return "", sdkerrors.Wrap(err, "claim capability")
|
||||
}
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
// OnChanOpenAck implements the IBCModule interface
|
||||
func (i IBCHandler) OnChanOpenAck(
|
||||
ctx sdk.Context,
|
||||
portID, channelID string,
|
||||
counterpartyChannelID string,
|
||||
counterpartyVersion string,
|
||||
) error {
|
||||
contractAddr, err := ContractFromPortID(portID)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrapf(err, "contract port id")
|
||||
}
|
||||
channelInfo, ok := i.channelKeeper.GetChannel(ctx, portID, channelID)
|
||||
if !ok {
|
||||
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
|
||||
}
|
||||
channelInfo.Counterparty.ChannelId = counterpartyChannelID
|
||||
|
||||
appVersion, ok := i.appVersionGetter.GetAppVersion(ctx, portID, channelID)
|
||||
if !ok {
|
||||
return sdkerrors.Wrapf(channeltypes.ErrInvalidChannelVersion, "port ID (%s) channel ID (%s)", portID, channelID)
|
||||
}
|
||||
|
||||
msg := wasmvmtypes.IBCChannelConnectMsg{
|
||||
OpenAck: &wasmvmtypes.IBCOpenAck{
|
||||
Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion),
|
||||
CounterpartyVersion: counterpartyVersion,
|
||||
},
|
||||
}
|
||||
return i.keeper.OnConnectChannel(ctx, contractAddr, msg)
|
||||
}
|
||||
|
||||
// OnChanOpenConfirm implements the IBCModule interface
|
||||
func (i IBCHandler) OnChanOpenConfirm(ctx sdk.Context, portID, channelID string) error {
|
||||
contractAddr, err := ContractFromPortID(portID)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrapf(err, "contract port id")
|
||||
}
|
||||
channelInfo, ok := i.channelKeeper.GetChannel(ctx, portID, channelID)
|
||||
if !ok {
|
||||
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
|
||||
}
|
||||
appVersion, ok := i.appVersionGetter.GetAppVersion(ctx, portID, channelID)
|
||||
if !ok {
|
||||
return sdkerrors.Wrapf(channeltypes.ErrInvalidChannelVersion, "port ID (%s) channel ID (%s)", portID, channelID)
|
||||
}
|
||||
msg := wasmvmtypes.IBCChannelConnectMsg{
|
||||
OpenConfirm: &wasmvmtypes.IBCOpenConfirm{
|
||||
Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion),
|
||||
},
|
||||
}
|
||||
return i.keeper.OnConnectChannel(ctx, contractAddr, msg)
|
||||
}
|
||||
|
||||
// OnChanCloseInit implements the IBCModule interface
|
||||
func (i IBCHandler) OnChanCloseInit(ctx sdk.Context, portID, channelID string) error {
|
||||
contractAddr, err := ContractFromPortID(portID)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrapf(err, "contract port id")
|
||||
}
|
||||
channelInfo, ok := i.channelKeeper.GetChannel(ctx, portID, channelID)
|
||||
if !ok {
|
||||
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
|
||||
}
|
||||
appVersion, ok := i.appVersionGetter.GetAppVersion(ctx, portID, channelID)
|
||||
if !ok {
|
||||
return sdkerrors.Wrapf(channeltypes.ErrInvalidChannelVersion, "port ID (%s) channel ID (%s)", portID, channelID)
|
||||
}
|
||||
|
||||
msg := wasmvmtypes.IBCChannelCloseMsg{
|
||||
CloseInit: &wasmvmtypes.IBCCloseInit{Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion)},
|
||||
}
|
||||
err = i.keeper.OnCloseChannel(ctx, contractAddr, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// emit events?
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// OnChanCloseConfirm implements the IBCModule interface
|
||||
func (i IBCHandler) OnChanCloseConfirm(ctx sdk.Context, portID, channelID string) error {
|
||||
// counterparty has closed the channel
|
||||
contractAddr, err := ContractFromPortID(portID)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrapf(err, "contract port id")
|
||||
}
|
||||
channelInfo, ok := i.channelKeeper.GetChannel(ctx, portID, channelID)
|
||||
if !ok {
|
||||
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
|
||||
}
|
||||
appVersion, ok := i.appVersionGetter.GetAppVersion(ctx, portID, channelID)
|
||||
if !ok {
|
||||
return sdkerrors.Wrapf(channeltypes.ErrInvalidChannelVersion, "port ID (%s) channel ID (%s)", portID, channelID)
|
||||
}
|
||||
|
||||
msg := wasmvmtypes.IBCChannelCloseMsg{
|
||||
CloseConfirm: &wasmvmtypes.IBCCloseConfirm{Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion)},
|
||||
}
|
||||
err = i.keeper.OnCloseChannel(ctx, contractAddr, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// emit events?
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func toWasmVMChannel(portID, channelID string, channelInfo channeltypes.Channel, appVersion string) wasmvmtypes.IBCChannel {
|
||||
return wasmvmtypes.IBCChannel{
|
||||
Endpoint: wasmvmtypes.IBCEndpoint{PortID: portID, ChannelID: channelID},
|
||||
CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{PortID: channelInfo.Counterparty.PortId, ChannelID: channelInfo.Counterparty.ChannelId},
|
||||
Order: channelInfo.Ordering.String(),
|
||||
Version: appVersion,
|
||||
ConnectionID: channelInfo.ConnectionHops[0], // At the moment this list must be of length 1. In the future multi-hop channels may be supported.
|
||||
}
|
||||
}
|
||||
|
||||
// OnRecvPacket implements the IBCModule interface
|
||||
func (i IBCHandler) OnRecvPacket(
|
||||
ctx sdk.Context,
|
||||
packet channeltypes.Packet,
|
||||
relayer sdk.AccAddress,
|
||||
) ibcexported.Acknowledgement {
|
||||
contractAddr, err := ContractFromPortID(packet.DestinationPort)
|
||||
if err != nil {
|
||||
return channeltypes.NewErrorAcknowledgement(sdkerrors.Wrapf(err, "contract port id"))
|
||||
}
|
||||
msg := wasmvmtypes.IBCPacketReceiveMsg{Packet: newIBCPacket(packet), Relayer: relayer.String()}
|
||||
ack, err := i.keeper.OnRecvPacket(ctx, contractAddr, msg)
|
||||
if err != nil {
|
||||
return channeltypes.NewErrorAcknowledgement(err)
|
||||
}
|
||||
return ContractConfirmStateAck(ack)
|
||||
}
|
||||
|
||||
var _ ibcexported.Acknowledgement = ContractConfirmStateAck{}
|
||||
|
||||
type ContractConfirmStateAck []byte
|
||||
|
||||
func (w ContractConfirmStateAck) Success() bool {
|
||||
return true // always commit state
|
||||
}
|
||||
|
||||
func (w ContractConfirmStateAck) Acknowledgement() []byte {
|
||||
return w
|
||||
}
|
||||
|
||||
// OnAcknowledgementPacket implements the IBCModule interface
|
||||
func (i IBCHandler) OnAcknowledgementPacket(
|
||||
ctx sdk.Context,
|
||||
packet channeltypes.Packet,
|
||||
acknowledgement []byte,
|
||||
relayer sdk.AccAddress,
|
||||
) error {
|
||||
contractAddr, err := ContractFromPortID(packet.SourcePort)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrapf(err, "contract port id")
|
||||
}
|
||||
|
||||
err = i.keeper.OnAckPacket(ctx, contractAddr, wasmvmtypes.IBCPacketAckMsg{
|
||||
Acknowledgement: wasmvmtypes.IBCAcknowledgement{Data: acknowledgement},
|
||||
OriginalPacket: newIBCPacket(packet),
|
||||
Relayer: relayer.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "on ack")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnTimeoutPacket implements the IBCModule interface
|
||||
func (i IBCHandler) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error {
|
||||
contractAddr, err := ContractFromPortID(packet.SourcePort)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrapf(err, "contract port id")
|
||||
}
|
||||
msg := wasmvmtypes.IBCPacketTimeoutMsg{Packet: newIBCPacket(packet), Relayer: relayer.String()}
|
||||
err = i.keeper.OnTimeoutPacket(ctx, contractAddr, msg)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "on timeout")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newIBCPacket(packet channeltypes.Packet) wasmvmtypes.IBCPacket {
|
||||
timeout := wasmvmtypes.IBCTimeout{
|
||||
Timestamp: packet.TimeoutTimestamp,
|
||||
}
|
||||
if !packet.TimeoutHeight.IsZero() {
|
||||
timeout.Block = &wasmvmtypes.IBCTimeoutBlock{
|
||||
Height: packet.TimeoutHeight.RevisionHeight,
|
||||
Revision: packet.TimeoutHeight.RevisionNumber,
|
||||
}
|
||||
}
|
||||
|
||||
return wasmvmtypes.IBCPacket{
|
||||
Data: packet.Data,
|
||||
Src: wasmvmtypes.IBCEndpoint{ChannelID: packet.SourceChannel, PortID: packet.SourcePort},
|
||||
Dest: wasmvmtypes.IBCEndpoint{ChannelID: packet.DestinationChannel, PortID: packet.DestinationPort},
|
||||
Sequence: packet.Sequence,
|
||||
Timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateChannelParams(channelID string) error {
|
||||
// NOTE: for escrow address security only 2^32 channels are allowed to be created
|
||||
// Issue: https://github.com/cosmos/cosmos-sdk/issues/7737
|
||||
channelSequence, err := channeltypes.ParseChannelSequence(channelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if channelSequence > math.MaxUint32 {
|
||||
return sdkerrors.Wrapf(types.ErrMaxIBCChannels, "channel sequence %d is greater than max allowed transfer channels %d", channelSequence, math.MaxUint32)
|
||||
}
|
||||
return nil
|
||||
}
|
126
x/wasm/ibc_integration_test.go
Normal file
@ -0,0 +1,126 @@
|
||||
package wasm_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
wasmvm "github.com/CosmWasm/wasmvm"
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
ibctesting "github.com/cosmos/ibc-go/v4/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
wasmibctesting "github.com/cerc-io/laconicd/x/wasm/ibctesting"
|
||||
wasmkeeper "github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting"
|
||||
)
|
||||
|
||||
func TestOnChanOpenInitVersion(t *testing.T) {
|
||||
const startVersion = "v1"
|
||||
specs := map[string]struct {
|
||||
contractRsp *wasmvmtypes.IBC3ChannelOpenResponse
|
||||
expVersion string
|
||||
}{
|
||||
"different version": {
|
||||
contractRsp: &wasmvmtypes.IBC3ChannelOpenResponse{Version: "v2"},
|
||||
expVersion: "v2",
|
||||
},
|
||||
"no response": {
|
||||
expVersion: startVersion,
|
||||
},
|
||||
"empty result": {
|
||||
contractRsp: &wasmvmtypes.IBC3ChannelOpenResponse{},
|
||||
expVersion: startVersion,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
myContract := &wasmtesting.MockIBCContractCallbacks{
|
||||
IBCChannelOpenFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) {
|
||||
return spec.contractRsp, 0, nil
|
||||
},
|
||||
}
|
||||
var (
|
||||
chainAOpts = []wasmkeeper.Option{
|
||||
wasmkeeper.WithWasmEngine(
|
||||
wasmtesting.NewIBCContractMockWasmer(myContract)),
|
||||
}
|
||||
coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts)
|
||||
chainA = coordinator.GetChain(wasmibctesting.GetChainID(0))
|
||||
chainB = coordinator.GetChain(wasmibctesting.GetChainID(1))
|
||||
myContractAddr = chainA.SeedNewContractInstance()
|
||||
contractInfo = chainA.App.WasmKeeper.GetContractInfo(chainA.GetContext(), myContractAddr)
|
||||
)
|
||||
|
||||
path := wasmibctesting.NewPath(chainA, chainB)
|
||||
coordinator.SetupConnections(path)
|
||||
|
||||
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
|
||||
PortID: contractInfo.IBCPortID,
|
||||
Version: startVersion,
|
||||
Order: channeltypes.UNORDERED,
|
||||
}
|
||||
require.NoError(t, path.EndpointA.ChanOpenInit())
|
||||
assert.Equal(t, spec.expVersion, path.EndpointA.ChannelConfig.Version)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnChanOpenTryVersion(t *testing.T) {
|
||||
const startVersion = ibctransfertypes.Version
|
||||
specs := map[string]struct {
|
||||
contractRsp *wasmvmtypes.IBC3ChannelOpenResponse
|
||||
expVersion string
|
||||
}{
|
||||
"different version": {
|
||||
contractRsp: &wasmvmtypes.IBC3ChannelOpenResponse{Version: "v2"},
|
||||
expVersion: "v2",
|
||||
},
|
||||
"no response": {
|
||||
expVersion: startVersion,
|
||||
},
|
||||
"empty result": {
|
||||
contractRsp: &wasmvmtypes.IBC3ChannelOpenResponse{},
|
||||
expVersion: startVersion,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
myContract := &wasmtesting.MockIBCContractCallbacks{
|
||||
IBCChannelOpenFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) {
|
||||
return spec.contractRsp, 0, nil
|
||||
},
|
||||
}
|
||||
var (
|
||||
chainAOpts = []wasmkeeper.Option{
|
||||
wasmkeeper.WithWasmEngine(
|
||||
wasmtesting.NewIBCContractMockWasmer(myContract)),
|
||||
}
|
||||
coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts)
|
||||
chainA = coordinator.GetChain(wasmibctesting.GetChainID(0))
|
||||
chainB = coordinator.GetChain(wasmibctesting.GetChainID(1))
|
||||
myContractAddr = chainA.SeedNewContractInstance()
|
||||
contractInfo = chainA.ContractInfo(myContractAddr)
|
||||
)
|
||||
|
||||
path := wasmibctesting.NewPath(chainA, chainB)
|
||||
coordinator.SetupConnections(path)
|
||||
|
||||
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
|
||||
PortID: contractInfo.IBCPortID,
|
||||
Version: startVersion,
|
||||
Order: channeltypes.UNORDERED,
|
||||
}
|
||||
path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{
|
||||
PortID: ibctransfertypes.PortID,
|
||||
Version: ibctransfertypes.Version,
|
||||
Order: channeltypes.UNORDERED,
|
||||
}
|
||||
|
||||
require.NoError(t, path.EndpointB.ChanOpenInit())
|
||||
require.NoError(t, path.EndpointA.ChanOpenTry())
|
||||
assert.Equal(t, spec.expVersion, path.EndpointA.ChannelConfig.Version)
|
||||
})
|
||||
}
|
||||
}
|
124
x/wasm/ibc_reflect_test.go
Normal file
@ -0,0 +1,124 @@
|
||||
package wasm_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
ibctesting "github.com/cosmos/ibc-go/v4/testing"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
wasmibctesting "github.com/cerc-io/laconicd/x/wasm/ibctesting"
|
||||
wasmkeeper "github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
)
|
||||
|
||||
func TestIBCReflectContract(t *testing.T) {
|
||||
// scenario:
|
||||
// chain A: ibc_reflect_send.wasm
|
||||
// chain B: reflect.wasm + ibc_reflect.wasm
|
||||
//
|
||||
// Chain A "ibc_reflect_send" sends a IBC packet "on channel connect" event to chain B "ibc_reflect"
|
||||
// "ibc_reflect" sends a submessage to "reflect" which is returned as submessage.
|
||||
|
||||
var (
|
||||
coordinator = wasmibctesting.NewCoordinator(t, 2)
|
||||
chainA = coordinator.GetChain(wasmibctesting.GetChainID(0))
|
||||
chainB = coordinator.GetChain(wasmibctesting.GetChainID(1))
|
||||
)
|
||||
coordinator.CommitBlock(chainA, chainB)
|
||||
|
||||
initMsg := []byte(`{}`)
|
||||
codeID := chainA.StoreCodeFile("./keeper/testdata/ibc_reflect_send.wasm").CodeID
|
||||
sendContractAddr := chainA.InstantiateContract(codeID, initMsg)
|
||||
|
||||
reflectID := chainB.StoreCodeFile("./keeper/testdata/reflect.wasm").CodeID
|
||||
initMsg = wasmkeeper.IBCReflectInitMsg{
|
||||
ReflectCodeID: reflectID,
|
||||
}.GetBytes(t)
|
||||
codeID = chainB.StoreCodeFile("./keeper/testdata/ibc_reflect.wasm").CodeID
|
||||
|
||||
reflectContractAddr := chainB.InstantiateContract(codeID, initMsg)
|
||||
var (
|
||||
sourcePortID = chainA.ContractInfo(sendContractAddr).IBCPortID
|
||||
counterpartPortID = chainB.ContractInfo(reflectContractAddr).IBCPortID
|
||||
)
|
||||
coordinator.CommitBlock(chainA, chainB)
|
||||
coordinator.UpdateTime()
|
||||
|
||||
require.Equal(t, chainA.CurrentHeader.Time, chainB.CurrentHeader.Time)
|
||||
path := wasmibctesting.NewPath(chainA, chainB)
|
||||
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
|
||||
PortID: sourcePortID,
|
||||
Version: "ibc-reflect-v1",
|
||||
Order: channeltypes.ORDERED,
|
||||
}
|
||||
path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{
|
||||
PortID: counterpartPortID,
|
||||
Version: "ibc-reflect-v1",
|
||||
Order: channeltypes.ORDERED,
|
||||
}
|
||||
|
||||
coordinator.SetupConnections(path)
|
||||
coordinator.CreateChannels(path)
|
||||
|
||||
// TODO: query both contracts directly to ensure they have registered the proper connection
|
||||
// (and the chainB has created a reflect contract)
|
||||
|
||||
// there should be one packet to relay back and forth (whoami)
|
||||
// TODO: how do I find the packet that was previously sent by the smart contract?
|
||||
// Coordinator.RecvPacket requires channeltypes.Packet as input?
|
||||
// Given the source (portID, channelID), we should be able to count how many packets are pending, query the data
|
||||
// and submit them to the other side (same with acks). This is what the real relayer does. I guess the test framework doesn't?
|
||||
|
||||
// Update: I dug through the code, especially channel.Keeper.SendPacket, and it only writes a commitment
|
||||
// only writes I see: https://github.com/cosmos/cosmos-sdk/blob/31fdee0228bd6f3e787489c8e4434aabc8facb7d/x/ibc/core/04-channel/keeper/packet.go#L115-L116
|
||||
// commitment is hashed packet: https://github.com/cosmos/cosmos-sdk/blob/31fdee0228bd6f3e787489c8e4434aabc8facb7d/x/ibc/core/04-channel/types/packet.go#L14-L34
|
||||
// how is the relayer supposed to get the original packet data??
|
||||
// eg. ibctransfer doesn't store the packet either: https://github.com/cosmos/cosmos-sdk/blob/master/x/ibc/applications/transfer/keeper/relay.go#L145-L162
|
||||
// ... or I guess the original packet data is only available in the event logs????
|
||||
// https://github.com/cosmos/cosmos-sdk/blob/31fdee0228bd6f3e787489c8e4434aabc8facb7d/x/ibc/core/04-channel/keeper/packet.go#L121-L132
|
||||
|
||||
// ensure the expected packet was prepared, and relay it
|
||||
require.Equal(t, 1, len(chainA.PendingSendPackets))
|
||||
require.Equal(t, 0, len(chainB.PendingSendPackets))
|
||||
err := coordinator.RelayAndAckPendingPackets(path)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(chainA.PendingSendPackets))
|
||||
require.Equal(t, 0, len(chainB.PendingSendPackets))
|
||||
|
||||
// let's query the source contract and make sure it registered an address
|
||||
query := ReflectSendQueryMsg{Account: &AccountQuery{ChannelID: path.EndpointA.ChannelID}}
|
||||
var account AccountResponse
|
||||
err = chainA.SmartQuery(sendContractAddr.String(), query, &account)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, account.RemoteAddr)
|
||||
require.Empty(t, account.RemoteBalance)
|
||||
|
||||
// close channel
|
||||
coordinator.CloseChannel(path)
|
||||
|
||||
// let's query the source contract and make sure it registered an address
|
||||
account = AccountResponse{}
|
||||
err = chainA.SmartQuery(sendContractAddr.String(), query, &account)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
type ReflectSendQueryMsg struct {
|
||||
Admin *struct{} `json:"admin,omitempty"`
|
||||
ListAccounts *struct{} `json:"list_accounts,omitempty"`
|
||||
Account *AccountQuery `json:"account,omitempty"`
|
||||
}
|
||||
|
||||
type AccountQuery struct {
|
||||
ChannelID string `json:"channel_id"`
|
||||
}
|
||||
|
||||
type AccountResponse struct {
|
||||
LastUpdateTime uint64 `json:"last_update_time,string"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
RemoteBalance wasmvmtypes.Coins `json:"remote_balance"`
|
||||
}
|
82
x/wasm/ibc_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMapToWasmVMIBCPacket(t *testing.T) {
|
||||
var myTimestamp uint64 = 1
|
||||
specs := map[string]struct {
|
||||
src channeltypes.Packet
|
||||
exp wasmvmtypes.IBCPacket
|
||||
}{
|
||||
"with height timeout": {
|
||||
src: IBCPacketFixture(),
|
||||
exp: wasmvmtypes.IBCPacket{
|
||||
Data: []byte("myData"),
|
||||
Src: wasmvmtypes.IBCEndpoint{PortID: "srcPort", ChannelID: "channel-1"},
|
||||
Dest: wasmvmtypes.IBCEndpoint{PortID: "destPort", ChannelID: "channel-2"},
|
||||
Sequence: 1,
|
||||
Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{Height: 1, Revision: 2}},
|
||||
},
|
||||
},
|
||||
"with time timeout": {
|
||||
src: IBCPacketFixture(func(p *channeltypes.Packet) {
|
||||
p.TimeoutTimestamp = myTimestamp
|
||||
p.TimeoutHeight = clienttypes.Height{}
|
||||
}),
|
||||
exp: wasmvmtypes.IBCPacket{
|
||||
Data: []byte("myData"),
|
||||
Src: wasmvmtypes.IBCEndpoint{PortID: "srcPort", ChannelID: "channel-1"},
|
||||
Dest: wasmvmtypes.IBCEndpoint{PortID: "destPort", ChannelID: "channel-2"},
|
||||
Sequence: 1,
|
||||
Timeout: wasmvmtypes.IBCTimeout{Timestamp: myTimestamp},
|
||||
},
|
||||
}, "with time and height timeout": {
|
||||
src: IBCPacketFixture(func(p *channeltypes.Packet) {
|
||||
p.TimeoutTimestamp = myTimestamp
|
||||
}),
|
||||
exp: wasmvmtypes.IBCPacket{
|
||||
Data: []byte("myData"),
|
||||
Src: wasmvmtypes.IBCEndpoint{PortID: "srcPort", ChannelID: "channel-1"},
|
||||
Dest: wasmvmtypes.IBCEndpoint{PortID: "destPort", ChannelID: "channel-2"},
|
||||
Sequence: 1,
|
||||
Timeout: wasmvmtypes.IBCTimeout{
|
||||
Block: &wasmvmtypes.IBCTimeoutBlock{Height: 1, Revision: 2},
|
||||
Timestamp: myTimestamp,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := newIBCPacket(spec.src)
|
||||
assert.Equal(t, spec.exp, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func IBCPacketFixture(mutators ...func(p *channeltypes.Packet)) channeltypes.Packet {
|
||||
r := channeltypes.Packet{
|
||||
Sequence: 1,
|
||||
SourcePort: "srcPort",
|
||||
SourceChannel: "channel-1",
|
||||
DestinationPort: "destPort",
|
||||
DestinationChannel: "channel-2",
|
||||
Data: []byte("myData"),
|
||||
TimeoutHeight: clienttypes.Height{
|
||||
RevisionHeight: 1,
|
||||
RevisionNumber: 2,
|
||||
},
|
||||
TimeoutTimestamp: 0,
|
||||
}
|
||||
for _, m := range mutators {
|
||||
m(&r)
|
||||
}
|
||||
return r
|
||||
}
|
2
x/wasm/ibctesting/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# testing package for ibc
|
||||
Customized version of cosmos-sdk x/ibc/testing
|
594
x/wasm/ibctesting/chain.go
Normal file
@ -0,0 +1,594 @@
|
||||
package ibctesting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper"
|
||||
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/staking/teststaking"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
commitmenttypes "github.com/cosmos/ibc-go/v4/modules/core/23-commitment/types"
|
||||
host "github.com/cosmos/ibc-go/v4/modules/core/24-host"
|
||||
"github.com/cosmos/ibc-go/v4/modules/core/exported"
|
||||
"github.com/cosmos/ibc-go/v4/modules/core/types"
|
||||
ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types"
|
||||
ibctesting "github.com/cosmos/ibc-go/v4/testing"
|
||||
"github.com/cosmos/ibc-go/v4/testing/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
tmprotoversion "github.com/tendermint/tendermint/proto/tendermint/version"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
tmversion "github.com/tendermint/tendermint/version"
|
||||
|
||||
"github.com/cerc-io/laconicd/app"
|
||||
"github.com/cerc-io/laconicd/app/params"
|
||||
"github.com/cerc-io/laconicd/x/wasm"
|
||||
)
|
||||
|
||||
var MaxAccounts = 10
|
||||
|
||||
type SenderAccount struct {
|
||||
SenderPrivKey cryptotypes.PrivKey
|
||||
SenderAccount authtypes.AccountI
|
||||
}
|
||||
|
||||
// TestChain is a testing struct that wraps a simapp with the last TM Header, the current ABCI
|
||||
// header and the validators of the TestChain. It also contains a field called ChainID. This
|
||||
// is the clientID that *other* chains use to refer to this TestChain. The SenderAccount
|
||||
// is used for delivering transactions through the application state.
|
||||
// NOTE: the actual application uses an empty chain-id for ease of testing.
|
||||
type TestChain struct {
|
||||
t *testing.T
|
||||
|
||||
Coordinator *Coordinator
|
||||
App *app.WasmApp
|
||||
ChainID string
|
||||
LastHeader *ibctmtypes.Header // header for last block height committed
|
||||
CurrentHeader tmproto.Header // header for current block height
|
||||
QueryServer types.QueryServer
|
||||
TxConfig client.TxConfig
|
||||
Codec codec.BinaryCodec
|
||||
|
||||
Vals *tmtypes.ValidatorSet
|
||||
NextVals *tmtypes.ValidatorSet
|
||||
|
||||
// Signers is a map from validator address to the PrivValidator
|
||||
// The map is converted into an array that is the same order as the validators right before signing commit
|
||||
// This ensures that signers will always be in correct order even as validator powers change.
|
||||
// If a test adds a new validator after chain creation, then the signer map must be updated to include
|
||||
// the new PrivValidator entry.
|
||||
Signers map[string]tmtypes.PrivValidator
|
||||
|
||||
// autogenerated sender private key
|
||||
SenderPrivKey cryptotypes.PrivKey
|
||||
SenderAccount authtypes.AccountI
|
||||
SenderAccounts []SenderAccount
|
||||
|
||||
PendingSendPackets []channeltypes.Packet
|
||||
}
|
||||
|
||||
type PacketAck struct {
|
||||
Packet channeltypes.Packet
|
||||
Ack []byte
|
||||
}
|
||||
|
||||
// NewTestChain initializes a new test chain with a default of 4 validators
|
||||
// Use this function if the tests do not need custom control over the validator set
|
||||
func NewTestChain(t *testing.T, coord *Coordinator, chainID string, opts ...wasm.Option) *TestChain {
|
||||
// generate validators private/public key
|
||||
var (
|
||||
validatorsPerChain = 4
|
||||
validators = make([]*tmtypes.Validator, 0, validatorsPerChain)
|
||||
signersByAddress = make(map[string]tmtypes.PrivValidator, validatorsPerChain)
|
||||
)
|
||||
|
||||
for i := 0; i < validatorsPerChain; i++ {
|
||||
privVal := mock.NewPV()
|
||||
pubKey, err := privVal.GetPubKey()
|
||||
require.NoError(t, err)
|
||||
validators = append(validators, tmtypes.NewValidator(pubKey, 1))
|
||||
signersByAddress[pubKey.Address().String()] = privVal
|
||||
}
|
||||
|
||||
// construct validator set;
|
||||
// Note that the validators are sorted by voting power
|
||||
// or, if equal, by address lexical order
|
||||
valSet := tmtypes.NewValidatorSet(validators)
|
||||
|
||||
return NewTestChainWithValSet(t, coord, chainID, valSet, signersByAddress, opts...)
|
||||
}
|
||||
|
||||
// NewTestChainWithValSet initializes a new TestChain instance with the given validator set
|
||||
// and signer array. It also initializes 10 Sender accounts with a balance of 10000000000000000000 coins of
|
||||
// bond denom to use for tests.
|
||||
//
|
||||
// The first block height is committed to state in order to allow for client creations on
|
||||
// counterparty chains. The TestChain will return with a block height starting at 2.
|
||||
//
|
||||
// Time management is handled by the Coordinator in order to ensure synchrony between chains.
|
||||
// Each update of any chain increments the block header time for all chains by 5 seconds.
|
||||
//
|
||||
// NOTE: to use a custom sender privkey and account for testing purposes, replace and modify this
|
||||
// constructor function.
|
||||
//
|
||||
// CONTRACT: Validator array must be provided in the order expected by Tendermint.
|
||||
// i.e. sorted first by power and then lexicographically by address.
|
||||
func NewTestChainWithValSet(t *testing.T, coord *Coordinator, chainID string, valSet *tmtypes.ValidatorSet, signers map[string]tmtypes.PrivValidator, opts ...wasm.Option) *TestChain {
|
||||
genAccs := []authtypes.GenesisAccount{}
|
||||
genBals := []banktypes.Balance{}
|
||||
senderAccs := []SenderAccount{}
|
||||
|
||||
// generate genesis accounts
|
||||
for i := 0; i < MaxAccounts; i++ {
|
||||
senderPrivKey := secp256k1.GenPrivKey()
|
||||
acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), uint64(i), 0)
|
||||
amount, ok := sdk.NewIntFromString("10000000000000000000")
|
||||
require.True(t, ok)
|
||||
|
||||
// add sender account
|
||||
balance := banktypes.Balance{
|
||||
Address: acc.GetAddress().String(),
|
||||
Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, amount)),
|
||||
}
|
||||
|
||||
genAccs = append(genAccs, acc)
|
||||
genBals = append(genBals, balance)
|
||||
|
||||
senderAcc := SenderAccount{
|
||||
SenderAccount: acc,
|
||||
SenderPrivKey: senderPrivKey,
|
||||
}
|
||||
|
||||
senderAccs = append(senderAccs, senderAcc)
|
||||
}
|
||||
|
||||
wasmApp := app.SetupWithGenesisValSet(t, valSet, genAccs, chainID, opts, genBals...)
|
||||
|
||||
// create current header and call begin block
|
||||
header := tmproto.Header{
|
||||
ChainID: chainID,
|
||||
Height: 1,
|
||||
Time: coord.CurrentTime.UTC(),
|
||||
}
|
||||
|
||||
txConfig := params.MakeEncodingConfig().TxConfig
|
||||
|
||||
// create an account to send transactions from
|
||||
chain := &TestChain{
|
||||
t: t,
|
||||
Coordinator: coord,
|
||||
ChainID: chainID,
|
||||
App: wasmApp,
|
||||
CurrentHeader: header,
|
||||
QueryServer: wasmApp.IBCKeeper,
|
||||
TxConfig: txConfig,
|
||||
Codec: wasmApp.AppCodec(),
|
||||
Vals: valSet,
|
||||
NextVals: valSet,
|
||||
Signers: signers,
|
||||
SenderPrivKey: senderAccs[0].SenderPrivKey,
|
||||
SenderAccount: senderAccs[0].SenderAccount,
|
||||
SenderAccounts: senderAccs,
|
||||
}
|
||||
|
||||
coord.CommitBlock(chain)
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
// GetContext returns the current context for the application.
|
||||
func (chain *TestChain) GetContext() sdk.Context {
|
||||
return chain.App.BaseApp.NewContext(false, chain.CurrentHeader)
|
||||
}
|
||||
|
||||
// QueryProof performs an abci query with the given key and returns the proto encoded merkle proof
|
||||
// for the query and the height at which the proof will succeed on a tendermint verifier.
|
||||
func (chain *TestChain) QueryProof(key []byte) ([]byte, clienttypes.Height) {
|
||||
return chain.QueryProofAtHeight(key, chain.App.LastBlockHeight())
|
||||
}
|
||||
|
||||
// QueryProofAtHeight performs an abci query with the given key and returns the proto encoded merkle proof
|
||||
// for the query and the height at which the proof will succeed on a tendermint verifier.
|
||||
func (chain *TestChain) QueryProofAtHeight(key []byte, height int64) ([]byte, clienttypes.Height) {
|
||||
res := chain.App.Query(abci.RequestQuery{
|
||||
Path: fmt.Sprintf("store/%s/key", host.StoreKey),
|
||||
Height: height - 1,
|
||||
Data: key,
|
||||
Prove: true,
|
||||
})
|
||||
|
||||
merkleProof, err := commitmenttypes.ConvertProofs(res.ProofOps)
|
||||
require.NoError(chain.t, err)
|
||||
|
||||
proof, err := chain.App.AppCodec().Marshal(&merkleProof)
|
||||
require.NoError(chain.t, err)
|
||||
|
||||
revision := clienttypes.ParseChainID(chain.ChainID)
|
||||
|
||||
// proof height + 1 is returned as the proof created corresponds to the height the proof
|
||||
// was created in the IAVL tree. Tendermint and subsequently the clients that rely on it
|
||||
// have heights 1 above the IAVL tree. Thus we return proof height + 1
|
||||
return proof, clienttypes.NewHeight(revision, uint64(res.Height)+1)
|
||||
}
|
||||
|
||||
// QueryUpgradeProof performs an abci query with the given key and returns the proto encoded merkle proof
|
||||
// for the query and the height at which the proof will succeed on a tendermint verifier.
|
||||
func (chain *TestChain) QueryUpgradeProof(key []byte, height uint64) ([]byte, clienttypes.Height) {
|
||||
res := chain.App.Query(abci.RequestQuery{
|
||||
Path: "store/upgrade/key",
|
||||
Height: int64(height - 1),
|
||||
Data: key,
|
||||
Prove: true,
|
||||
})
|
||||
|
||||
merkleProof, err := commitmenttypes.ConvertProofs(res.ProofOps)
|
||||
require.NoError(chain.t, err)
|
||||
|
||||
proof, err := chain.App.AppCodec().Marshal(&merkleProof)
|
||||
require.NoError(chain.t, err)
|
||||
|
||||
revision := clienttypes.ParseChainID(chain.ChainID)
|
||||
|
||||
// proof height + 1 is returned as the proof created corresponds to the height the proof
|
||||
// was created in the IAVL tree. Tendermint and subsequently the clients that rely on it
|
||||
// have heights 1 above the IAVL tree. Thus we return proof height + 1
|
||||
return proof, clienttypes.NewHeight(revision, uint64(res.Height+1))
|
||||
}
|
||||
|
||||
// QueryConsensusStateProof performs an abci query for a consensus state
|
||||
// stored on the given clientID. The proof and consensusHeight are returned.
|
||||
func (chain *TestChain) QueryConsensusStateProof(clientID string) ([]byte, clienttypes.Height) {
|
||||
clientState := chain.GetClientState(clientID)
|
||||
|
||||
consensusHeight := clientState.GetLatestHeight().(clienttypes.Height)
|
||||
consensusKey := host.FullConsensusStateKey(clientID, consensusHeight)
|
||||
proofConsensus, _ := chain.QueryProof(consensusKey)
|
||||
|
||||
return proofConsensus, consensusHeight
|
||||
}
|
||||
|
||||
// NextBlock sets the last header to the current header and increments the current header to be
|
||||
// at the next block height. It does not update the time as that is handled by the Coordinator.
|
||||
// It will call Endblock and Commit and apply the validator set changes to the next validators
|
||||
// of the next block being created. This follows the Tendermint protocol of applying valset changes
|
||||
// returned on block `n` to the validators of block `n+2`.
|
||||
// It calls BeginBlock with the new block created before returning.
|
||||
func (chain *TestChain) NextBlock() {
|
||||
res := chain.App.EndBlock(abci.RequestEndBlock{Height: chain.CurrentHeader.Height})
|
||||
|
||||
chain.App.Commit()
|
||||
|
||||
// set the last header to the current header
|
||||
// use nil trusted fields
|
||||
chain.LastHeader = chain.CurrentTMClientHeader()
|
||||
|
||||
// val set changes returned from previous block get applied to the next validators
|
||||
// of this block. See tendermint spec for details.
|
||||
chain.Vals = chain.NextVals
|
||||
chain.NextVals = ibctesting.ApplyValSetChanges(chain.t, chain.Vals, res.ValidatorUpdates)
|
||||
|
||||
// increment the current header
|
||||
chain.CurrentHeader = tmproto.Header{
|
||||
ChainID: chain.ChainID,
|
||||
Height: chain.App.LastBlockHeight() + 1,
|
||||
AppHash: chain.App.LastCommitID().Hash,
|
||||
// NOTE: the time is increased by the coordinator to maintain time synchrony amongst
|
||||
// chains.
|
||||
Time: chain.CurrentHeader.Time,
|
||||
ValidatorsHash: chain.Vals.Hash(),
|
||||
NextValidatorsHash: chain.NextVals.Hash(),
|
||||
}
|
||||
|
||||
chain.App.BeginBlock(abci.RequestBeginBlock{Header: chain.CurrentHeader})
|
||||
}
|
||||
|
||||
// sendMsgs delivers a transaction through the application without returning the result.
|
||||
func (chain *TestChain) sendMsgs(msgs ...sdk.Msg) error {
|
||||
_, err := chain.SendMsgs(msgs...)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendMsgs delivers a transaction through the application. It updates the senders sequence
|
||||
// number and updates the TestChain's headers. It returns the result and error if one
|
||||
// occurred.
|
||||
func (chain *TestChain) SendMsgs(msgs ...sdk.Msg) (*sdk.Result, error) {
|
||||
// ensure the chain has the latest time
|
||||
chain.Coordinator.UpdateTimeForChain(chain)
|
||||
|
||||
_, r, err := app.SignAndDeliver(
|
||||
chain.t,
|
||||
chain.TxConfig,
|
||||
chain.App.BaseApp,
|
||||
chain.GetContext().BlockHeader(),
|
||||
msgs,
|
||||
chain.ChainID,
|
||||
[]uint64{chain.SenderAccount.GetAccountNumber()},
|
||||
[]uint64{chain.SenderAccount.GetSequence()},
|
||||
chain.SenderPrivKey,
|
||||
)
|
||||
|
||||
// NextBlock calls app.Commit()
|
||||
chain.NextBlock()
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
|
||||
// increment sequence for successful transaction execution
|
||||
err = chain.SenderAccount.SetSequence(chain.SenderAccount.GetSequence() + 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chain.Coordinator.IncrementTime()
|
||||
|
||||
chain.captureIBCEvents(r)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (chain *TestChain) captureIBCEvents(r *sdk.Result) {
|
||||
toSend := getSendPackets(r.Events)
|
||||
if len(toSend) > 0 {
|
||||
// Keep a queue on the chain that we can relay in tests
|
||||
chain.PendingSendPackets = append(chain.PendingSendPackets, toSend...)
|
||||
}
|
||||
}
|
||||
|
||||
// GetClientState retrieves the client state for the provided clientID. The client is
|
||||
// expected to exist otherwise testing will fail.
|
||||
func (chain *TestChain) GetClientState(clientID string) exported.ClientState {
|
||||
clientState, found := chain.App.IBCKeeper.ClientKeeper.GetClientState(chain.GetContext(), clientID)
|
||||
require.True(chain.t, found)
|
||||
|
||||
return clientState
|
||||
}
|
||||
|
||||
// GetConsensusState retrieves the consensus state for the provided clientID and height.
|
||||
// It will return a success boolean depending on if consensus state exists or not.
|
||||
func (chain *TestChain) GetConsensusState(clientID string, height exported.Height) (exported.ConsensusState, bool) {
|
||||
return chain.App.IBCKeeper.ClientKeeper.GetClientConsensusState(chain.GetContext(), clientID, height)
|
||||
}
|
||||
|
||||
// GetValsAtHeight will return the validator set of the chain at a given height. It will return
|
||||
// a success boolean depending on if the validator set exists or not at that height.
|
||||
func (chain *TestChain) GetValsAtHeight(height int64) (*tmtypes.ValidatorSet, bool) {
|
||||
histInfo, ok := chain.App.StakingKeeper.GetHistoricalInfo(chain.GetContext(), height)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
valSet := stakingtypes.Validators(histInfo.Valset)
|
||||
|
||||
tmValidators, err := teststaking.ToTmValidators(valSet, sdk.DefaultPowerReduction)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tmtypes.NewValidatorSet(tmValidators), true
|
||||
}
|
||||
|
||||
// GetAcknowledgement retrieves an acknowledgement for the provided packet. If the
|
||||
// acknowledgement does not exist then testing will fail.
|
||||
func (chain *TestChain) GetAcknowledgement(packet exported.PacketI) []byte {
|
||||
ack, found := chain.App.IBCKeeper.ChannelKeeper.GetPacketAcknowledgement(chain.GetContext(), packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
|
||||
require.True(chain.t, found)
|
||||
|
||||
return ack
|
||||
}
|
||||
|
||||
// GetPrefix returns the prefix for used by a chain in connection creation
|
||||
func (chain *TestChain) GetPrefix() commitmenttypes.MerklePrefix {
|
||||
return commitmenttypes.NewMerklePrefix(chain.App.IBCKeeper.ConnectionKeeper.GetCommitmentPrefix().Bytes())
|
||||
}
|
||||
|
||||
// ConstructUpdateTMClientHeader will construct a valid 07-tendermint Header to update the
|
||||
// light client on the source chain.
|
||||
func (chain *TestChain) ConstructUpdateTMClientHeader(counterparty *TestChain, clientID string) (*ibctmtypes.Header, error) {
|
||||
return chain.ConstructUpdateTMClientHeaderWithTrustedHeight(counterparty, clientID, clienttypes.ZeroHeight())
|
||||
}
|
||||
|
||||
// ConstructUpdateTMClientHeader will construct a valid 07-tendermint Header to update the
|
||||
// light client on the source chain.
|
||||
func (chain *TestChain) ConstructUpdateTMClientHeaderWithTrustedHeight(counterparty *TestChain, clientID string, trustedHeight clienttypes.Height) (*ibctmtypes.Header, error) {
|
||||
header := counterparty.LastHeader
|
||||
// Relayer must query for LatestHeight on client to get TrustedHeight if the trusted height is not set
|
||||
if trustedHeight.IsZero() {
|
||||
trustedHeight = chain.GetClientState(clientID).GetLatestHeight().(clienttypes.Height)
|
||||
}
|
||||
var (
|
||||
tmTrustedVals *tmtypes.ValidatorSet
|
||||
ok bool
|
||||
)
|
||||
// Once we get TrustedHeight from client, we must query the validators from the counterparty chain
|
||||
// If the LatestHeight == LastHeader.Height, then TrustedValidators are current validators
|
||||
// If LatestHeight < LastHeader.Height, we can query the historical validator set from HistoricalInfo
|
||||
if trustedHeight == counterparty.LastHeader.GetHeight() {
|
||||
tmTrustedVals = counterparty.Vals
|
||||
} else {
|
||||
// NOTE: We need to get validators from counterparty at height: trustedHeight+1
|
||||
// since the last trusted validators for a header at height h
|
||||
// is the NextValidators at h+1 committed to in header h by
|
||||
// NextValidatorsHash
|
||||
tmTrustedVals, ok = counterparty.GetValsAtHeight(int64(trustedHeight.RevisionHeight + 1))
|
||||
if !ok {
|
||||
return nil, sdkerrors.Wrapf(ibctmtypes.ErrInvalidHeaderHeight, "could not retrieve trusted validators at trustedHeight: %d", trustedHeight)
|
||||
}
|
||||
}
|
||||
// inject trusted fields into last header
|
||||
// for now assume revision number is 0
|
||||
header.TrustedHeight = trustedHeight
|
||||
|
||||
trustedVals, err := tmTrustedVals.ToProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header.TrustedValidators = trustedVals
|
||||
|
||||
return header, nil
|
||||
}
|
||||
|
||||
// ExpireClient fast forwards the chain's block time by the provided amount of time which will
|
||||
// expire any clients with a trusting period less than or equal to this amount of time.
|
||||
func (chain *TestChain) ExpireClient(amount time.Duration) {
|
||||
chain.Coordinator.IncrementTimeBy(amount)
|
||||
}
|
||||
|
||||
// CurrentTMClientHeader creates a TM header using the current header parameters
|
||||
// on the chain. The trusted fields in the header are set to nil.
|
||||
func (chain *TestChain) CurrentTMClientHeader() *ibctmtypes.Header {
|
||||
return chain.CreateTMClientHeader(chain.ChainID, chain.CurrentHeader.Height, clienttypes.Height{}, chain.CurrentHeader.Time, chain.Vals, chain.NextVals, nil, chain.Signers)
|
||||
}
|
||||
|
||||
// CreateTMClientHeader creates a TM header to update the TM client. Args are passed in to allow
|
||||
// caller flexibility to use params that differ from the chain.
|
||||
func (chain *TestChain) CreateTMClientHeader(chainID string, blockHeight int64, trustedHeight clienttypes.Height, timestamp time.Time, tmValSet, nextVals, tmTrustedVals *tmtypes.ValidatorSet, signers map[string]tmtypes.PrivValidator) *ibctmtypes.Header {
|
||||
var (
|
||||
valSet *tmproto.ValidatorSet
|
||||
trustedVals *tmproto.ValidatorSet
|
||||
)
|
||||
require.NotNil(chain.t, tmValSet)
|
||||
|
||||
vsetHash := tmValSet.Hash()
|
||||
nextValHash := nextVals.Hash()
|
||||
|
||||
tmHeader := tmtypes.Header{
|
||||
Version: tmprotoversion.Consensus{Block: tmversion.BlockProtocol, App: 2},
|
||||
ChainID: chainID,
|
||||
Height: blockHeight,
|
||||
Time: timestamp,
|
||||
LastBlockID: MakeBlockID(make([]byte, tmhash.Size), 10_000, make([]byte, tmhash.Size)),
|
||||
LastCommitHash: chain.App.LastCommitID().Hash,
|
||||
DataHash: tmhash.Sum([]byte("data_hash")),
|
||||
ValidatorsHash: vsetHash,
|
||||
NextValidatorsHash: nextValHash,
|
||||
ConsensusHash: tmhash.Sum([]byte("consensus_hash")),
|
||||
AppHash: chain.CurrentHeader.AppHash,
|
||||
LastResultsHash: tmhash.Sum([]byte("last_results_hash")),
|
||||
EvidenceHash: tmhash.Sum([]byte("evidence_hash")),
|
||||
ProposerAddress: tmValSet.Proposer.Address, //nolint:staticcheck
|
||||
}
|
||||
|
||||
hhash := tmHeader.Hash()
|
||||
blockID := MakeBlockID(hhash, 3, tmhash.Sum([]byte("part_set")))
|
||||
voteSet := tmtypes.NewVoteSet(chainID, blockHeight, 1, tmproto.PrecommitType, tmValSet)
|
||||
|
||||
// MakeCommit expects a signer array in the same order as the validator array.
|
||||
// Thus we iterate over the ordered validator set and construct a signer array
|
||||
// from the signer map in the same order.
|
||||
signerArr := make([]tmtypes.PrivValidator, len(tmValSet.Validators))
|
||||
for i, v := range tmValSet.Validators {
|
||||
signerArr[i] = signers[v.Address.String()]
|
||||
}
|
||||
|
||||
commit, err := tmtypes.MakeCommit(blockID, blockHeight, 1, voteSet, signerArr, timestamp)
|
||||
require.NoError(chain.t, err)
|
||||
|
||||
signedHeader := &tmproto.SignedHeader{
|
||||
Header: tmHeader.ToProto(),
|
||||
Commit: commit.ToProto(),
|
||||
}
|
||||
|
||||
valSet, err = tmValSet.ToProto()
|
||||
require.NoError(chain.t, err)
|
||||
|
||||
if tmTrustedVals != nil {
|
||||
trustedVals, err = tmTrustedVals.ToProto()
|
||||
require.NoError(chain.t, err)
|
||||
}
|
||||
|
||||
// The trusted fields may be nil. They may be filled before relaying messages to a client.
|
||||
// The relayer is responsible for querying client and injecting appropriate trusted fields.
|
||||
return &ibctmtypes.Header{
|
||||
SignedHeader: signedHeader,
|
||||
ValidatorSet: valSet,
|
||||
TrustedHeight: trustedHeight,
|
||||
TrustedValidators: trustedVals,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeBlockID copied unimported test functions from tmtypes to use them here
|
||||
func MakeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) tmtypes.BlockID {
|
||||
return tmtypes.BlockID{
|
||||
Hash: hash,
|
||||
PartSetHeader: tmtypes.PartSetHeader{
|
||||
Total: partSetSize,
|
||||
Hash: partSetHash,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePortCapability binds and claims a capability for the given portID if it does not
|
||||
// already exist. This function will fail testing on any resulting error.
|
||||
// NOTE: only creation of a capability for a transfer or mock port is supported
|
||||
// Other applications must bind to the port in InitGenesis or modify this code.
|
||||
func (chain *TestChain) CreatePortCapability(scopedKeeper capabilitykeeper.ScopedKeeper, portID string) {
|
||||
// check if the portId is already binded, if not bind it
|
||||
_, ok := chain.App.ScopedIBCKeeper.GetCapability(chain.GetContext(), host.PortPath(portID))
|
||||
if !ok {
|
||||
// create capability using the IBC capability keeper
|
||||
cap, err := chain.App.ScopedIBCKeeper.NewCapability(chain.GetContext(), host.PortPath(portID))
|
||||
require.NoError(chain.t, err)
|
||||
|
||||
// claim capability using the scopedKeeper
|
||||
err = scopedKeeper.ClaimCapability(chain.GetContext(), cap, host.PortPath(portID))
|
||||
require.NoError(chain.t, err)
|
||||
}
|
||||
|
||||
chain.NextBlock()
|
||||
}
|
||||
|
||||
// GetPortCapability returns the port capability for the given portID. The capability must
|
||||
// exist, otherwise testing will fail.
|
||||
func (chain *TestChain) GetPortCapability(portID string) *capabilitytypes.Capability {
|
||||
cap, ok := chain.App.ScopedIBCKeeper.GetCapability(chain.GetContext(), host.PortPath(portID))
|
||||
require.True(chain.t, ok)
|
||||
|
||||
return cap
|
||||
}
|
||||
|
||||
// CreateChannelCapability binds and claims a capability for the given portID and channelID
|
||||
// if it does not already exist. This function will fail testing on any resulting error. The
|
||||
// scoped keeper passed in will claim the new capability.
|
||||
func (chain *TestChain) CreateChannelCapability(scopedKeeper capabilitykeeper.ScopedKeeper, portID, channelID string) {
|
||||
capName := host.ChannelCapabilityPath(portID, channelID)
|
||||
// check if the portId is already binded, if not bind it
|
||||
_, ok := chain.App.ScopedIBCKeeper.GetCapability(chain.GetContext(), capName)
|
||||
if !ok {
|
||||
cap, err := chain.App.ScopedIBCKeeper.NewCapability(chain.GetContext(), capName)
|
||||
require.NoError(chain.t, err)
|
||||
err = scopedKeeper.ClaimCapability(chain.GetContext(), cap, capName)
|
||||
require.NoError(chain.t, err)
|
||||
}
|
||||
|
||||
chain.NextBlock()
|
||||
}
|
||||
|
||||
// GetChannelCapability returns the channel capability for the given portID and channelID.
|
||||
// The capability must exist, otherwise testing will fail.
|
||||
func (chain *TestChain) GetChannelCapability(portID, channelID string) *capabilitytypes.Capability {
|
||||
cap, ok := chain.App.ScopedIBCKeeper.GetCapability(chain.GetContext(), host.ChannelCapabilityPath(portID, channelID))
|
||||
require.True(chain.t, ok)
|
||||
|
||||
return cap
|
||||
}
|
||||
|
||||
func (chain *TestChain) Balance(acc sdk.AccAddress, denom string) sdk.Coin {
|
||||
return chain.App.BankKeeper.GetBalance(chain.GetContext(), acc, denom)
|
||||
}
|
||||
|
||||
func (chain *TestChain) AllBalances(acc sdk.AccAddress) sdk.Coins {
|
||||
return chain.App.BankKeeper.GetAllBalances(chain.GetContext(), acc)
|
||||
}
|
317
x/wasm/ibctesting/coordinator.go
Normal file
@ -0,0 +1,317 @@
|
||||
package ibctesting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
host "github.com/cosmos/ibc-go/v4/modules/core/24-host"
|
||||
ibctesting "github.com/cosmos/ibc-go/v4/testing"
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
wasmkeeper "github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
)
|
||||
|
||||
const ChainIDPrefix = "testchain"
|
||||
|
||||
var (
|
||||
globalStartTime = time.Date(2020, 12, 4, 10, 30, 0, 0, time.UTC)
|
||||
TimeIncrement = time.Second * 5
|
||||
)
|
||||
|
||||
// Coordinator is a testing struct which contains N TestChain's. It handles keeping all chains
|
||||
// in sync with regards to time.
|
||||
type Coordinator struct {
|
||||
t *testing.T
|
||||
|
||||
CurrentTime time.Time
|
||||
Chains map[string]*TestChain
|
||||
}
|
||||
|
||||
// NewCoordinator initializes Coordinator with N TestChain's
|
||||
func NewCoordinator(t *testing.T, n int, opts ...[]wasmkeeper.Option) *Coordinator {
|
||||
chains := make(map[string]*TestChain)
|
||||
coord := &Coordinator{
|
||||
t: t,
|
||||
CurrentTime: globalStartTime,
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
chainID := GetChainID(i)
|
||||
var x []wasmkeeper.Option
|
||||
if len(opts) > i {
|
||||
x = opts[i]
|
||||
}
|
||||
chains[chainID] = NewTestChain(t, coord, chainID, x...)
|
||||
}
|
||||
coord.Chains = chains
|
||||
|
||||
return coord
|
||||
}
|
||||
|
||||
// IncrementTime iterates through all the TestChain's and increments their current header time
|
||||
// by 5 seconds.
|
||||
//
|
||||
// CONTRACT: this function must be called after every Commit on any TestChain.
|
||||
func (coord *Coordinator) IncrementTime() {
|
||||
coord.IncrementTimeBy(TimeIncrement)
|
||||
}
|
||||
|
||||
// IncrementTimeBy iterates through all the TestChain's and increments their current header time
|
||||
// by specified time.
|
||||
func (coord *Coordinator) IncrementTimeBy(increment time.Duration) {
|
||||
coord.CurrentTime = coord.CurrentTime.Add(increment).UTC()
|
||||
coord.UpdateTime()
|
||||
}
|
||||
|
||||
// UpdateTime updates all clocks for the TestChains to the current global time.
|
||||
func (coord *Coordinator) UpdateTime() {
|
||||
for _, chain := range coord.Chains {
|
||||
coord.UpdateTimeForChain(chain)
|
||||
}
|
||||
![]() ## Iteration over map
Iteration over map may be a possible source of non-determinism
[Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/624)
|
||||
}
|
||||
|
||||
// UpdateTimeForChain updates the clock for a specific chain.
|
||||
func (coord *Coordinator) UpdateTimeForChain(chain *TestChain) {
|
||||
chain.CurrentHeader.Time = coord.CurrentTime.UTC()
|
||||
chain.App.BeginBlock(abci.RequestBeginBlock{Header: chain.CurrentHeader})
|
||||
}
|
||||
|
||||
// Setup constructs a TM client, connection, and channel on both chains provided. It will
|
||||
// fail if any error occurs. The clientID's, TestConnections, and TestChannels are returned
|
||||
// for both chains. The channels created are connected to the ibc-transfer application.
|
||||
func (coord *Coordinator) Setup(path *Path) {
|
||||
coord.SetupConnections(path)
|
||||
|
||||
// channels can also be referenced through the returned connections
|
||||
coord.CreateChannels(path)
|
||||
}
|
||||
|
||||
// SetupClients is a helper function to create clients on both chains. It assumes the
|
||||
// caller does not anticipate any errors.
|
||||
func (coord *Coordinator) SetupClients(path *Path) {
|
||||
err := path.EndpointA.CreateClient()
|
||||
require.NoError(coord.t, err)
|
||||
|
||||
err = path.EndpointB.CreateClient()
|
||||
require.NoError(coord.t, err)
|
||||
}
|
||||
|
||||
// SetupClientConnections is a helper function to create clients and the appropriate
|
||||
// connections on both the source and counterparty chain. It assumes the caller does not
|
||||
// anticipate any errors.
|
||||
func (coord *Coordinator) SetupConnections(path *Path) {
|
||||
coord.SetupClients(path)
|
||||
|
||||
coord.CreateConnections(path)
|
||||
}
|
||||
|
||||
// CreateConnection constructs and executes connection handshake messages in order to create
|
||||
// OPEN channels on chainA and chainB. The connection information of for chainA and chainB
|
||||
// are returned within a TestConnection struct. The function expects the connections to be
|
||||
// successfully opened otherwise testing will fail.
|
||||
func (coord *Coordinator) CreateConnections(path *Path) {
|
||||
err := path.EndpointA.ConnOpenInit()
|
||||
require.NoError(coord.t, err)
|
||||
|
||||
err = path.EndpointB.ConnOpenTry()
|
||||
require.NoError(coord.t, err)
|
||||
|
||||
err = path.EndpointA.ConnOpenAck()
|
||||
require.NoError(coord.t, err)
|
||||
|
||||
err = path.EndpointB.ConnOpenConfirm()
|
||||
require.NoError(coord.t, err)
|
||||
|
||||
// ensure counterparty is up to date
|
||||
err = path.EndpointA.UpdateClient()
|
||||
require.NoError(coord.t, err)
|
||||
}
|
||||
|
||||
// CreateMockChannels constructs and executes channel handshake messages to create OPEN
|
||||
// channels that use a mock application module that returns nil on all callbacks. This
|
||||
// function is expects the channels to be successfully opened otherwise testing will
|
||||
// fail.
|
||||
func (coord *Coordinator) CreateMockChannels(path *Path) {
|
||||
path.EndpointA.ChannelConfig.PortID = ibctesting.MockPort
|
||||
path.EndpointB.ChannelConfig.PortID = ibctesting.MockPort
|
||||
|
||||
coord.CreateChannels(path)
|
||||
}
|
||||
|
||||
// CreateTransferChannels constructs and executes channel handshake messages to create OPEN
|
||||
// ibc-transfer channels on chainA and chainB. The function expects the channels to be
|
||||
// successfully opened otherwise testing will fail.
|
||||
func (coord *Coordinator) CreateTransferChannels(path *Path) {
|
||||
path.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort
|
||||
path.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort
|
||||
|
||||
coord.CreateChannels(path)
|
||||
}
|
||||
|
||||
// CreateChannel constructs and executes channel handshake messages in order to create
|
||||
// OPEN channels on chainA and chainB. The function expects the channels to be successfully
|
||||
// opened otherwise testing will fail.
|
||||
func (coord *Coordinator) CreateChannels(path *Path) {
|
||||
err := path.EndpointA.ChanOpenInit()
|
||||
require.NoError(coord.t, err)
|
||||
|
||||
err = path.EndpointB.ChanOpenTry()
|
||||
require.NoError(coord.t, err)
|
||||
|
||||
err = path.EndpointA.ChanOpenAck()
|
||||
require.NoError(coord.t, err)
|
||||
|
||||
err = path.EndpointB.ChanOpenConfirm()
|
||||
require.NoError(coord.t, err)
|
||||
|
||||
// ensure counterparty is up to date
|
||||
err = path.EndpointA.UpdateClient()
|
||||
require.NoError(coord.t, err)
|
||||
}
|
||||
|
||||
// GetChain returns the TestChain using the given chainID and returns an error if it does
|
||||
// not exist.
|
||||
func (coord *Coordinator) GetChain(chainID string) *TestChain {
|
||||
chain, found := coord.Chains[chainID]
|
||||
require.True(coord.t, found, fmt.Sprintf("%s chain does not exist", chainID))
|
||||
return chain
|
||||
}
|
||||
|
||||
// GetChainID returns the chainID used for the provided index.
|
||||
func GetChainID(index int) string {
|
||||
return ChainIDPrefix + strconv.Itoa(index)
|
||||
}
|
||||
|
||||
// CommitBlock commits a block on the provided indexes and then increments the global time.
|
||||
//
|
||||
// CONTRACT: the passed in list of indexes must not contain duplicates
|
||||
func (coord *Coordinator) CommitBlock(chains ...*TestChain) {
|
||||
for _, chain := range chains {
|
||||
chain.NextBlock()
|
||||
}
|
||||
coord.IncrementTime()
|
||||
}
|
||||
|
||||
// CommitNBlocks commits n blocks to state and updates the block height by 1 for each commit.
|
||||
func (coord *Coordinator) CommitNBlocks(chain *TestChain, n uint64) {
|
||||
for i := uint64(0); i < n; i++ {
|
||||
chain.App.BeginBlock(abci.RequestBeginBlock{Header: chain.CurrentHeader})
|
||||
chain.NextBlock()
|
||||
coord.IncrementTime()
|
||||
}
|
||||
}
|
||||
|
||||
// ConnOpenInitOnBothChains initializes a connection on both endpoints with the state INIT
|
||||
// using the OpenInit handshake call.
|
||||
func (coord *Coordinator) ConnOpenInitOnBothChains(path *Path) error {
|
||||
if err := path.EndpointA.ConnOpenInit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := path.EndpointB.ConnOpenInit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := path.EndpointA.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := path.EndpointB.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChanOpenInitOnBothChains initializes a channel on the source chain and counterparty chain
|
||||
// with the state INIT using the OpenInit handshake call.
|
||||
func (coord *Coordinator) ChanOpenInitOnBothChains(path *Path) error {
|
||||
// NOTE: only creation of a capability for a transfer or mock port is supported
|
||||
// Other applications must bind to the port in InitGenesis or modify this code.
|
||||
|
||||
if err := path.EndpointA.ChanOpenInit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := path.EndpointB.ChanOpenInit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := path.EndpointA.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := path.EndpointB.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RelayAndAckPendingPackets sends pending packages from path.EndpointA to the counterparty chain and acks
|
||||
func (coord *Coordinator) RelayAndAckPendingPackets(path *Path) error {
|
||||
// get all the packet to relay src->dest
|
||||
src := path.EndpointA
|
||||
coord.t.Logf("Relay: %d Packets A->B, %d Packets B->A\n", len(src.Chain.PendingSendPackets), len(path.EndpointB.Chain.PendingSendPackets))
|
||||
for i, v := range src.Chain.PendingSendPackets {
|
||||
err := path.RelayPacket(v, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
src.Chain.PendingSendPackets = append(src.Chain.PendingSendPackets[0:i], src.Chain.PendingSendPackets[i+1:]...)
|
||||
}
|
||||
|
||||
src = path.EndpointB
|
||||
for i, v := range src.Chain.PendingSendPackets {
|
||||
err := path.RelayPacket(v, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
src.Chain.PendingSendPackets = append(src.Chain.PendingSendPackets[0:i], src.Chain.PendingSendPackets[i+1:]...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TimeoutPendingPackets returns the package to source chain to let the IBC app revert any operation.
|
||||
// from A to A
|
||||
func (coord *Coordinator) TimeoutPendingPackets(path *Path) error {
|
||||
src := path.EndpointA
|
||||
dest := path.EndpointB
|
||||
|
||||
toSend := src.Chain.PendingSendPackets
|
||||
coord.t.Logf("Timeout %d Packets A->A\n", len(toSend))
|
||||
|
||||
if err := src.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Increment time and commit block so that 5 second delay period passes between send and receive
|
||||
coord.IncrementTime()
|
||||
coord.CommitBlock(src.Chain, dest.Chain)
|
||||
for _, packet := range toSend {
|
||||
// get proof of packet unreceived on dest
|
||||
packetKey := host.PacketReceiptKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
|
||||
proofUnreceived, proofHeight := dest.QueryProof(packetKey)
|
||||
timeoutMsg := channeltypes.NewMsgTimeout(packet, packet.Sequence, proofUnreceived, proofHeight, src.Chain.SenderAccount.GetAddress().String())
|
||||
err := src.Chain.sendMsgs(timeoutMsg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
src.Chain.PendingSendPackets = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseChannel close channel on both sides
|
||||
func (coord *Coordinator) CloseChannel(path *Path) {
|
||||
err := path.EndpointA.ChanCloseInit()
|
||||
require.NoError(coord.t, err)
|
||||
coord.IncrementTime()
|
||||
err = path.EndpointB.UpdateClient()
|
||||
require.NoError(coord.t, err)
|
||||
err = path.EndpointB.ChanCloseConfirm()
|
||||
require.NoError(coord.t, err)
|
||||
}
|
597
x/wasm/ibctesting/endpoint.go
Normal file
@ -0,0 +1,597 @@
|
||||
package ibctesting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types"
|
||||
connectiontypes "github.com/cosmos/ibc-go/v4/modules/core/03-connection/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
commitmenttypes "github.com/cosmos/ibc-go/v4/modules/core/23-commitment/types"
|
||||
host "github.com/cosmos/ibc-go/v4/modules/core/24-host"
|
||||
"github.com/cosmos/ibc-go/v4/modules/core/exported"
|
||||
ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types"
|
||||
ibctesting "github.com/cosmos/ibc-go/v4/testing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Endpoint is a which represents a channel endpoint and its associated
|
||||
// client and connections. It contains client, connection, and channel
|
||||
// configuration parameters. Endpoint functions will utilize the parameters
|
||||
// set in the configuration structs when executing IBC messages.
|
||||
type Endpoint struct {
|
||||
Chain *TestChain
|
||||
Counterparty *Endpoint
|
||||
ClientID string
|
||||
ConnectionID string
|
||||
ChannelID string
|
||||
|
||||
ClientConfig ibctesting.ClientConfig
|
||||
ConnectionConfig *ibctesting.ConnectionConfig
|
||||
ChannelConfig *ibctesting.ChannelConfig
|
||||
}
|
||||
|
||||
// NewEndpoint constructs a new endpoint without the counterparty.
|
||||
// CONTRACT: the counterparty endpoint must be set by the caller.
|
||||
func NewEndpoint(
|
||||
chain *TestChain, clientConfig ibctesting.ClientConfig,
|
||||
connectionConfig *ibctesting.ConnectionConfig, channelConfig *ibctesting.ChannelConfig,
|
||||
) *Endpoint {
|
||||
return &Endpoint{
|
||||
Chain: chain,
|
||||
ClientConfig: clientConfig,
|
||||
ConnectionConfig: connectionConfig,
|
||||
ChannelConfig: channelConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDefaultEndpoint constructs a new endpoint using default values.
|
||||
// CONTRACT: the counterparty endpoitn must be set by the caller.
|
||||
func NewDefaultEndpoint(chain *TestChain) *Endpoint {
|
||||
return &Endpoint{
|
||||
Chain: chain,
|
||||
ClientConfig: ibctesting.NewTendermintConfig(),
|
||||
ConnectionConfig: ibctesting.NewConnectionConfig(),
|
||||
ChannelConfig: ibctesting.NewChannelConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// QueryProof queries proof associated with this endpoint using the lastest client state
|
||||
// height on the counterparty chain.
|
||||
func (endpoint *Endpoint) QueryProof(key []byte) ([]byte, clienttypes.Height) {
|
||||
// obtain the counterparty client representing the chain associated with the endpoint
|
||||
clientState := endpoint.Counterparty.Chain.GetClientState(endpoint.Counterparty.ClientID)
|
||||
|
||||
// query proof on the counterparty using the latest height of the IBC client
|
||||
return endpoint.QueryProofAtHeight(key, clientState.GetLatestHeight().GetRevisionHeight())
|
||||
}
|
||||
|
||||
// QueryProofAtHeight queries proof associated with this endpoint using the proof height
|
||||
// provided
|
||||
func (endpoint *Endpoint) QueryProofAtHeight(key []byte, height uint64) ([]byte, clienttypes.Height) {
|
||||
// query proof on the counterparty using the latest height of the IBC client
|
||||
return endpoint.Chain.QueryProofAtHeight(key, int64(height))
|
||||
}
|
||||
|
||||
// CreateClient creates an IBC client on the endpoint. It will update the
|
||||
// clientID for the endpoint if the message is successfully executed.
|
||||
// NOTE: a solo machine client will be created with an empty diversifier.
|
||||
func (endpoint *Endpoint) CreateClient() (err error) {
|
||||
// ensure counterparty has committed state
|
||||
endpoint.Chain.Coordinator.CommitBlock(endpoint.Counterparty.Chain)
|
||||
|
||||
var (
|
||||
clientState exported.ClientState
|
||||
consensusState exported.ConsensusState
|
||||
)
|
||||
|
||||
switch endpoint.ClientConfig.GetClientType() {
|
||||
case exported.Tendermint:
|
||||
tmConfig, ok := endpoint.ClientConfig.(*ibctesting.TendermintConfig)
|
||||
require.True(endpoint.Chain.t, ok)
|
||||
|
||||
height := endpoint.Counterparty.Chain.LastHeader.GetHeight().(clienttypes.Height)
|
||||
clientState = ibctmtypes.NewClientState(
|
||||
endpoint.Counterparty.Chain.ChainID, tmConfig.TrustLevel, tmConfig.TrustingPeriod, tmConfig.UnbondingPeriod, tmConfig.MaxClockDrift,
|
||||
height, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath, tmConfig.AllowUpdateAfterExpiry, tmConfig.AllowUpdateAfterMisbehaviour,
|
||||
)
|
||||
consensusState = endpoint.Counterparty.Chain.LastHeader.ConsensusState()
|
||||
case exported.Solomachine:
|
||||
// TODO
|
||||
// solo := NewSolomachine(chain.t, endpoint.Chain.Codec, clientID, "", 1)
|
||||
// clientState = solo.ClientState()
|
||||
// consensusState = solo.ConsensusState()
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("client type %s is not supported", endpoint.ClientConfig.GetClientType())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := clienttypes.NewMsgCreateClient(
|
||||
clientState, consensusState, endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
require.NoError(endpoint.Chain.t, err)
|
||||
|
||||
res, err := endpoint.Chain.SendMsgs(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint.ClientID, err = ibctesting.ParseClientIDFromEvents(res.GetEvents())
|
||||
![]() Should
|
||||
require.NoError(endpoint.Chain.t, err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateClient updates the IBC client associated with the endpoint.
|
||||
func (endpoint *Endpoint) UpdateClient() (err error) {
|
||||
// ensure counterparty has committed state
|
||||
endpoint.Chain.Coordinator.CommitBlock(endpoint.Counterparty.Chain)
|
||||
|
||||
var header exported.Header
|
||||
|
||||
switch endpoint.ClientConfig.GetClientType() {
|
||||
case exported.Tendermint:
|
||||
header, err = endpoint.Chain.ConstructUpdateTMClientHeader(endpoint.Counterparty.Chain, endpoint.ClientID)
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("client type %s is not supported", endpoint.ClientConfig.GetClientType())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := clienttypes.NewMsgUpdateClient(
|
||||
endpoint.ClientID, header,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
require.NoError(endpoint.Chain.t, err)
|
||||
|
||||
return endpoint.Chain.sendMsgs(msg)
|
||||
}
|
||||
|
||||
// ConnOpenInit will construct and execute a MsgConnectionOpenInit on the associated endpoint.
|
||||
func (endpoint *Endpoint) ConnOpenInit() error {
|
||||
msg := connectiontypes.NewMsgConnectionOpenInit(
|
||||
endpoint.ClientID,
|
||||
endpoint.Counterparty.ClientID,
|
||||
endpoint.Counterparty.Chain.GetPrefix(), ibctesting.DefaultOpenInitVersion, endpoint.ConnectionConfig.DelayPeriod,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
res, err := endpoint.Chain.SendMsgs(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint.ConnectionID, err = ibctesting.ParseConnectionIDFromEvents(res.GetEvents())
|
||||
![]() Should
|
||||
require.NoError(endpoint.Chain.t, err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnOpenTry will construct and execute a MsgConnectionOpenTry on the associated endpoint.
|
||||
func (endpoint *Endpoint) ConnOpenTry() error {
|
||||
if err := endpoint.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
counterpartyClient, proofClient, proofConsensus, consensusHeight, proofInit, proofHeight := endpoint.QueryConnectionHandshakeProof()
|
||||
|
||||
msg := connectiontypes.NewMsgConnectionOpenTry(
|
||||
endpoint.ClientID, endpoint.Counterparty.ConnectionID, endpoint.Counterparty.ClientID,
|
||||
counterpartyClient, endpoint.Counterparty.Chain.GetPrefix(), []*connectiontypes.Version{ibctesting.ConnectionVersion}, endpoint.ConnectionConfig.DelayPeriod,
|
||||
proofInit, proofClient, proofConsensus,
|
||||
proofHeight, consensusHeight,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
res, err := endpoint.Chain.SendMsgs(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if endpoint.ConnectionID == "" {
|
||||
endpoint.ConnectionID, err = ibctesting.ParseConnectionIDFromEvents(res.GetEvents())
|
||||
![]() Should
|
||||
require.NoError(endpoint.Chain.t, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnOpenAck will construct and execute a MsgConnectionOpenAck on the associated endpoint.
|
||||
func (endpoint *Endpoint) ConnOpenAck() error {
|
||||
if err := endpoint.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
counterpartyClient, proofClient, proofConsensus, consensusHeight, proofTry, proofHeight := endpoint.QueryConnectionHandshakeProof()
|
||||
|
||||
msg := connectiontypes.NewMsgConnectionOpenAck(
|
||||
endpoint.ConnectionID, endpoint.Counterparty.ConnectionID, counterpartyClient, // testing doesn't use flexible selection
|
||||
proofTry, proofClient, proofConsensus,
|
||||
proofHeight, consensusHeight,
|
||||
ibctesting.ConnectionVersion,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
return endpoint.Chain.sendMsgs(msg)
|
||||
}
|
||||
|
||||
// ConnOpenConfirm will construct and execute a MsgConnectionOpenConfirm on the associated endpoint.
|
||||
func (endpoint *Endpoint) ConnOpenConfirm() error {
|
||||
if err := endpoint.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
connectionKey := host.ConnectionKey(endpoint.Counterparty.ConnectionID)
|
||||
proof, height := endpoint.Counterparty.Chain.QueryProof(connectionKey)
|
||||
|
||||
msg := connectiontypes.NewMsgConnectionOpenConfirm(
|
||||
endpoint.ConnectionID,
|
||||
proof, height,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
return endpoint.Chain.sendMsgs(msg)
|
||||
}
|
||||
|
||||
// QueryConnectionHandshakeProof returns all the proofs necessary to execute OpenTry or Open Ack of
|
||||
// the connection handshakes. It returns the counterparty client state, proof of the counterparty
|
||||
// client state, proof of the counterparty consensus state, the consensus state height, proof of
|
||||
// the counterparty connection, and the proof height for all the proofs returned.
|
||||
func (endpoint *Endpoint) QueryConnectionHandshakeProof() (
|
||||
clientState exported.ClientState, proofClient,
|
||||
proofConsensus []byte, consensusHeight clienttypes.Height,
|
||||
proofConnection []byte, proofHeight clienttypes.Height,
|
||||
) {
|
||||
// obtain the client state on the counterparty chain
|
||||
clientState = endpoint.Counterparty.Chain.GetClientState(endpoint.Counterparty.ClientID)
|
||||
|
||||
// query proof for the client state on the counterparty
|
||||
clientKey := host.FullClientStateKey(endpoint.Counterparty.ClientID)
|
||||
proofClient, proofHeight = endpoint.Counterparty.QueryProof(clientKey)
|
||||
|
||||
consensusHeight = clientState.GetLatestHeight().(clienttypes.Height)
|
||||
|
||||
// query proof for the consensus state on the counterparty
|
||||
consensusKey := host.FullConsensusStateKey(endpoint.Counterparty.ClientID, consensusHeight)
|
||||
proofConsensus, _ = endpoint.Counterparty.QueryProofAtHeight(consensusKey, proofHeight.GetRevisionHeight())
|
||||
|
||||
// query proof for the connection on the counterparty
|
||||
connectionKey := host.ConnectionKey(endpoint.Counterparty.ConnectionID)
|
||||
proofConnection, _ = endpoint.Counterparty.QueryProofAtHeight(connectionKey, proofHeight.GetRevisionHeight())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ChanOpenInit will construct and execute a MsgChannelOpenInit on the associated endpoint.
|
||||
func (endpoint *Endpoint) ChanOpenInit() error {
|
||||
msg := channeltypes.NewMsgChannelOpenInit(
|
||||
endpoint.ChannelConfig.PortID,
|
||||
endpoint.ChannelConfig.Version, endpoint.ChannelConfig.Order, []string{endpoint.ConnectionID},
|
||||
endpoint.Counterparty.ChannelConfig.PortID,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
res, err := endpoint.Chain.SendMsgs(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint.ChannelID, err = ibctesting.ParseChannelIDFromEvents(res.GetEvents())
|
||||
![]() Should
|
||||
require.NoError(endpoint.Chain.t, err)
|
||||
|
||||
// update version to selected app version
|
||||
// NOTE: this update must be performed after SendMsgs()
|
||||
endpoint.ChannelConfig.Version = endpoint.GetChannel().Version
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChanOpenTry will construct and execute a MsgChannelOpenTry on the associated endpoint.
|
||||
func (endpoint *Endpoint) ChanOpenTry() error {
|
||||
if err := endpoint.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channelKey := host.ChannelKey(endpoint.Counterparty.ChannelConfig.PortID, endpoint.Counterparty.ChannelID)
|
||||
proof, height := endpoint.Counterparty.Chain.QueryProof(channelKey)
|
||||
|
||||
msg := channeltypes.NewMsgChannelOpenTry(
|
||||
endpoint.ChannelConfig.PortID,
|
||||
endpoint.ChannelConfig.Version, endpoint.ChannelConfig.Order, []string{endpoint.ConnectionID},
|
||||
endpoint.Counterparty.ChannelConfig.PortID, endpoint.Counterparty.ChannelID, endpoint.Counterparty.ChannelConfig.Version,
|
||||
proof, height,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
res, err := endpoint.Chain.SendMsgs(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if endpoint.ChannelID == "" {
|
||||
endpoint.ChannelID, err = ibctesting.ParseChannelIDFromEvents(res.GetEvents())
|
||||
![]() Should
|
||||
require.NoError(endpoint.Chain.t, err)
|
||||
}
|
||||
|
||||
// update version to selected app version
|
||||
// NOTE: this update must be performed after the endpoint channelID is set
|
||||
endpoint.ChannelConfig.Version = endpoint.GetChannel().Version
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChanOpenAck will construct and execute a MsgChannelOpenAck on the associated endpoint.
|
||||
func (endpoint *Endpoint) ChanOpenAck() error {
|
||||
if err := endpoint.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channelKey := host.ChannelKey(endpoint.Counterparty.ChannelConfig.PortID, endpoint.Counterparty.ChannelID)
|
||||
proof, height := endpoint.Counterparty.Chain.QueryProof(channelKey)
|
||||
|
||||
msg := channeltypes.NewMsgChannelOpenAck(
|
||||
endpoint.ChannelConfig.PortID, endpoint.ChannelID,
|
||||
endpoint.Counterparty.ChannelID, endpoint.Counterparty.ChannelConfig.Version, // testing doesn't use flexible selection
|
||||
proof, height,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
if err := endpoint.Chain.sendMsgs(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint.ChannelConfig.Version = endpoint.GetChannel().Version
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChanOpenConfirm will construct and execute a MsgChannelOpenConfirm on the associated endpoint.
|
||||
func (endpoint *Endpoint) ChanOpenConfirm() error {
|
||||
if err := endpoint.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channelKey := host.ChannelKey(endpoint.Counterparty.ChannelConfig.PortID, endpoint.Counterparty.ChannelID)
|
||||
proof, height := endpoint.Counterparty.Chain.QueryProof(channelKey)
|
||||
|
||||
msg := channeltypes.NewMsgChannelOpenConfirm(
|
||||
endpoint.ChannelConfig.PortID, endpoint.ChannelID,
|
||||
proof, height,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
return endpoint.Chain.sendMsgs(msg)
|
||||
}
|
||||
|
||||
// ChanCloseInit will construct and execute a MsgChannelCloseInit on the associated endpoint.
|
||||
//
|
||||
// NOTE: does not work with ibc-transfer module
|
||||
func (endpoint *Endpoint) ChanCloseInit() error {
|
||||
msg := channeltypes.NewMsgChannelCloseInit(
|
||||
endpoint.ChannelConfig.PortID, endpoint.ChannelID,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
return endpoint.Chain.sendMsgs(msg)
|
||||
}
|
||||
|
||||
// ChanCloseConfirm will construct and execute a NewMsgChannelCloseConfirm on the associated endpoint.
|
||||
func (endpoint *Endpoint) ChanCloseConfirm() error {
|
||||
channelKey := host.ChannelKey(endpoint.Counterparty.ChannelConfig.PortID, endpoint.Counterparty.ChannelID)
|
||||
proof, proofHeight := endpoint.Counterparty.QueryProof(channelKey)
|
||||
|
||||
msg := channeltypes.NewMsgChannelCloseConfirm(
|
||||
endpoint.ChannelConfig.PortID, endpoint.ChannelID,
|
||||
proof, proofHeight,
|
||||
endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
return endpoint.Chain.sendMsgs(msg)
|
||||
}
|
||||
|
||||
// SendPacket sends a packet through the channel keeper using the associated endpoint
|
||||
// The counterparty client is updated so proofs can be sent to the counterparty chain.
|
||||
func (endpoint *Endpoint) SendPacket(packet exported.PacketI) error {
|
||||
channelCap := endpoint.Chain.GetChannelCapability(packet.GetSourcePort(), packet.GetSourceChannel())
|
||||
|
||||
// no need to send message, acting as a module
|
||||
err := endpoint.Chain.App.IBCKeeper.ChannelKeeper.SendPacket(endpoint.Chain.GetContext(), channelCap, packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// commit changes since no message was sent
|
||||
endpoint.Chain.Coordinator.CommitBlock(endpoint.Chain)
|
||||
|
||||
return endpoint.Counterparty.UpdateClient()
|
||||
}
|
||||
|
||||
// RecvPacket receives a packet on the associated endpoint.
|
||||
// The counterparty client is updated.
|
||||
func (endpoint *Endpoint) RecvPacket(packet channeltypes.Packet) error {
|
||||
_, err := endpoint.RecvPacketWithResult(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecvPacketWithResult receives a packet on the associated endpoint and the result
|
||||
// of the transaction is returned. The counterparty client is updated.
|
||||
func (endpoint *Endpoint) RecvPacketWithResult(packet channeltypes.Packet) (*sdk.Result, error) {
|
||||
// get proof of packet commitment on source
|
||||
packetKey := host.PacketCommitmentKey(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
|
||||
proof, proofHeight := endpoint.Counterparty.Chain.QueryProof(packetKey)
|
||||
|
||||
recvMsg := channeltypes.NewMsgRecvPacket(packet, proof, proofHeight, endpoint.Chain.SenderAccount.GetAddress().String())
|
||||
|
||||
// receive on counterparty and update source client
|
||||
res, err := endpoint.Chain.SendMsgs(recvMsg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := endpoint.Counterparty.UpdateClient(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// WriteAcknowledgement writes an acknowledgement on the channel associated with the endpoint.
|
||||
// The counterparty client is updated.
|
||||
func (endpoint *Endpoint) WriteAcknowledgement(ack exported.Acknowledgement, packet exported.PacketI) error {
|
||||
channelCap := endpoint.Chain.GetChannelCapability(packet.GetDestPort(), packet.GetDestChannel())
|
||||
|
||||
// no need to send message, acting as a handler
|
||||
err := endpoint.Chain.App.IBCKeeper.ChannelKeeper.WriteAcknowledgement(endpoint.Chain.GetContext(), channelCap, packet, ack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// commit changes since no message was sent
|
||||
endpoint.Chain.Coordinator.CommitBlock(endpoint.Chain)
|
||||
|
||||
return endpoint.Counterparty.UpdateClient()
|
||||
}
|
||||
|
||||
// AcknowledgePacket sends a MsgAcknowledgement to the channel associated with the endpoint.
|
||||
func (endpoint *Endpoint) AcknowledgePacket(packet channeltypes.Packet, ack []byte) error {
|
||||
// get proof of acknowledgement on counterparty
|
||||
packetKey := host.PacketAcknowledgementKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
|
||||
proof, proofHeight := endpoint.Counterparty.QueryProof(packetKey)
|
||||
|
||||
ackMsg := channeltypes.NewMsgAcknowledgement(packet, ack, proof, proofHeight, endpoint.Chain.SenderAccount.GetAddress().String())
|
||||
|
||||
return endpoint.Chain.sendMsgs(ackMsg)
|
||||
}
|
||||
|
||||
// TimeoutPacket sends a MsgTimeout to the channel associated with the endpoint.
|
||||
func (endpoint *Endpoint) TimeoutPacket(packet channeltypes.Packet) error {
|
||||
// get proof for timeout based on channel order
|
||||
var packetKey []byte
|
||||
|
||||
switch endpoint.ChannelConfig.Order {
|
||||
case channeltypes.ORDERED:
|
||||
packetKey = host.NextSequenceRecvKey(packet.GetDestPort(), packet.GetDestChannel())
|
||||
case channeltypes.UNORDERED:
|
||||
packetKey = host.PacketReceiptKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
|
||||
default:
|
||||
return fmt.Errorf("unsupported order type %s", endpoint.ChannelConfig.Order)
|
||||
}
|
||||
|
||||
proof, proofHeight := endpoint.Counterparty.QueryProof(packetKey)
|
||||
nextSeqRecv, found := endpoint.Counterparty.Chain.App.IBCKeeper.ChannelKeeper.GetNextSequenceRecv(endpoint.Counterparty.Chain.GetContext(), endpoint.ChannelConfig.PortID, endpoint.ChannelID)
|
||||
require.True(endpoint.Chain.t, found)
|
||||
|
||||
timeoutMsg := channeltypes.NewMsgTimeout(
|
||||
packet, nextSeqRecv,
|
||||
proof, proofHeight, endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
|
||||
return endpoint.Chain.sendMsgs(timeoutMsg)
|
||||
}
|
||||
|
||||
// TimeoutOnClose sends a MsgTimeoutOnClose to the channel associated with the endpoint.
|
||||
func (endpoint *Endpoint) TimeoutOnClose(packet channeltypes.Packet) error {
|
||||
// get proof for timeout based on channel order
|
||||
var packetKey []byte
|
||||
|
||||
switch endpoint.ChannelConfig.Order {
|
||||
case channeltypes.ORDERED:
|
||||
packetKey = host.NextSequenceRecvKey(packet.GetDestPort(), packet.GetDestChannel())
|
||||
case channeltypes.UNORDERED:
|
||||
packetKey = host.PacketReceiptKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
|
||||
default:
|
||||
return fmt.Errorf("unsupported order type %s", endpoint.ChannelConfig.Order)
|
||||
}
|
||||
|
||||
proof, proofHeight := endpoint.Counterparty.QueryProof(packetKey)
|
||||
|
||||
channelKey := host.ChannelKey(packet.GetDestPort(), packet.GetDestChannel())
|
||||
proofClosed, _ := endpoint.Counterparty.QueryProof(channelKey)
|
||||
|
||||
nextSeqRecv, found := endpoint.Counterparty.Chain.App.IBCKeeper.ChannelKeeper.GetNextSequenceRecv(endpoint.Counterparty.Chain.GetContext(), endpoint.ChannelConfig.PortID, endpoint.ChannelID)
|
||||
require.True(endpoint.Chain.t, found)
|
||||
|
||||
timeoutOnCloseMsg := channeltypes.NewMsgTimeoutOnClose(
|
||||
packet, nextSeqRecv,
|
||||
proof, proofClosed, proofHeight, endpoint.Chain.SenderAccount.GetAddress().String(),
|
||||
)
|
||||
|
||||
return endpoint.Chain.sendMsgs(timeoutOnCloseMsg)
|
||||
}
|
||||
|
||||
// SetChannelClosed sets a channel state to CLOSED.
|
||||
func (endpoint *Endpoint) SetChannelClosed() error {
|
||||
channel := endpoint.GetChannel()
|
||||
|
||||
channel.State = channeltypes.CLOSED
|
||||
endpoint.Chain.App.IBCKeeper.ChannelKeeper.SetChannel(endpoint.Chain.GetContext(), endpoint.ChannelConfig.PortID, endpoint.ChannelID, channel)
|
||||
|
||||
endpoint.Chain.Coordinator.CommitBlock(endpoint.Chain)
|
||||
|
||||
return endpoint.Counterparty.UpdateClient()
|
||||
}
|
||||
|
||||
// GetClientState retrieves the Client State for this endpoint. The
|
||||
// client state is expected to exist otherwise testing will fail.
|
||||
func (endpoint *Endpoint) GetClientState() exported.ClientState {
|
||||
return endpoint.Chain.GetClientState(endpoint.ClientID)
|
||||
}
|
||||
|
||||
// SetClientState sets the client state for this endpoint.
|
||||
func (endpoint *Endpoint) SetClientState(clientState exported.ClientState) {
|
||||
endpoint.Chain.App.IBCKeeper.ClientKeeper.SetClientState(endpoint.Chain.GetContext(), endpoint.ClientID, clientState)
|
||||
}
|
||||
|
||||
// GetConsensusState retrieves the Consensus State for this endpoint at the provided height.
|
||||
// The consensus state is expected to exist otherwise testing will fail.
|
||||
func (endpoint *Endpoint) GetConsensusState(height exported.Height) exported.ConsensusState {
|
||||
consensusState, found := endpoint.Chain.GetConsensusState(endpoint.ClientID, height)
|
||||
require.True(endpoint.Chain.t, found)
|
||||
|
||||
return consensusState
|
||||
}
|
||||
|
||||
// SetConsensusState sets the consensus state for this endpoint.
|
||||
func (endpoint *Endpoint) SetConsensusState(consensusState exported.ConsensusState, height exported.Height) {
|
||||
endpoint.Chain.App.IBCKeeper.ClientKeeper.SetClientConsensusState(endpoint.Chain.GetContext(), endpoint.ClientID, height, consensusState)
|
||||
}
|
||||
|
||||
// GetConnection retrieves an IBC Connection for the endpoint. The
|
||||
// connection is expected to exist otherwise testing will fail.
|
||||
func (endpoint *Endpoint) GetConnection() connectiontypes.ConnectionEnd {
|
||||
connection, found := endpoint.Chain.App.IBCKeeper.ConnectionKeeper.GetConnection(endpoint.Chain.GetContext(), endpoint.ConnectionID)
|
||||
require.True(endpoint.Chain.t, found)
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
// SetConnection sets the connection for this endpoint.
|
||||
func (endpoint *Endpoint) SetConnection(connection connectiontypes.ConnectionEnd) {
|
||||
endpoint.Chain.App.IBCKeeper.ConnectionKeeper.SetConnection(endpoint.Chain.GetContext(), endpoint.ConnectionID, connection)
|
||||
}
|
||||
|
||||
// GetChannel retrieves an IBC Channel for the endpoint. The channel
|
||||
// is expected to exist otherwise testing will fail.
|
||||
func (endpoint *Endpoint) GetChannel() channeltypes.Channel {
|
||||
channel, found := endpoint.Chain.App.IBCKeeper.ChannelKeeper.GetChannel(endpoint.Chain.GetContext(), endpoint.ChannelConfig.PortID, endpoint.ChannelID)
|
||||
require.True(endpoint.Chain.t, found)
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
// SetChannel sets the channel for this endpoint.
|
||||
func (endpoint *Endpoint) SetChannel(channel channeltypes.Channel) {
|
||||
endpoint.Chain.App.IBCKeeper.ChannelKeeper.SetChannel(endpoint.Chain.GetContext(), endpoint.ChannelConfig.PortID, endpoint.ChannelID, channel)
|
||||
}
|
||||
|
||||
// QueryClientStateProof performs and abci query for a client stat associated
|
||||
// with this endpoint and returns the ClientState along with the proof.
|
||||
func (endpoint *Endpoint) QueryClientStateProof() (exported.ClientState, []byte) {
|
||||
// retrieve client state to provide proof for
|
||||
clientState := endpoint.GetClientState()
|
||||
|
||||
clientKey := host.FullClientStateKey(endpoint.ClientID)
|
||||
proofClient, _ := endpoint.QueryProof(clientKey)
|
||||
|
||||
return clientState, proofClient
|
||||
}
|
118
x/wasm/ibctesting/event_utils.go
Normal file
@ -0,0 +1,118 @@
|
||||
package ibctesting
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
)
|
||||
|
||||
func getSendPackets(evts []abci.Event) []channeltypes.Packet {
|
||||
var res []channeltypes.Packet
|
||||
for _, evt := range evts {
|
||||
if evt.Type == channeltypes.EventTypeSendPacket {
|
||||
packet := parsePacketFromEvent(evt)
|
||||
res = append(res, packet)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Used for various debug statements above when needed... do not remove
|
||||
// func showEvent(evt abci.Event) {
|
||||
// fmt.Printf("evt.Type: %s\n", evt.Type)
|
||||
// for _, attr := range evt.Attributes {
|
||||
// fmt.Printf(" %s = %s\n", string(attr.Key), string(attr.Value))
|
||||
// }
|
||||
//}
|
||||
|
||||
func parsePacketFromEvent(evt abci.Event) channeltypes.Packet {
|
||||
return channeltypes.Packet{
|
||||
Sequence: getUintField(evt, channeltypes.AttributeKeySequence),
|
||||
SourcePort: getField(evt, channeltypes.AttributeKeySrcPort),
|
||||
SourceChannel: getField(evt, channeltypes.AttributeKeySrcChannel),
|
||||
DestinationPort: getField(evt, channeltypes.AttributeKeyDstPort),
|
||||
DestinationChannel: getField(evt, channeltypes.AttributeKeyDstChannel),
|
||||
Data: getHexField(evt, channeltypes.AttributeKeyDataHex),
|
||||
TimeoutHeight: parseTimeoutHeight(getField(evt, channeltypes.AttributeKeyTimeoutHeight)),
|
||||
TimeoutTimestamp: getUintField(evt, channeltypes.AttributeKeyTimeoutTimestamp),
|
||||
}
|
||||
}
|
||||
|
||||
func getHexField(evt abci.Event, key string) []byte {
|
||||
got := getField(evt, key)
|
||||
if got == "" {
|
||||
return nil
|
||||
}
|
||||
bz, err := hex.DecodeString(got)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bz
|
||||
}
|
||||
|
||||
// return the value for the attribute with the given name
|
||||
func getField(evt abci.Event, key string) string {
|
||||
for _, attr := range evt.Attributes {
|
||||
if string(attr.Key) == key {
|
||||
return string(attr.Value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getUintField(evt abci.Event, key string) uint64 {
|
||||
raw := getField(evt, key)
|
||||
return toUint64(raw)
|
||||
}
|
||||
|
||||
func toUint64(raw string) uint64 {
|
||||
if raw == "" {
|
||||
return 0
|
||||
}
|
||||
i, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func parseTimeoutHeight(raw string) clienttypes.Height {
|
||||
chunks := strings.Split(raw, "-")
|
||||
return clienttypes.Height{
|
||||
RevisionNumber: toUint64(chunks[0]),
|
||||
RevisionHeight: toUint64(chunks[1]),
|
||||
}
|
||||
}
|
||||
|
||||
func ParsePortIDFromEvents(events sdk.Events) (string, error) {
|
||||
for _, ev := range events {
|
||||
if ev.Type == channeltypes.EventTypeChannelOpenInit || ev.Type == channeltypes.EventTypeChannelOpenTry {
|
||||
for _, attr := range ev.Attributes {
|
||||
if string(attr.Key) == channeltypes.AttributeKeyPortID {
|
||||
return string(attr.Value), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("port id event attribute not found")
|
||||
}
|
||||
|
||||
func ParseChannelVersionFromEvents(events sdk.Events) (string, error) {
|
||||
for _, ev := range events {
|
||||
if ev.Type == channeltypes.EventTypeChannelOpenInit || ev.Type == channeltypes.EventTypeChannelOpenTry {
|
||||
for _, attr := range ev.Attributes {
|
||||
if string(attr.Key) == channeltypes.AttributeVersion {
|
||||
return string(attr.Value), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("version event attribute not found")
|
||||
}
|
52
x/wasm/ibctesting/faucet.go
Normal file
@ -0,0 +1,52 @@
|
||||
package ibctesting
|
||||
|
||||
import (
|
||||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/app"
|
||||
)
|
||||
|
||||
// Fund an address with the given amount in default denom
|
||||
func (chain *TestChain) Fund(addr sdk.AccAddress, amount sdk.Int) {
|
||||
require.NoError(chain.t, chain.sendMsgs(&banktypes.MsgSend{
|
||||
FromAddress: chain.SenderAccount.GetAddress().String(),
|
||||
ToAddress: addr.String(),
|
||||
Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, amount)),
|
||||
}))
|
||||
}
|
||||
|
||||
// SendNonDefaultSenderMsgs delivers a transaction through the application. It returns the result and error if one
|
||||
// occurred.
|
||||
func (chain *TestChain) SendNonDefaultSenderMsgs(senderPrivKey cryptotypes.PrivKey, msgs ...sdk.Msg) (*sdk.Result, error) {
|
||||
require.NotEqual(chain.t, chain.SenderPrivKey, senderPrivKey, "use SendMsgs method")
|
||||
|
||||
// ensure the chain has the latest time
|
||||
chain.Coordinator.UpdateTimeForChain(chain)
|
||||
|
||||
addr := sdk.AccAddress(senderPrivKey.PubKey().Address().Bytes())
|
||||
account := chain.App.AccountKeeper.GetAccount(chain.GetContext(), addr)
|
||||
require.NotNil(chain.t, account)
|
||||
_, r, err := app.SignAndDeliver(
|
||||
chain.t,
|
||||
chain.TxConfig,
|
||||
chain.App.BaseApp,
|
||||
chain.GetContext().BlockHeader(),
|
||||
msgs,
|
||||
chain.ChainID,
|
||||
[]uint64{account.GetAccountNumber()},
|
||||
[]uint64{account.GetSequence()},
|
||||
senderPrivKey,
|
||||
)
|
||||
|
||||
// SignAndDeliver calls app.Commit()
|
||||
chain.NextBlock()
|
||||
chain.Coordinator.IncrementTime()
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
chain.captureIBCEvents(r)
|
||||
return r, nil
|
||||
}
|
113
x/wasm/ibctesting/path.go
Normal file
@ -0,0 +1,113 @@
|
||||
package ibctesting
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
ibctesting "github.com/cosmos/ibc-go/v4/testing"
|
||||
)
|
||||
|
||||
// Path contains two endpoints representing two chains connected over IBC
|
||||
type Path struct {
|
||||
EndpointA *Endpoint
|
||||
EndpointB *Endpoint
|
||||
}
|
||||
|
||||
// NewPath constructs an endpoint for each chain using the default values
|
||||
// for the endpoints. Each endpoint is updated to have a pointer to the
|
||||
// counterparty endpoint.
|
||||
func NewPath(chainA, chainB *TestChain) *Path {
|
||||
endpointA := NewDefaultEndpoint(chainA)
|
||||
endpointB := NewDefaultEndpoint(chainB)
|
||||
|
||||
endpointA.Counterparty = endpointB
|
||||
endpointB.Counterparty = endpointA
|
||||
|
||||
return &Path{
|
||||
EndpointA: endpointA,
|
||||
EndpointB: endpointB,
|
||||
}
|
||||
}
|
||||
|
||||
// SetChannelOrdered sets the channel order for both endpoints to ORDERED.
|
||||
func (path *Path) SetChannelOrdered() {
|
||||
path.EndpointA.ChannelConfig.Order = channeltypes.ORDERED
|
||||
path.EndpointB.ChannelConfig.Order = channeltypes.ORDERED
|
||||
}
|
||||
|
||||
// RelayPacket attempts to relay the packet first on EndpointA and then on EndpointB
|
||||
// if EndpointA does not contain a packet commitment for that packet. An error is returned
|
||||
// if a relay step fails or the packet commitment does not exist on either endpoint.
|
||||
func (path *Path) RelayPacket(packet channeltypes.Packet, ack []byte) error {
|
||||
pc := path.EndpointA.Chain.App.IBCKeeper.ChannelKeeper.GetPacketCommitment(path.EndpointA.Chain.GetContext(), packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
|
||||
if bytes.Equal(pc, channeltypes.CommitPacket(path.EndpointA.Chain.App.AppCodec(), packet)) {
|
||||
|
||||
// packet found, relay from A to B
|
||||
if err := path.EndpointB.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := path.EndpointB.RecvPacketWithResult(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ack, err := ibctesting.ParseAckFromEvents(res.GetEvents())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := path.EndpointA.AcknowledgePacket(packet, ack); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
pc = path.EndpointB.Chain.App.IBCKeeper.ChannelKeeper.GetPacketCommitment(path.EndpointB.Chain.GetContext(), packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
|
||||
if bytes.Equal(pc, channeltypes.CommitPacket(path.EndpointB.Chain.App.AppCodec(), packet)) {
|
||||
|
||||
// packet found, relay B to A
|
||||
if err := path.EndpointA.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := path.EndpointA.RecvPacketWithResult(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ack, err := ibctesting.ParseAckFromEvents(res.GetEvents())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := path.EndpointB.AcknowledgePacket(packet, ack); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("packet commitment does not exist on either endpoint for provided packet")
|
||||
}
|
||||
|
||||
// SendMsg delivers the provided messages to the chain. The counterparty
|
||||
// client is updated with the new source consensus state.
|
||||
func (path *Path) SendMsg(msgs ...sdk.Msg) error {
|
||||
if err := path.EndpointA.Chain.sendMsgs(msgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := path.EndpointA.UpdateClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
return path.EndpointB.UpdateClient()
|
||||
}
|
||||
|
||||
func (path *Path) Invert() *Path {
|
||||
return &Path{
|
||||
EndpointA: path.EndpointB,
|
||||
EndpointB: path.EndpointA,
|
||||
}
|
||||
}
|
138
x/wasm/ibctesting/wasm.go
Normal file
@ -0,0 +1,138 @@
|
||||
package ibctesting
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
ibctesting "github.com/cosmos/ibc-go/v4/testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/golang/protobuf/proto" //nolint
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/rand"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
var wasmIdent = []byte("\x00\x61\x73\x6D")
|
||||
|
||||
// SeedNewContractInstance stores some wasm code and instantiates a new contract on this chain.
|
||||
// This method can be called to prepare the store with some valid CodeInfo and ContractInfo. The returned
|
||||
// Address is the contract address for this instance. Test should make use of this data and/or use NewIBCContractMockWasmer
|
||||
// for using a contract mock in Go.
|
||||
func (chain *TestChain) SeedNewContractInstance() sdk.AccAddress {
|
||||
pInstResp := chain.StoreCode(append(wasmIdent, rand.Bytes(10)...))
|
||||
codeID := pInstResp.CodeID
|
||||
|
||||
anyAddressStr := chain.SenderAccount.GetAddress().String()
|
||||
initMsg := []byte(fmt.Sprintf(`{"verifier": %q, "beneficiary": %q}`, anyAddressStr, anyAddressStr))
|
||||
return chain.InstantiateContract(codeID, initMsg)
|
||||
}
|
||||
|
||||
func (chain *TestChain) StoreCodeFile(filename string) types.MsgStoreCodeResponse {
|
||||
wasmCode, err := os.ReadFile(filename)
|
||||
require.NoError(chain.t, err)
|
||||
if strings.HasSuffix(filename, "wasm") { // compress for gas limit
|
||||
var buf bytes.Buffer
|
||||
gz := gzip.NewWriter(&buf)
|
||||
_, err := gz.Write(wasmCode)
|
||||
require.NoError(chain.t, err)
|
||||
err = gz.Close()
|
||||
require.NoError(chain.t, err)
|
||||
wasmCode = buf.Bytes()
|
||||
}
|
||||
return chain.StoreCode(wasmCode)
|
||||
}
|
||||
|
||||
func (chain *TestChain) StoreCode(byteCode []byte) types.MsgStoreCodeResponse {
|
||||
storeMsg := &types.MsgStoreCode{
|
||||
Sender: chain.SenderAccount.GetAddress().String(),
|
||||
WASMByteCode: byteCode,
|
||||
}
|
||||
r, err := chain.SendMsgs(storeMsg)
|
||||
require.NoError(chain.t, err)
|
||||
protoResult := chain.parseSDKResultData(r)
|
||||
require.Len(chain.t, protoResult.Data, 1)
|
||||
// unmarshal protobuf response from data
|
||||
var pInstResp types.MsgStoreCodeResponse
|
||||
require.NoError(chain.t, pInstResp.Unmarshal(protoResult.Data[0].Data))
|
||||
require.NotEmpty(chain.t, pInstResp.CodeID)
|
||||
require.NotEmpty(chain.t, pInstResp.Checksum)
|
||||
return pInstResp
|
||||
}
|
||||
|
||||
func (chain *TestChain) InstantiateContract(codeID uint64, initMsg []byte) sdk.AccAddress {
|
||||
instantiateMsg := &types.MsgInstantiateContract{
|
||||
Sender: chain.SenderAccount.GetAddress().String(),
|
||||
Admin: chain.SenderAccount.GetAddress().String(),
|
||||
CodeID: codeID,
|
||||
Label: "ibc-test",
|
||||
Msg: initMsg,
|
||||
Funds: sdk.Coins{ibctesting.TestCoin},
|
||||
}
|
||||
|
||||
r, err := chain.SendMsgs(instantiateMsg)
|
||||
require.NoError(chain.t, err)
|
||||
protoResult := chain.parseSDKResultData(r)
|
||||
require.Len(chain.t, protoResult.Data, 1)
|
||||
|
||||
var pExecResp types.MsgInstantiateContractResponse
|
||||
require.NoError(chain.t, pExecResp.Unmarshal(protoResult.Data[0].Data))
|
||||
a, err := sdk.AccAddressFromBech32(pExecResp.Address)
|
||||
require.NoError(chain.t, err)
|
||||
return a
|
||||
}
|
||||
|
||||
// SmartQuery This will serialize the query message and submit it to the contract.
|
||||
// The response is parsed into the provided interface.
|
||||
// Usage: SmartQuery(addr, QueryMsg{Foo: 1}, &response)
|
||||
func (chain *TestChain) SmartQuery(contractAddr string, queryMsg interface{}, response interface{}) error {
|
||||
msg, err := json.Marshal(queryMsg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := types.QuerySmartContractStateRequest{
|
||||
Address: contractAddr,
|
||||
QueryData: msg,
|
||||
}
|
||||
reqBin, err := proto.Marshal(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: what is the query?
|
||||
res := chain.App.Query(abci.RequestQuery{
|
||||
Path: "/cosmwasm.wasm.v1.Query/SmartContractState",
|
||||
Data: reqBin,
|
||||
})
|
||||
|
||||
if res.Code != 0 {
|
||||
return fmt.Errorf("query failed: (%d) %s", res.Code, res.Log)
|
||||
}
|
||||
|
||||
// unpack protobuf
|
||||
var resp types.QuerySmartContractStateResponse
|
||||
err = proto.Unmarshal(res.Value, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// unpack json content
|
||||
return json.Unmarshal(resp.Data, response)
|
||||
}
|
||||
|
||||
func (chain *TestChain) parseSDKResultData(r *sdk.Result) sdk.TxMsgData {
|
||||
var protoResult sdk.TxMsgData
|
||||
require.NoError(chain.t, proto.Unmarshal(r.Data, &protoResult))
|
||||
return protoResult
|
||||
}
|
||||
|
||||
// ContractInfo is a helper function to returns the ContractInfo for the given contract address
|
||||
func (chain *TestChain) ContractInfo(contractAddr sdk.AccAddress) *types.ContractInfo {
|
||||
return chain.App.WasmKeeper.GetContractInfo(chain.GetContext(), contractAddr)
|
||||
}
|
41
x/wasm/ioutils/ioutil.go
Normal file
@ -0,0 +1,41 @@
|
||||
package ioutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// Uncompress expects a valid gzip source to unpack or fails. See IsGzip
|
||||
func Uncompress(gzipSrc []byte, limit uint64) ([]byte, error) {
|
||||
if uint64(len(gzipSrc)) > limit {
|
||||
return nil, types.ErrLimit
|
||||
}
|
||||
zr, err := gzip.NewReader(bytes.NewReader(gzipSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zr.Multistream(false)
|
||||
defer zr.Close()
|
||||
return io.ReadAll(LimitReader(zr, int64(limit)))
|
||||
}
|
||||
|
||||
// LimitReader returns a Reader that reads from r
|
||||
// but stops with types.ErrLimit after n bytes.
|
||||
// The underlying implementation is a *io.LimitedReader.
|
||||
func LimitReader(r io.Reader, n int64) io.Reader {
|
||||
return &LimitedReader{r: &io.LimitedReader{R: r, N: n}}
|
||||
}
|
||||
|
||||
type LimitedReader struct {
|
||||
r *io.LimitedReader
|
||||
}
|
||||
|
||||
func (l *LimitedReader) Read(p []byte) (n int, err error) {
|
||||
if l.r.N <= 0 {
|
||||
return 0, types.ErrLimit
|
||||
}
|
||||
return l.r.Read(p)
|
||||
}
|
82
x/wasm/ioutils/ioutil_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
package ioutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestUncompress(t *testing.T) {
|
||||
wasmRaw, err := os.ReadFile("../keeper/testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
wasmGzipped, err := os.ReadFile("../keeper/testdata/hackatom.wasm.gzip")
|
||||
require.NoError(t, err)
|
||||
|
||||
const maxSize = 400_000
|
||||
|
||||
specs := map[string]struct {
|
||||
src []byte
|
||||
expError error
|
||||
expResult []byte
|
||||
}{
|
||||
"handle wasm compressed": {
|
||||
src: wasmGzipped,
|
||||
expResult: wasmRaw,
|
||||
},
|
||||
"handle gzip identifier only": {
|
||||
src: gzipIdent,
|
||||
expError: io.ErrUnexpectedEOF,
|
||||
},
|
||||
"handle broken gzip": {
|
||||
src: append(gzipIdent, byte(0x1)),
|
||||
expError: io.ErrUnexpectedEOF,
|
||||
},
|
||||
"handle incomplete gzip": {
|
||||
src: wasmGzipped[:len(wasmGzipped)-5],
|
||||
expError: io.ErrUnexpectedEOF,
|
||||
},
|
||||
"handle limit gzip output": {
|
||||
src: asGzip(bytes.Repeat([]byte{0x1}, maxSize)),
|
||||
expResult: bytes.Repeat([]byte{0x1}, maxSize),
|
||||
},
|
||||
"handle big gzip output": {
|
||||
src: asGzip(bytes.Repeat([]byte{0x1}, maxSize+1)),
|
||||
expError: types.ErrLimit,
|
||||
},
|
||||
"handle other big gzip output": {
|
||||
src: asGzip(bytes.Repeat([]byte{0x1}, 2*maxSize)),
|
||||
expError: types.ErrLimit,
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
r, err := Uncompress(spec.src, maxSize)
|
||||
require.True(t, errors.Is(spec.expError, err), "exp %v got %+v", spec.expError, err)
|
||||
if spec.expError != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, spec.expResult, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func asGzip(src []byte) []byte {
|
||||
var buf bytes.Buffer
|
||||
zipper := gzip.NewWriter(&buf)
|
||||
if _, err := io.Copy(zipper, bytes.NewReader(src)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := zipper.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
43
x/wasm/ioutils/utils.go
Normal file
@ -0,0 +1,43 @@
|
||||
package ioutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
)
|
||||
|
||||
// Note: []byte can never be const as they are inherently mutable
|
||||
var (
|
||||
// magic bytes to identify gzip.
|
||||
// See https://www.ietf.org/rfc/rfc1952.txt
|
||||
// and https://github.com/golang/go/blob/master/src/net/http/sniff.go#L186
|
||||
gzipIdent = []byte("\x1F\x8B\x08")
|
||||
|
||||
wasmIdent = []byte("\x00\x61\x73\x6D")
|
||||
)
|
||||
|
||||
// IsGzip returns checks if the file contents are gzip compressed
|
||||
func IsGzip(input []byte) bool {
|
||||
return len(input) >= 3 && bytes.Equal(gzipIdent, input[0:3])
|
||||
}
|
||||
|
||||
// IsWasm checks if the file contents are of wasm binary
|
||||
func IsWasm(input []byte) bool {
|
||||
return bytes.Equal(input[:4], wasmIdent)
|
||||
}
|
||||
|
||||
// GzipIt compresses the input ([]byte)
|
||||
func GzipIt(input []byte) ([]byte, error) {
|
||||
// Create gzip writer.
|
||||
var b bytes.Buffer
|
||||
w := gzip.NewWriter(&b)
|
||||
_, err := w.Write(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = w.Close() // You must close this first to flush the bytes to the buffer.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
68
x/wasm/ioutils/utils_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
package ioutils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func GetTestData() ([]byte, []byte, []byte, error) {
|
||||
wasmCode, err := os.ReadFile("../keeper/testdata/hackatom.wasm")
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
gzipData, err := GzipIt(wasmCode)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
someRandomStr := []byte("hello world")
|
||||
|
||||
return wasmCode, someRandomStr, gzipData, nil
|
||||
}
|
||||
|
||||
func TestIsWasm(t *testing.T) {
|
||||
wasmCode, someRandomStr, gzipData, err := GetTestData()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("should return false for some random string data")
|
||||
require.False(t, IsWasm(someRandomStr))
|
||||
t.Log("should return false for gzip data")
|
||||
require.False(t, IsWasm(gzipData))
|
||||
t.Log("should return true for exact wasm")
|
||||
require.True(t, IsWasm(wasmCode))
|
||||
}
|
||||
|
||||
func TestIsGzip(t *testing.T) {
|
||||
wasmCode, someRandomStr, gzipData, err := GetTestData()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.False(t, IsGzip(wasmCode))
|
||||
require.False(t, IsGzip(someRandomStr))
|
||||
require.False(t, IsGzip(nil))
|
||||
require.True(t, IsGzip(gzipData[0:3]))
|
||||
require.True(t, IsGzip(gzipData))
|
||||
}
|
||||
|
||||
func TestGzipIt(t *testing.T) {
|
||||
wasmCode, someRandomStr, _, err := GetTestData()
|
||||
originalGzipData := []byte{
|
||||
31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 202, 72, 205, 201, 201, 87, 40, 207, 47, 202, 73, 1,
|
||||
4, 0, 0, 255, 255, 133, 17, 74, 13, 11, 0, 0, 0,
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("gzip wasm with no error")
|
||||
_, err = GzipIt(wasmCode)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("gzip of a string should return exact gzip data")
|
||||
strToGzip, err := GzipIt(someRandomStr)
|
||||
|
||||
require.True(t, IsGzip(strToGzip))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, originalGzipData, strToGzip)
|
||||
}
|
76
x/wasm/keeper/addresses.go
Normal file
@ -0,0 +1,76 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/address"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// AddressGenerator abstract address generator to be used for a single contract address
|
||||
type AddressGenerator func(ctx sdk.Context, codeID uint64, checksum []byte) sdk.AccAddress
|
||||
|
||||
// ClassicAddressGenerator generates a contract address using codeID and instanceID sequence
|
||||
func (k Keeper) ClassicAddressGenerator() AddressGenerator {
|
||||
return func(ctx sdk.Context, codeID uint64, _ []byte) sdk.AccAddress {
|
||||
instanceID := k.autoIncrementID(ctx, types.KeyLastInstanceID)
|
||||
return BuildContractAddressClassic(codeID, instanceID)
|
||||
}
|
||||
}
|
||||
|
||||
// PredicableAddressGenerator generates a predictable contract address
|
||||
func PredicableAddressGenerator(creator sdk.AccAddress, salt []byte, msg []byte, fixMsg bool) AddressGenerator {
|
||||
return func(ctx sdk.Context, _ uint64, checksum []byte) sdk.AccAddress {
|
||||
if !fixMsg { // clear msg to not be included in the address generation
|
||||
msg = []byte{}
|
||||
}
|
||||
return BuildContractAddressPredictable(checksum, creator, salt, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// BuildContractAddressClassic builds an sdk account address for a contract.
|
||||
func BuildContractAddressClassic(codeID, instanceID uint64) sdk.AccAddress {
|
||||
contractID := make([]byte, 16)
|
||||
binary.BigEndian.PutUint64(contractID[:8], codeID)
|
||||
binary.BigEndian.PutUint64(contractID[8:], instanceID)
|
||||
return address.Module(types.ModuleName, contractID)[:types.ContractAddrLen]
|
||||
}
|
||||
|
||||
// BuildContractAddressPredictable generates a contract address for the wasm module with len = types.ContractAddrLen using the
|
||||
// Cosmos SDK address.Module function.
|
||||
// Internally a key is built containing:
|
||||
// (len(checksum) | checksum | len(sender_address) | sender_address | len(salt) | salt| len(initMsg) | initMsg).
|
||||
//
|
||||
// All method parameter values must be valid and not nil.
|
||||
func BuildContractAddressPredictable(checksum []byte, creator sdk.AccAddress, salt, initMsg types.RawContractMessage) sdk.AccAddress {
|
||||
if len(checksum) != 32 {
|
||||
panic("invalid checksum")
|
||||
}
|
||||
if err := sdk.VerifyAddressFormat(creator); err != nil {
|
||||
panic(fmt.Sprintf("creator: %s", err))
|
||||
}
|
||||
if err := types.ValidateSalt(salt); err != nil {
|
||||
panic(fmt.Sprintf("salt: %s", err))
|
||||
}
|
||||
if err := initMsg.ValidateBasic(); len(initMsg) != 0 && err != nil {
|
||||
panic(fmt.Sprintf("initMsg: %s", err))
|
||||
}
|
||||
checksum = UInt64LengthPrefix(checksum)
|
||||
creator = UInt64LengthPrefix(creator)
|
||||
salt = UInt64LengthPrefix(salt)
|
||||
initMsg = UInt64LengthPrefix(initMsg)
|
||||
key := make([]byte, len(checksum)+len(creator)+len(salt)+len(initMsg))
|
||||
copy(key[0:], checksum)
|
||||
copy(key[len(checksum):], creator)
|
||||
copy(key[len(checksum)+len(creator):], salt)
|
||||
copy(key[len(checksum)+len(creator)+len(salt):], initMsg)
|
||||
return address.Module(types.ModuleName, key)[:types.ContractAddrLen]
|
||||
}
|
||||
|
||||
// UInt64LengthPrefix prepend big endian encoded byte length
|
||||
func UInt64LengthPrefix(bz []byte) []byte {
|
||||
return append(sdk.Uint64ToBigEndian(uint64(len(bz))), bz...)
|
||||
}
|
432
x/wasm/keeper/addresses_test.go
Normal file
@ -0,0 +1,432 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
)
|
||||
|
||||
func TestBuildContractAddress(t *testing.T) {
|
||||
x, y := sdk.GetConfig().GetBech32AccountAddrPrefix(), sdk.GetConfig().GetBech32AccountPubPrefix()
|
||||
t.Cleanup(func() {
|
||||
sdk.GetConfig().SetBech32PrefixForAccount(x, y)
|
||||
})
|
||||
sdk.GetConfig().SetBech32PrefixForAccount("purple", "purple")
|
||||
|
||||
// test vectors generated via cosmjs: https://github.com/cosmos/cosmjs/pull/1253/files
|
||||
type Spec struct {
|
||||
In struct {
|
||||
Checksum tmbytes.HexBytes `json:"checksum"`
|
||||
Creator sdk.AccAddress `json:"creator"`
|
||||
Salt tmbytes.HexBytes `json:"salt"`
|
||||
Msg string `json:"msg"`
|
||||
} `json:"in"`
|
||||
Out struct {
|
||||
Address sdk.AccAddress `json:"address"`
|
||||
} `json:"out"`
|
||||
}
|
||||
var specs []Spec
|
||||
require.NoError(t, json.Unmarshal([]byte(goldenMasterPredictableContractAddr), &specs))
|
||||
require.NotEmpty(t, specs)
|
||||
for i, spec := range specs {
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
// when
|
||||
gotAddr := BuildContractAddressPredictable(spec.In.Checksum, spec.In.Creator, spec.In.Salt.Bytes(), []byte(spec.In.Msg))
|
||||
|
||||
require.Equal(t, spec.Out.Address.String(), gotAddr.String())
|
||||
require.NoError(t, sdk.VerifyAddressFormat(gotAddr))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const goldenMasterPredictableContractAddr = `[
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "61",
|
||||
"msg": null
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000001610000000000000000",
|
||||
"addressData": "5e865d3e45ad3e961f77fd77d46543417ced44d924dc3e079b5415ff6775f847"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1t6r960j945lfv8mhl4mage2rg97w63xeynwrupum2s2l7em4lprs9ce5hk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "61",
|
||||
"msg": "{}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc00000000000000016100000000000000027b7d",
|
||||
"addressData": "0995499608947a5281e2c7ebd71bdb26a1ad981946dad57f6c4d3ee35de77835"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1px25n9sgj3a99q0zcl4awx7my6s6mxqegmdd2lmvf5lwxh080q6suttktr"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "61",
|
||||
"msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d",
|
||||
"addressData": "83326e554723b15bac664ceabc8a5887e27003abe9fbd992af8c7bcea4745167"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1svexu428ywc4htrxfn4tezjcsl38qqata8aany4033auafr529ns4v254c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": null
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000",
|
||||
"addressData": "9384c6248c0bb171e306fd7da0993ec1e20eba006452a3a9e078883eb3594564"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1jwzvvfyvpwchrccxl476pxf7c83qawsqv3f2820q0zyrav6eg4jqdcq7gc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": "{}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d",
|
||||
"addressData": "9a8d5f98fb186825401a26206158e7a1213311a9b6a87944469913655af52ffb"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1n2x4lx8mrp5z2sq6ycsxzk885ysnxydfk658j3zxnyfk2kh49lasesxf6j"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d",
|
||||
"addressData": "932f07bc53f7d0b0b43cb5a54ac3e245b205e6ae6f7c1d991dc6af4a2ff9ac18"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1jvhs00zn7lgtpdpukkj54slzgkeqte4wda7pmxgac6h55tle4svq8cmp60"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "61",
|
||||
"msg": null
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000001610000000000000000",
|
||||
"addressData": "9725e94f528d8b78d33c25f3dfcd60e6142d8be60ab36f6a5b59036fd51560db"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1juj7jn6j3k9h35euyhealntquc2zmzlxp2ek76jmtypkl4g4vrdsfwmwxk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "61",
|
||||
"msg": "{}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff00000000000000016100000000000000027b7d",
|
||||
"addressData": "b056e539bbaf447ba18f3f13b792970111fc78933eb6700f4d227b5216d63658"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1kptw2wdm4az8hgv08ufm0y5hqyglc7yn86m8qr6dyfa4y9kkxevqmkm9q3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "61",
|
||||
"msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d",
|
||||
"addressData": "6c98434180f052294ff89fb6d2dae34f9f4468b0b8e6e7c002b2a44adee39abd"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1djvyxsvq7pfzjnlcn7md9khrf705g69shrnw0sqzk2jy4hhrn27sjh2ysy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": null
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000",
|
||||
"addressData": "0aaf1c31c5d529d21d898775bc35b3416f47bfd99188c334c6c716102cbd3101"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1p2h3cvw9655ay8vfsa6mcddng9h5007ejxyvxdxxcutpqt9axyqsagmmay"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": "{}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d",
|
||||
"addressData": "36fe6ab732187cdfed46290b448b32eb7f4798e7a4968b0537de8a842cbf030e"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1xmlx4dejrp7dlm2x9y95fzejadl50x885jtgkpfhm69ggt9lqv8qk3vn4f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d",
|
||||
"addressData": "a0d0c942adac6f3e5e7c23116c4c42a24e96e0ab75f53690ec2d3de16067c751"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple15rgvjs4d43hnuhnuyvgkcnzz5f8fdc9twh6ndy8v9577zcr8cags40l9dt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "61",
|
||||
"msg": null
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000001610000000000000000",
|
||||
"addressData": "b95c467218d408a0f93046f227b6ece7fe18133ff30113db4d2a7becdfeca141"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1h9wyvusc6sy2p7fsgmez0dhvullpsyel7vq38k6d9fa7ehlv59qsvnyh36"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "61",
|
||||
"msg": "{}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc00000000000000016100000000000000027b7d",
|
||||
"addressData": "23fe45dbbd45dc6cd25244a74b6e99e7a65bf0bac2f2842a05049d37555a3ae6"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1y0lytkaaghwxe5jjgjn5km5eu7n9hu96ctegg2s9qjwnw4268tnqxhg60a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "61",
|
||||
"msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d",
|
||||
"addressData": "6faea261ed63baa65b05726269e83b217fa6205dc7d9fb74f9667d004a69c082"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1d7h2yc0dvwa2vkc9wf3xn6pmy9l6vgzaclvlka8eve7sqjnfczpqqsdnwu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": null
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000",
|
||||
"addressData": "67a3ab6384729925fdb144574628ce96836fe098d2c6be4e84ac106b2728d96c"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1v736kcuyw2vjtld3g3t5v2xwj6pklcyc6trtun5y4sgxkfegm9kq7vgpnt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": "{}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d",
|
||||
"addressData": "23a121263bfce05c144f4af86f3d8a9f87dc56f9dc48dbcffc8c5a614da4c661"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1ywsjzf3mlns9c9z0ftux70v2n7rac4hem3ydhnlu33dxzndycesssc7x2m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d",
|
||||
"addressData": "dd90dba6d6fcd5fb9c9c8f536314eb1bb29cb5aa084b633c5806b926a5636b58"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1mkgdhfkkln2lh8yu3afkx98trwefedd2pp9kx0zcq6ujdftrddvq50esay"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "61",
|
||||
"msg": null
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000001610000000000000000",
|
||||
"addressData": "547a743022f4f1af05b102f57bf1c1c7d7ee81bae427dc20d36b2c4ec57612ae"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple123a8gvpz7nc67pd3qt6hhuwpclt7aqd6usnacgxndvkya3tkz2hq5hz38f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "61",
|
||||
"msg": "{}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff00000000000000016100000000000000027b7d",
|
||||
"addressData": "416e169110e4b411bc53162d7503b2bbf14d6b36b1413a4f4c9af622696e9665"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1g9hpdygsuj6pr0znzckh2qajh0c566ekk9qn5n6vntmzy6twjejsrl9alk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "61",
|
||||
"msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d",
|
||||
"addressData": "619a0988b92d8796cea91dea63cbb1f1aefa4a6b6ee5c5d1e937007252697220"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1vxdqnz9e9kredn4frh4x8ja37xh05jntdmjut50fxuq8y5nfwgsquu9mxh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": null
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000",
|
||||
"addressData": "d8af856a6a04852d19b647ad6d4336eb26e077f740aef1a0331db34d299a885a"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1mzhc26n2qjzj6xdkg7kk6sekavnwqalhgzh0rgpnrke562v63pdq8grp8q"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": "{}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d",
|
||||
"addressData": "c7fb7bea96daab23e416c4fcf328215303005e1d0d5424257335568e5381e33c"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1clahh65km24j8eqkcn70x2pp2vpsqhsap42zgftnx4tgu5upuv7q9ywjws"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": {
|
||||
"checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b",
|
||||
"creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m",
|
||||
"creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff",
|
||||
"salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae",
|
||||
"msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}"
|
||||
},
|
||||
"intermediate": {
|
||||
"key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d",
|
||||
"addressData": "ccdf9dea141a6c2475870529ab38fae9dec30df28e005894fe6578b66133ab4a"
|
||||
},
|
||||
"out": {
|
||||
"address": "purple1en0em6s5rfkzgav8q556kw86a80vxr0j3cq93987v4utvcfn4d9q0tql4w"
|
||||
}
|
||||
}
|
||||
]
|
||||
`
|
96
x/wasm/keeper/ante.go
Normal file
@ -0,0 +1,96 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// CountTXDecorator ante handler to count the tx position in a block.
|
||||
type CountTXDecorator struct {
|
||||
storeKey sdk.StoreKey
|
||||
}
|
||||
|
||||
// NewCountTXDecorator constructor
|
||||
func NewCountTXDecorator(storeKey sdk.StoreKey) *CountTXDecorator {
|
||||
return &CountTXDecorator{storeKey: storeKey}
|
||||
}
|
||||
|
||||
// AnteHandle handler stores a tx counter with current height encoded in the store to let the app handle
|
||||
// global rollback behavior instead of keeping state in the handler itself.
|
||||
// The ante handler passes the counter value via sdk.Context upstream. See `types.TXCounter(ctx)` to read the value.
|
||||
// Simulations don't get a tx counter value assigned.
|
||||
func (a CountTXDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
|
||||
if simulate {
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
||||
store := ctx.KVStore(a.storeKey)
|
||||
currentHeight := ctx.BlockHeight()
|
||||
|
||||
var txCounter uint32 // start with 0
|
||||
// load counter when exists
|
||||
if bz := store.Get(types.TXCounterPrefix); bz != nil {
|
||||
lastHeight, val := decodeHeightCounter(bz)
|
||||
if currentHeight == lastHeight {
|
||||
// then use stored counter
|
||||
txCounter = val
|
||||
} // else use `0` from above to start with
|
||||
}
|
||||
// store next counter value for current height
|
||||
store.Set(types.TXCounterPrefix, encodeHeightCounter(currentHeight, txCounter+1))
|
||||
|
||||
return next(types.WithTXCounter(ctx, txCounter), tx, simulate)
|
||||
}
|
||||
|
||||
func encodeHeightCounter(height int64, counter uint32) []byte {
|
||||
b := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(b, counter)
|
||||
return append(sdk.Uint64ToBigEndian(uint64(height)), b...)
|
||||
}
|
||||
|
||||
func decodeHeightCounter(bz []byte) (int64, uint32) {
|
||||
return int64(sdk.BigEndianToUint64(bz[0:8])), binary.BigEndian.Uint32(bz[8:])
|
||||
}
|
||||
|
||||
// LimitSimulationGasDecorator ante decorator to limit gas in simulation calls
|
||||
type LimitSimulationGasDecorator struct {
|
||||
gasLimit *sdk.Gas
|
||||
}
|
||||
|
||||
// NewLimitSimulationGasDecorator constructor accepts nil value to fallback to block gas limit.
|
||||
func NewLimitSimulationGasDecorator(gasLimit *sdk.Gas) *LimitSimulationGasDecorator {
|
||||
if gasLimit != nil && *gasLimit == 0 {
|
||||
panic("gas limit must not be zero")
|
||||
}
|
||||
return &LimitSimulationGasDecorator{gasLimit: gasLimit}
|
||||
}
|
||||
|
||||
// AnteHandle that limits the maximum gas available in simulations only.
|
||||
// A custom max value can be configured and will be applied when set. The value should not
|
||||
// exceed the max block gas limit.
|
||||
// Different values on nodes are not consensus breaking as they affect only
|
||||
// simulations but may have effect on client user experience.
|
||||
//
|
||||
// When no custom value is set then the max block gas is used as default limit.
|
||||
func (d LimitSimulationGasDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
|
||||
if !simulate {
|
||||
// Wasm code is not executed in checkTX so that we don't need to limit it further.
|
||||
// Tendermint rejects the TX afterwards when the tx.gas > max block gas.
|
||||
// On deliverTX we rely on the tendermint/sdk mechanics that ensure
|
||||
// tx has gas set and gas < max block gas
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
||||
|
||||
// apply custom node gas limit
|
||||
if d.gasLimit != nil {
|
||||
return next(ctx.WithGasMeter(sdk.NewGasMeter(*d.gasLimit)), tx, simulate)
|
||||
}
|
||||
|
||||
// default to max block gas when set, to be on the safe side
|
||||
if maxGas := ctx.ConsensusParams().GetBlock().MaxGas; maxGas > 0 {
|
||||
return next(ctx.WithGasMeter(sdk.NewGasMeter(sdk.Gas(maxGas))), tx, simulate)
|
||||
}
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
189
x/wasm/keeper/ante_test.go
Normal file
@ -0,0 +1,189 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/store"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestCountTxDecorator(t *testing.T) {
|
||||
keyWasm := sdk.NewKVStoreKey(types.StoreKey)
|
||||
db := dbm.NewMemDB()
|
||||
ms := store.NewCommitMultiStore(db)
|
||||
ms.MountStoreWithDB(keyWasm, sdk.StoreTypeIAVL, db)
|
||||
require.NoError(t, ms.LoadLatestVersion())
|
||||
const myCurrentBlockHeight = 100
|
||||
|
||||
specs := map[string]struct {
|
||||
setupDB func(t *testing.T, ctx sdk.Context)
|
||||
simulate bool
|
||||
nextAssertAnte func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error)
|
||||
expErr bool
|
||||
}{
|
||||
"no initial counter set": {
|
||||
setupDB: func(t *testing.T, ctx sdk.Context) {},
|
||||
nextAssertAnte: func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
|
||||
gotCounter, ok := types.TXCounter(ctx)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, uint32(0), gotCounter)
|
||||
// and stored +1
|
||||
bz := ctx.MultiStore().GetKVStore(keyWasm).Get(types.TXCounterPrefix)
|
||||
assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, myCurrentBlockHeight, 0, 0, 0, 1}, bz)
|
||||
return ctx, nil
|
||||
},
|
||||
},
|
||||
"persistent counter incremented - big endian": {
|
||||
setupDB: func(t *testing.T, ctx sdk.Context) {
|
||||
bz := []byte{0, 0, 0, 0, 0, 0, 0, myCurrentBlockHeight, 1, 0, 0, 2}
|
||||
ctx.MultiStore().GetKVStore(keyWasm).Set(types.TXCounterPrefix, bz)
|
||||
},
|
||||
nextAssertAnte: func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
|
||||
gotCounter, ok := types.TXCounter(ctx)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, uint32(1<<24+2), gotCounter)
|
||||
// and stored +1
|
||||
bz := ctx.MultiStore().GetKVStore(keyWasm).Get(types.TXCounterPrefix)
|
||||
assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, myCurrentBlockHeight, 1, 0, 0, 3}, bz)
|
||||
return ctx, nil
|
||||
},
|
||||
},
|
||||
"old height counter replaced": {
|
||||
setupDB: func(t *testing.T, ctx sdk.Context) {
|
||||
previousHeight := byte(myCurrentBlockHeight - 1)
|
||||
bz := []byte{0, 0, 0, 0, 0, 0, 0, previousHeight, 0, 0, 0, 1}
|
||||
ctx.MultiStore().GetKVStore(keyWasm).Set(types.TXCounterPrefix, bz)
|
||||
},
|
||||
nextAssertAnte: func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
|
||||
gotCounter, ok := types.TXCounter(ctx)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, uint32(0), gotCounter)
|
||||
// and stored +1
|
||||
bz := ctx.MultiStore().GetKVStore(keyWasm).Get(types.TXCounterPrefix)
|
||||
assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, myCurrentBlockHeight, 0, 0, 0, 1}, bz)
|
||||
return ctx, nil
|
||||
},
|
||||
},
|
||||
"simulation not persisted": {
|
||||
setupDB: func(t *testing.T, ctx sdk.Context) {
|
||||
},
|
||||
simulate: true,
|
||||
nextAssertAnte: func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
|
||||
_, ok := types.TXCounter(ctx)
|
||||
assert.False(t, ok)
|
||||
require.True(t, simulate)
|
||||
// and not stored
|
||||
assert.False(t, ctx.MultiStore().GetKVStore(keyWasm).Has(types.TXCounterPrefix))
|
||||
return ctx, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := sdk.NewContext(ms.CacheMultiStore(), tmproto.Header{
|
||||
Height: myCurrentBlockHeight,
|
||||
Time: time.Date(2021, time.September, 27, 12, 0, 0, 0, time.UTC),
|
||||
}, false, log.NewNopLogger())
|
||||
|
||||
spec.setupDB(t, ctx)
|
||||
var anyTx sdk.Tx
|
||||
|
||||
// when
|
||||
ante := keeper.NewCountTXDecorator(keyWasm)
|
||||
_, gotErr := ante.AnteHandle(ctx, anyTx, spec.simulate, spec.nextAssertAnte)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitSimulationGasDecorator(t *testing.T) {
|
||||
var (
|
||||
hundred sdk.Gas = 100
|
||||
zero sdk.Gas = 0
|
||||
)
|
||||
specs := map[string]struct {
|
||||
customLimit *sdk.Gas
|
||||
consumeGas sdk.Gas
|
||||
maxBlockGas int64
|
||||
simulation bool
|
||||
expErr interface{}
|
||||
}{
|
||||
"custom limit set": {
|
||||
customLimit: &hundred,
|
||||
consumeGas: hundred + 1,
|
||||
maxBlockGas: -1,
|
||||
simulation: true,
|
||||
expErr: sdk.ErrorOutOfGas{Descriptor: "testing"},
|
||||
},
|
||||
"block limit set": {
|
||||
maxBlockGas: 100,
|
||||
consumeGas: hundred + 1,
|
||||
simulation: true,
|
||||
expErr: sdk.ErrorOutOfGas{Descriptor: "testing"},
|
||||
},
|
||||
"no limits set": {
|
||||
maxBlockGas: -1,
|
||||
consumeGas: hundred + 1,
|
||||
simulation: true,
|
||||
},
|
||||
"both limits set, custom applies": {
|
||||
customLimit: &hundred,
|
||||
consumeGas: hundred - 1,
|
||||
maxBlockGas: 10,
|
||||
simulation: true,
|
||||
},
|
||||
"not a simulation": {
|
||||
customLimit: &hundred,
|
||||
consumeGas: hundred + 1,
|
||||
simulation: false,
|
||||
},
|
||||
"zero custom limit": {
|
||||
customLimit: &zero,
|
||||
simulation: true,
|
||||
expErr: "gas limit must not be zero",
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
nextAnte := consumeGasAnteHandler(spec.consumeGas)
|
||||
ctx := sdk.Context{}.
|
||||
WithGasMeter(sdk.NewInfiniteGasMeter()).
|
||||
WithConsensusParams(&abci.ConsensusParams{
|
||||
Block: &abci.BlockParams{MaxGas: spec.maxBlockGas},
|
||||
})
|
||||
// when
|
||||
if spec.expErr != nil {
|
||||
require.PanicsWithValue(t, spec.expErr, func() {
|
||||
ante := keeper.NewLimitSimulationGasDecorator(spec.customLimit)
|
||||
ante.AnteHandle(ctx, nil, spec.simulation, nextAnte)
|
||||
})
|
||||
return
|
||||
}
|
||||
ante := keeper.NewLimitSimulationGasDecorator(spec.customLimit)
|
||||
ante.AnteHandle(ctx, nil, spec.simulation, nextAnte)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func consumeGasAnteHandler(gasToConsume sdk.Gas) sdk.AnteHandler {
|
||||
return func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
|
||||
ctx.GasMeter().ConsumeGas(gasToConsume, "testing")
|
||||
return ctx, nil
|
||||
}
|
||||
}
|
43
x/wasm/keeper/api.go
Normal file
@ -0,0 +1,43 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
wasmvm "github.com/CosmWasm/wasmvm"
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultGasCostHumanAddress is how moch SDK gas we charge to convert to a human address format
|
||||
DefaultGasCostHumanAddress = 5
|
||||
// DefaultGasCostCanonicalAddress is how moch SDK gas we charge to convert to a canonical address format
|
||||
DefaultGasCostCanonicalAddress = 4
|
||||
|
||||
// DefaultDeserializationCostPerByte The formular should be `len(data) * deserializationCostPerByte`
|
||||
DefaultDeserializationCostPerByte = 1
|
||||
)
|
||||
|
||||
var (
|
||||
costHumanize = DefaultGasCostHumanAddress * DefaultGasMultiplier
|
||||
costCanonical = DefaultGasCostCanonicalAddress * DefaultGasMultiplier
|
||||
costJSONDeserialization = wasmvmtypes.UFraction{
|
||||
Numerator: DefaultDeserializationCostPerByte * DefaultGasMultiplier,
|
||||
Denominator: 1,
|
||||
}
|
||||
)
|
||||
|
||||
func humanAddress(canon []byte) (string, uint64, error) {
|
||||
if err := sdk.VerifyAddressFormat(canon); err != nil {
|
||||
return "", costHumanize, err
|
||||
}
|
||||
return sdk.AccAddress(canon).String(), costHumanize, nil
|
||||
}
|
||||
|
||||
func canonicalAddress(human string) ([]byte, uint64, error) {
|
||||
bz, err := sdk.AccAddressFromBech32(human)
|
||||
return bz, costCanonical, err
|
||||
}
|
||||
|
||||
var cosmwasmAPI = wasmvm.GoAPI{
|
||||
HumanAddress: humanAddress,
|
||||
CanonicalAddress: canonicalAddress,
|
||||
}
|
63
x/wasm/keeper/authz_policy.go
Normal file
@ -0,0 +1,63 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// ChainAccessConfigs chain settings
|
||||
type ChainAccessConfigs struct {
|
||||
Upload types.AccessConfig
|
||||
Instantiate types.AccessConfig
|
||||
}
|
||||
|
||||
// NewChainAccessConfigs constructor
|
||||
func NewChainAccessConfigs(upload types.AccessConfig, instantiate types.AccessConfig) ChainAccessConfigs {
|
||||
return ChainAccessConfigs{Upload: upload, Instantiate: instantiate}
|
||||
}
|
||||
|
||||
type AuthorizationPolicy interface {
|
||||
CanCreateCode(chainConfigs ChainAccessConfigs, actor sdk.AccAddress, contractConfig types.AccessConfig) bool
|
||||
CanInstantiateContract(c types.AccessConfig, actor sdk.AccAddress) bool
|
||||
CanModifyContract(admin, actor sdk.AccAddress) bool
|
||||
CanModifyCodeAccessConfig(creator, actor sdk.AccAddress, isSubset bool) bool
|
||||
}
|
||||
|
||||
type DefaultAuthorizationPolicy struct{}
|
||||
|
||||
func (p DefaultAuthorizationPolicy) CanCreateCode(chainConfigs ChainAccessConfigs, actor sdk.AccAddress, contractConfig types.AccessConfig) bool {
|
||||
return chainConfigs.Upload.Allowed(actor) &&
|
||||
contractConfig.IsSubset(chainConfigs.Instantiate)
|
||||
}
|
||||
|
||||
func (p DefaultAuthorizationPolicy) CanInstantiateContract(config types.AccessConfig, actor sdk.AccAddress) bool {
|
||||
return config.Allowed(actor)
|
||||
}
|
||||
|
||||
func (p DefaultAuthorizationPolicy) CanModifyContract(admin, actor sdk.AccAddress) bool {
|
||||
return admin != nil && admin.Equals(actor)
|
||||
}
|
||||
|
||||
func (p DefaultAuthorizationPolicy) CanModifyCodeAccessConfig(creator, actor sdk.AccAddress, isSubset bool) bool {
|
||||
return creator != nil && creator.Equals(actor) && isSubset
|
||||
}
|
||||
|
||||
type GovAuthorizationPolicy struct{}
|
||||
|
||||
// CanCreateCode implements AuthorizationPolicy.CanCreateCode to allow gov actions. Always returns true.
|
||||
func (p GovAuthorizationPolicy) CanCreateCode(ChainAccessConfigs, sdk.AccAddress, types.AccessConfig) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p GovAuthorizationPolicy) CanInstantiateContract(types.AccessConfig, sdk.AccAddress) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p GovAuthorizationPolicy) CanModifyContract(sdk.AccAddress, sdk.AccAddress) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p GovAuthorizationPolicy) CanModifyCodeAccessConfig(sdk.AccAddress, sdk.AccAddress, bool) bool {
|
||||
return true
|
||||
}
|
345
x/wasm/keeper/authz_policy_test.go
Normal file
@ -0,0 +1,345 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestDefaultAuthzPolicyCanCreateCode(t *testing.T) {
|
||||
myActorAddress := RandomAccountAddress(t)
|
||||
otherAddress := RandomAccountAddress(t)
|
||||
specs := map[string]struct {
|
||||
chainConfigs ChainAccessConfigs
|
||||
contractInstConf types.AccessConfig
|
||||
actor sdk.AccAddress
|
||||
exp bool
|
||||
panics bool
|
||||
}{
|
||||
"upload nobody": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AllowNobody, types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
exp: false,
|
||||
},
|
||||
"upload everybody": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
exp: true,
|
||||
},
|
||||
"upload only address - same": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AccessTypeOnlyAddress.With(myActorAddress), types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
exp: true,
|
||||
},
|
||||
"upload only address - different": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AccessTypeOnlyAddress.With(otherAddress), types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
exp: false,
|
||||
},
|
||||
"upload any address - included": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AccessTypeAnyOfAddresses.With(otherAddress, myActorAddress), types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
exp: true,
|
||||
},
|
||||
"upload any address - not included": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AccessTypeAnyOfAddresses.With(otherAddress), types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
exp: false,
|
||||
},
|
||||
"contract config - subtype": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowEverybody),
|
||||
contractInstConf: types.AccessTypeAnyOfAddresses.With(myActorAddress),
|
||||
exp: true,
|
||||
},
|
||||
"contract config - not subtype": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowNobody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
exp: false,
|
||||
},
|
||||
"upload undefined config - panics": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AccessConfig{}, types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
panics: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
policy := DefaultAuthorizationPolicy{}
|
||||
if !spec.panics {
|
||||
got := policy.CanCreateCode(spec.chainConfigs, myActorAddress, spec.contractInstConf)
|
||||
assert.Equal(t, spec.exp, got)
|
||||
return
|
||||
}
|
||||
assert.Panics(t, func() {
|
||||
policy.CanCreateCode(spec.chainConfigs, myActorAddress, spec.contractInstConf)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAuthzPolicyCanInstantiateContract(t *testing.T) {
|
||||
myActorAddress := RandomAccountAddress(t)
|
||||
otherAddress := RandomAccountAddress(t)
|
||||
specs := map[string]struct {
|
||||
config types.AccessConfig
|
||||
actor sdk.AccAddress
|
||||
exp bool
|
||||
panics bool
|
||||
}{
|
||||
"nobody": {
|
||||
config: types.AllowNobody,
|
||||
exp: false,
|
||||
},
|
||||
"everybody": {
|
||||
config: types.AllowEverybody,
|
||||
exp: true,
|
||||
},
|
||||
"only address - same": {
|
||||
config: types.AccessTypeOnlyAddress.With(myActorAddress),
|
||||
exp: true,
|
||||
},
|
||||
"only address - different": {
|
||||
config: types.AccessTypeOnlyAddress.With(otherAddress),
|
||||
exp: false,
|
||||
},
|
||||
"any address - included": {
|
||||
config: types.AccessTypeAnyOfAddresses.With(otherAddress, myActorAddress),
|
||||
exp: true,
|
||||
},
|
||||
"any address - not included": {
|
||||
config: types.AccessTypeAnyOfAddresses.With(otherAddress),
|
||||
exp: false,
|
||||
},
|
||||
"undefined config - panics": {
|
||||
config: types.AccessConfig{},
|
||||
panics: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
policy := DefaultAuthorizationPolicy{}
|
||||
if !spec.panics {
|
||||
got := policy.CanInstantiateContract(spec.config, myActorAddress)
|
||||
assert.Equal(t, spec.exp, got)
|
||||
return
|
||||
}
|
||||
assert.Panics(t, func() {
|
||||
policy.CanInstantiateContract(spec.config, myActorAddress)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAuthzPolicyCanModifyContract(t *testing.T) {
|
||||
myActorAddress := RandomAccountAddress(t)
|
||||
otherAddress := RandomAccountAddress(t)
|
||||
|
||||
specs := map[string]struct {
|
||||
admin sdk.AccAddress
|
||||
exp bool
|
||||
}{
|
||||
"same as actor": {
|
||||
admin: myActorAddress,
|
||||
exp: true,
|
||||
},
|
||||
"different admin": {
|
||||
admin: otherAddress,
|
||||
exp: false,
|
||||
},
|
||||
"no admin": {
|
||||
exp: false,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
policy := DefaultAuthorizationPolicy{}
|
||||
got := policy.CanModifyContract(spec.admin, myActorAddress)
|
||||
assert.Equal(t, spec.exp, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAuthzPolicyCanModifyCodeAccessConfig(t *testing.T) {
|
||||
myActorAddress := RandomAccountAddress(t)
|
||||
otherAddress := RandomAccountAddress(t)
|
||||
|
||||
specs := map[string]struct {
|
||||
admin sdk.AccAddress
|
||||
subset bool
|
||||
exp bool
|
||||
}{
|
||||
"same as actor - subset": {
|
||||
admin: myActorAddress,
|
||||
subset: true,
|
||||
exp: true,
|
||||
},
|
||||
"same as actor - not subset": {
|
||||
admin: myActorAddress,
|
||||
subset: false,
|
||||
exp: false,
|
||||
},
|
||||
"different admin": {
|
||||
admin: otherAddress,
|
||||
exp: false,
|
||||
},
|
||||
"no admin": {
|
||||
exp: false,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
policy := DefaultAuthorizationPolicy{}
|
||||
got := policy.CanModifyCodeAccessConfig(spec.admin, myActorAddress, spec.subset)
|
||||
assert.Equal(t, spec.exp, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovAuthzPolicyCanCreateCode(t *testing.T) {
|
||||
myActorAddress := RandomAccountAddress(t)
|
||||
otherAddress := RandomAccountAddress(t)
|
||||
specs := map[string]struct {
|
||||
chainConfigs ChainAccessConfigs
|
||||
contractInstConf types.AccessConfig
|
||||
actor sdk.AccAddress
|
||||
}{
|
||||
"upload nobody": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AllowNobody, types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
},
|
||||
"upload everybody": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
},
|
||||
"upload only address - same": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AccessTypeOnlyAddress.With(myActorAddress), types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
},
|
||||
"upload only address - different": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AccessTypeOnlyAddress.With(otherAddress), types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
},
|
||||
"upload any address - included": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AccessTypeAnyOfAddresses.With(otherAddress, myActorAddress), types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
},
|
||||
"upload any address - not included": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AccessTypeAnyOfAddresses.With(otherAddress), types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
},
|
||||
"contract config - subtype": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowEverybody),
|
||||
contractInstConf: types.AccessTypeAnyOfAddresses.With(myActorAddress),
|
||||
},
|
||||
"contract config - not subtype": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowNobody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
},
|
||||
"upload undefined config - not panics": {
|
||||
chainConfigs: NewChainAccessConfigs(types.AccessConfig{}, types.AllowEverybody),
|
||||
contractInstConf: types.AllowEverybody,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
policy := GovAuthorizationPolicy{}
|
||||
got := policy.CanCreateCode(spec.chainConfigs, myActorAddress, spec.contractInstConf)
|
||||
assert.True(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovAuthzPolicyCanInstantiateContract(t *testing.T) {
|
||||
myActorAddress := RandomAccountAddress(t)
|
||||
otherAddress := RandomAccountAddress(t)
|
||||
specs := map[string]struct {
|
||||
config types.AccessConfig
|
||||
actor sdk.AccAddress
|
||||
}{
|
||||
"nobody": {
|
||||
config: types.AllowNobody,
|
||||
},
|
||||
"everybody": {
|
||||
config: types.AllowEverybody,
|
||||
},
|
||||
"only address - same": {
|
||||
config: types.AccessTypeOnlyAddress.With(myActorAddress),
|
||||
},
|
||||
"only address - different": {
|
||||
config: types.AccessTypeOnlyAddress.With(otherAddress),
|
||||
},
|
||||
"any address - included": {
|
||||
config: types.AccessTypeAnyOfAddresses.With(otherAddress, myActorAddress),
|
||||
},
|
||||
"any address - not included": {
|
||||
config: types.AccessTypeAnyOfAddresses.With(otherAddress),
|
||||
},
|
||||
"undefined config - panics": {
|
||||
config: types.AccessConfig{},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
policy := GovAuthorizationPolicy{}
|
||||
got := policy.CanInstantiateContract(spec.config, myActorAddress)
|
||||
assert.True(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovAuthzPolicyCanModifyContract(t *testing.T) {
|
||||
myActorAddress := RandomAccountAddress(t)
|
||||
otherAddress := RandomAccountAddress(t)
|
||||
|
||||
specs := map[string]struct {
|
||||
admin sdk.AccAddress
|
||||
}{
|
||||
"same as actor": {
|
||||
admin: myActorAddress,
|
||||
},
|
||||
"different admin": {
|
||||
admin: otherAddress,
|
||||
},
|
||||
"no admin": {},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
policy := GovAuthorizationPolicy{}
|
||||
got := policy.CanModifyContract(spec.admin, myActorAddress)
|
||||
assert.True(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovAuthzPolicyCanModifyCodeAccessConfig(t *testing.T) {
|
||||
myActorAddress := RandomAccountAddress(t)
|
||||
otherAddress := RandomAccountAddress(t)
|
||||
|
||||
specs := map[string]struct {
|
||||
admin sdk.AccAddress
|
||||
subset bool
|
||||
}{
|
||||
"same as actor - subset": {
|
||||
admin: myActorAddress,
|
||||
subset: true,
|
||||
},
|
||||
"same as actor - not subset": {
|
||||
admin: myActorAddress,
|
||||
subset: false,
|
||||
},
|
||||
"different admin": {
|
||||
admin: otherAddress,
|
||||
},
|
||||
"no admin": {},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
policy := GovAuthorizationPolicy{}
|
||||
got := policy.CanModifyCodeAccessConfig(spec.admin, myActorAddress, spec.subset)
|
||||
assert.True(t, got)
|
||||
})
|
||||
}
|
||||
}
|
102
x/wasm/keeper/bench_test.go
Normal file
@ -0,0 +1,102 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
"github.com/stretchr/testify/require"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// BenchmarkVerification benchmarks secp256k1 verification which is 1000 gas based on cpu time.
|
||||
//
|
||||
// Just this function is copied from
|
||||
// https://github.com/cosmos/cosmos-sdk/blob/90e9370bd80d9a3d41f7203ddb71166865561569/crypto/keys/internal/benchmarking/bench.go#L48-L62
|
||||
// And thus under the GO license (BSD style)
|
||||
func BenchmarkGasNormalization(b *testing.B) {
|
||||
priv := secp256k1.GenPrivKey()
|
||||
pub := priv.PubKey()
|
||||
|
||||
// use a short message, so this time doesn't get dominated by hashing.
|
||||
message := []byte("Hello, world!")
|
||||
signature, err := priv.Sign(message)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pub.VerifySignature(message, signature)
|
||||
}
|
||||
}
|
||||
|
||||
// By comparing the timing for queries on pinned vs unpinned, the difference gives us the overhead of
|
||||
// instantiating an unpinned contract. That value can be used to determine a reasonable gas price
|
||||
// for the InstantiationCost
|
||||
func BenchmarkInstantiationOverhead(b *testing.B) {
|
||||
specs := map[string]struct {
|
||||
pinned bool
|
||||
db func() dbm.DB
|
||||
}{
|
||||
"unpinned, memory db": {
|
||||
db: func() dbm.DB { return dbm.NewMemDB() },
|
||||
},
|
||||
"pinned, memory db": {
|
||||
db: func() dbm.DB { return dbm.NewMemDB() },
|
||||
pinned: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
wasmConfig := types.WasmConfig{MemoryCacheSize: 0}
|
||||
ctx, keepers := createTestInput(b, false, AvailableCapabilities, wasmConfig, spec.db())
|
||||
example := InstantiateHackatomExampleContract(b, ctx, keepers)
|
||||
if spec.pinned {
|
||||
require.NoError(b, keepers.ContractKeeper.PinCode(ctx, example.CodeID))
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := keepers.WasmKeeper.QuerySmart(ctx, example.Contract, []byte(`{"verifier":{}}`))
|
||||
require.NoError(b, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the time it takes to compile some wasm code the first time.
|
||||
// This will help us adjust pricing for UploadCode
|
||||
func BenchmarkCompilation(b *testing.B) {
|
||||
specs := map[string]struct {
|
||||
wasmFile string
|
||||
}{
|
||||
"hackatom": {
|
||||
wasmFile: "./testdata/hackatom.wasm",
|
||||
},
|
||||
"burner": {
|
||||
wasmFile: "./testdata/burner.wasm",
|
||||
},
|
||||
"ibc_reflect": {
|
||||
wasmFile: "./testdata/ibc_reflect.wasm",
|
||||
},
|
||||
}
|
||||
|
||||
for name, spec := range specs {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
wasmConfig := types.WasmConfig{MemoryCacheSize: 0}
|
||||
db := dbm.NewMemDB()
|
||||
ctx, keepers := createTestInput(b, false, AvailableCapabilities, wasmConfig, db)
|
||||
|
||||
// print out code size for comparisons
|
||||
code, err := os.ReadFile(spec.wasmFile)
|
||||
require.NoError(b, err)
|
||||
b.Logf("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b(size: %d) ", len(code))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = StoreExampleContract(b, ctx, keepers, spec.wasmFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
130
x/wasm/keeper/contract_keeper.go
Normal file
@ -0,0 +1,130 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
var _ types.ContractOpsKeeper = PermissionedKeeper{}
|
||||
|
||||
// decoratedKeeper contains a subset of the wasm keeper that are already or can be guarded by an authorization policy in the future
|
||||
type decoratedKeeper interface {
|
||||
create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, instantiateAccess *types.AccessConfig, authZ AuthorizationPolicy) (codeID uint64, checksum []byte, err error)
|
||||
|
||||
instantiate(
|
||||
ctx sdk.Context,
|
||||
codeID uint64,
|
||||
creator, admin sdk.AccAddress,
|
||||
initMsg []byte,
|
||||
label string,
|
||||
deposit sdk.Coins,
|
||||
addressGenerator AddressGenerator,
|
||||
authZ AuthorizationPolicy,
|
||||
) (sdk.AccAddress, []byte, error)
|
||||
|
||||
migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte, authZ AuthorizationPolicy) ([]byte, error)
|
||||
setContractAdmin(ctx sdk.Context, contractAddress, caller, newAdmin sdk.AccAddress, authZ AuthorizationPolicy) error
|
||||
pinCode(ctx sdk.Context, codeID uint64) error
|
||||
unpinCode(ctx sdk.Context, codeID uint64) error
|
||||
execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins) ([]byte, error)
|
||||
Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte) ([]byte, error)
|
||||
setContractInfoExtension(ctx sdk.Context, contract sdk.AccAddress, extra types.ContractInfoExtension) error
|
||||
setAccessConfig(ctx sdk.Context, codeID uint64, caller sdk.AccAddress, newConfig types.AccessConfig, autz AuthorizationPolicy) error
|
||||
ClassicAddressGenerator() AddressGenerator
|
||||
}
|
||||
|
||||
type PermissionedKeeper struct {
|
||||
authZPolicy AuthorizationPolicy
|
||||
nested decoratedKeeper
|
||||
}
|
||||
|
||||
func NewPermissionedKeeper(nested decoratedKeeper, authZPolicy AuthorizationPolicy) *PermissionedKeeper {
|
||||
return &PermissionedKeeper{authZPolicy: authZPolicy, nested: nested}
|
||||
}
|
||||
|
||||
func NewGovPermissionKeeper(nested decoratedKeeper) *PermissionedKeeper {
|
||||
return NewPermissionedKeeper(nested, GovAuthorizationPolicy{})
|
||||
}
|
||||
|
||||
func NewDefaultPermissionKeeper(nested decoratedKeeper) *PermissionedKeeper {
|
||||
return NewPermissionedKeeper(nested, DefaultAuthorizationPolicy{})
|
||||
}
|
||||
|
||||
func (p PermissionedKeeper) Create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, instantiateAccess *types.AccessConfig) (codeID uint64, checksum []byte, err error) {
|
||||
return p.nested.create(ctx, creator, wasmCode, instantiateAccess, p.authZPolicy)
|
||||
}
|
||||
|
||||
// Instantiate creates an instance of a WASM contract using the classic sequence based address generator
|
||||
func (p PermissionedKeeper) Instantiate(
|
||||
ctx sdk.Context,
|
||||
codeID uint64,
|
||||
creator, admin sdk.AccAddress,
|
||||
initMsg []byte,
|
||||
label string,
|
||||
deposit sdk.Coins,
|
||||
) (sdk.AccAddress, []byte, error) {
|
||||
return p.nested.instantiate(ctx, codeID, creator, admin, initMsg, label, deposit, p.nested.ClassicAddressGenerator(), p.authZPolicy)
|
||||
}
|
||||
|
||||
// Instantiate2 creates an instance of a WASM contract using the predictable address generator
|
||||
func (p PermissionedKeeper) Instantiate2(
|
||||
ctx sdk.Context,
|
||||
codeID uint64,
|
||||
creator, admin sdk.AccAddress,
|
||||
initMsg []byte,
|
||||
label string,
|
||||
deposit sdk.Coins,
|
||||
salt []byte,
|
||||
fixMsg bool,
|
||||
) (sdk.AccAddress, []byte, error) {
|
||||
return p.nested.instantiate(
|
||||
ctx,
|
||||
codeID,
|
||||
creator,
|
||||
admin,
|
||||
initMsg,
|
||||
label,
|
||||
deposit,
|
||||
PredicableAddressGenerator(creator, salt, initMsg, fixMsg),
|
||||
p.authZPolicy,
|
||||
)
|
||||
}
|
||||
|
||||
func (p PermissionedKeeper) Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins) ([]byte, error) {
|
||||
return p.nested.execute(ctx, contractAddress, caller, msg, coins)
|
||||
}
|
||||
|
||||
func (p PermissionedKeeper) Migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte) ([]byte, error) {
|
||||
return p.nested.migrate(ctx, contractAddress, caller, newCodeID, msg, p.authZPolicy)
|
||||
}
|
||||
|
||||
func (p PermissionedKeeper) Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte) ([]byte, error) {
|
||||
return p.nested.Sudo(ctx, contractAddress, msg)
|
||||
}
|
||||
|
||||
func (p PermissionedKeeper) UpdateContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newAdmin sdk.AccAddress) error {
|
||||
return p.nested.setContractAdmin(ctx, contractAddress, caller, newAdmin, p.authZPolicy)
|
||||
}
|
||||
|
||||
func (p PermissionedKeeper) ClearContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress) error {
|
||||
return p.nested.setContractAdmin(ctx, contractAddress, caller, nil, p.authZPolicy)
|
||||
}
|
||||
|
||||
func (p PermissionedKeeper) PinCode(ctx sdk.Context, codeID uint64) error {
|
||||
return p.nested.pinCode(ctx, codeID)
|
||||
}
|
||||
|
||||
func (p PermissionedKeeper) UnpinCode(ctx sdk.Context, codeID uint64) error {
|
||||
return p.nested.unpinCode(ctx, codeID)
|
||||
}
|
||||
|
||||
// SetContractInfoExtension updates the extra attributes that can be stored with the contract info
|
||||
func (p PermissionedKeeper) SetContractInfoExtension(ctx sdk.Context, contract sdk.AccAddress, extra types.ContractInfoExtension) error {
|
||||
return p.nested.setContractInfoExtension(ctx, contract, extra)
|
||||
}
|
||||
|
||||
// SetAccessConfig updates the access config of a code id.
|
||||
func (p PermissionedKeeper) SetAccessConfig(ctx sdk.Context, codeID uint64, caller sdk.AccAddress, newConfig types.AccessConfig) error {
|
||||
return p.nested.setAccessConfig(ctx, codeID, caller, newConfig, p.authZPolicy)
|
||||
}
|
168
x/wasm/keeper/contract_keeper_test.go
Normal file
@ -0,0 +1,168 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestInstantiate2(t *testing.T) {
|
||||
parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
example := StoreHackatomExampleContract(t, parentCtx, keepers)
|
||||
otherExample := StoreReflectContract(t, parentCtx, keepers)
|
||||
mock := &wasmtesting.MockWasmer{}
|
||||
wasmtesting.MakeInstantiable(mock)
|
||||
keepers.WasmKeeper.wasmVM = mock // set mock to not fail on contract init message
|
||||
|
||||
verifierAddr := RandomAccountAddress(t)
|
||||
beneficiaryAddr := RandomAccountAddress(t)
|
||||
initMsg := mustMarshal(t, HackatomExampleInitMsg{Verifier: verifierAddr, Beneficiary: beneficiaryAddr})
|
||||
|
||||
otherAddr := keepers.Faucet.NewFundedRandomAccount(parentCtx, sdk.NewInt64Coin("denom", 1_000_000_000))
|
||||
|
||||
const (
|
||||
mySalt = "my salt"
|
||||
myLabel = "my label"
|
||||
)
|
||||
// create instances for duplicate checks
|
||||
exampleContract := func(t *testing.T, ctx sdk.Context, fixMsg bool) {
|
||||
_, _, err := keepers.ContractKeeper.Instantiate2(
|
||||
ctx,
|
||||
example.CodeID,
|
||||
example.CreatorAddr,
|
||||
nil,
|
||||
initMsg,
|
||||
myLabel,
|
||||
sdk.NewCoins(sdk.NewInt64Coin("denom", 1)),
|
||||
[]byte(mySalt),
|
||||
fixMsg,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
exampleWithFixMsg := func(t *testing.T, ctx sdk.Context) {
|
||||
exampleContract(t, ctx, true)
|
||||
}
|
||||
exampleWithoutFixMsg := func(t *testing.T, ctx sdk.Context) {
|
||||
exampleContract(t, ctx, false)
|
||||
}
|
||||
specs := map[string]struct {
|
||||
setup func(t *testing.T, ctx sdk.Context)
|
||||
codeID uint64
|
||||
sender sdk.AccAddress
|
||||
salt []byte
|
||||
initMsg json.RawMessage
|
||||
fixMsg bool
|
||||
expErr error
|
||||
}{
|
||||
"fix msg - generates different address than without fixMsg": {
|
||||
setup: exampleWithoutFixMsg,
|
||||
codeID: example.CodeID,
|
||||
sender: example.CreatorAddr,
|
||||
salt: []byte(mySalt),
|
||||
initMsg: initMsg,
|
||||
fixMsg: true,
|
||||
},
|
||||
"fix msg - different sender": {
|
||||
setup: exampleWithFixMsg,
|
||||
codeID: example.CodeID,
|
||||
sender: otherAddr,
|
||||
salt: []byte(mySalt),
|
||||
initMsg: initMsg,
|
||||
fixMsg: true,
|
||||
},
|
||||
"fix msg - different code": {
|
||||
setup: exampleWithFixMsg,
|
||||
codeID: otherExample.CodeID,
|
||||
sender: example.CreatorAddr,
|
||||
salt: []byte(mySalt),
|
||||
initMsg: []byte(`{}`),
|
||||
fixMsg: true,
|
||||
},
|
||||
"fix msg - different salt": {
|
||||
setup: exampleWithFixMsg,
|
||||
codeID: example.CodeID,
|
||||
sender: example.CreatorAddr,
|
||||
salt: []byte("other salt"),
|
||||
initMsg: initMsg,
|
||||
fixMsg: true,
|
||||
},
|
||||
"fix msg - different init msg": {
|
||||
setup: exampleWithFixMsg,
|
||||
codeID: example.CodeID,
|
||||
sender: example.CreatorAddr,
|
||||
salt: []byte(mySalt),
|
||||
initMsg: mustMarshal(t, HackatomExampleInitMsg{Verifier: otherAddr, Beneficiary: beneficiaryAddr}),
|
||||
fixMsg: true,
|
||||
},
|
||||
"different sender": {
|
||||
setup: exampleWithoutFixMsg,
|
||||
codeID: example.CodeID,
|
||||
sender: otherAddr,
|
||||
salt: []byte(mySalt),
|
||||
initMsg: initMsg,
|
||||
},
|
||||
"different code": {
|
||||
setup: exampleWithoutFixMsg,
|
||||
codeID: otherExample.CodeID,
|
||||
sender: example.CreatorAddr,
|
||||
salt: []byte(mySalt),
|
||||
initMsg: []byte(`{}`),
|
||||
},
|
||||
"different salt": {
|
||||
setup: exampleWithoutFixMsg,
|
||||
codeID: example.CodeID,
|
||||
sender: example.CreatorAddr,
|
||||
salt: []byte(`other salt`),
|
||||
initMsg: initMsg,
|
||||
},
|
||||
"different init msg - reject same address": {
|
||||
setup: exampleWithoutFixMsg,
|
||||
codeID: example.CodeID,
|
||||
sender: example.CreatorAddr,
|
||||
salt: []byte(mySalt),
|
||||
initMsg: mustMarshal(t, HackatomExampleInitMsg{Verifier: otherAddr, Beneficiary: beneficiaryAddr}),
|
||||
expErr: types.ErrDuplicate,
|
||||
},
|
||||
"fix msg - long msg": {
|
||||
setup: exampleWithFixMsg,
|
||||
codeID: example.CodeID,
|
||||
sender: otherAddr,
|
||||
salt: []byte(mySalt),
|
||||
initMsg: []byte(fmt.Sprintf(`{"foo":%q}`, strings.Repeat("b", math.MaxInt16+1))), // too long kills CI
|
||||
fixMsg: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx, _ := parentCtx.CacheContext()
|
||||
spec.setup(t, ctx)
|
||||
gotAddr, _, gotErr := keepers.ContractKeeper.Instantiate2(
|
||||
ctx,
|
||||
spec.codeID,
|
||||
spec.sender,
|
||||
nil,
|
||||
spec.initMsg,
|
||||
myLabel,
|
||||
sdk.NewCoins(sdk.NewInt64Coin("denom", 2)),
|
||||
spec.salt,
|
||||
spec.fixMsg,
|
||||
)
|
||||
if spec.expErr != nil {
|
||||
assert.ErrorIs(t, gotErr, spec.expErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
assert.NotEmpty(t, gotAddr)
|
||||
})
|
||||
}
|
||||
}
|
67
x/wasm/keeper/events.go
Normal file
@ -0,0 +1,67 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// newWasmModuleEvent creates with wasm module event for interacting with the given contract. Adds custom attributes
|
||||
// to this event.
|
||||
func newWasmModuleEvent(customAttributes []wasmvmtypes.EventAttribute, contractAddr sdk.AccAddress) (sdk.Events, error) {
|
||||
attrs, err := contractSDKEventAttributes(customAttributes, contractAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// each wasm invocation always returns one sdk.Event
|
||||
return sdk.Events{sdk.NewEvent(types.WasmModuleEventType, attrs...)}, nil
|
||||
}
|
||||
|
||||
const eventTypeMinLength = 2
|
||||
|
||||
// newCustomEvents converts wasmvm events from a contract response to sdk type events
|
||||
func newCustomEvents(evts wasmvmtypes.Events, contractAddr sdk.AccAddress) (sdk.Events, error) {
|
||||
events := make(sdk.Events, 0, len(evts))
|
||||
for _, e := range evts {
|
||||
typ := strings.TrimSpace(e.Type)
|
||||
if len(typ) <= eventTypeMinLength {
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalidEvent, fmt.Sprintf("Event type too short: '%s'", typ))
|
||||
}
|
||||
attributes, err := contractSDKEventAttributes(e.Attributes, contractAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events = append(events, sdk.NewEvent(fmt.Sprintf("%s%s", types.CustomContractEventPrefix, typ), attributes...))
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// convert and add contract address issuing this event
|
||||
func contractSDKEventAttributes(customAttributes []wasmvmtypes.EventAttribute, contractAddr sdk.AccAddress) ([]sdk.Attribute, error) {
|
||||
attrs := []sdk.Attribute{sdk.NewAttribute(types.AttributeKeyContractAddr, contractAddr.String())}
|
||||
// append attributes from wasm to the sdk.Event
|
||||
for _, l := range customAttributes {
|
||||
// ensure key and value are non-empty (and trim what is there)
|
||||
key := strings.TrimSpace(l.Key)
|
||||
if len(key) == 0 {
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalidEvent, fmt.Sprintf("Empty attribute key. Value: %s", l.Value))
|
||||
}
|
||||
value := strings.TrimSpace(l.Value)
|
||||
// TODO: check if this is legal in the SDK - if it is, we can remove this check
|
||||
if len(value) == 0 {
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalidEvent, fmt.Sprintf("Empty attribute value. Key: %s", key))
|
||||
}
|
||||
// and reserve all _* keys for our use (not contract)
|
||||
if strings.HasPrefix(key, types.AttributeReservedPrefix) {
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalidEvent, fmt.Sprintf("Attribute key starts with reserved prefix %s: '%s'", types.AttributeReservedPrefix, key))
|
||||
}
|
||||
attrs = append(attrs, sdk.NewAttribute(key, value))
|
||||
}
|
||||
return attrs, nil
|
||||
}
|
290
x/wasm/keeper/events_test.go
Normal file
@ -0,0 +1,290 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestHasWasmModuleEvent(t *testing.T) {
|
||||
myContractAddr := RandomAccountAddress(t)
|
||||
specs := map[string]struct {
|
||||
srcEvents []sdk.Event
|
||||
exp bool
|
||||
}{
|
||||
"event found": {
|
||||
srcEvents: []sdk.Event{
|
||||
sdk.NewEvent(types.WasmModuleEventType, sdk.NewAttribute("_contract_address", myContractAddr.String())),
|
||||
},
|
||||
exp: true,
|
||||
},
|
||||
"different event: not found": {
|
||||
srcEvents: []sdk.Event{
|
||||
sdk.NewEvent(types.CustomContractEventPrefix, sdk.NewAttribute("_contract_address", myContractAddr.String())),
|
||||
},
|
||||
exp: false,
|
||||
},
|
||||
"event with different address: not found": {
|
||||
srcEvents: []sdk.Event{
|
||||
sdk.NewEvent(types.WasmModuleEventType, sdk.NewAttribute("_contract_address", RandomBech32AccountAddress(t))),
|
||||
},
|
||||
exp: false,
|
||||
},
|
||||
"no event": {
|
||||
srcEvents: []sdk.Event{},
|
||||
exp: false,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
em := sdk.NewEventManager()
|
||||
em.EmitEvents(spec.srcEvents)
|
||||
ctx := sdk.Context{}.WithContext(context.Background()).WithEventManager(em)
|
||||
|
||||
got := hasWasmModuleEvent(ctx, myContractAddr)
|
||||
assert.Equal(t, spec.exp, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCustomEvents(t *testing.T) {
|
||||
myContract := RandomAccountAddress(t)
|
||||
specs := map[string]struct {
|
||||
src wasmvmtypes.Events
|
||||
exp sdk.Events
|
||||
isError bool
|
||||
}{
|
||||
"all good": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "foo",
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}},
|
||||
}},
|
||||
exp: sdk.Events{sdk.NewEvent("wasm-foo",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()),
|
||||
sdk.NewAttribute("myKey", "myVal"))},
|
||||
},
|
||||
"multiple attributes": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "foo",
|
||||
Attributes: []wasmvmtypes.EventAttribute{
|
||||
{Key: "myKey", Value: "myVal"},
|
||||
{Key: "myOtherKey", Value: "myOtherVal"},
|
||||
},
|
||||
}},
|
||||
exp: sdk.Events{sdk.NewEvent("wasm-foo",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()),
|
||||
sdk.NewAttribute("myKey", "myVal"),
|
||||
sdk.NewAttribute("myOtherKey", "myOtherVal"))},
|
||||
},
|
||||
"multiple events": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "foo",
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}},
|
||||
}, {
|
||||
Type: "bar",
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "otherKey", Value: "otherVal"}},
|
||||
}},
|
||||
exp: sdk.Events{
|
||||
sdk.NewEvent("wasm-foo",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()),
|
||||
sdk.NewAttribute("myKey", "myVal")),
|
||||
sdk.NewEvent("wasm-bar",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()),
|
||||
sdk.NewAttribute("otherKey", "otherVal")),
|
||||
},
|
||||
},
|
||||
"without attributes": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "foo",
|
||||
}},
|
||||
exp: sdk.Events{sdk.NewEvent("wasm-foo",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()))},
|
||||
},
|
||||
"error on short event type": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "f",
|
||||
}},
|
||||
isError: true,
|
||||
},
|
||||
"error on _contract_address": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "foo",
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "_contract_address", Value: RandomBech32AccountAddress(t)}},
|
||||
}},
|
||||
isError: true,
|
||||
},
|
||||
"error on reserved prefix": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "wasm",
|
||||
Attributes: []wasmvmtypes.EventAttribute{
|
||||
{Key: "_reserved", Value: "is skipped"},
|
||||
{Key: "normal", Value: "is used"},
|
||||
},
|
||||
}},
|
||||
isError: true,
|
||||
},
|
||||
"error on empty value": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "boom",
|
||||
Attributes: []wasmvmtypes.EventAttribute{
|
||||
{Key: "some", Value: "data"},
|
||||
{Key: "key", Value: ""},
|
||||
},
|
||||
}},
|
||||
isError: true,
|
||||
},
|
||||
"error on empty key": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "boom",
|
||||
Attributes: []wasmvmtypes.EventAttribute{
|
||||
{Key: "some", Value: "data"},
|
||||
{Key: "", Value: "value"},
|
||||
},
|
||||
}},
|
||||
isError: true,
|
||||
},
|
||||
"error on whitespace type": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: " f ",
|
||||
Attributes: []wasmvmtypes.EventAttribute{
|
||||
{Key: "some", Value: "data"},
|
||||
},
|
||||
}},
|
||||
isError: true,
|
||||
},
|
||||
"error on only whitespace key": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "boom",
|
||||
Attributes: []wasmvmtypes.EventAttribute{
|
||||
{Key: "some", Value: "data"},
|
||||
{Key: "\n\n\n\n", Value: "value"},
|
||||
},
|
||||
}},
|
||||
isError: true,
|
||||
},
|
||||
"error on only whitespace value": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: "boom",
|
||||
Attributes: []wasmvmtypes.EventAttribute{
|
||||
{Key: "some", Value: "data"},
|
||||
{Key: "myKey", Value: " \t\r\n"},
|
||||
},
|
||||
}},
|
||||
isError: true,
|
||||
},
|
||||
"strip out whitespace": {
|
||||
src: wasmvmtypes.Events{{
|
||||
Type: " food\n",
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "my Key", Value: "\tmyVal"}},
|
||||
}},
|
||||
exp: sdk.Events{sdk.NewEvent("wasm-food",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()),
|
||||
sdk.NewAttribute("my Key", "myVal"))},
|
||||
},
|
||||
"empty event elements": {
|
||||
src: make(wasmvmtypes.Events, 10),
|
||||
isError: true,
|
||||
},
|
||||
"nil": {
|
||||
exp: sdk.Events{},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gotEvent, err := newCustomEvents(spec.src, myContract)
|
||||
if spec.isError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, spec.exp, gotEvent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWasmModuleEvent(t *testing.T) {
|
||||
myContract := RandomAccountAddress(t)
|
||||
specs := map[string]struct {
|
||||
src []wasmvmtypes.EventAttribute
|
||||
exp sdk.Events
|
||||
isError bool
|
||||
}{
|
||||
"all good": {
|
||||
src: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}},
|
||||
exp: sdk.Events{sdk.NewEvent("wasm",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()),
|
||||
sdk.NewAttribute("myKey", "myVal"))},
|
||||
},
|
||||
"multiple attributes": {
|
||||
src: []wasmvmtypes.EventAttribute{
|
||||
{Key: "myKey", Value: "myVal"},
|
||||
{Key: "myOtherKey", Value: "myOtherVal"},
|
||||
},
|
||||
exp: sdk.Events{sdk.NewEvent("wasm",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()),
|
||||
sdk.NewAttribute("myKey", "myVal"),
|
||||
sdk.NewAttribute("myOtherKey", "myOtherVal"))},
|
||||
},
|
||||
"without attributes": {
|
||||
exp: sdk.Events{sdk.NewEvent("wasm",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()))},
|
||||
},
|
||||
"error on _contract_address": {
|
||||
src: []wasmvmtypes.EventAttribute{{Key: "_contract_address", Value: RandomBech32AccountAddress(t)}},
|
||||
isError: true,
|
||||
},
|
||||
"error on whitespace key": {
|
||||
src: []wasmvmtypes.EventAttribute{{Key: " ", Value: "value"}},
|
||||
isError: true,
|
||||
},
|
||||
"error on whitespace value": {
|
||||
src: []wasmvmtypes.EventAttribute{{Key: "key", Value: "\n\n\n"}},
|
||||
isError: true,
|
||||
},
|
||||
"strip whitespace": {
|
||||
src: []wasmvmtypes.EventAttribute{{Key: " my-real-key ", Value: "\n\n\nsome-val\t\t\t"}},
|
||||
exp: sdk.Events{sdk.NewEvent("wasm",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()),
|
||||
sdk.NewAttribute("my-real-key", "some-val"))},
|
||||
},
|
||||
"empty elements": {
|
||||
src: make([]wasmvmtypes.EventAttribute, 10),
|
||||
isError: true,
|
||||
},
|
||||
"nil": {
|
||||
exp: sdk.Events{sdk.NewEvent("wasm",
|
||||
sdk.NewAttribute("_contract_address", myContract.String()),
|
||||
)},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gotEvent, err := newWasmModuleEvent(spec.src, myContract)
|
||||
if spec.isError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, spec.exp, gotEvent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// returns true when a wasm module event was emitted for this contract already
|
||||
func hasWasmModuleEvent(ctx sdk.Context, contractAddr sdk.AccAddress) bool {
|
||||
for _, e := range ctx.EventManager().Events() {
|
||||
if e.Type == types.WasmModuleEventType {
|
||||
for _, a := range e.Attributes {
|
||||
if string(a.Key) == types.AttributeKeyContractAddr && string(a.Value) == contractAddr.String() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
252
x/wasm/keeper/gas_register.go
Normal file
@ -0,0 +1,252 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultGasMultiplier is how many CosmWasm gas points = 1 Cosmos SDK gas point.
|
||||
//
|
||||
// CosmWasm gas strategy is documented in https://github.com/CosmWasm/cosmwasm/blob/v1.0.0-beta/docs/GAS.md.
|
||||
// Cosmos SDK reference costs can be found here: https://github.com/cosmos/cosmos-sdk/blob/v0.42.10/store/types/gas.go#L198-L209.
|
||||
//
|
||||
// The original multiplier of 100 up to CosmWasm 0.16 was based on
|
||||
// "A write at ~3000 gas and ~200us = 10 gas per us (microsecond) cpu/io
|
||||
// Rough timing have 88k gas at 90us, which is equal to 1k sdk gas... (one read)"
|
||||
// as well as manual Wasmer benchmarks from 2019. This was then multiplied by 150_000
|
||||
// in the 0.16 -> 1.0 upgrade (https://github.com/CosmWasm/cosmwasm/pull/1120).
|
||||
//
|
||||
// The multiplier deserves more reproducible benchmarking and a strategy that allows easy adjustments.
|
||||
// This is tracked in https://github.com/CosmWasm/wasmd/issues/566 and https://github.com/CosmWasm/wasmd/issues/631.
|
||||
// Gas adjustments are consensus breaking but may happen in any release marked as consensus breaking.
|
||||
// Do not make assumptions on how much gas an operation will consume in places that are hard to adjust,
|
||||
// such as hardcoding them in contracts.
|
||||
//
|
||||
// Please note that all gas prices returned to wasmvm should have this multiplied.
|
||||
// Benchmarks and numbers were discussed in: https://github.com/CosmWasm/wasmd/pull/634#issuecomment-938055852
|
||||
DefaultGasMultiplier uint64 = 140_000_000
|
||||
// DefaultInstanceCost is how much SDK gas we charge each time we load a WASM instance.
|
||||
// Creating a new instance is costly, and this helps put a recursion limit to contracts calling contracts.
|
||||
// Benchmarks and numbers were discussed in: https://github.com/CosmWasm/wasmd/pull/634#issuecomment-938056803
|
||||
DefaultInstanceCost uint64 = 60_000
|
||||
// DefaultCompileCost is how much SDK gas is charged *per byte* for compiling WASM code.
|
||||
// Benchmarks and numbers were discussed in: https://github.com/CosmWasm/wasmd/pull/634#issuecomment-938056803
|
||||
DefaultCompileCost uint64 = 3
|
||||
// DefaultEventAttributeDataCost is how much SDK gas is charged *per byte* for attribute data in events.
|
||||
// This is used with len(key) + len(value)
|
||||
DefaultEventAttributeDataCost uint64 = 1
|
||||
// DefaultContractMessageDataCost is how much SDK gas is charged *per byte* of the message that goes to the contract
|
||||
// This is used with len(msg). Note that the message is deserialized in the receiving contract and this is charged
|
||||
// with wasm gas already. The derserialization of results is also charged in wasmvm. I am unsure if we need to add
|
||||
// additional costs here.
|
||||
// Note: also used for error fields on reply, and data on reply. Maybe these should be pulled out to a different (non-zero) field
|
||||
DefaultContractMessageDataCost uint64 = 0
|
||||
// DefaultPerAttributeCost is how much SDK gas we charge per attribute count.
|
||||
DefaultPerAttributeCost uint64 = 10
|
||||
// DefaultPerCustomEventCost is how much SDK gas we charge per event count.
|
||||
DefaultPerCustomEventCost uint64 = 20
|
||||
// DefaultEventAttributeDataFreeTier number of bytes of total attribute data we do not charge.
|
||||
DefaultEventAttributeDataFreeTier = 100
|
||||
)
|
||||
|
||||
// default: 0.15 gas.
|
||||
// see https://github.com/CosmWasm/wasmd/pull/898#discussion_r937727200
|
||||
var defaultPerByteUncompressCost = wasmvmtypes.UFraction{
|
||||
Numerator: 15,
|
||||
Denominator: 100,
|
||||
}
|
||||
|
||||
// DefaultPerByteUncompressCost is how much SDK gas we charge per source byte to unpack
|
||||
func DefaultPerByteUncompressCost() wasmvmtypes.UFraction {
|
||||
return defaultPerByteUncompressCost
|
||||
}
|
||||
|
||||
// GasRegister abstract source for gas costs
|
||||
type GasRegister interface {
|
||||
// NewContractInstanceCosts costs to crate a new contract instance from code
|
||||
NewContractInstanceCosts(pinned bool, msgLen int) sdk.Gas
|
||||
// CompileCosts costs to persist and "compile" a new wasm contract
|
||||
CompileCosts(byteLength int) sdk.Gas
|
||||
// UncompressCosts costs to unpack a new wasm contract
|
||||
UncompressCosts(byteLength int) sdk.Gas
|
||||
// InstantiateContractCosts costs when interacting with a wasm contract
|
||||
InstantiateContractCosts(pinned bool, msgLen int) sdk.Gas
|
||||
// ReplyCosts costs to to handle a message reply
|
||||
ReplyCosts(pinned bool, reply wasmvmtypes.Reply) sdk.Gas
|
||||
// EventCosts costs to persist an event
|
||||
EventCosts(attrs []wasmvmtypes.EventAttribute, events wasmvmtypes.Events) sdk.Gas
|
||||
// ToWasmVMGas converts from sdk gas to wasmvm gas
|
||||
ToWasmVMGas(source sdk.Gas) uint64
|
||||
// FromWasmVMGas converts from wasmvm gas to sdk gas
|
||||
FromWasmVMGas(source uint64) sdk.Gas
|
||||
}
|
||||
|
||||
// WasmGasRegisterConfig config type
|
||||
type WasmGasRegisterConfig struct {
|
||||
// InstanceCost costs when interacting with a wasm contract
|
||||
InstanceCost sdk.Gas
|
||||
// CompileCosts costs to persist and "compile" a new wasm contract
|
||||
CompileCost sdk.Gas
|
||||
// UncompressCost costs per byte to unpack a contract
|
||||
UncompressCost wasmvmtypes.UFraction
|
||||
// GasMultiplier is how many cosmwasm gas points = 1 sdk gas point
|
||||
// SDK reference costs can be found here: https://github.com/cosmos/cosmos-sdk/blob/02c6c9fafd58da88550ab4d7d494724a477c8a68/store/types/gas.go#L153-L164
|
||||
GasMultiplier sdk.Gas
|
||||
// EventPerAttributeCost is how much SDK gas is charged *per byte* for attribute data in events.
|
||||
// This is used with len(key) + len(value)
|
||||
EventPerAttributeCost sdk.Gas
|
||||
// EventAttributeDataCost is how much SDK gas is charged *per byte* for attribute data in events.
|
||||
// This is used with len(key) + len(value)
|
||||
EventAttributeDataCost sdk.Gas
|
||||
// EventAttributeDataFreeTier number of bytes of total attribute data that is free of charge
|
||||
EventAttributeDataFreeTier uint64
|
||||
// ContractMessageDataCost SDK gas charged *per byte* of the message that goes to the contract
|
||||
// This is used with len(msg)
|
||||
ContractMessageDataCost sdk.Gas
|
||||
// CustomEventCost cost per custom event
|
||||
CustomEventCost uint64
|
||||
}
|
||||
|
||||
// DefaultGasRegisterConfig default values
|
||||
func DefaultGasRegisterConfig() WasmGasRegisterConfig {
|
||||
return WasmGasRegisterConfig{
|
||||
InstanceCost: DefaultInstanceCost,
|
||||
CompileCost: DefaultCompileCost,
|
||||
GasMultiplier: DefaultGasMultiplier,
|
||||
EventPerAttributeCost: DefaultPerAttributeCost,
|
||||
CustomEventCost: DefaultPerCustomEventCost,
|
||||
EventAttributeDataCost: DefaultEventAttributeDataCost,
|
||||
EventAttributeDataFreeTier: DefaultEventAttributeDataFreeTier,
|
||||
ContractMessageDataCost: DefaultContractMessageDataCost,
|
||||
UncompressCost: DefaultPerByteUncompressCost(),
|
||||
}
|
||||
}
|
||||
|
||||
// WasmGasRegister implements GasRegister interface
|
||||
type WasmGasRegister struct {
|
||||
c WasmGasRegisterConfig
|
||||
}
|
||||
|
||||
// NewDefaultWasmGasRegister creates instance with default values
|
||||
func NewDefaultWasmGasRegister() WasmGasRegister {
|
||||
return NewWasmGasRegister(DefaultGasRegisterConfig())
|
||||
}
|
||||
|
||||
// NewWasmGasRegister constructor
|
||||
func NewWasmGasRegister(c WasmGasRegisterConfig) WasmGasRegister {
|
||||
if c.GasMultiplier == 0 {
|
||||
panic(sdkerrors.Wrap(sdkerrors.ErrLogic, "GasMultiplier can not be 0"))
|
||||
}
|
||||
return WasmGasRegister{
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
// NewContractInstanceCosts costs to crate a new contract instance from code
|
||||
func (g WasmGasRegister) NewContractInstanceCosts(pinned bool, msgLen int) storetypes.Gas {
|
||||
return g.InstantiateContractCosts(pinned, msgLen)
|
||||
}
|
||||
|
||||
// CompileCosts costs to persist and "compile" a new wasm contract
|
||||
func (g WasmGasRegister) CompileCosts(byteLength int) storetypes.Gas {
|
||||
if byteLength < 0 {
|
||||
panic(sdkerrors.Wrap(types.ErrInvalid, "negative length"))
|
||||
}
|
||||
return g.c.CompileCost * uint64(byteLength)
|
||||
}
|
||||
|
||||
// UncompressCosts costs to unpack a new wasm contract
|
||||
func (g WasmGasRegister) UncompressCosts(byteLength int) sdk.Gas {
|
||||
if byteLength < 0 {
|
||||
panic(sdkerrors.Wrap(types.ErrInvalid, "negative length"))
|
||||
}
|
||||
return g.c.UncompressCost.Mul(uint64(byteLength)).Floor()
|
||||
}
|
||||
|
||||
// InstantiateContractCosts costs when interacting with a wasm contract
|
||||
func (g WasmGasRegister) InstantiateContractCosts(pinned bool, msgLen int) sdk.Gas {
|
||||
if msgLen < 0 {
|
||||
panic(sdkerrors.Wrap(types.ErrInvalid, "negative length"))
|
||||
}
|
||||
dataCosts := sdk.Gas(msgLen) * g.c.ContractMessageDataCost
|
||||
if pinned {
|
||||
return dataCosts
|
||||
}
|
||||
return g.c.InstanceCost + dataCosts
|
||||
}
|
||||
|
||||
// ReplyCosts costs to to handle a message reply
|
||||
func (g WasmGasRegister) ReplyCosts(pinned bool, reply wasmvmtypes.Reply) sdk.Gas {
|
||||
var eventGas sdk.Gas
|
||||
msgLen := len(reply.Result.Err)
|
||||
if reply.Result.Ok != nil {
|
||||
msgLen += len(reply.Result.Ok.Data)
|
||||
var attrs []wasmvmtypes.EventAttribute
|
||||
for _, e := range reply.Result.Ok.Events {
|
||||
eventGas += sdk.Gas(len(e.Type)) * g.c.EventAttributeDataCost
|
||||
attrs = append(attrs, e.Attributes...)
|
||||
}
|
||||
// apply free tier on the whole set not per event
|
||||
eventGas += g.EventCosts(attrs, nil)
|
||||
}
|
||||
return eventGas + g.InstantiateContractCosts(pinned, msgLen)
|
||||
}
|
||||
|
||||
// EventCosts costs to persist an event
|
||||
func (g WasmGasRegister) EventCosts(attrs []wasmvmtypes.EventAttribute, events wasmvmtypes.Events) sdk.Gas {
|
||||
gas, remainingFreeTier := g.eventAttributeCosts(attrs, g.c.EventAttributeDataFreeTier)
|
||||
for _, e := range events {
|
||||
gas += g.c.CustomEventCost
|
||||
gas += sdk.Gas(len(e.Type)) * g.c.EventAttributeDataCost // no free tier with event type
|
||||
var attrCost sdk.Gas
|
||||
attrCost, remainingFreeTier = g.eventAttributeCosts(e.Attributes, remainingFreeTier)
|
||||
gas += attrCost
|
||||
}
|
||||
return gas
|
||||
}
|
||||
|
||||
func (g WasmGasRegister) eventAttributeCosts(attrs []wasmvmtypes.EventAttribute, freeTier uint64) (sdk.Gas, uint64) {
|
||||
if len(attrs) == 0 {
|
||||
return 0, freeTier
|
||||
}
|
||||
var storedBytes uint64
|
||||
for _, l := range attrs {
|
||||
storedBytes += uint64(len(l.Key)) + uint64(len(l.Value))
|
||||
}
|
||||
storedBytes, freeTier = calcWithFreeTier(storedBytes, freeTier)
|
||||
// total Length * costs + attribute count * costs
|
||||
r := sdk.NewIntFromUint64(g.c.EventAttributeDataCost).Mul(sdk.NewIntFromUint64(storedBytes)).
|
||||
Add(sdk.NewIntFromUint64(g.c.EventPerAttributeCost).Mul(sdk.NewIntFromUint64(uint64(len(attrs)))))
|
||||
if !r.IsUint64() {
|
||||
panic(sdk.ErrorOutOfGas{Descriptor: "overflow"})
|
||||
}
|
||||
return r.Uint64(), freeTier
|
||||
}
|
||||
|
||||
// apply free tier
|
||||
func calcWithFreeTier(storedBytes uint64, freeTier uint64) (uint64, uint64) {
|
||||
if storedBytes <= freeTier {
|
||||
return 0, freeTier - storedBytes
|
||||
}
|
||||
storedBytes -= freeTier
|
||||
return storedBytes, 0
|
||||
}
|
||||
|
||||
// ToWasmVMGas convert to wasmVM contract runtime gas unit
|
||||
func (g WasmGasRegister) ToWasmVMGas(source storetypes.Gas) uint64 {
|
||||
x := source * g.c.GasMultiplier
|
||||
if x < source {
|
||||
panic(sdk.ErrorOutOfGas{Descriptor: "overflow"})
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// FromWasmVMGas converts to SDK gas unit
|
||||
func (g WasmGasRegister) FromWasmVMGas(source uint64) sdk.Gas {
|
||||
return source / g.c.GasMultiplier
|
||||
}
|
472
x/wasm/keeper/gas_register_test.go
Normal file
@ -0,0 +1,472 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCompileCosts(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
srcLen int
|
||||
srcConfig WasmGasRegisterConfig
|
||||
exp sdk.Gas
|
||||
expPanic bool
|
||||
}{
|
||||
"one byte": {
|
||||
srcLen: 1,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(3), // DefaultCompileCost
|
||||
},
|
||||
"zero byte": {
|
||||
srcLen: 0,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(0),
|
||||
},
|
||||
"negative len": {
|
||||
srcLen: -1,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
expPanic: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if spec.expPanic {
|
||||
assert.Panics(t, func() {
|
||||
NewWasmGasRegister(spec.srcConfig).CompileCosts(spec.srcLen)
|
||||
})
|
||||
return
|
||||
}
|
||||
gotGas := NewWasmGasRegister(spec.srcConfig).CompileCosts(spec.srcLen)
|
||||
assert.Equal(t, spec.exp, gotGas)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewContractInstanceCosts(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
srcLen int
|
||||
srcConfig WasmGasRegisterConfig
|
||||
pinned bool
|
||||
exp sdk.Gas
|
||||
expPanic bool
|
||||
}{
|
||||
"small msg - pinned": {
|
||||
srcLen: 1,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
pinned: true,
|
||||
exp: DefaultContractMessageDataCost,
|
||||
},
|
||||
"big msg - pinned": {
|
||||
srcLen: math.MaxUint32,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
pinned: true,
|
||||
exp: DefaultContractMessageDataCost * sdk.Gas(math.MaxUint32),
|
||||
},
|
||||
"empty msg - pinned": {
|
||||
srcLen: 0,
|
||||
pinned: true,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(0),
|
||||
},
|
||||
"small msg - unpinned": {
|
||||
srcLen: 1,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: DefaultContractMessageDataCost + DefaultInstanceCost,
|
||||
},
|
||||
"big msg - unpinned": {
|
||||
srcLen: math.MaxUint32,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(DefaultContractMessageDataCost*math.MaxUint32 + DefaultInstanceCost),
|
||||
},
|
||||
"empty msg - unpinned": {
|
||||
srcLen: 0,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(DefaultInstanceCost),
|
||||
},
|
||||
|
||||
"negative len": {
|
||||
srcLen: -1,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
expPanic: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if spec.expPanic {
|
||||
assert.Panics(t, func() {
|
||||
NewWasmGasRegister(spec.srcConfig).NewContractInstanceCosts(spec.pinned, spec.srcLen)
|
||||
})
|
||||
return
|
||||
}
|
||||
gotGas := NewWasmGasRegister(spec.srcConfig).NewContractInstanceCosts(spec.pinned, spec.srcLen)
|
||||
assert.Equal(t, spec.exp, gotGas)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContractInstanceCosts(t *testing.T) {
|
||||
// same as TestNewContractInstanceCosts currently
|
||||
specs := map[string]struct {
|
||||
srcLen int
|
||||
srcConfig WasmGasRegisterConfig
|
||||
pinned bool
|
||||
exp sdk.Gas
|
||||
expPanic bool
|
||||
}{
|
||||
"small msg - pinned": {
|
||||
srcLen: 1,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
pinned: true,
|
||||
exp: DefaultContractMessageDataCost,
|
||||
},
|
||||
"big msg - pinned": {
|
||||
srcLen: math.MaxUint32,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
pinned: true,
|
||||
exp: sdk.Gas(DefaultContractMessageDataCost * math.MaxUint32),
|
||||
},
|
||||
"empty msg - pinned": {
|
||||
srcLen: 0,
|
||||
pinned: true,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(0),
|
||||
},
|
||||
"small msg - unpinned": {
|
||||
srcLen: 1,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: DefaultContractMessageDataCost + DefaultInstanceCost,
|
||||
},
|
||||
"big msg - unpinned": {
|
||||
srcLen: math.MaxUint32,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(DefaultContractMessageDataCost*math.MaxUint32 + DefaultInstanceCost),
|
||||
},
|
||||
"empty msg - unpinned": {
|
||||
srcLen: 0,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(DefaultInstanceCost),
|
||||
},
|
||||
|
||||
"negative len": {
|
||||
srcLen: -1,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
expPanic: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if spec.expPanic {
|
||||
assert.Panics(t, func() {
|
||||
NewWasmGasRegister(spec.srcConfig).InstantiateContractCosts(spec.pinned, spec.srcLen)
|
||||
})
|
||||
return
|
||||
}
|
||||
gotGas := NewWasmGasRegister(spec.srcConfig).InstantiateContractCosts(spec.pinned, spec.srcLen)
|
||||
assert.Equal(t, spec.exp, gotGas)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplyCost(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
src wasmvmtypes.Reply
|
||||
srcConfig WasmGasRegisterConfig
|
||||
pinned bool
|
||||
exp sdk.Gas
|
||||
expPanic bool
|
||||
}{
|
||||
"subcall response with events and data - pinned": {
|
||||
src: wasmvmtypes.Reply{
|
||||
Result: wasmvmtypes.SubMsgResult{
|
||||
Ok: &wasmvmtypes.SubMsgResponse{
|
||||
Events: []wasmvmtypes.Event{
|
||||
{Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myData"}}},
|
||||
},
|
||||
Data: []byte{0x1},
|
||||
},
|
||||
},
|
||||
},
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
pinned: true,
|
||||
exp: sdk.Gas(3*DefaultEventAttributeDataCost + DefaultPerAttributeCost + DefaultContractMessageDataCost), // 3 == len("foo")
|
||||
},
|
||||
"subcall response with events - pinned": {
|
||||
src: wasmvmtypes.Reply{
|
||||
Result: wasmvmtypes.SubMsgResult{
|
||||
Ok: &wasmvmtypes.SubMsgResponse{
|
||||
Events: []wasmvmtypes.Event{
|
||||
{Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myData"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
pinned: true,
|
||||
exp: sdk.Gas(3*DefaultEventAttributeDataCost + DefaultPerAttributeCost), // 3 == len("foo")
|
||||
},
|
||||
"subcall response with events exceeds free tier- pinned": {
|
||||
src: wasmvmtypes.Reply{
|
||||
Result: wasmvmtypes.SubMsgResult{
|
||||
Ok: &wasmvmtypes.SubMsgResponse{
|
||||
Events: []wasmvmtypes.Event{
|
||||
{Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: strings.Repeat("x", DefaultEventAttributeDataFreeTier), Value: "myData"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
pinned: true,
|
||||
exp: sdk.Gas((3+6)*DefaultEventAttributeDataCost + DefaultPerAttributeCost), // 3 == len("foo"), 6 == len("myData")
|
||||
},
|
||||
"subcall response error - pinned": {
|
||||
src: wasmvmtypes.Reply{
|
||||
Result: wasmvmtypes.SubMsgResult{
|
||||
Err: "foo",
|
||||
},
|
||||
},
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
pinned: true,
|
||||
exp: 3 * DefaultContractMessageDataCost,
|
||||
},
|
||||
"subcall response with events and data - unpinned": {
|
||||
src: wasmvmtypes.Reply{
|
||||
Result: wasmvmtypes.SubMsgResult{
|
||||
Ok: &wasmvmtypes.SubMsgResponse{
|
||||
Events: []wasmvmtypes.Event{
|
||||
{Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myData"}}},
|
||||
},
|
||||
Data: []byte{0x1},
|
||||
},
|
||||
},
|
||||
},
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(DefaultInstanceCost + 3*DefaultEventAttributeDataCost + DefaultPerAttributeCost + DefaultContractMessageDataCost),
|
||||
},
|
||||
"subcall response with events - unpinned": {
|
||||
src: wasmvmtypes.Reply{
|
||||
Result: wasmvmtypes.SubMsgResult{
|
||||
Ok: &wasmvmtypes.SubMsgResponse{
|
||||
Events: []wasmvmtypes.Event{
|
||||
{Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myData"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(DefaultInstanceCost + 3*DefaultEventAttributeDataCost + DefaultPerAttributeCost),
|
||||
},
|
||||
"subcall response with events exceeds free tier- unpinned": {
|
||||
src: wasmvmtypes.Reply{
|
||||
Result: wasmvmtypes.SubMsgResult{
|
||||
Ok: &wasmvmtypes.SubMsgResponse{
|
||||
Events: []wasmvmtypes.Event{
|
||||
{Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: strings.Repeat("x", DefaultEventAttributeDataFreeTier), Value: "myData"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(DefaultInstanceCost + (3+6)*DefaultEventAttributeDataCost + DefaultPerAttributeCost), // 3 == len("foo"), 6 == len("myData")
|
||||
},
|
||||
"subcall response error - unpinned": {
|
||||
src: wasmvmtypes.Reply{
|
||||
Result: wasmvmtypes.SubMsgResult{
|
||||
Err: "foo",
|
||||
},
|
||||
},
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: sdk.Gas(DefaultInstanceCost + 3*DefaultContractMessageDataCost),
|
||||
},
|
||||
"subcall response with empty events": {
|
||||
src: wasmvmtypes.Reply{
|
||||
Result: wasmvmtypes.SubMsgResult{
|
||||
Ok: &wasmvmtypes.SubMsgResponse{
|
||||
Events: make([]wasmvmtypes.Event, 10),
|
||||
},
|
||||
},
|
||||
},
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: DefaultInstanceCost,
|
||||
},
|
||||
"subcall response with events unset": {
|
||||
src: wasmvmtypes.Reply{
|
||||
Result: wasmvmtypes.SubMsgResult{
|
||||
Ok: &wasmvmtypes.SubMsgResponse{},
|
||||
},
|
||||
},
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
exp: DefaultInstanceCost,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if spec.expPanic {
|
||||
assert.Panics(t, func() {
|
||||
NewWasmGasRegister(spec.srcConfig).ReplyCosts(spec.pinned, spec.src)
|
||||
})
|
||||
return
|
||||
}
|
||||
gotGas := NewWasmGasRegister(spec.srcConfig).ReplyCosts(spec.pinned, spec.src)
|
||||
assert.Equal(t, spec.exp, gotGas)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventCosts(t *testing.T) {
|
||||
// most cases are covered in TestReplyCost already. This ensures some edge cases
|
||||
specs := map[string]struct {
|
||||
srcAttrs []wasmvmtypes.EventAttribute
|
||||
srcEvents wasmvmtypes.Events
|
||||
expGas sdk.Gas
|
||||
}{
|
||||
"empty events": {
|
||||
srcEvents: make([]wasmvmtypes.Event, 1),
|
||||
expGas: DefaultPerCustomEventCost,
|
||||
},
|
||||
"empty attributes": {
|
||||
srcAttrs: make([]wasmvmtypes.EventAttribute, 1),
|
||||
expGas: DefaultPerAttributeCost,
|
||||
},
|
||||
"both nil": {
|
||||
expGas: 0,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gotGas := NewDefaultWasmGasRegister().EventCosts(spec.srcAttrs, spec.srcEvents)
|
||||
assert.Equal(t, spec.expGas, gotGas)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToWasmVMGasConversion(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
src storetypes.Gas
|
||||
srcConfig WasmGasRegisterConfig
|
||||
exp uint64
|
||||
expPanic bool
|
||||
}{
|
||||
"0": {
|
||||
src: 0,
|
||||
exp: 0,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
},
|
||||
"max": {
|
||||
srcConfig: WasmGasRegisterConfig{
|
||||
GasMultiplier: 1,
|
||||
},
|
||||
src: math.MaxUint64,
|
||||
exp: math.MaxUint64,
|
||||
},
|
||||
"overflow": {
|
||||
srcConfig: WasmGasRegisterConfig{
|
||||
GasMultiplier: 2,
|
||||
},
|
||||
src: math.MaxUint64,
|
||||
expPanic: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if spec.expPanic {
|
||||
assert.Panics(t, func() {
|
||||
r := NewWasmGasRegister(spec.srcConfig)
|
||||
_ = r.ToWasmVMGas(spec.src)
|
||||
})
|
||||
return
|
||||
}
|
||||
r := NewWasmGasRegister(spec.srcConfig)
|
||||
got := r.ToWasmVMGas(spec.src)
|
||||
assert.Equal(t, spec.exp, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromWasmVMGasConversion(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
src uint64
|
||||
exp storetypes.Gas
|
||||
srcConfig WasmGasRegisterConfig
|
||||
expPanic bool
|
||||
}{
|
||||
"0": {
|
||||
src: 0,
|
||||
exp: 0,
|
||||
srcConfig: DefaultGasRegisterConfig(),
|
||||
},
|
||||
"max": {
|
||||
srcConfig: WasmGasRegisterConfig{
|
||||
GasMultiplier: 1,
|
||||
},
|
||||
src: math.MaxUint64,
|
||||
exp: math.MaxUint64,
|
||||
},
|
||||
"missconfigured": {
|
||||
srcConfig: WasmGasRegisterConfig{
|
||||
GasMultiplier: 0,
|
||||
},
|
||||
src: 1,
|
||||
expPanic: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if spec.expPanic {
|
||||
assert.Panics(t, func() {
|
||||
r := NewWasmGasRegister(spec.srcConfig)
|
||||
_ = r.FromWasmVMGas(spec.src)
|
||||
})
|
||||
return
|
||||
}
|
||||
r := NewWasmGasRegister(spec.srcConfig)
|
||||
got := r.FromWasmVMGas(spec.src)
|
||||
assert.Equal(t, spec.exp, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUncompressCosts(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
lenIn int
|
||||
exp sdk.Gas
|
||||
expPanic bool
|
||||
}{
|
||||
"0": {
|
||||
exp: 0,
|
||||
},
|
||||
"even": {
|
||||
lenIn: 100,
|
||||
exp: 15,
|
||||
},
|
||||
"round down when uneven": {
|
||||
lenIn: 19,
|
||||
exp: 2,
|
||||
},
|
||||
"max len": {
|
||||
lenIn: types.MaxWasmSize,
|
||||
exp: 122880,
|
||||
},
|
||||
"invalid len": {
|
||||
lenIn: -1,
|
||||
expPanic: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if spec.expPanic {
|
||||
assert.Panics(t, func() { NewDefaultWasmGasRegister().UncompressCosts(spec.lenIn) })
|
||||
return
|
||||
}
|
||||
got := NewDefaultWasmGasRegister().UncompressCosts(spec.lenIn)
|
||||
assert.Equal(t, spec.exp, got)
|
||||
})
|
||||
}
|
||||
}
|
116
x/wasm/keeper/genesis.go
Normal file
@ -0,0 +1,116 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// ValidatorSetSource is a subset of the staking keeper
|
||||
type ValidatorSetSource interface {
|
||||
ApplyAndReturnValidatorSetUpdates(sdk.Context) (updates []abci.ValidatorUpdate, err error)
|
||||
}
|
||||
|
||||
// InitGenesis sets supply information for genesis.
|
||||
//
|
||||
// CONTRACT: all types of accounts must have been already initialized/created
|
||||
func InitGenesis(ctx sdk.Context, keeper *Keeper, data types.GenesisState) ([]abci.ValidatorUpdate, error) {
|
||||
contractKeeper := NewGovPermissionKeeper(keeper)
|
||||
keeper.SetParams(ctx, data.Params)
|
||||
var maxCodeID uint64
|
||||
for i, code := range data.Codes {
|
||||
err := keeper.importCode(ctx, code.CodeID, code.CodeInfo, code.CodeBytes)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrapf(err, "code %d with id: %d", i, code.CodeID)
|
||||
}
|
||||
if code.CodeID > maxCodeID {
|
||||
maxCodeID = code.CodeID
|
||||
}
|
||||
if code.Pinned {
|
||||
if err := contractKeeper.PinCode(ctx, code.CodeID); err != nil {
|
||||
return nil, sdkerrors.Wrapf(err, "contract number %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var maxContractID int
|
||||
for i, contract := range data.Contracts {
|
||||
contractAddr, err := sdk.AccAddressFromBech32(contract.ContractAddress)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrapf(err, "address in contract number %d", i)
|
||||
}
|
||||
err = keeper.importContract(ctx, contractAddr, &contract.ContractInfo, contract.ContractState, contract.ContractCodeHistory)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrapf(err, "contract number %d", i)
|
||||
}
|
||||
maxContractID = i + 1 // not ideal but max(contractID) is not persisted otherwise
|
||||
}
|
||||
|
||||
for i, seq := range data.Sequences {
|
||||
err := keeper.importAutoIncrementID(ctx, seq.IDKey, seq.Value)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrapf(err, "sequence number %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check seq values
|
||||
seqVal := keeper.PeekAutoIncrementID(ctx, types.KeyLastCodeID)
|
||||
if seqVal <= maxCodeID {
|
||||
return nil, sdkerrors.Wrapf(types.ErrInvalid, "seq %s with value: %d must be greater than: %d ", string(types.KeyLastCodeID), seqVal, maxCodeID)
|
||||
}
|
||||
seqVal = keeper.PeekAutoIncrementID(ctx, types.KeyLastInstanceID)
|
||||
if seqVal <= uint64(maxContractID) {
|
||||
return nil, sdkerrors.Wrapf(types.ErrInvalid, "seq %s with value: %d must be greater than: %d ", string(types.KeyLastInstanceID), seqVal, maxContractID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ExportGenesis returns a GenesisState for a given context and keeper.
|
||||
func ExportGenesis(ctx sdk.Context, keeper *Keeper) *types.GenesisState {
|
||||
var genState types.GenesisState
|
||||
|
||||
genState.Params = keeper.GetParams(ctx)
|
||||
|
||||
keeper.IterateCodeInfos(ctx, func(codeID uint64, info types.CodeInfo) bool {
|
||||
bytecode, err := keeper.GetByteCode(ctx, codeID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
genState.Codes = append(genState.Codes, types.Code{
|
||||
CodeID: codeID,
|
||||
CodeInfo: info,
|
||||
CodeBytes: bytecode,
|
||||
Pinned: keeper.IsPinnedCode(ctx, codeID),
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
keeper.IterateContractInfo(ctx, func(addr sdk.AccAddress, contract types.ContractInfo) bool {
|
||||
var state []types.Model
|
||||
keeper.IterateContractState(ctx, addr, func(key, value []byte) bool {
|
||||
state = append(state, types.Model{Key: key, Value: value})
|
||||
return false
|
||||
})
|
||||
|
||||
contractCodeHistory := keeper.GetContractHistory(ctx, addr)
|
||||
|
||||
genState.Contracts = append(genState.Contracts, types.Contract{
|
||||
ContractAddress: addr.String(),
|
||||
ContractInfo: contract,
|
||||
ContractState: state,
|
||||
ContractCodeHistory: contractCodeHistory,
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
for _, k := range [][]byte{types.KeyLastCodeID, types.KeyLastInstanceID} {
|
||||
genState.Sequences = append(genState.Sequences, types.Sequence{
|
||||
IDKey: k,
|
||||
Value: keeper.PeekAutoIncrementID(ctx, k),
|
||||
})
|
||||
}
|
||||
|
||||
return &genState
|
||||
}
|
671
x/wasm/keeper/genesis_test.go
Normal file
@ -0,0 +1,671 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/store"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
|
||||
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
||||
distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper"
|
||||
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||
paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper"
|
||||
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
||||
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
wasmTypes "github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
const firstCodeID = 1
|
||||
|
||||
func TestGenesisExportImport(t *testing.T) {
|
||||
wasmKeeper, srcCtx, srcStoreKeys := setupKeeper(t)
|
||||
contractKeeper := NewGovPermissionKeeper(wasmKeeper)
|
||||
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
// store some test data
|
||||
f := fuzz.New().Funcs(ModelFuzzers...)
|
||||
|
||||
wasmKeeper.SetParams(srcCtx, types.DefaultParams())
|
||||
|
||||
for i := 0; i < 25; i++ {
|
||||
var (
|
||||
codeInfo types.CodeInfo
|
||||
contract types.ContractInfo
|
||||
stateModels []types.Model
|
||||
history []types.ContractCodeHistoryEntry
|
||||
pinned bool
|
||||
contractExtension bool
|
||||
)
|
||||
f.Fuzz(&codeInfo)
|
||||
f.Fuzz(&contract)
|
||||
f.Fuzz(&stateModels)
|
||||
f.NilChance(0).Fuzz(&history)
|
||||
f.Fuzz(&pinned)
|
||||
f.Fuzz(&contractExtension)
|
||||
|
||||
creatorAddr, err := sdk.AccAddressFromBech32(codeInfo.Creator)
|
||||
require.NoError(t, err)
|
||||
codeID, _, err := contractKeeper.Create(srcCtx, creatorAddr, wasmCode, &codeInfo.InstantiateConfig)
|
||||
require.NoError(t, err)
|
||||
if pinned {
|
||||
contractKeeper.PinCode(srcCtx, codeID)
|
||||
}
|
||||
if contractExtension {
|
||||
anyTime := time.Now().UTC()
|
||||
var nestedType govtypes.TextProposal
|
||||
f.NilChance(0).Fuzz(&nestedType)
|
||||
myExtension, err := govtypes.NewProposal(&nestedType, 1, anyTime, anyTime)
|
||||
require.NoError(t, err)
|
||||
contract.SetExtension(&myExtension)
|
||||
}
|
||||
|
||||
contract.CodeID = codeID
|
||||
contractAddr := wasmKeeper.ClassicAddressGenerator()(srcCtx, codeID, nil)
|
||||
wasmKeeper.storeContractInfo(srcCtx, contractAddr, &contract)
|
||||
wasmKeeper.appendToContractHistory(srcCtx, contractAddr, history...)
|
||||
wasmKeeper.importContractState(srcCtx, contractAddr, stateModels)
|
||||
}
|
||||
var wasmParams types.Params
|
||||
f.NilChance(0).Fuzz(&wasmParams)
|
||||
wasmKeeper.SetParams(srcCtx, wasmParams)
|
||||
|
||||
// export
|
||||
exportedState := ExportGenesis(srcCtx, wasmKeeper)
|
||||
// order should not matter
|
||||
rand.Shuffle(len(exportedState.Codes), func(i, j int) {
|
||||
exportedState.Codes[i], exportedState.Codes[j] = exportedState.Codes[j], exportedState.Codes[i]
|
||||
})
|
||||
rand.Shuffle(len(exportedState.Contracts), func(i, j int) {
|
||||
exportedState.Contracts[i], exportedState.Contracts[j] = exportedState.Contracts[j], exportedState.Contracts[i]
|
||||
})
|
||||
rand.Shuffle(len(exportedState.Sequences), func(i, j int) {
|
||||
exportedState.Sequences[i], exportedState.Sequences[j] = exportedState.Sequences[j], exportedState.Sequences[i]
|
||||
})
|
||||
exportedGenesis, err := wasmKeeper.cdc.MarshalJSON(exportedState)
|
||||
require.NoError(t, err)
|
||||
|
||||
// setup new instances
|
||||
dstKeeper, dstCtx, dstStoreKeys := setupKeeper(t)
|
||||
|
||||
// reset contract code index in source DB for comparison with dest DB
|
||||
wasmKeeper.IterateContractInfo(srcCtx, func(address sdk.AccAddress, info wasmTypes.ContractInfo) bool {
|
||||
creatorAddress := sdk.MustAccAddressFromBech32(info.Creator)
|
||||
history := wasmKeeper.GetContractHistory(srcCtx, address)
|
||||
|
||||
wasmKeeper.addToContractCodeSecondaryIndex(srcCtx, address, history[len(history)-1])
|
||||
wasmKeeper.addToContractCreatorSecondaryIndex(srcCtx, creatorAddress, history[0].Updated, address)
|
||||
return false
|
||||
})
|
||||
|
||||
// re-import
|
||||
var importState wasmTypes.GenesisState
|
||||
err = dstKeeper.cdc.UnmarshalJSON(exportedGenesis, &importState)
|
||||
require.NoError(t, err)
|
||||
InitGenesis(dstCtx, dstKeeper, importState)
|
||||
|
||||
// compare whole DB
|
||||
for j := range srcStoreKeys {
|
||||
srcIT := srcCtx.KVStore(srcStoreKeys[j]).Iterator(nil, nil)
|
||||
dstIT := dstCtx.KVStore(dstStoreKeys[j]).Iterator(nil, nil)
|
||||
|
||||
for i := 0; srcIT.Valid(); i++ {
|
||||
require.True(t, dstIT.Valid(), "[%s] destination DB has less elements than source. Missing: %x", srcStoreKeys[j].Name(), srcIT.Key())
|
||||
require.Equal(t, srcIT.Key(), dstIT.Key(), i)
|
||||
require.Equal(t, srcIT.Value(), dstIT.Value(), "[%s] element (%d): %X", srcStoreKeys[j].Name(), i, srcIT.Key())
|
||||
dstIT.Next()
|
||||
srcIT.Next()
|
||||
}
|
||||
if !assert.False(t, dstIT.Valid()) {
|
||||
t.Fatalf("dest Iterator still has key :%X", dstIT.Key())
|
||||
}
|
||||
srcIT.Close()
|
||||
dstIT.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenesisInit(t *testing.T) {
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
myCodeInfo := wasmTypes.CodeInfoFixture(wasmTypes.WithSHA256CodeHash(wasmCode))
|
||||
specs := map[string]struct {
|
||||
src types.GenesisState
|
||||
expSuccess bool
|
||||
}{
|
||||
"happy path: code info correct": {
|
||||
src: types.GenesisState{
|
||||
Codes: []types.Code{{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}},
|
||||
Sequences: []types.Sequence{
|
||||
{IDKey: types.KeyLastCodeID, Value: 2},
|
||||
{IDKey: types.KeyLastInstanceID, Value: 1},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
expSuccess: true,
|
||||
},
|
||||
"happy path: code ids can contain gaps": {
|
||||
src: types.GenesisState{
|
||||
Codes: []types.Code{{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}, {
|
||||
CodeID: 3,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}},
|
||||
Sequences: []types.Sequence{
|
||||
{IDKey: types.KeyLastCodeID, Value: 10},
|
||||
{IDKey: types.KeyLastInstanceID, Value: 1},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
expSuccess: true,
|
||||
},
|
||||
"happy path: code order does not matter": {
|
||||
src: types.GenesisState{
|
||||
Codes: []types.Code{{
|
||||
CodeID: 2,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}, {
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}},
|
||||
Contracts: nil,
|
||||
Sequences: []types.Sequence{
|
||||
{IDKey: types.KeyLastCodeID, Value: 3},
|
||||
{IDKey: types.KeyLastInstanceID, Value: 1},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
expSuccess: true,
|
||||
},
|
||||
"prevent code hash mismatch": {src: types.GenesisState{
|
||||
Codes: []types.Code{{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: wasmTypes.CodeInfoFixture(func(i *wasmTypes.CodeInfo) { i.CodeHash = make([]byte, sha256.Size) }),
|
||||
CodeBytes: wasmCode,
|
||||
}},
|
||||
Params: types.DefaultParams(),
|
||||
}},
|
||||
"prevent duplicate codeIDs": {src: types.GenesisState{
|
||||
Codes: []types.Code{
|
||||
{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
},
|
||||
{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
}},
|
||||
"codes with same checksum can be pinned": {
|
||||
src: types.GenesisState{
|
||||
Codes: []types.Code{
|
||||
{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
Pinned: true,
|
||||
},
|
||||
{
|
||||
CodeID: 2,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
Pinned: true,
|
||||
},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
},
|
||||
"happy path: code id in info and contract do match": {
|
||||
src: types.GenesisState{
|
||||
Codes: []types.Code{{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}},
|
||||
Contracts: []types.Contract{
|
||||
{
|
||||
ContractAddress: BuildContractAddressClassic(1, 1).String(),
|
||||
ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields),
|
||||
ContractCodeHistory: []types.ContractCodeHistoryEntry{
|
||||
{
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 1,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()},
|
||||
Msg: []byte(`{}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sequences: []types.Sequence{
|
||||
{IDKey: types.KeyLastCodeID, Value: 2},
|
||||
{IDKey: types.KeyLastInstanceID, Value: 2},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
expSuccess: true,
|
||||
},
|
||||
"happy path: code info with two contracts": {
|
||||
src: types.GenesisState{
|
||||
Codes: []types.Code{{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}},
|
||||
Contracts: []types.Contract{
|
||||
{
|
||||
ContractAddress: BuildContractAddressClassic(1, 1).String(),
|
||||
ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields),
|
||||
ContractCodeHistory: []types.ContractCodeHistoryEntry{
|
||||
{
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 1,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()},
|
||||
Msg: []byte(`{}`),
|
||||
},
|
||||
},
|
||||
}, {
|
||||
ContractAddress: BuildContractAddressClassic(1, 2).String(),
|
||||
ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields),
|
||||
ContractCodeHistory: []types.ContractCodeHistoryEntry{
|
||||
{
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 1,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()},
|
||||
Msg: []byte(`{"foo":"bar"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sequences: []types.Sequence{
|
||||
{IDKey: types.KeyLastCodeID, Value: 2},
|
||||
{IDKey: types.KeyLastInstanceID, Value: 3},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
expSuccess: true,
|
||||
},
|
||||
"prevent contracts that points to non existing codeID": {
|
||||
src: types.GenesisState{
|
||||
Contracts: []types.Contract{
|
||||
{
|
||||
ContractAddress: BuildContractAddressClassic(1, 1).String(),
|
||||
ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields),
|
||||
ContractCodeHistory: []types.ContractCodeHistoryEntry{
|
||||
{
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 1,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()},
|
||||
Msg: []byte(`{"foo":"bar"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
},
|
||||
"prevent duplicate contract address": {
|
||||
src: types.GenesisState{
|
||||
Codes: []types.Code{{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}},
|
||||
Contracts: []types.Contract{
|
||||
{
|
||||
ContractAddress: BuildContractAddressClassic(1, 1).String(),
|
||||
ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields),
|
||||
ContractCodeHistory: []types.ContractCodeHistoryEntry{
|
||||
{
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 1,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()},
|
||||
Msg: []byte(`{"foo":"bar"}`),
|
||||
},
|
||||
},
|
||||
}, {
|
||||
ContractAddress: BuildContractAddressClassic(1, 1).String(),
|
||||
ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields),
|
||||
ContractCodeHistory: []types.ContractCodeHistoryEntry{
|
||||
{
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 1,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()},
|
||||
Msg: []byte(`{"other":"value"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
},
|
||||
"prevent duplicate contract model keys": {
|
||||
src: types.GenesisState{
|
||||
Codes: []types.Code{{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}},
|
||||
Contracts: []types.Contract{
|
||||
{
|
||||
ContractAddress: BuildContractAddressClassic(1, 1).String(),
|
||||
ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields),
|
||||
ContractState: []types.Model{
|
||||
{
|
||||
Key: []byte{0x1},
|
||||
Value: []byte("foo"),
|
||||
},
|
||||
{
|
||||
Key: []byte{0x1},
|
||||
Value: []byte("bar"),
|
||||
},
|
||||
},
|
||||
ContractCodeHistory: []types.ContractCodeHistoryEntry{
|
||||
{
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 1,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()},
|
||||
Msg: []byte(`{"foo":"bar"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
},
|
||||
"prevent duplicate sequences": {
|
||||
src: types.GenesisState{
|
||||
Sequences: []types.Sequence{
|
||||
{IDKey: []byte("foo"), Value: 1},
|
||||
{IDKey: []byte("foo"), Value: 9999},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
},
|
||||
"prevent code id seq init value == max codeID used": {
|
||||
src: types.GenesisState{
|
||||
Codes: []types.Code{{
|
||||
CodeID: 2,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}},
|
||||
Sequences: []types.Sequence{
|
||||
{IDKey: types.KeyLastCodeID, Value: 1},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
},
|
||||
"prevent contract id seq init value == count contracts": {
|
||||
src: types.GenesisState{
|
||||
Codes: []types.Code{{
|
||||
CodeID: firstCodeID,
|
||||
CodeInfo: myCodeInfo,
|
||||
CodeBytes: wasmCode,
|
||||
}},
|
||||
Contracts: []types.Contract{
|
||||
{
|
||||
ContractAddress: BuildContractAddressClassic(1, 1).String(),
|
||||
ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields),
|
||||
ContractCodeHistory: []types.ContractCodeHistoryEntry{
|
||||
{
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 1,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()},
|
||||
Msg: []byte(`{}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sequences: []types.Sequence{
|
||||
{IDKey: types.KeyLastCodeID, Value: 2},
|
||||
{IDKey: types.KeyLastInstanceID, Value: 1},
|
||||
},
|
||||
Params: types.DefaultParams(),
|
||||
},
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
keeper, ctx, _ := setupKeeper(t)
|
||||
|
||||
require.NoError(t, types.ValidateGenesis(spec.src))
|
||||
_, gotErr := InitGenesis(ctx, keeper, spec.src)
|
||||
if !spec.expSuccess {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
|
||||
for _, c := range spec.src.Codes {
|
||||
assert.Equal(t, c.Pinned, keeper.IsPinnedCode(ctx, c.CodeID))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportContractWithCodeHistoryPreserved(t *testing.T) {
|
||||
genesisTemplate := `
|
||||
{
|
||||
"params":{
|
||||
"code_upload_access": {
|
||||
"permission": "Everybody"
|
||||
},
|
||||
"instantiate_default_permission": "Everybody"
|
||||
},
|
||||
"codes": [
|
||||
{
|
||||
"code_id": "1",
|
||||
"code_info": {
|
||||
"code_hash": %q,
|
||||
"creator": "cosmos1qtu5n0cnhfkjj6l2rq97hmky9fd89gwca9yarx",
|
||||
"instantiate_config": {
|
||||
"permission": "OnlyAddress",
|
||||
"address": "cosmos1qtu5n0cnhfkjj6l2rq97hmky9fd89gwca9yarx"
|
||||
}
|
||||
},
|
||||
"code_bytes": %q
|
||||
}
|
||||
],
|
||||
"contracts": [
|
||||
{
|
||||
"contract_address": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr",
|
||||
"contract_info": {
|
||||
"code_id": "1",
|
||||
"creator": "cosmos13x849jzd03vne42ynpj25hn8npjecxqrjghd8x",
|
||||
"admin": "cosmos1h5t8zxmjr30e9dqghtlpl40f2zz5cgey6esxtn",
|
||||
"label": "ȀĴnZV芢毤",
|
||||
"created": {
|
||||
"block_height" : "100",
|
||||
"tx_index" : "10"
|
||||
}
|
||||
},
|
||||
"contract_code_history": [
|
||||
{
|
||||
"operation": "CONTRACT_CODE_HISTORY_OPERATION_TYPE_INIT",
|
||||
"code_id": "1",
|
||||
"updated": {
|
||||
"block_height" : "100",
|
||||
"tx_index" : "10"
|
||||
},
|
||||
"msg": {"foo": "bar"}
|
||||
},
|
||||
{
|
||||
"operation": "CONTRACT_CODE_HISTORY_OPERATION_TYPE_MIGRATE",
|
||||
"code_id": "1",
|
||||
"updated": {
|
||||
"block_height" : "200",
|
||||
"tx_index" : "10"
|
||||
},
|
||||
"msg": {"other": "msg"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"sequences": [
|
||||
{"id_key": "BGxhc3RDb2RlSWQ=", "value": "2"},
|
||||
{"id_key": "BGxhc3RDb250cmFjdElk", "value": "3"}
|
||||
]
|
||||
}`
|
||||
keeper, ctx, _ := setupKeeper(t)
|
||||
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
wasmCodeHash := sha256.Sum256(wasmCode)
|
||||
enc64 := base64.StdEncoding.EncodeToString
|
||||
genesisStr := fmt.Sprintf(genesisTemplate, enc64(wasmCodeHash[:]), enc64(wasmCode))
|
||||
|
||||
var importState wasmTypes.GenesisState
|
||||
err = keeper.cdc.UnmarshalJSON([]byte(genesisStr), &importState)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, importState.ValidateBasic(), genesisStr)
|
||||
|
||||
ctx = ctx.WithBlockHeight(0).WithGasMeter(sdk.NewInfiniteGasMeter())
|
||||
|
||||
// when
|
||||
_, err = InitGenesis(ctx, keeper, importState)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify wasm code
|
||||
gotWasmCode, err := keeper.GetByteCode(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, wasmCode, gotWasmCode, "byte code does not match")
|
||||
|
||||
// verify code info
|
||||
gotCodeInfo := keeper.GetCodeInfo(ctx, 1)
|
||||
require.NotNil(t, gotCodeInfo)
|
||||
codeCreatorAddr := "cosmos1qtu5n0cnhfkjj6l2rq97hmky9fd89gwca9yarx"
|
||||
expCodeInfo := types.CodeInfo{
|
||||
CodeHash: wasmCodeHash[:],
|
||||
Creator: codeCreatorAddr,
|
||||
InstantiateConfig: wasmTypes.AccessConfig{
|
||||
Permission: types.AccessTypeOnlyAddress,
|
||||
Address: codeCreatorAddr,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expCodeInfo, *gotCodeInfo)
|
||||
|
||||
// verify contract
|
||||
contractAddr, _ := sdk.AccAddressFromBech32("cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr")
|
||||
gotContractInfo := keeper.GetContractInfo(ctx, contractAddr)
|
||||
require.NotNil(t, gotContractInfo)
|
||||
contractCreatorAddr := "cosmos13x849jzd03vne42ynpj25hn8npjecxqrjghd8x"
|
||||
adminAddr := "cosmos1h5t8zxmjr30e9dqghtlpl40f2zz5cgey6esxtn"
|
||||
|
||||
expContractInfo := types.ContractInfo{
|
||||
CodeID: firstCodeID,
|
||||
Creator: contractCreatorAddr,
|
||||
Admin: adminAddr,
|
||||
Label: "ȀĴnZV芢毤",
|
||||
Created: &types.AbsoluteTxPosition{BlockHeight: 100, TxIndex: 10},
|
||||
}
|
||||
assert.Equal(t, expContractInfo, *gotContractInfo)
|
||||
|
||||
expHistory := []types.ContractCodeHistoryEntry{
|
||||
{
|
||||
Operation: types.ContractCodeHistoryOperationTypeInit,
|
||||
CodeID: firstCodeID,
|
||||
Updated: &types.AbsoluteTxPosition{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 10,
|
||||
},
|
||||
Msg: []byte(`{"foo": "bar"}`),
|
||||
},
|
||||
{
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: firstCodeID,
|
||||
Updated: &types.AbsoluteTxPosition{
|
||||
BlockHeight: 200,
|
||||
TxIndex: 10,
|
||||
},
|
||||
Msg: []byte(`{"other": "msg"}`),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expHistory, keeper.GetContractHistory(ctx, contractAddr))
|
||||
assert.Equal(t, uint64(2), keeper.PeekAutoIncrementID(ctx, types.KeyLastCodeID))
|
||||
assert.Equal(t, uint64(3), keeper.PeekAutoIncrementID(ctx, types.KeyLastInstanceID))
|
||||
}
|
||||
|
||||
func setupKeeper(t *testing.T) (*Keeper, sdk.Context, []sdk.StoreKey) {
|
||||
t.Helper()
|
||||
tempDir, err := os.MkdirTemp("", "wasm")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { os.RemoveAll(tempDir) })
|
||||
var (
|
||||
keyParams = sdk.NewKVStoreKey(paramtypes.StoreKey)
|
||||
tkeyParams = sdk.NewTransientStoreKey(paramtypes.TStoreKey)
|
||||
keyWasm = sdk.NewKVStoreKey(wasmTypes.StoreKey)
|
||||
)
|
||||
|
||||
db := dbm.NewMemDB()
|
||||
ms := store.NewCommitMultiStore(db)
|
||||
ms.MountStoreWithDB(keyWasm, sdk.StoreTypeIAVL, db)
|
||||
ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db)
|
||||
ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db)
|
||||
require.NoError(t, ms.LoadLatestVersion())
|
||||
|
||||
ctx := sdk.NewContext(ms, tmproto.Header{
|
||||
Height: 1234567,
|
||||
Time: time.Date(2020, time.April, 22, 12, 0, 0, 0, time.UTC),
|
||||
}, false, log.NewNopLogger())
|
||||
|
||||
encodingConfig := MakeEncodingConfig(t)
|
||||
// register an example extension. must be protobuf
|
||||
encodingConfig.InterfaceRegistry.RegisterImplementations(
|
||||
(*types.ContractInfoExtension)(nil),
|
||||
&govtypes.Proposal{},
|
||||
)
|
||||
// also registering gov interfaces for nested Any type
|
||||
govtypes.RegisterInterfaces(encodingConfig.InterfaceRegistry)
|
||||
|
||||
wasmConfig := wasmTypes.DefaultWasmConfig()
|
||||
pk := paramskeeper.NewKeeper(encodingConfig.Marshaler, encodingConfig.Amino, keyParams, tkeyParams)
|
||||
|
||||
srcKeeper := NewKeeper(
|
||||
encodingConfig.Marshaler,
|
||||
keyWasm,
|
||||
pk.Subspace(wasmTypes.ModuleName),
|
||||
authkeeper.AccountKeeper{},
|
||||
&bankkeeper.BaseKeeper{},
|
||||
stakingkeeper.Keeper{},
|
||||
distributionkeeper.Keeper{},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
tempDir,
|
||||
wasmConfig,
|
||||
AvailableCapabilities,
|
||||
)
|
||||
return &srcKeeper, ctx, []sdk.StoreKey{keyWasm, keyParams}
|
||||
}
|
226
x/wasm/keeper/handler_plugin.go
Normal file
@ -0,0 +1,226 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
"github.com/cosmos/cosmos-sdk/baseapp"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
host "github.com/cosmos/ibc-go/v4/modules/core/24-host"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// msgEncoder is an extension point to customize encodings
|
||||
type msgEncoder interface {
|
||||
// Encode converts wasmvm message to n cosmos message types
|
||||
Encode(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Msg, error)
|
||||
}
|
||||
|
||||
// MessageRouter ADR 031 request type routing
|
||||
type MessageRouter interface {
|
||||
Handler(msg sdk.Msg) baseapp.MsgServiceHandler
|
||||
}
|
||||
|
||||
// SDKMessageHandler can handles messages that can be encoded into sdk.Message types and routed.
|
||||
type SDKMessageHandler struct {
|
||||
router MessageRouter
|
||||
encoders msgEncoder
|
||||
}
|
||||
|
||||
func NewDefaultMessageHandler(
|
||||
router MessageRouter,
|
||||
channelKeeper types.ChannelKeeper,
|
||||
capabilityKeeper types.CapabilityKeeper,
|
||||
bankKeeper types.Burner,
|
||||
unpacker codectypes.AnyUnpacker,
|
||||
portSource types.ICS20TransferPortSource,
|
||||
customEncoders ...*MessageEncoders,
|
||||
) Messenger {
|
||||
encoders := DefaultEncoders(unpacker, portSource)
|
||||
for _, e := range customEncoders {
|
||||
encoders = encoders.Merge(e)
|
||||
}
|
||||
return NewMessageHandlerChain(
|
||||
NewSDKMessageHandler(router, encoders),
|
||||
NewIBCRawPacketHandler(channelKeeper, capabilityKeeper),
|
||||
NewBurnCoinMessageHandler(bankKeeper),
|
||||
)
|
||||
}
|
||||
|
||||
func NewSDKMessageHandler(router MessageRouter, encoders msgEncoder) SDKMessageHandler {
|
||||
return SDKMessageHandler{
|
||||
router: router,
|
||||
encoders: encoders,
|
||||
}
|
||||
}
|
||||
|
||||
func (h SDKMessageHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
sdkMsgs, err := h.encoders.Encode(ctx, contractAddr, contractIBCPortID, msg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, sdkMsg := range sdkMsgs {
|
||||
res, err := h.handleSdkMessage(ctx, contractAddr, sdkMsg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// append data
|
||||
data = append(data, res.Data)
|
||||
// append events
|
||||
sdkEvents := make([]sdk.Event, len(res.Events))
|
||||
for i := range res.Events {
|
||||
sdkEvents[i] = sdk.Event(res.Events[i])
|
||||
}
|
||||
events = append(events, sdkEvents...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h SDKMessageHandler) handleSdkMessage(ctx sdk.Context, contractAddr sdk.Address, msg sdk.Msg) (*sdk.Result, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// make sure this account can send it
|
||||
for _, acct := range msg.GetSigners() {
|
||||
if !acct.Equals(contractAddr) {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "contract doesn't have permission")
|
||||
}
|
||||
}
|
||||
|
||||
// find the handler and execute it
|
||||
if handler := h.router.Handler(msg); handler != nil {
|
||||
// ADR 031 request type routing
|
||||
msgResult, err := handler(ctx, msg)
|
||||
return msgResult, err
|
||||
}
|
||||
// legacy sdk.Msg routing
|
||||
// Assuming that the app developer has migrated all their Msgs to
|
||||
// proto messages and has registered all `Msg services`, then this
|
||||
// path should never be called, because all those Msgs should be
|
||||
// registered within the `msgServiceRouter` already.
|
||||
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "can't route message %+v", msg)
|
||||
}
|
||||
|
||||
// MessageHandlerChain defines a chain of handlers that are called one by one until it can be handled.
|
||||
type MessageHandlerChain struct {
|
||||
handlers []Messenger
|
||||
}
|
||||
|
||||
func NewMessageHandlerChain(first Messenger, others ...Messenger) *MessageHandlerChain {
|
||||
r := &MessageHandlerChain{handlers: append([]Messenger{first}, others...)}
|
||||
for i := range r.handlers {
|
||||
if r.handlers[i] == nil {
|
||||
panic(fmt.Sprintf("handler must not be nil at position : %d", i))
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// DispatchMsg dispatch message and calls chained handlers one after another in
|
||||
// order to find the right one to process given message. If a handler cannot
|
||||
// process given message (returns ErrUnknownMsg), its result is ignored and the
|
||||
// next handler is executed.
|
||||
func (m MessageHandlerChain) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Event, [][]byte, error) {
|
||||
for _, h := range m.handlers {
|
||||
events, data, err := h.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg)
|
||||
switch {
|
||||
case err == nil:
|
||||
return events, data, nil
|
||||
case errors.Is(err, types.ErrUnknownMsg):
|
||||
continue
|
||||
default:
|
||||
return events, data, err
|
||||
}
|
||||
}
|
||||
return nil, nil, sdkerrors.Wrap(types.ErrUnknownMsg, "no handler found")
|
||||
}
|
||||
|
||||
// IBCRawPacketHandler handels IBC.SendPacket messages which are published to an IBC channel.
|
||||
type IBCRawPacketHandler struct {
|
||||
channelKeeper types.ChannelKeeper
|
||||
capabilityKeeper types.CapabilityKeeper
|
||||
}
|
||||
|
||||
func NewIBCRawPacketHandler(chk types.ChannelKeeper, cak types.CapabilityKeeper) IBCRawPacketHandler {
|
||||
return IBCRawPacketHandler{channelKeeper: chk, capabilityKeeper: cak}
|
||||
}
|
||||
|
||||
// DispatchMsg publishes a raw IBC packet onto the channel.
|
||||
func (h IBCRawPacketHandler) DispatchMsg(ctx sdk.Context, _ sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
if msg.IBC == nil || msg.IBC.SendPacket == nil {
|
||||
return nil, nil, types.ErrUnknownMsg
|
||||
}
|
||||
if contractIBCPortID == "" {
|
||||
return nil, nil, sdkerrors.Wrapf(types.ErrUnsupportedForContract, "ibc not supported")
|
||||
}
|
||||
contractIBCChannelID := msg.IBC.SendPacket.ChannelID
|
||||
if contractIBCChannelID == "" {
|
||||
return nil, nil, sdkerrors.Wrapf(types.ErrEmpty, "ibc channel")
|
||||
}
|
||||
|
||||
sequence, found := h.channelKeeper.GetNextSequenceSend(ctx, contractIBCPortID, contractIBCChannelID)
|
||||
if !found {
|
||||
return nil, nil, sdkerrors.Wrapf(channeltypes.ErrSequenceSendNotFound,
|
||||
"source port: %s, source channel: %s", contractIBCPortID, contractIBCChannelID,
|
||||
)
|
||||
}
|
||||
|
||||
channelInfo, ok := h.channelKeeper.GetChannel(ctx, contractIBCPortID, contractIBCChannelID)
|
||||
if !ok {
|
||||
return nil, nil, sdkerrors.Wrap(channeltypes.ErrInvalidChannel, "not found")
|
||||
}
|
||||
channelCap, ok := h.capabilityKeeper.GetCapability(ctx, host.ChannelCapabilityPath(contractIBCPortID, contractIBCChannelID))
|
||||
if !ok {
|
||||
return nil, nil, sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
|
||||
}
|
||||
packet := channeltypes.NewPacket(
|
||||
msg.IBC.SendPacket.Data,
|
||||
sequence,
|
||||
contractIBCPortID,
|
||||
contractIBCChannelID,
|
||||
channelInfo.Counterparty.PortId,
|
||||
channelInfo.Counterparty.ChannelId,
|
||||
ConvertWasmIBCTimeoutHeightToCosmosHeight(msg.IBC.SendPacket.Timeout.Block),
|
||||
msg.IBC.SendPacket.Timeout.Timestamp,
|
||||
)
|
||||
return nil, nil, h.channelKeeper.SendPacket(ctx, channelCap, packet)
|
||||
}
|
||||
|
||||
var _ Messenger = MessageHandlerFunc(nil)
|
||||
|
||||
// MessageHandlerFunc is a helper to construct a function based message handler.
|
||||
type MessageHandlerFunc func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error)
|
||||
|
||||
// DispatchMsg delegates dispatching of provided message into the MessageHandlerFunc.
|
||||
func (m MessageHandlerFunc) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return m(ctx, contractAddr, contractIBCPortID, msg)
|
||||
}
|
||||
|
||||
// NewBurnCoinMessageHandler handles wasmvm.BurnMsg messages
|
||||
func NewBurnCoinMessageHandler(burner types.Burner) MessageHandlerFunc {
|
||||
return func(ctx sdk.Context, contractAddr sdk.AccAddress, _ string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
if msg.Bank != nil && msg.Bank.Burn != nil {
|
||||
coins, err := ConvertWasmCoinsToSdkCoins(msg.Bank.Burn.Amount)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if coins.IsZero() {
|
||||
return nil, nil, types.ErrEmpty.Wrap("amount")
|
||||
}
|
||||
if err := burner.SendCoinsFromAccountToModule(ctx, contractAddr, types.ModuleName, coins); err != nil {
|
||||
return nil, nil, sdkerrors.Wrap(err, "transfer to module")
|
||||
}
|
||||
if err := burner.BurnCoins(ctx, types.ModuleName, coins); err != nil {
|
||||
return nil, nil, sdkerrors.Wrap(err, "burn coins")
|
||||
}
|
||||
moduleLogger(ctx).Info("Burned", "amount", coins)
|
||||
return nil, nil, nil
|
||||
}
|
||||
return nil, nil, types.ErrUnknownMsg
|
||||
}
|
||||
}
|
393
x/wasm/keeper/handler_plugin_encoders.go
Normal file
@ -0,0 +1,393 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
|
||||
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
|
||||
ibcclienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
type (
|
||||
BankEncoder func(sender sdk.AccAddress, msg *wasmvmtypes.BankMsg) ([]sdk.Msg, error)
|
||||
CustomEncoder func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error)
|
||||
DistributionEncoder func(sender sdk.AccAddress, msg *wasmvmtypes.DistributionMsg) ([]sdk.Msg, error)
|
||||
StakingEncoder func(sender sdk.AccAddress, msg *wasmvmtypes.StakingMsg) ([]sdk.Msg, error)
|
||||
StargateEncoder func(sender sdk.AccAddress, msg *wasmvmtypes.StargateMsg) ([]sdk.Msg, error)
|
||||
WasmEncoder func(sender sdk.AccAddress, msg *wasmvmtypes.WasmMsg) ([]sdk.Msg, error)
|
||||
IBCEncoder func(ctx sdk.Context, sender sdk.AccAddress, contractIBCPortID string, msg *wasmvmtypes.IBCMsg) ([]sdk.Msg, error)
|
||||
)
|
||||
|
||||
type MessageEncoders struct {
|
||||
Bank func(sender sdk.AccAddress, msg *wasmvmtypes.BankMsg) ([]sdk.Msg, error)
|
||||
Custom func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error)
|
||||
Distribution func(sender sdk.AccAddress, msg *wasmvmtypes.DistributionMsg) ([]sdk.Msg, error)
|
||||
IBC func(ctx sdk.Context, sender sdk.AccAddress, contractIBCPortID string, msg *wasmvmtypes.IBCMsg) ([]sdk.Msg, error)
|
||||
Staking func(sender sdk.AccAddress, msg *wasmvmtypes.StakingMsg) ([]sdk.Msg, error)
|
||||
Stargate func(sender sdk.AccAddress, msg *wasmvmtypes.StargateMsg) ([]sdk.Msg, error)
|
||||
Wasm func(sender sdk.AccAddress, msg *wasmvmtypes.WasmMsg) ([]sdk.Msg, error)
|
||||
Gov func(sender sdk.AccAddress, msg *wasmvmtypes.GovMsg) ([]sdk.Msg, error)
|
||||
}
|
||||
|
||||
func DefaultEncoders(unpacker codectypes.AnyUnpacker, portSource types.ICS20TransferPortSource) MessageEncoders {
|
||||
return MessageEncoders{
|
||||
Bank: EncodeBankMsg,
|
||||
Custom: NoCustomMsg,
|
||||
Distribution: EncodeDistributionMsg,
|
||||
IBC: EncodeIBCMsg(portSource),
|
||||
Staking: EncodeStakingMsg,
|
||||
Stargate: EncodeStargateMsg(unpacker),
|
||||
Wasm: EncodeWasmMsg,
|
||||
Gov: EncodeGovMsg,
|
||||
}
|
||||
}
|
||||
|
||||
func (e MessageEncoders) Merge(o *MessageEncoders) MessageEncoders {
|
||||
if o == nil {
|
||||
return e
|
||||
}
|
||||
if o.Bank != nil {
|
||||
e.Bank = o.Bank
|
||||
}
|
||||
if o.Custom != nil {
|
||||
e.Custom = o.Custom
|
||||
}
|
||||
if o.Distribution != nil {
|
||||
e.Distribution = o.Distribution
|
||||
}
|
||||
if o.IBC != nil {
|
||||
e.IBC = o.IBC
|
||||
}
|
||||
if o.Staking != nil {
|
||||
e.Staking = o.Staking
|
||||
}
|
||||
if o.Stargate != nil {
|
||||
e.Stargate = o.Stargate
|
||||
}
|
||||
if o.Wasm != nil {
|
||||
e.Wasm = o.Wasm
|
||||
}
|
||||
if o.Gov != nil {
|
||||
e.Gov = o.Gov
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e MessageEncoders) Encode(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Msg, error) {
|
||||
switch {
|
||||
case msg.Bank != nil:
|
||||
return e.Bank(contractAddr, msg.Bank)
|
||||
case msg.Custom != nil:
|
||||
return e.Custom(contractAddr, msg.Custom)
|
||||
case msg.Distribution != nil:
|
||||
return e.Distribution(contractAddr, msg.Distribution)
|
||||
case msg.IBC != nil:
|
||||
return e.IBC(ctx, contractAddr, contractIBCPortID, msg.IBC)
|
||||
case msg.Staking != nil:
|
||||
return e.Staking(contractAddr, msg.Staking)
|
||||
case msg.Stargate != nil:
|
||||
return e.Stargate(contractAddr, msg.Stargate)
|
||||
case msg.Wasm != nil:
|
||||
return e.Wasm(contractAddr, msg.Wasm)
|
||||
case msg.Gov != nil:
|
||||
return EncodeGovMsg(contractAddr, msg.Gov)
|
||||
}
|
||||
return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of Wasm")
|
||||
}
|
||||
|
||||
func EncodeBankMsg(sender sdk.AccAddress, msg *wasmvmtypes.BankMsg) ([]sdk.Msg, error) {
|
||||
if msg.Send == nil {
|
||||
return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of Bank")
|
||||
}
|
||||
if len(msg.Send.Amount) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
toSend, err := ConvertWasmCoinsToSdkCoins(msg.Send.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkMsg := banktypes.MsgSend{
|
||||
FromAddress: sender.String(),
|
||||
ToAddress: msg.Send.ToAddress,
|
||||
Amount: toSend,
|
||||
}
|
||||
return []sdk.Msg{&sdkMsg}, nil
|
||||
}
|
||||
|
||||
func NoCustomMsg(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) {
|
||||
return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "custom variant not supported")
|
||||
}
|
||||
|
||||
func EncodeDistributionMsg(sender sdk.AccAddress, msg *wasmvmtypes.DistributionMsg) ([]sdk.Msg, error) {
|
||||
switch {
|
||||
case msg.SetWithdrawAddress != nil:
|
||||
setMsg := distributiontypes.MsgSetWithdrawAddress{
|
||||
DelegatorAddress: sender.String(),
|
||||
WithdrawAddress: msg.SetWithdrawAddress.Address,
|
||||
}
|
||||
return []sdk.Msg{&setMsg}, nil
|
||||
case msg.WithdrawDelegatorReward != nil:
|
||||
withdrawMsg := distributiontypes.MsgWithdrawDelegatorReward{
|
||||
DelegatorAddress: sender.String(),
|
||||
ValidatorAddress: msg.WithdrawDelegatorReward.Validator,
|
||||
}
|
||||
return []sdk.Msg{&withdrawMsg}, nil
|
||||
default:
|
||||
return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of Distribution")
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeStakingMsg(sender sdk.AccAddress, msg *wasmvmtypes.StakingMsg) ([]sdk.Msg, error) {
|
||||
switch {
|
||||
case msg.Delegate != nil:
|
||||
coin, err := ConvertWasmCoinToSdkCoin(msg.Delegate.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkMsg := stakingtypes.MsgDelegate{
|
||||
DelegatorAddress: sender.String(),
|
||||
ValidatorAddress: msg.Delegate.Validator,
|
||||
Amount: coin,
|
||||
}
|
||||
return []sdk.Msg{&sdkMsg}, nil
|
||||
|
||||
case msg.Redelegate != nil:
|
||||
coin, err := ConvertWasmCoinToSdkCoin(msg.Redelegate.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkMsg := stakingtypes.MsgBeginRedelegate{
|
||||
DelegatorAddress: sender.String(),
|
||||
ValidatorSrcAddress: msg.Redelegate.SrcValidator,
|
||||
ValidatorDstAddress: msg.Redelegate.DstValidator,
|
||||
Amount: coin,
|
||||
}
|
||||
return []sdk.Msg{&sdkMsg}, nil
|
||||
case msg.Undelegate != nil:
|
||||
coin, err := ConvertWasmCoinToSdkCoin(msg.Undelegate.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkMsg := stakingtypes.MsgUndelegate{
|
||||
DelegatorAddress: sender.String(),
|
||||
ValidatorAddress: msg.Undelegate.Validator,
|
||||
Amount: coin,
|
||||
}
|
||||
return []sdk.Msg{&sdkMsg}, nil
|
||||
default:
|
||||
return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of Staking")
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeStargateMsg(unpacker codectypes.AnyUnpacker) StargateEncoder {
|
||||
return func(sender sdk.AccAddress, msg *wasmvmtypes.StargateMsg) ([]sdk.Msg, error) {
|
||||
any := codectypes.Any{
|
||||
TypeUrl: msg.TypeURL,
|
||||
Value: msg.Value,
|
||||
}
|
||||
var sdkMsg sdk.Msg
|
||||
if err := unpacker.UnpackAny(&any, &sdkMsg); err != nil {
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalidMsg, fmt.Sprintf("Cannot unpack proto message with type URL: %s", msg.TypeURL))
|
||||
}
|
||||
if err := codectypes.UnpackInterfaces(sdkMsg, unpacker); err != nil {
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalidMsg, fmt.Sprintf("UnpackInterfaces inside msg: %s", err))
|
||||
}
|
||||
return []sdk.Msg{sdkMsg}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeWasmMsg(sender sdk.AccAddress, msg *wasmvmtypes.WasmMsg) ([]sdk.Msg, error) {
|
||||
switch {
|
||||
case msg.Execute != nil:
|
||||
coins, err := ConvertWasmCoinsToSdkCoins(msg.Execute.Funds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sdkMsg := types.MsgExecuteContract{
|
||||
Sender: sender.String(),
|
||||
Contract: msg.Execute.ContractAddr,
|
||||
Msg: msg.Execute.Msg,
|
||||
Funds: coins,
|
||||
}
|
||||
return []sdk.Msg{&sdkMsg}, nil
|
||||
case msg.Instantiate != nil:
|
||||
coins, err := ConvertWasmCoinsToSdkCoins(msg.Instantiate.Funds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sdkMsg := types.MsgInstantiateContract{
|
||||
Sender: sender.String(),
|
||||
CodeID: msg.Instantiate.CodeID,
|
||||
Label: msg.Instantiate.Label,
|
||||
Msg: msg.Instantiate.Msg,
|
||||
Admin: msg.Instantiate.Admin,
|
||||
Funds: coins,
|
||||
}
|
||||
return []sdk.Msg{&sdkMsg}, nil
|
||||
case msg.Instantiate2 != nil:
|
||||
coins, err := ConvertWasmCoinsToSdkCoins(msg.Instantiate2.Funds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sdkMsg := types.MsgInstantiateContract2{
|
||||
Sender: sender.String(),
|
||||
Admin: msg.Instantiate2.Admin,
|
||||
CodeID: msg.Instantiate2.CodeID,
|
||||
Label: msg.Instantiate2.Label,
|
||||
Msg: msg.Instantiate2.Msg,
|
||||
Funds: coins,
|
||||
Salt: msg.Instantiate2.Salt,
|
||||
// FixMsg is discouraged, see: https://medium.com/cosmwasm/dev-note-3-limitations-of-instantiate2-and-how-to-deal-with-them-a3f946874230
|
||||
FixMsg: false,
|
||||
}
|
||||
return []sdk.Msg{&sdkMsg}, nil
|
||||
case msg.Migrate != nil:
|
||||
sdkMsg := types.MsgMigrateContract{
|
||||
Sender: sender.String(),
|
||||
Contract: msg.Migrate.ContractAddr,
|
||||
CodeID: msg.Migrate.NewCodeID,
|
||||
Msg: msg.Migrate.Msg,
|
||||
}
|
||||
return []sdk.Msg{&sdkMsg}, nil
|
||||
case msg.ClearAdmin != nil:
|
||||
sdkMsg := types.MsgClearAdmin{
|
||||
Sender: sender.String(),
|
||||
Contract: msg.ClearAdmin.ContractAddr,
|
||||
}
|
||||
return []sdk.Msg{&sdkMsg}, nil
|
||||
case msg.UpdateAdmin != nil:
|
||||
sdkMsg := types.MsgUpdateAdmin{
|
||||
Sender: sender.String(),
|
||||
Contract: msg.UpdateAdmin.ContractAddr,
|
||||
NewAdmin: msg.UpdateAdmin.Admin,
|
||||
}
|
||||
return []sdk.Msg{&sdkMsg}, nil
|
||||
default:
|
||||
return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of Wasm")
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeIBCMsg(portSource types.ICS20TransferPortSource) func(ctx sdk.Context, sender sdk.AccAddress, contractIBCPortID string, msg *wasmvmtypes.IBCMsg) ([]sdk.Msg, error) {
|
||||
return func(ctx sdk.Context, sender sdk.AccAddress, contractIBCPortID string, msg *wasmvmtypes.IBCMsg) ([]sdk.Msg, error) {
|
||||
switch {
|
||||
case msg.CloseChannel != nil:
|
||||
return []sdk.Msg{&channeltypes.MsgChannelCloseInit{
|
||||
PortId: PortIDForContract(sender),
|
||||
ChannelId: msg.CloseChannel.ChannelID,
|
||||
Signer: sender.String(),
|
||||
}}, nil
|
||||
case msg.Transfer != nil:
|
||||
amount, err := ConvertWasmCoinToSdkCoin(msg.Transfer.Amount)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "amount")
|
||||
}
|
||||
msg := &ibctransfertypes.MsgTransfer{
|
||||
SourcePort: portSource.GetPort(ctx),
|
||||
SourceChannel: msg.Transfer.ChannelID,
|
||||
Token: amount,
|
||||
Sender: sender.String(),
|
||||
Receiver: msg.Transfer.ToAddress,
|
||||
TimeoutHeight: ConvertWasmIBCTimeoutHeightToCosmosHeight(msg.Transfer.Timeout.Block),
|
||||
TimeoutTimestamp: msg.Transfer.Timeout.Timestamp,
|
||||
}
|
||||
return []sdk.Msg{msg}, nil
|
||||
default:
|
||||
return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of IBC")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeGovMsg(sender sdk.AccAddress, msg *wasmvmtypes.GovMsg) ([]sdk.Msg, error) {
|
||||
switch {
|
||||
case msg.Vote != nil:
|
||||
voteOption, err := convertVoteOption(msg.Vote.Vote)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "vote option")
|
||||
}
|
||||
m := govtypes.NewMsgVote(sender, msg.Vote.ProposalId, voteOption)
|
||||
return []sdk.Msg{m}, nil
|
||||
case msg.VoteWeighted != nil:
|
||||
opts := make([]govtypes.WeightedVoteOption, len(msg.VoteWeighted.Options))
|
||||
for i, v := range msg.VoteWeighted.Options {
|
||||
weight, err := sdk.NewDecFromStr(v.Weight)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrapf(err, "weight for vote %d", i+1)
|
||||
}
|
||||
voteOption, err := convertVoteOption(v.Option)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "vote option")
|
||||
}
|
||||
opts[i] = govtypes.WeightedVoteOption{Option: voteOption, Weight: weight}
|
||||
}
|
||||
m := govtypes.NewMsgVoteWeighted(sender, msg.VoteWeighted.ProposalId, opts)
|
||||
return []sdk.Msg{m}, nil
|
||||
|
||||
default:
|
||||
return nil, types.ErrUnknownMsg.Wrap("unknown variant of gov")
|
||||
}
|
||||
}
|
||||
|
||||
func convertVoteOption(s interface{}) (govtypes.VoteOption, error) {
|
||||
var option govtypes.VoteOption
|
||||
switch s {
|
||||
case wasmvmtypes.Yes:
|
||||
option = govtypes.OptionYes
|
||||
case wasmvmtypes.No:
|
||||
option = govtypes.OptionNo
|
||||
case wasmvmtypes.NoWithVeto:
|
||||
option = govtypes.OptionNoWithVeto
|
||||
case wasmvmtypes.Abstain:
|
||||
option = govtypes.OptionAbstain
|
||||
default:
|
||||
return govtypes.OptionEmpty, types.ErrInvalid
|
||||
}
|
||||
return option, nil
|
||||
}
|
||||
|
||||
// ConvertWasmIBCTimeoutHeightToCosmosHeight converts a wasmvm type ibc timeout height to ibc module type height
|
||||
func ConvertWasmIBCTimeoutHeightToCosmosHeight(ibcTimeoutBlock *wasmvmtypes.IBCTimeoutBlock) ibcclienttypes.Height {
|
||||
if ibcTimeoutBlock == nil {
|
||||
return ibcclienttypes.NewHeight(0, 0)
|
||||
}
|
||||
return ibcclienttypes.NewHeight(ibcTimeoutBlock.Revision, ibcTimeoutBlock.Height)
|
||||
}
|
||||
|
||||
// ConvertWasmCoinsToSdkCoins converts the wasm vm type coins to sdk type coins
|
||||
func ConvertWasmCoinsToSdkCoins(coins []wasmvmtypes.Coin) (sdk.Coins, error) {
|
||||
var toSend sdk.Coins
|
||||
for _, coin := range coins {
|
||||
c, err := ConvertWasmCoinToSdkCoin(coin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
toSend = toSend.Add(c)
|
||||
}
|
||||
return toSend.Sort(), nil
|
||||
}
|
||||
|
||||
// ConvertWasmCoinToSdkCoin converts a wasm vm type coin to sdk type coin
|
||||
func ConvertWasmCoinToSdkCoin(coin wasmvmtypes.Coin) (sdk.Coin, error) {
|
||||
amount, ok := sdk.NewIntFromString(coin.Amount)
|
||||
if !ok {
|
||||
return sdk.Coin{}, sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, coin.Amount+coin.Denom)
|
||||
}
|
||||
r := sdk.Coin{
|
||||
Denom: coin.Denom,
|
||||
Amount: amount,
|
||||
}
|
||||
return r, r.Validate()
|
||||
}
|
932
x/wasm/keeper/handler_plugin_encoders_test.go
Normal file
@ -0,0 +1,932 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||
ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
|
||||
clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestEncoding(t *testing.T) {
|
||||
var (
|
||||
addr1 = RandomAccountAddress(t)
|
||||
addr2 = RandomAccountAddress(t)
|
||||
addr3 = RandomAccountAddress(t)
|
||||
invalidAddr = "xrnd1d02kd90n38qvr3qb9qof83fn2d2"
|
||||
)
|
||||
valAddr := make(sdk.ValAddress, types.SDKAddrLen)
|
||||
valAddr[0] = 12
|
||||
valAddr2 := make(sdk.ValAddress, types.SDKAddrLen)
|
||||
valAddr2[1] = 123
|
||||
|
||||
jsonMsg := types.RawContractMessage(`{"foo": 123}`)
|
||||
|
||||
bankMsg := &banktypes.MsgSend{
|
||||
FromAddress: addr2.String(),
|
||||
ToAddress: addr1.String(),
|
||||
Amount: sdk.Coins{
|
||||
sdk.NewInt64Coin("uatom", 12345),
|
||||
sdk.NewInt64Coin("utgd", 54321),
|
||||
},
|
||||
}
|
||||
bankMsgBin, err := proto.Marshal(bankMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := codectypes.NewAnyWithValue(types.StoreCodeProposalFixture())
|
||||
require.NoError(t, err)
|
||||
|
||||
proposalMsg := &govtypes.MsgSubmitProposal{
|
||||
Proposer: addr1.String(),
|
||||
InitialDeposit: sdk.NewCoins(sdk.NewInt64Coin("uatom", 12345)),
|
||||
Content: content,
|
||||
}
|
||||
proposalMsgBin, err := proto.Marshal(proposalMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := map[string]struct {
|
||||
sender sdk.AccAddress
|
||||
srcMsg wasmvmtypes.CosmosMsg
|
||||
srcContractIBCPort string
|
||||
transferPortSource types.ICS20TransferPortSource
|
||||
// set if valid
|
||||
output []sdk.Msg
|
||||
// set if expect mapping fails
|
||||
expError bool
|
||||
// set if sdk validate basic should fail
|
||||
expInvalid bool
|
||||
}{
|
||||
"simple send": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Bank: &wasmvmtypes.BankMsg{
|
||||
Send: &wasmvmtypes.SendMsg{
|
||||
ToAddress: addr2.String(),
|
||||
Amount: []wasmvmtypes.Coin{
|
||||
{
|
||||
Denom: "uatom",
|
||||
Amount: "12345",
|
||||
},
|
||||
{
|
||||
Denom: "usdt",
|
||||
Amount: "54321",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&banktypes.MsgSend{
|
||||
FromAddress: addr1.String(),
|
||||
ToAddress: addr2.String(),
|
||||
Amount: sdk.Coins{
|
||||
sdk.NewInt64Coin("uatom", 12345),
|
||||
sdk.NewInt64Coin("usdt", 54321),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid send amount": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Bank: &wasmvmtypes.BankMsg{
|
||||
Send: &wasmvmtypes.SendMsg{
|
||||
ToAddress: addr2.String(),
|
||||
Amount: []wasmvmtypes.Coin{
|
||||
{
|
||||
Denom: "uatom",
|
||||
Amount: "123.456",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expError: true,
|
||||
},
|
||||
"invalid address": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Bank: &wasmvmtypes.BankMsg{
|
||||
Send: &wasmvmtypes.SendMsg{
|
||||
ToAddress: invalidAddr,
|
||||
Amount: []wasmvmtypes.Coin{
|
||||
{
|
||||
Denom: "uatom",
|
||||
Amount: "7890",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expError: false, // addresses are checked in the handler
|
||||
expInvalid: true,
|
||||
output: []sdk.Msg{
|
||||
&banktypes.MsgSend{
|
||||
FromAddress: addr1.String(),
|
||||
ToAddress: invalidAddr,
|
||||
Amount: sdk.Coins{
|
||||
sdk.NewInt64Coin("uatom", 7890),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"wasm execute": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Wasm: &wasmvmtypes.WasmMsg{
|
||||
Execute: &wasmvmtypes.ExecuteMsg{
|
||||
ContractAddr: addr2.String(),
|
||||
Msg: jsonMsg,
|
||||
Funds: []wasmvmtypes.Coin{
|
||||
wasmvmtypes.NewCoin(12, "eth"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&types.MsgExecuteContract{
|
||||
Sender: addr1.String(),
|
||||
Contract: addr2.String(),
|
||||
Msg: jsonMsg,
|
||||
Funds: sdk.NewCoins(sdk.NewInt64Coin("eth", 12)),
|
||||
},
|
||||
},
|
||||
},
|
||||
"wasm instantiate": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Wasm: &wasmvmtypes.WasmMsg{
|
||||
Instantiate: &wasmvmtypes.InstantiateMsg{
|
||||
CodeID: 7,
|
||||
Msg: jsonMsg,
|
||||
Funds: []wasmvmtypes.Coin{
|
||||
wasmvmtypes.NewCoin(123, "eth"),
|
||||
},
|
||||
Label: "myLabel",
|
||||
Admin: addr2.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&types.MsgInstantiateContract{
|
||||
Sender: addr1.String(),
|
||||
CodeID: 7,
|
||||
Label: "myLabel",
|
||||
Msg: jsonMsg,
|
||||
Funds: sdk.NewCoins(sdk.NewInt64Coin("eth", 123)),
|
||||
Admin: addr2.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
"wasm instantiate2": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Wasm: &wasmvmtypes.WasmMsg{
|
||||
Instantiate2: &wasmvmtypes.Instantiate2Msg{
|
||||
CodeID: 7,
|
||||
Msg: jsonMsg,
|
||||
Funds: []wasmvmtypes.Coin{
|
||||
wasmvmtypes.NewCoin(123, "eth"),
|
||||
},
|
||||
Label: "myLabel",
|
||||
Admin: addr2.String(),
|
||||
Salt: []byte("mySalt"),
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&types.MsgInstantiateContract2{
|
||||
Sender: addr1.String(),
|
||||
Admin: addr2.String(),
|
||||
CodeID: 7,
|
||||
Label: "myLabel",
|
||||
Msg: jsonMsg,
|
||||
Funds: sdk.NewCoins(sdk.NewInt64Coin("eth", 123)),
|
||||
Salt: []byte("mySalt"),
|
||||
FixMsg: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
"wasm migrate": {
|
||||
sender: addr2,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Wasm: &wasmvmtypes.WasmMsg{
|
||||
Migrate: &wasmvmtypes.MigrateMsg{
|
||||
ContractAddr: addr1.String(),
|
||||
NewCodeID: 12,
|
||||
Msg: jsonMsg,
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&types.MsgMigrateContract{
|
||||
Sender: addr2.String(),
|
||||
Contract: addr1.String(),
|
||||
CodeID: 12,
|
||||
Msg: jsonMsg,
|
||||
},
|
||||
},
|
||||
},
|
||||
"wasm update admin": {
|
||||
sender: addr2,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Wasm: &wasmvmtypes.WasmMsg{
|
||||
UpdateAdmin: &wasmvmtypes.UpdateAdminMsg{
|
||||
ContractAddr: addr1.String(),
|
||||
Admin: addr3.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&types.MsgUpdateAdmin{
|
||||
Sender: addr2.String(),
|
||||
Contract: addr1.String(),
|
||||
NewAdmin: addr3.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
"wasm clear admin": {
|
||||
sender: addr2,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Wasm: &wasmvmtypes.WasmMsg{
|
||||
ClearAdmin: &wasmvmtypes.ClearAdminMsg{
|
||||
ContractAddr: addr1.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&types.MsgClearAdmin{
|
||||
Sender: addr2.String(),
|
||||
Contract: addr1.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
"staking delegate": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Staking: &wasmvmtypes.StakingMsg{
|
||||
Delegate: &wasmvmtypes.DelegateMsg{
|
||||
Validator: valAddr.String(),
|
||||
Amount: wasmvmtypes.NewCoin(777, "stake"),
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&stakingtypes.MsgDelegate{
|
||||
DelegatorAddress: addr1.String(),
|
||||
ValidatorAddress: valAddr.String(),
|
||||
Amount: sdk.NewInt64Coin("stake", 777),
|
||||
},
|
||||
},
|
||||
},
|
||||
"staking delegate to non-validator": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Staking: &wasmvmtypes.StakingMsg{
|
||||
Delegate: &wasmvmtypes.DelegateMsg{
|
||||
Validator: addr2.String(),
|
||||
Amount: wasmvmtypes.NewCoin(777, "stake"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expError: false, // fails in the handler
|
||||
output: []sdk.Msg{
|
||||
&stakingtypes.MsgDelegate{
|
||||
DelegatorAddress: addr1.String(),
|
||||
ValidatorAddress: addr2.String(),
|
||||
Amount: sdk.NewInt64Coin("stake", 777),
|
||||
},
|
||||
},
|
||||
},
|
||||
"staking undelegate": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Staking: &wasmvmtypes.StakingMsg{
|
||||
Undelegate: &wasmvmtypes.UndelegateMsg{
|
||||
Validator: valAddr.String(),
|
||||
Amount: wasmvmtypes.NewCoin(555, "stake"),
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&stakingtypes.MsgUndelegate{
|
||||
DelegatorAddress: addr1.String(),
|
||||
ValidatorAddress: valAddr.String(),
|
||||
Amount: sdk.NewInt64Coin("stake", 555),
|
||||
},
|
||||
},
|
||||
},
|
||||
"staking redelegate": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Staking: &wasmvmtypes.StakingMsg{
|
||||
Redelegate: &wasmvmtypes.RedelegateMsg{
|
||||
SrcValidator: valAddr.String(),
|
||||
DstValidator: valAddr2.String(),
|
||||
Amount: wasmvmtypes.NewCoin(222, "stake"),
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&stakingtypes.MsgBeginRedelegate{
|
||||
DelegatorAddress: addr1.String(),
|
||||
ValidatorSrcAddress: valAddr.String(),
|
||||
ValidatorDstAddress: valAddr2.String(),
|
||||
Amount: sdk.NewInt64Coin("stake", 222),
|
||||
},
|
||||
},
|
||||
},
|
||||
"staking withdraw (explicit recipient)": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Distribution: &wasmvmtypes.DistributionMsg{
|
||||
WithdrawDelegatorReward: &wasmvmtypes.WithdrawDelegatorRewardMsg{
|
||||
Validator: valAddr2.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&distributiontypes.MsgWithdrawDelegatorReward{
|
||||
DelegatorAddress: addr1.String(),
|
||||
ValidatorAddress: valAddr2.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
"staking set withdraw address": {
|
||||
sender: addr1,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Distribution: &wasmvmtypes.DistributionMsg{
|
||||
SetWithdrawAddress: &wasmvmtypes.SetWithdrawAddressMsg{
|
||||
Address: addr2.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&distributiontypes.MsgSetWithdrawAddress{
|
||||
DelegatorAddress: addr1.String(),
|
||||
WithdrawAddress: addr2.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
"stargate encoded bank msg": {
|
||||
sender: addr2,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Stargate: &wasmvmtypes.StargateMsg{
|
||||
TypeURL: "/cosmos.bank.v1beta1.MsgSend",
|
||||
Value: bankMsgBin,
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{bankMsg},
|
||||
},
|
||||
"stargate encoded msg with any type": {
|
||||
sender: addr2,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Stargate: &wasmvmtypes.StargateMsg{
|
||||
TypeURL: "/cosmos.gov.v1beta1.MsgSubmitProposal",
|
||||
Value: proposalMsgBin,
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{proposalMsg},
|
||||
},
|
||||
"stargate encoded invalid typeUrl": {
|
||||
sender: addr2,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Stargate: &wasmvmtypes.StargateMsg{
|
||||
TypeURL: "/cosmos.bank.v2.MsgSend",
|
||||
Value: bankMsgBin,
|
||||
},
|
||||
},
|
||||
expError: true,
|
||||
},
|
||||
"IBC transfer with block timeout": {
|
||||
sender: addr1,
|
||||
srcContractIBCPort: "myIBCPort",
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
IBC: &wasmvmtypes.IBCMsg{
|
||||
Transfer: &wasmvmtypes.TransferMsg{
|
||||
ChannelID: "myChanID",
|
||||
ToAddress: addr2.String(),
|
||||
Amount: wasmvmtypes.Coin{
|
||||
Denom: "ALX",
|
||||
Amount: "1",
|
||||
},
|
||||
Timeout: wasmvmtypes.IBCTimeout{
|
||||
Block: &wasmvmtypes.IBCTimeoutBlock{Revision: 1, Height: 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
transferPortSource: wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string {
|
||||
return "myTransferPort"
|
||||
}},
|
||||
output: []sdk.Msg{
|
||||
&ibctransfertypes.MsgTransfer{
|
||||
SourcePort: "myTransferPort",
|
||||
SourceChannel: "myChanID",
|
||||
Token: sdk.Coin{
|
||||
Denom: "ALX",
|
||||
Amount: sdk.NewInt(1),
|
||||
},
|
||||
Sender: addr1.String(),
|
||||
Receiver: addr2.String(),
|
||||
TimeoutHeight: clienttypes.Height{RevisionNumber: 1, RevisionHeight: 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
"IBC transfer with time timeout": {
|
||||
sender: addr1,
|
||||
srcContractIBCPort: "myIBCPort",
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
IBC: &wasmvmtypes.IBCMsg{
|
||||
Transfer: &wasmvmtypes.TransferMsg{
|
||||
ChannelID: "myChanID",
|
||||
ToAddress: addr2.String(),
|
||||
Amount: wasmvmtypes.Coin{
|
||||
Denom: "ALX",
|
||||
Amount: "1",
|
||||
},
|
||||
Timeout: wasmvmtypes.IBCTimeout{Timestamp: 100},
|
||||
},
|
||||
},
|
||||
},
|
||||
transferPortSource: wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string {
|
||||
return "transfer"
|
||||
}},
|
||||
output: []sdk.Msg{
|
||||
&ibctransfertypes.MsgTransfer{
|
||||
SourcePort: "transfer",
|
||||
SourceChannel: "myChanID",
|
||||
Token: sdk.Coin{
|
||||
Denom: "ALX",
|
||||
Amount: sdk.NewInt(1),
|
||||
},
|
||||
Sender: addr1.String(),
|
||||
Receiver: addr2.String(),
|
||||
TimeoutTimestamp: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
"IBC transfer with time and height timeout": {
|
||||
sender: addr1,
|
||||
srcContractIBCPort: "myIBCPort",
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
IBC: &wasmvmtypes.IBCMsg{
|
||||
Transfer: &wasmvmtypes.TransferMsg{
|
||||
ChannelID: "myChanID",
|
||||
ToAddress: addr2.String(),
|
||||
Amount: wasmvmtypes.Coin{
|
||||
Denom: "ALX",
|
||||
Amount: "1",
|
||||
},
|
||||
Timeout: wasmvmtypes.IBCTimeout{Timestamp: 100, Block: &wasmvmtypes.IBCTimeoutBlock{Height: 1, Revision: 2}},
|
||||
},
|
||||
},
|
||||
},
|
||||
transferPortSource: wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string {
|
||||
return "transfer"
|
||||
}},
|
||||
output: []sdk.Msg{
|
||||
&ibctransfertypes.MsgTransfer{
|
||||
SourcePort: "transfer",
|
||||
SourceChannel: "myChanID",
|
||||
Token: sdk.Coin{
|
||||
Denom: "ALX",
|
||||
Amount: sdk.NewInt(1),
|
||||
},
|
||||
Sender: addr1.String(),
|
||||
Receiver: addr2.String(),
|
||||
TimeoutTimestamp: 100,
|
||||
TimeoutHeight: clienttypes.NewHeight(2, 1),
|
||||
},
|
||||
},
|
||||
},
|
||||
"IBC close channel": {
|
||||
sender: addr1,
|
||||
srcContractIBCPort: "myIBCPort",
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
IBC: &wasmvmtypes.IBCMsg{
|
||||
CloseChannel: &wasmvmtypes.CloseChannelMsg{
|
||||
ChannelID: "channel-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&channeltypes.MsgChannelCloseInit{
|
||||
PortId: "wasm." + addr1.String(),
|
||||
ChannelId: "channel-1",
|
||||
Signer: addr1.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
encodingConfig := MakeEncodingConfig(t)
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var ctx sdk.Context
|
||||
encoder := DefaultEncoders(encodingConfig.Marshaler, tc.transferPortSource)
|
||||
res, err := encoder.Encode(ctx, tc.sender, tc.srcContractIBCPort, tc.srcMsg)
|
||||
if tc.expError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.output, res)
|
||||
}
|
||||
// and valid sdk message
|
||||
for _, v := range res {
|
||||
gotErr := v.ValidateBasic()
|
||||
if tc.expInvalid {
|
||||
assert.Error(t, gotErr)
|
||||
} else {
|
||||
assert.NoError(t, gotErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeGovMsg(t *testing.T) {
|
||||
myAddr := RandomAccountAddress(t)
|
||||
|
||||
cases := map[string]struct {
|
||||
sender sdk.AccAddress
|
||||
srcMsg wasmvmtypes.CosmosMsg
|
||||
transferPortSource types.ICS20TransferPortSource
|
||||
// set if valid
|
||||
output []sdk.Msg
|
||||
// set if expect mapping fails
|
||||
expError bool
|
||||
// set if sdk validate basic should fail
|
||||
expInvalid bool
|
||||
}{
|
||||
"Gov vote: yes": {
|
||||
sender: myAddr,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Gov: &wasmvmtypes.GovMsg{
|
||||
Vote: &wasmvmtypes.VoteMsg{ProposalId: 1, Vote: wasmvmtypes.Yes},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&govtypes.MsgVote{
|
||||
ProposalId: 1,
|
||||
Voter: myAddr.String(),
|
||||
Option: govtypes.OptionYes,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Gov vote: No": {
|
||||
sender: myAddr,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Gov: &wasmvmtypes.GovMsg{
|
||||
Vote: &wasmvmtypes.VoteMsg{ProposalId: 1, Vote: wasmvmtypes.No},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&govtypes.MsgVote{
|
||||
ProposalId: 1,
|
||||
Voter: myAddr.String(),
|
||||
Option: govtypes.OptionNo,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Gov vote: Abstain": {
|
||||
sender: myAddr,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Gov: &wasmvmtypes.GovMsg{
|
||||
Vote: &wasmvmtypes.VoteMsg{ProposalId: 10, Vote: wasmvmtypes.Abstain},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&govtypes.MsgVote{
|
||||
ProposalId: 10,
|
||||
Voter: myAddr.String(),
|
||||
Option: govtypes.OptionAbstain,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Gov vote: No with veto": {
|
||||
sender: myAddr,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Gov: &wasmvmtypes.GovMsg{
|
||||
Vote: &wasmvmtypes.VoteMsg{ProposalId: 1, Vote: wasmvmtypes.NoWithVeto},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&govtypes.MsgVote{
|
||||
ProposalId: 1,
|
||||
Voter: myAddr.String(),
|
||||
Option: govtypes.OptionNoWithVeto,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Gov vote: unset option": {
|
||||
sender: myAddr,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Gov: &wasmvmtypes.GovMsg{
|
||||
Vote: &wasmvmtypes.VoteMsg{ProposalId: 1},
|
||||
},
|
||||
},
|
||||
expError: true,
|
||||
},
|
||||
"Gov weighted vote: single vote": {
|
||||
sender: myAddr,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Gov: &wasmvmtypes.GovMsg{
|
||||
VoteWeighted: &wasmvmtypes.VoteWeightedMsg{
|
||||
ProposalId: 1,
|
||||
Options: []wasmvmtypes.WeightedVoteOption{
|
||||
{Option: wasmvmtypes.Yes, Weight: "1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&govtypes.MsgVoteWeighted{
|
||||
ProposalId: 1,
|
||||
Voter: myAddr.String(),
|
||||
Options: []govtypes.WeightedVoteOption{
|
||||
{Option: govtypes.OptionYes, Weight: sdk.NewDec(1)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"Gov weighted vote: splitted": {
|
||||
sender: myAddr,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Gov: &wasmvmtypes.GovMsg{
|
||||
VoteWeighted: &wasmvmtypes.VoteWeightedMsg{
|
||||
ProposalId: 1,
|
||||
Options: []wasmvmtypes.WeightedVoteOption{
|
||||
{Option: wasmvmtypes.Yes, Weight: "0.23"},
|
||||
{Option: wasmvmtypes.No, Weight: "0.24"},
|
||||
{Option: wasmvmtypes.Abstain, Weight: "0.26"},
|
||||
{Option: wasmvmtypes.NoWithVeto, Weight: "0.27"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&govtypes.MsgVoteWeighted{
|
||||
ProposalId: 1,
|
||||
Voter: myAddr.String(),
|
||||
Options: []govtypes.WeightedVoteOption{
|
||||
{Option: govtypes.OptionYes, Weight: sdk.NewDecWithPrec(23, 2)},
|
||||
{Option: govtypes.OptionNo, Weight: sdk.NewDecWithPrec(24, 2)},
|
||||
{Option: govtypes.OptionAbstain, Weight: sdk.NewDecWithPrec(26, 2)},
|
||||
{Option: govtypes.OptionNoWithVeto, Weight: sdk.NewDecWithPrec(27, 2)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"Gov weighted vote: duplicate option": {
|
||||
sender: myAddr,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Gov: &wasmvmtypes.GovMsg{
|
||||
VoteWeighted: &wasmvmtypes.VoteWeightedMsg{
|
||||
ProposalId: 1,
|
||||
Options: []wasmvmtypes.WeightedVoteOption{
|
||||
{Option: wasmvmtypes.Yes, Weight: "0.5"},
|
||||
{Option: wasmvmtypes.Yes, Weight: "0.5"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&govtypes.MsgVoteWeighted{
|
||||
ProposalId: 1,
|
||||
Voter: myAddr.String(),
|
||||
Options: []govtypes.WeightedVoteOption{
|
||||
{Option: govtypes.OptionYes, Weight: sdk.NewDecWithPrec(5, 1)},
|
||||
{Option: govtypes.OptionYes, Weight: sdk.NewDecWithPrec(5, 1)},
|
||||
},
|
||||
},
|
||||
},
|
||||
expInvalid: true,
|
||||
},
|
||||
"Gov weighted vote: weight sum exceeds 1": {
|
||||
sender: myAddr,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Gov: &wasmvmtypes.GovMsg{
|
||||
VoteWeighted: &wasmvmtypes.VoteWeightedMsg{
|
||||
ProposalId: 1,
|
||||
Options: []wasmvmtypes.WeightedVoteOption{
|
||||
{Option: wasmvmtypes.Yes, Weight: "0.51"},
|
||||
{Option: wasmvmtypes.No, Weight: "0.5"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&govtypes.MsgVoteWeighted{
|
||||
ProposalId: 1,
|
||||
Voter: myAddr.String(),
|
||||
Options: []govtypes.WeightedVoteOption{
|
||||
{Option: govtypes.OptionYes, Weight: sdk.NewDecWithPrec(51, 2)},
|
||||
{Option: govtypes.OptionNo, Weight: sdk.NewDecWithPrec(5, 1)},
|
||||
},
|
||||
},
|
||||
},
|
||||
expInvalid: true,
|
||||
},
|
||||
"Gov weighted vote: weight sum less than 1": {
|
||||
sender: myAddr,
|
||||
srcMsg: wasmvmtypes.CosmosMsg{
|
||||
Gov: &wasmvmtypes.GovMsg{
|
||||
VoteWeighted: &wasmvmtypes.VoteWeightedMsg{
|
||||
ProposalId: 1,
|
||||
Options: []wasmvmtypes.WeightedVoteOption{
|
||||
{Option: wasmvmtypes.Yes, Weight: "0.49"},
|
||||
{Option: wasmvmtypes.No, Weight: "0.5"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: []sdk.Msg{
|
||||
&govtypes.MsgVoteWeighted{
|
||||
ProposalId: 1,
|
||||
Voter: myAddr.String(),
|
||||
Options: []govtypes.WeightedVoteOption{
|
||||
{Option: govtypes.OptionYes, Weight: sdk.NewDecWithPrec(49, 2)},
|
||||
{Option: govtypes.OptionNo, Weight: sdk.NewDecWithPrec(5, 1)},
|
||||
},
|
||||
},
|
||||
},
|
||||
expInvalid: true,
|
||||
},
|
||||
}
|
||||
encodingConfig := MakeEncodingConfig(t)
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var ctx sdk.Context
|
||||
encoder := DefaultEncoders(encodingConfig.Marshaler, tc.transferPortSource)
|
||||
res, gotEncErr := encoder.Encode(ctx, tc.sender, "myIBCPort", tc.srcMsg)
|
||||
if tc.expError {
|
||||
assert.Error(t, gotEncErr)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, gotEncErr)
|
||||
assert.Equal(t, tc.output, res)
|
||||
}
|
||||
// and valid sdk message
|
||||
for _, v := range res {
|
||||
gotErr := v.ValidateBasic()
|
||||
if tc.expInvalid {
|
||||
assert.Error(t, gotErr)
|
||||
} else {
|
||||
assert.NoError(t, gotErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertWasmCoinToSdkCoin(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
src wasmvmtypes.Coin
|
||||
expErr bool
|
||||
expVal sdk.Coin
|
||||
}{
|
||||
"all good": {
|
||||
src: wasmvmtypes.Coin{
|
||||
Denom: "foo",
|
||||
Amount: "1",
|
||||
},
|
||||
expVal: sdk.NewCoin("foo", sdk.NewIntFromUint64(1)),
|
||||
},
|
||||
"negative amount": {
|
||||
src: wasmvmtypes.Coin{
|
||||
Denom: "foo",
|
||||
Amount: "-1",
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
"denom to short": {
|
||||
src: wasmvmtypes.Coin{
|
||||
Denom: "f",
|
||||
Amount: "1",
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
"invalid demum char": {
|
||||
src: wasmvmtypes.Coin{
|
||||
Denom: "&fff",
|
||||
Amount: "1",
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
"not a number amount": {
|
||||
src: wasmvmtypes.Coin{
|
||||
Denom: "foo",
|
||||
Amount: "bar",
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gotVal, gotErr := ConvertWasmCoinToSdkCoin(spec.src)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
assert.Equal(t, spec.expVal, gotVal)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertWasmCoinsToSdkCoins(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
src []wasmvmtypes.Coin
|
||||
exp sdk.Coins
|
||||
expErr bool
|
||||
}{
|
||||
"empty": {
|
||||
src: []wasmvmtypes.Coin{},
|
||||
exp: nil,
|
||||
},
|
||||
"single coin": {
|
||||
src: []wasmvmtypes.Coin{{Denom: "foo", Amount: "1"}},
|
||||
exp: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1))),
|
||||
},
|
||||
"multiple coins": {
|
||||
src: []wasmvmtypes.Coin{
|
||||
{Denom: "foo", Amount: "1"},
|
||||
{Denom: "bar", Amount: "2"},
|
||||
},
|
||||
exp: sdk.NewCoins(
|
||||
sdk.NewCoin("bar", sdk.NewInt(2)),
|
||||
sdk.NewCoin("foo", sdk.NewInt(1)),
|
||||
),
|
||||
},
|
||||
"sorted": {
|
||||
src: []wasmvmtypes.Coin{
|
||||
{Denom: "foo", Amount: "1"},
|
||||
{Denom: "other", Amount: "1"},
|
||||
{Denom: "bar", Amount: "1"},
|
||||
},
|
||||
exp: []sdk.Coin{
|
||||
sdk.NewCoin("bar", sdk.NewInt(1)),
|
||||
sdk.NewCoin("foo", sdk.NewInt(1)),
|
||||
sdk.NewCoin("other", sdk.NewInt(1)),
|
||||
},
|
||||
},
|
||||
"zero amounts dropped": {
|
||||
src: []wasmvmtypes.Coin{
|
||||
{Denom: "foo", Amount: "1"},
|
||||
{Denom: "bar", Amount: "0"},
|
||||
},
|
||||
exp: sdk.NewCoins(
|
||||
sdk.NewCoin("foo", sdk.NewInt(1)),
|
||||
),
|
||||
},
|
||||
"duplicate denoms merged": {
|
||||
src: []wasmvmtypes.Coin{
|
||||
{Denom: "foo", Amount: "1"},
|
||||
{Denom: "foo", Amount: "1"},
|
||||
},
|
||||
exp: []sdk.Coin{sdk.NewCoin("foo", sdk.NewInt(2))},
|
||||
},
|
||||
"duplicate denoms with one 0 amount does not fail": {
|
||||
src: []wasmvmtypes.Coin{
|
||||
{Denom: "foo", Amount: "0"},
|
||||
{Denom: "foo", Amount: "1"},
|
||||
},
|
||||
exp: []sdk.Coin{sdk.NewCoin("foo", sdk.NewInt(1))},
|
||||
},
|
||||
"empty denom rejected": {
|
||||
src: []wasmvmtypes.Coin{{Denom: "", Amount: "1"}},
|
||||
expErr: true,
|
||||
},
|
||||
"invalid denom rejected": {
|
||||
src: []wasmvmtypes.Coin{{Denom: "!%&", Amount: "1"}},
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gotCoins, gotErr := ConvertWasmCoinsToSdkCoins(spec.src)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
assert.Equal(t, spec.exp, gotCoins)
|
||||
assert.NoError(t, gotCoins.Validate())
|
||||
})
|
||||
}
|
||||
}
|
410
x/wasm/keeper/handler_plugin_test.go
Normal file
@ -0,0 +1,410 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
wasmvm "github.com/CosmWasm/wasmvm"
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
"github.com/cosmos/cosmos-sdk/baseapp"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
|
||||
clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestMessageHandlerChainDispatch(t *testing.T) {
|
||||
capturingHandler, gotMsgs := wasmtesting.NewCapturingMessageHandler()
|
||||
|
||||
alwaysUnknownMsgHandler := &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, nil, types.ErrUnknownMsg
|
||||
},
|
||||
}
|
||||
|
||||
assertNotCalledHandler := &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
t.Fatal("not expected to be called")
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
myMsg := wasmvmtypes.CosmosMsg{Custom: []byte(`{}`)}
|
||||
specs := map[string]struct {
|
||||
handlers []Messenger
|
||||
expErr *sdkerrors.Error
|
||||
expEvents []sdk.Event
|
||||
}{
|
||||
"single handler": {
|
||||
handlers: []Messenger{capturingHandler},
|
||||
},
|
||||
"passed to next handler": {
|
||||
handlers: []Messenger{alwaysUnknownMsgHandler, capturingHandler},
|
||||
},
|
||||
"stops iteration when handled": {
|
||||
handlers: []Messenger{capturingHandler, assertNotCalledHandler},
|
||||
},
|
||||
"stops iteration on handler error": {
|
||||
handlers: []Messenger{&wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, nil, types.ErrInvalidMsg
|
||||
},
|
||||
}, assertNotCalledHandler},
|
||||
expErr: types.ErrInvalidMsg,
|
||||
},
|
||||
"return events when handle": {
|
||||
handlers: []Messenger{
|
||||
&wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
_, data, _ = capturingHandler.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg)
|
||||
return []sdk.Event{sdk.NewEvent("myEvent", sdk.NewAttribute("foo", "bar"))}, data, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
expEvents: []sdk.Event{sdk.NewEvent("myEvent", sdk.NewAttribute("foo", "bar"))},
|
||||
},
|
||||
"return error when none can handle": {
|
||||
handlers: []Messenger{alwaysUnknownMsgHandler},
|
||||
expErr: types.ErrUnknownMsg,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
*gotMsgs = make([]wasmvmtypes.CosmosMsg, 0)
|
||||
|
||||
// when
|
||||
h := MessageHandlerChain{spec.handlers}
|
||||
gotEvents, gotData, gotErr := h.DispatchMsg(sdk.Context{}, RandomAccountAddress(t), "anyPort", myMsg)
|
||||
|
||||
// then
|
||||
require.True(t, spec.expErr.Is(gotErr), "exp %v but got %#+v", spec.expErr, gotErr)
|
||||
if spec.expErr != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, []wasmvmtypes.CosmosMsg{myMsg}, *gotMsgs)
|
||||
assert.Equal(t, [][]byte{{1}}, gotData) // {1} is default in capturing handler
|
||||
assert.Equal(t, spec.expEvents, gotEvents)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDKMessageHandlerDispatch(t *testing.T) {
|
||||
myEvent := sdk.NewEvent("myEvent", sdk.NewAttribute("foo", "bar"))
|
||||
const myData = "myData"
|
||||
myRouterResult := sdk.Result{
|
||||
Data: []byte(myData),
|
||||
Events: sdk.Events{myEvent}.ToABCIEvents(),
|
||||
}
|
||||
|
||||
var gotMsg []sdk.Msg
|
||||
capturingMessageRouter := wasmtesting.MessageRouterFunc(func(msg sdk.Msg) baseapp.MsgServiceHandler {
|
||||
return func(ctx sdk.Context, req sdk.Msg) (*sdk.Result, error) {
|
||||
gotMsg = append(gotMsg, msg)
|
||||
return &myRouterResult, nil
|
||||
}
|
||||
})
|
||||
noRouteMessageRouter := wasmtesting.MessageRouterFunc(func(msg sdk.Msg) baseapp.MsgServiceHandler {
|
||||
return nil
|
||||
})
|
||||
myContractAddr := RandomAccountAddress(t)
|
||||
myContractMessage := wasmvmtypes.CosmosMsg{Custom: []byte("{}")}
|
||||
|
||||
specs := map[string]struct {
|
||||
srcRoute MessageRouter
|
||||
srcEncoder CustomEncoder
|
||||
expErr *sdkerrors.Error
|
||||
expMsgDispatched int
|
||||
}{
|
||||
"all good": {
|
||||
srcRoute: capturingMessageRouter,
|
||||
srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) {
|
||||
myMsg := types.MsgExecuteContract{
|
||||
Sender: myContractAddr.String(),
|
||||
Contract: RandomBech32AccountAddress(t),
|
||||
Msg: []byte("{}"),
|
||||
}
|
||||
return []sdk.Msg{&myMsg}, nil
|
||||
},
|
||||
expMsgDispatched: 1,
|
||||
},
|
||||
"multiple output msgs": {
|
||||
srcRoute: capturingMessageRouter,
|
||||
srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) {
|
||||
first := &types.MsgExecuteContract{
|
||||
Sender: myContractAddr.String(),
|
||||
Contract: RandomBech32AccountAddress(t),
|
||||
Msg: []byte("{}"),
|
||||
}
|
||||
second := &types.MsgExecuteContract{
|
||||
Sender: myContractAddr.String(),
|
||||
Contract: RandomBech32AccountAddress(t),
|
||||
Msg: []byte("{}"),
|
||||
}
|
||||
return []sdk.Msg{first, second}, nil
|
||||
},
|
||||
expMsgDispatched: 2,
|
||||
},
|
||||
"invalid sdk message rejected": {
|
||||
srcRoute: capturingMessageRouter,
|
||||
srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) {
|
||||
invalidMsg := types.MsgExecuteContract{
|
||||
Sender: myContractAddr.String(),
|
||||
Contract: RandomBech32AccountAddress(t),
|
||||
Msg: []byte("INVALID_JSON"),
|
||||
}
|
||||
return []sdk.Msg{&invalidMsg}, nil
|
||||
},
|
||||
expErr: types.ErrInvalid,
|
||||
},
|
||||
"invalid sender rejected": {
|
||||
srcRoute: capturingMessageRouter,
|
||||
srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) {
|
||||
invalidMsg := types.MsgExecuteContract{
|
||||
Sender: RandomBech32AccountAddress(t),
|
||||
Contract: RandomBech32AccountAddress(t),
|
||||
Msg: []byte("{}"),
|
||||
}
|
||||
return []sdk.Msg{&invalidMsg}, nil
|
||||
},
|
||||
expErr: sdkerrors.ErrUnauthorized,
|
||||
},
|
||||
"unroutable message rejected": {
|
||||
srcRoute: noRouteMessageRouter,
|
||||
srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) {
|
||||
myMsg := types.MsgExecuteContract{
|
||||
Sender: myContractAddr.String(),
|
||||
Contract: RandomBech32AccountAddress(t),
|
||||
Msg: []byte("{}"),
|
||||
}
|
||||
return []sdk.Msg{&myMsg}, nil
|
||||
},
|
||||
expErr: sdkerrors.ErrUnknownRequest,
|
||||
},
|
||||
"encoding error passed": {
|
||||
srcRoute: capturingMessageRouter,
|
||||
srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) {
|
||||
myErr := types.ErrUnpinContractFailed // any error that is not used
|
||||
return nil, myErr
|
||||
},
|
||||
expErr: types.ErrUnpinContractFailed,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gotMsg = make([]sdk.Msg, 0)
|
||||
|
||||
// when
|
||||
ctx := sdk.Context{}
|
||||
h := NewSDKMessageHandler(spec.srcRoute, MessageEncoders{Custom: spec.srcEncoder})
|
||||
gotEvents, gotData, gotErr := h.DispatchMsg(ctx, myContractAddr, "myPort", myContractMessage)
|
||||
|
||||
// then
|
||||
require.True(t, spec.expErr.Is(gotErr), "exp %v but got %#+v", spec.expErr, gotErr)
|
||||
if spec.expErr != nil {
|
||||
require.Len(t, gotMsg, 0)
|
||||
return
|
||||
}
|
||||
assert.Len(t, gotMsg, spec.expMsgDispatched)
|
||||
for i := 0; i < spec.expMsgDispatched; i++ {
|
||||
assert.Equal(t, myEvent, gotEvents[i])
|
||||
assert.Equal(t, []byte(myData), gotData[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIBCRawPacketHandler(t *testing.T) {
|
||||
ibcPort := "contractsIBCPort"
|
||||
var ctx sdk.Context
|
||||
|
||||
var capturedPacket ibcexported.PacketI
|
||||
|
||||
chanKeeper := &wasmtesting.MockChannelKeeper{
|
||||
GetNextSequenceSendFn: func(ctx sdk.Context, portID, channelID string) (uint64, bool) {
|
||||
return 1, true
|
||||
},
|
||||
GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channeltypes.Channel, bool) {
|
||||
return channeltypes.Channel{
|
||||
Counterparty: channeltypes.NewCounterparty(
|
||||
"other-port",
|
||||
"other-channel-1",
|
||||
),
|
||||
}, true
|
||||
},
|
||||
SendPacketFn: func(ctx sdk.Context, channelCap *capabilitytypes.Capability, packet ibcexported.PacketI) error {
|
||||
capturedPacket = packet
|
||||
return nil
|
||||
},
|
||||
}
|
||||
capKeeper := &wasmtesting.MockCapabilityKeeper{
|
||||
GetCapabilityFn: func(ctx sdk.Context, name string) (*capabilitytypes.Capability, bool) {
|
||||
return &capabilitytypes.Capability{}, true
|
||||
},
|
||||
}
|
||||
|
||||
specs := map[string]struct {
|
||||
srcMsg wasmvmtypes.SendPacketMsg
|
||||
chanKeeper types.ChannelKeeper
|
||||
capKeeper types.CapabilityKeeper
|
||||
expPacketSent channeltypes.Packet
|
||||
expErr *sdkerrors.Error
|
||||
}{
|
||||
"all good": {
|
||||
srcMsg: wasmvmtypes.SendPacketMsg{
|
||||
ChannelID: "channel-1",
|
||||
Data: []byte("myData"),
|
||||
Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{Revision: 1, Height: 2}},
|
||||
},
|
||||
chanKeeper: chanKeeper,
|
||||
capKeeper: capKeeper,
|
||||
expPacketSent: channeltypes.Packet{
|
||||
Sequence: 1,
|
||||
SourcePort: ibcPort,
|
||||
SourceChannel: "channel-1",
|
||||
DestinationPort: "other-port",
|
||||
DestinationChannel: "other-channel-1",
|
||||
Data: []byte("myData"),
|
||||
TimeoutHeight: clienttypes.Height{RevisionNumber: 1, RevisionHeight: 2},
|
||||
},
|
||||
},
|
||||
"sequence not found returns error": {
|
||||
srcMsg: wasmvmtypes.SendPacketMsg{
|
||||
ChannelID: "channel-1",
|
||||
Data: []byte("myData"),
|
||||
Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{Revision: 1, Height: 2}},
|
||||
},
|
||||
chanKeeper: &wasmtesting.MockChannelKeeper{
|
||||
GetNextSequenceSendFn: func(ctx sdk.Context, portID, channelID string) (uint64, bool) {
|
||||
return 0, false
|
||||
},
|
||||
},
|
||||
expErr: channeltypes.ErrSequenceSendNotFound,
|
||||
},
|
||||
"capability not found returns error": {
|
||||
srcMsg: wasmvmtypes.SendPacketMsg{
|
||||
ChannelID: "channel-1",
|
||||
Data: []byte("myData"),
|
||||
Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{Revision: 1, Height: 2}},
|
||||
},
|
||||
chanKeeper: chanKeeper,
|
||||
capKeeper: wasmtesting.MockCapabilityKeeper{
|
||||
GetCapabilityFn: func(ctx sdk.Context, name string) (*capabilitytypes.Capability, bool) {
|
||||
return nil, false
|
||||
},
|
||||
},
|
||||
expErr: channeltypes.ErrChannelCapabilityNotFound,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
capturedPacket = nil
|
||||
// when
|
||||
h := NewIBCRawPacketHandler(spec.chanKeeper, spec.capKeeper)
|
||||
data, evts, gotErr := h.DispatchMsg(ctx, RandomAccountAddress(t), ibcPort, wasmvmtypes.CosmosMsg{IBC: &wasmvmtypes.IBCMsg{SendPacket: &spec.srcMsg}})
|
||||
// then
|
||||
require.True(t, spec.expErr.Is(gotErr), "exp %v but got %#+v", spec.expErr, gotErr)
|
||||
if spec.expErr != nil {
|
||||
return
|
||||
}
|
||||
assert.Nil(t, data)
|
||||
assert.Nil(t, evts)
|
||||
assert.Equal(t, spec.expPacketSent, capturedPacket)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBurnCoinMessageHandlerIntegration(t *testing.T) {
|
||||
// testing via full keeper setup so that we are confident the
|
||||
// module permissions are set correct and no other handler
|
||||
// picks the message in the default handler chain
|
||||
ctx, keepers := CreateDefaultTestInput(t)
|
||||
// set some supply
|
||||
keepers.Faucet.NewFundedRandomAccount(ctx, sdk.NewCoin("denom", sdk.NewInt(10_000_000)))
|
||||
k := keepers.WasmKeeper
|
||||
|
||||
example := InstantiateHackatomExampleContract(t, ctx, keepers) // with deposit of 100 stake
|
||||
|
||||
before, err := keepers.BankKeeper.TotalSupply(sdk.WrapSDKContext(ctx), &banktypes.QueryTotalSupplyRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
specs := map[string]struct {
|
||||
msg wasmvmtypes.BurnMsg
|
||||
expErr bool
|
||||
}{
|
||||
"all good": {
|
||||
msg: wasmvmtypes.BurnMsg{
|
||||
Amount: wasmvmtypes.Coins{{
|
||||
Denom: "denom",
|
||||
Amount: "100",
|
||||
}},
|
||||
},
|
||||
},
|
||||
"not enough funds in contract": {
|
||||
msg: wasmvmtypes.BurnMsg{
|
||||
Amount: wasmvmtypes.Coins{{
|
||||
Denom: "denom",
|
||||
Amount: "101",
|
||||
}},
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
"zero amount rejected": {
|
||||
msg: wasmvmtypes.BurnMsg{
|
||||
Amount: wasmvmtypes.Coins{{
|
||||
Denom: "denom",
|
||||
Amount: "0",
|
||||
}},
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
"unknown denom - insufficient funds": {
|
||||
msg: wasmvmtypes.BurnMsg{
|
||||
Amount: wasmvmtypes.Coins{{
|
||||
Denom: "unknown",
|
||||
Amount: "1",
|
||||
}},
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
parentCtx := ctx
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx, _ = parentCtx.CacheContext()
|
||||
k.wasmVM = &wasmtesting.MockWasmer{ExecuteFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) {
|
||||
return &wasmvmtypes.Response{
|
||||
Messages: []wasmvmtypes.SubMsg{
|
||||
{Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{Burn: &spec.msg}}, ReplyOn: wasmvmtypes.ReplyNever},
|
||||
},
|
||||
}, 0, nil
|
||||
}}
|
||||
|
||||
// when
|
||||
_, err = k.execute(ctx, example.Contract, example.CreatorAddr, nil, nil)
|
||||
|
||||
// then
|
||||
if spec.expErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// and total supply reduced by burned amount
|
||||
after, err := keepers.BankKeeper.TotalSupply(sdk.WrapSDKContext(ctx), &banktypes.QueryTotalSupplyRequest{})
|
||||
require.NoError(t, err)
|
||||
diff := before.Supply.Sub(after.Supply)
|
||||
assert.Equal(t, sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(100))), diff)
|
||||
})
|
||||
}
|
||||
|
||||
// test cases:
|
||||
// not enough money to burn
|
||||
}
|
56
x/wasm/keeper/ibc.go
Normal file
@ -0,0 +1,56 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
|
||||
host "github.com/cosmos/ibc-go/v4/modules/core/24-host"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// bindIbcPort will reserve the port.
|
||||
// returns a string name of the port or error if we cannot bind it.
|
||||
// this will fail if call twice.
|
||||
func (k Keeper) bindIbcPort(ctx sdk.Context, portID string) error {
|
||||
cap := k.portKeeper.BindPort(ctx, portID)
|
||||
return k.ClaimCapability(ctx, cap, host.PortPath(portID))
|
||||
}
|
||||
|
||||
// ensureIbcPort is like registerIbcPort, but it checks if we already hold the port
|
||||
// before calling register, so this is safe to call multiple times.
|
||||
// Returns success if we already registered or just registered and error if we cannot
|
||||
// (lack of permissions or someone else has it)
|
||||
func (k Keeper) ensureIbcPort(ctx sdk.Context, contractAddr sdk.AccAddress) (string, error) {
|
||||
portID := PortIDForContract(contractAddr)
|
||||
if _, ok := k.capabilityKeeper.GetCapability(ctx, host.PortPath(portID)); ok {
|
||||
return portID, nil
|
||||
}
|
||||
return portID, k.bindIbcPort(ctx, portID)
|
||||
}
|
||||
|
||||
const portIDPrefix = "wasm."
|
||||
|
||||
func PortIDForContract(addr sdk.AccAddress) string {
|
||||
return portIDPrefix + addr.String()
|
||||
}
|
||||
|
||||
func ContractFromPortID(portID string) (sdk.AccAddress, error) {
|
||||
if !strings.HasPrefix(portID, portIDPrefix) {
|
||||
return nil, sdkerrors.Wrapf(types.ErrInvalid, "without prefix")
|
||||
}
|
||||
return sdk.AccAddressFromBech32(portID[len(portIDPrefix):])
|
||||
}
|
||||
|
||||
// AuthenticateCapability wraps the scopedKeeper's AuthenticateCapability function
|
||||
func (k Keeper) AuthenticateCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) bool {
|
||||
return k.capabilityKeeper.AuthenticateCapability(ctx, cap, name)
|
||||
}
|
||||
|
||||
// ClaimCapability allows the transfer module to claim a capability
|
||||
// that IBC module passes to it
|
||||
func (k Keeper) ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error {
|
||||
return k.capabilityKeeper.ClaimCapability(ctx, cap, name)
|
||||
}
|
82
x/wasm/keeper/ibc_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDontBindPortNonIBCContract(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
example := InstantiateHackatomExampleContract(t, ctx, keepers) // ensure we bound the port
|
||||
_, _, err := keepers.IBCKeeper.PortKeeper.LookupModuleByPort(ctx, keepers.WasmKeeper.GetContractInfo(ctx, example.Contract).IBCPortID)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBindingPortForIBCContractOnInstantiate(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
example := InstantiateIBCReflectContract(t, ctx, keepers) // ensure we bound the port
|
||||
owner, _, err := keepers.IBCKeeper.PortKeeper.LookupModuleByPort(ctx, keepers.WasmKeeper.GetContractInfo(ctx, example.Contract).IBCPortID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "wasm", owner)
|
||||
|
||||
initMsgBz := IBCReflectInitMsg{
|
||||
ReflectCodeID: example.ReflectCodeID,
|
||||
}.GetBytes(t)
|
||||
|
||||
// create a second contract should give yet another portID (and different address)
|
||||
creator := RandomAccountAddress(t)
|
||||
addr, _, err := keepers.ContractKeeper.Instantiate(ctx, example.CodeID, creator, nil, initMsgBz, "ibc-reflect-2", nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, example.Contract, addr)
|
||||
|
||||
portID2 := PortIDForContract(addr)
|
||||
owner, _, err = keepers.IBCKeeper.PortKeeper.LookupModuleByPort(ctx, portID2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "wasm", owner)
|
||||
}
|
||||
|
||||
func TestContractFromPortID(t *testing.T) {
|
||||
contractAddr := BuildContractAddressClassic(1, 100)
|
||||
specs := map[string]struct {
|
||||
srcPort string
|
||||
expAddr sdk.AccAddress
|
||||
expErr bool
|
||||
}{
|
||||
"all good": {
|
||||
srcPort: fmt.Sprintf("wasm.%s", contractAddr.String()),
|
||||
expAddr: contractAddr,
|
||||
},
|
||||
"without prefix": {
|
||||
srcPort: contractAddr.String(),
|
||||
expErr: true,
|
||||
},
|
||||
"invalid prefix": {
|
||||
srcPort: fmt.Sprintf("wasmx.%s", contractAddr.String()),
|
||||
expErr: true,
|
||||
},
|
||||
"without separator char": {
|
||||
srcPort: fmt.Sprintf("wasm%s", contractAddr.String()),
|
||||
expErr: true,
|
||||
},
|
||||
"invalid account": {
|
||||
srcPort: "wasm.foobar",
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gotAddr, gotErr := ContractFromPortID(spec.srcPort)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
assert.Equal(t, spec.expAddr, gotAddr)
|
||||
})
|
||||
}
|
||||
}
|
1188
x/wasm/keeper/keeper.go
Normal file
69
x/wasm/keeper/keeper_cgo.go
Normal file
@ -0,0 +1,69 @@
|
||||
//go:build cgo
|
||||
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
wasmvm "github.com/CosmWasm/wasmvm"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// NewKeeper creates a new contract Keeper instance
|
||||
// If customEncoders is non-nil, we can use this to override some of the message handler, especially custom
|
||||
func NewKeeper(
|
||||
cdc codec.Codec,
|
||||
storeKey sdk.StoreKey,
|
||||
paramSpace paramtypes.Subspace,
|
||||
accountKeeper types.AccountKeeper,
|
||||
bankKeeper types.BankKeeper,
|
||||
stakingKeeper types.StakingKeeper,
|
||||
distKeeper types.DistributionKeeper,
|
||||
channelKeeper types.ChannelKeeper,
|
||||
portKeeper types.PortKeeper,
|
||||
capabilityKeeper types.CapabilityKeeper,
|
||||
portSource types.ICS20TransferPortSource,
|
||||
router MessageRouter,
|
||||
queryRouter GRPCQueryRouter,
|
||||
homeDir string,
|
||||
wasmConfig types.WasmConfig,
|
||||
availableCapabilities string,
|
||||
opts ...Option,
|
||||
) Keeper {
|
||||
wasmer, err := wasmvm.NewVM(filepath.Join(homeDir, "wasm"), availableCapabilities, contractMemoryLimit, wasmConfig.ContractDebugMode, wasmConfig.MemoryCacheSize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// set KeyTable if it has not already been set
|
||||
if !paramSpace.HasKeyTable() {
|
||||
paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable())
|
||||
}
|
||||
|
||||
keeper := &Keeper{
|
||||
storeKey: storeKey,
|
||||
cdc: cdc,
|
||||
wasmVM: wasmer,
|
||||
accountKeeper: accountKeeper,
|
||||
bank: NewBankCoinTransferrer(bankKeeper),
|
||||
accountPruner: NewVestingCoinBurner(bankKeeper),
|
||||
portKeeper: portKeeper,
|
||||
capabilityKeeper: capabilityKeeper,
|
||||
messenger: NewDefaultMessageHandler(router, channelKeeper, capabilityKeeper, bankKeeper, cdc, portSource),
|
||||
queryGasLimit: wasmConfig.SmartQueryGasLimit,
|
||||
paramSpace: paramSpace,
|
||||
gasRegister: NewDefaultWasmGasRegister(),
|
||||
maxQueryStackSize: types.DefaultMaxQueryStackSize,
|
||||
acceptedAccountTypes: defaultAcceptedAccountTypes,
|
||||
}
|
||||
keeper.wasmVMQueryHandler = DefaultQueryPlugins(bankKeeper, stakingKeeper, distKeeper, channelKeeper, keeper)
|
||||
for _, o := range opts {
|
||||
o.apply(keeper)
|
||||
}
|
||||
// not updateable, yet
|
||||
keeper.wasmVMResponseHandler = NewDefaultWasmVMContractResponseHandler(NewMessageDispatcher(keeper.messenger, keeper))
|
||||
return *keeper
|
||||
}
|
35
x/wasm/keeper/keeper_no_cgo.go
Normal file
@ -0,0 +1,35 @@
|
||||
//go:build !cgo
|
||||
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// NewKeeper creates a new contract Keeper instance
|
||||
// If customEncoders is non-nil, we can use this to override some of the message handler, especially custom
|
||||
func NewKeeper(
|
||||
cdc codec.Codec,
|
||||
storeKey sdk.StoreKey,
|
||||
paramSpace paramtypes.Subspace,
|
||||
accountKeeper types.AccountKeeper,
|
||||
bankKeeper types.BankKeeper,
|
||||
stakingKeeper types.StakingKeeper,
|
||||
distKeeper types.DistributionKeeper,
|
||||
channelKeeper types.ChannelKeeper,
|
||||
portKeeper types.PortKeeper,
|
||||
capabilityKeeper types.CapabilityKeeper,
|
||||
portSource types.ICS20TransferPortSource,
|
||||
router MessageRouter,
|
||||
queryRouter GRPCQueryRouter,
|
||||
homeDir string,
|
||||
wasmConfig types.WasmConfig,
|
||||
availableCapabilities string,
|
||||
opts ...Option,
|
||||
) Keeper {
|
||||
panic("not implemented, please build with cgo enabled")
|
||||
}
|
2410
x/wasm/keeper/keeper_test.go
Normal file
154
x/wasm/keeper/legacy_querier.go
Normal file
@ -0,0 +1,154 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
![]() Sensitive package importCertain system packages contain functions which may be a possible source of non-determinism ## Sensitive package import
Certain system packages contain functions which may be a possible source of non-determinism
[Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/631)
|
||||
"strconv"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
const (
|
||||
QueryListContractByCode = "list-contracts-by-code"
|
||||
QueryGetContract = "contract-info"
|
||||
QueryGetContractState = "contract-state"
|
||||
QueryGetCode = "code"
|
||||
QueryListCode = "list-code"
|
||||
QueryContractHistory = "contract-history"
|
||||
)
|
||||
|
||||
const (
|
||||
QueryMethodContractStateSmart = "smart"
|
||||
QueryMethodContractStateAll = "all"
|
||||
QueryMethodContractStateRaw = "raw"
|
||||
)
|
||||
|
||||
// NewLegacyQuerier creates a new querier
|
||||
// Deprecated: the rest support will be removed. You can use the GRPC gateway instead
|
||||
func NewLegacyQuerier(keeper types.ViewKeeper, gasLimit sdk.Gas) sdk.Querier {
|
||||
return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) {
|
||||
var (
|
||||
rsp interface{}
|
||||
err error
|
||||
)
|
||||
switch path[0] {
|
||||
case QueryGetContract:
|
||||
addr, addrErr := sdk.AccAddressFromBech32(path[1])
|
||||
if addrErr != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, addrErr.Error())
|
||||
}
|
||||
rsp, err = queryContractInfo(ctx, addr, keeper)
|
||||
case QueryListContractByCode:
|
||||
codeID, parseErr := strconv.ParseUint(path[1], 10, 64)
|
||||
if parseErr != nil {
|
||||
return nil, sdkerrors.Wrapf(types.ErrInvalid, "code id: %s", parseErr.Error())
|
||||
}
|
||||
rsp = queryContractListByCode(ctx, codeID, keeper)
|
||||
case QueryGetContractState:
|
||||
if len(path) < 3 {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown data query endpoint")
|
||||
}
|
||||
return queryContractState(ctx, path[1], path[2], req.Data, gasLimit, keeper)
|
||||
case QueryGetCode:
|
||||
codeID, parseErr := strconv.ParseUint(path[1], 10, 64)
|
||||
if parseErr != nil {
|
||||
return nil, sdkerrors.Wrapf(types.ErrInvalid, "code id: %s", parseErr.Error())
|
||||
}
|
||||
rsp, err = queryCode(ctx, codeID, keeper)
|
||||
case QueryListCode:
|
||||
rsp, err = queryCodeList(ctx, keeper)
|
||||
case QueryContractHistory:
|
||||
contractAddr, addrErr := sdk.AccAddressFromBech32(path[1])
|
||||
if addrErr != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, addrErr.Error())
|
||||
}
|
||||
rsp, err = queryContractHistory(ctx, contractAddr, keeper)
|
||||
default:
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown data query endpoint")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rsp == nil || reflect.ValueOf(rsp).IsNil() {
|
||||
![]() Impossible interface nil checkThis value can never be nil, since it is a wrapped interface value. ## Impossible interface nil check
This value can never be nil, since it is a wrapped interface value.
[Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/634)
|
||||
return nil, nil
|
||||
}
|
||||
bz, err := json.MarshalIndent(rsp, "", " ")
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||
}
|
||||
return bz, nil
|
||||
}
|
||||
}
|
||||
|
||||
func queryContractState(ctx sdk.Context, bech, queryMethod string, data []byte, gasLimit sdk.Gas, keeper types.ViewKeeper) (json.RawMessage, error) {
|
||||
contractAddr, err := sdk.AccAddressFromBech32(bech)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, bech)
|
||||
}
|
||||
|
||||
switch queryMethod {
|
||||
case QueryMethodContractStateAll:
|
||||
resultData := make([]types.Model, 0)
|
||||
// this returns a serialized json object (which internally encoded binary fields properly)
|
||||
keeper.IterateContractState(ctx, contractAddr, func(key, value []byte) bool {
|
||||
resultData = append(resultData, types.Model{Key: key, Value: value})
|
||||
return false
|
||||
})
|
||||
bz, err := json.Marshal(resultData)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||
}
|
||||
return bz, nil
|
||||
case QueryMethodContractStateRaw:
|
||||
// this returns the raw data from the state, base64-encoded
|
||||
return keeper.QueryRaw(ctx, contractAddr, data), nil
|
||||
case QueryMethodContractStateSmart:
|
||||
// we enforce a subjective gas limit on all queries to avoid infinite loops
|
||||
ctx = ctx.WithGasMeter(sdk.NewGasMeter(gasLimit))
|
||||
msg := types.RawContractMessage(data)
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "json msg")
|
||||
}
|
||||
// this returns raw bytes (must be base64-encoded)
|
||||
bz, err := keeper.QuerySmart(ctx, contractAddr, msg)
|
||||
return bz, err
|
||||
default:
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, queryMethod)
|
||||
}
|
||||
}
|
||||
|
||||
func queryCodeList(ctx sdk.Context, keeper types.ViewKeeper) ([]types.CodeInfoResponse, error) {
|
||||
var info []types.CodeInfoResponse
|
||||
keeper.IterateCodeInfos(ctx, func(i uint64, res types.CodeInfo) bool {
|
||||
info = append(info, types.CodeInfoResponse{
|
||||
CodeID: i,
|
||||
Creator: res.Creator,
|
||||
DataHash: res.CodeHash,
|
||||
InstantiatePermission: res.InstantiateConfig,
|
||||
})
|
||||
return false
|
||||
})
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func queryContractHistory(ctx sdk.Context, contractAddr sdk.AccAddress, keeper types.ViewKeeper) ([]types.ContractCodeHistoryEntry, error) {
|
||||
history := keeper.GetContractHistory(ctx, contractAddr)
|
||||
// redact response
|
||||
for i := range history {
|
||||
history[i].Updated = nil
|
||||
}
|
||||
return history, nil
|
||||
}
|
||||
|
||||
func queryContractListByCode(ctx sdk.Context, codeID uint64, keeper types.ViewKeeper) []string {
|
||||
var contracts []string
|
||||
keeper.IterateContractsByCode(ctx, codeID, func(addr sdk.AccAddress) bool {
|
||||
contracts = append(contracts, addr.String())
|
||||
return false
|
||||
})
|
||||
return contracts
|
||||
}
|
364
x/wasm/keeper/legacy_querier_test.go
Normal file
@ -0,0 +1,364 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestLegacyQueryContractState(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit.Add(deposit...)...)
|
||||
anyAddr := keepers.Faucet.NewFundedRandomAccount(ctx, sdk.NewInt64Coin("denom", 5000))
|
||||
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
contractID, _, err := keepers.ContractKeeper.Create(ctx, creator, wasmCode, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, bob := keyPubAddr()
|
||||
initMsg := HackatomExampleInitMsg{
|
||||
Verifier: anyAddr,
|
||||
Beneficiary: bob,
|
||||
}
|
||||
initMsgBz, err := json.Marshal(initMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
addr, _, err := keepers.ContractKeeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract to query", deposit)
|
||||
require.NoError(t, err)
|
||||
|
||||
contractModel := []types.Model{
|
||||
{Key: []byte("foo"), Value: []byte(`"bar"`)},
|
||||
{Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)},
|
||||
}
|
||||
keeper.importContractState(ctx, addr, contractModel)
|
||||
|
||||
// this gets us full error, not redacted sdk.Error
|
||||
var defaultQueryGasLimit sdk.Gas = 3000000
|
||||
q := NewLegacyQuerier(keeper, defaultQueryGasLimit)
|
||||
|
||||
specs := map[string]struct {
|
||||
srcPath []string
|
||||
srcReq abci.RequestQuery
|
||||
// smart and raw queries (not all queries) return raw bytes from contract not []types.Model
|
||||
// if this is set, then we just compare - (should be json encoded string)
|
||||
expRes []byte
|
||||
// if success and expSmartRes is not set, we parse into []types.Model and compare (all state)
|
||||
expModelLen int
|
||||
expModelContains []types.Model
|
||||
expErr error
|
||||
}{
|
||||
"query all": {
|
||||
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateAll},
|
||||
expModelLen: 3,
|
||||
expModelContains: []types.Model{
|
||||
{Key: []byte("foo"), Value: []byte(`"bar"`)},
|
||||
{Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)},
|
||||
},
|
||||
},
|
||||
"query raw key": {
|
||||
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw},
|
||||
srcReq: abci.RequestQuery{Data: []byte("foo")},
|
||||
expRes: []byte(`"bar"`),
|
||||
},
|
||||
"query raw binary key": {
|
||||
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw},
|
||||
srcReq: abci.RequestQuery{Data: []byte{0x0, 0x1}},
|
||||
expRes: []byte(`{"count":8}`),
|
||||
},
|
||||
"query smart": {
|
||||
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateSmart},
|
||||
srcReq: abci.RequestQuery{Data: []byte(`{"verifier":{}}`)},
|
||||
expRes: []byte(fmt.Sprintf(`{"verifier":"%s"}`, anyAddr.String())),
|
||||
},
|
||||
"query smart invalid request": {
|
||||
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateSmart},
|
||||
srcReq: abci.RequestQuery{Data: []byte(`{"raw":{"key":"config"}}`)},
|
||||
expErr: types.ErrQueryFailed,
|
||||
},
|
||||
"query smart with invalid json": {
|
||||
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateSmart},
|
||||
srcReq: abci.RequestQuery{Data: []byte(`not a json string`)},
|
||||
expErr: types.ErrInvalid,
|
||||
},
|
||||
"query non-existent raw key": {
|
||||
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw},
|
||||
srcReq: abci.RequestQuery{Data: []byte("i do not exist")},
|
||||
expRes: nil,
|
||||
},
|
||||
"query empty raw key": {
|
||||
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw},
|
||||
srcReq: abci.RequestQuery{Data: []byte("")},
|
||||
expRes: nil,
|
||||
},
|
||||
"query nil raw key": {
|
||||
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw},
|
||||
srcReq: abci.RequestQuery{Data: nil},
|
||||
expRes: nil,
|
||||
},
|
||||
"query raw with unknown address": {
|
||||
srcPath: []string{QueryGetContractState, anyAddr.String(), QueryMethodContractStateRaw},
|
||||
expRes: nil,
|
||||
},
|
||||
"query all with unknown address": {
|
||||
srcPath: []string{QueryGetContractState, anyAddr.String(), QueryMethodContractStateAll},
|
||||
expModelLen: 0,
|
||||
},
|
||||
"query smart with unknown address": {
|
||||
srcPath: []string{QueryGetContractState, anyAddr.String(), QueryMethodContractStateSmart},
|
||||
srcReq: abci.RequestQuery{Data: []byte(`{}`)},
|
||||
expModelLen: 0,
|
||||
expErr: types.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
binResult, err := q(ctx, spec.srcPath, spec.srcReq)
|
||||
// require.True(t, spec.expErr.Is(err), "unexpected error")
|
||||
require.True(t, errors.Is(err, spec.expErr), err)
|
||||
|
||||
// if smart query, check custom response
|
||||
if spec.srcPath[2] != QueryMethodContractStateAll {
|
||||
require.Equal(t, spec.expRes, binResult)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, check returned models
|
||||
var r []types.Model
|
||||
if spec.expErr == nil {
|
||||
require.NoError(t, json.Unmarshal(binResult, &r))
|
||||
require.NotNil(t, r)
|
||||
}
|
||||
require.Len(t, r, spec.expModelLen)
|
||||
// and in result set
|
||||
for _, v := range spec.expModelContains {
|
||||
assert.Contains(t, r, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyQueryContractListByCodeOrdering(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000000))
|
||||
topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 500))
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit.Add(deposit...)...)
|
||||
anyAddr := keepers.Faucet.NewFundedRandomAccount(ctx, topUp...)
|
||||
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, wasmCode, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, bob := keyPubAddr()
|
||||
initMsg := HackatomExampleInitMsg{
|
||||
Verifier: anyAddr,
|
||||
Beneficiary: bob,
|
||||
}
|
||||
initMsgBz, err := json.Marshal(initMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// manage some realistic block settings
|
||||
var h int64 = 10
|
||||
setBlock := func(ctx sdk.Context, height int64) sdk.Context {
|
||||
ctx = ctx.WithBlockHeight(height)
|
||||
meter := sdk.NewGasMeter(1000000)
|
||||
ctx = ctx.WithGasMeter(meter)
|
||||
ctx = ctx.WithBlockGasMeter(meter)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// create 10 contracts with real block/gas setup
|
||||
for i := range [10]int{} {
|
||||
// 3 tx per block, so we ensure both comparisons work
|
||||
if i%3 == 0 {
|
||||
ctx = setBlock(ctx, h)
|
||||
h++
|
||||
}
|
||||
_, _, err = keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, fmt.Sprintf("contract %d", i), topUp)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// query and check the results are properly sorted
|
||||
var defaultQueryGasLimit sdk.Gas = 3000000
|
||||
q := NewLegacyQuerier(keeper, defaultQueryGasLimit)
|
||||
|
||||
query := []string{QueryListContractByCode, fmt.Sprintf("%d", codeID)}
|
||||
data := abci.RequestQuery{}
|
||||
res, err := q(ctx, query, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
var contracts []string
|
||||
err = json.Unmarshal(res, &contracts)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 10, len(contracts))
|
||||
|
||||
for _, contract := range contracts {
|
||||
assert.NotEmpty(t, contract)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyQueryContractHistory(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
var otherAddr sdk.AccAddress = bytes.Repeat([]byte{0x2}, types.ContractAddrLen)
|
||||
|
||||
specs := map[string]struct {
|
||||
srcQueryAddr sdk.AccAddress
|
||||
srcHistory []types.ContractCodeHistoryEntry
|
||||
expContent []types.ContractCodeHistoryEntry
|
||||
}{
|
||||
"response with internal fields cleared": {
|
||||
srcHistory: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeGenesis,
|
||||
CodeID: firstCodeID,
|
||||
Updated: types.NewAbsoluteTxPosition(ctx),
|
||||
Msg: []byte(`"init message"`),
|
||||
}},
|
||||
expContent: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeGenesis,
|
||||
CodeID: firstCodeID,
|
||||
Msg: []byte(`"init message"`),
|
||||
}},
|
||||
},
|
||||
"response with multiple entries": {
|
||||
srcHistory: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeInit,
|
||||
CodeID: firstCodeID,
|
||||
Updated: types.NewAbsoluteTxPosition(ctx),
|
||||
Msg: []byte(`"init message"`),
|
||||
}, {
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 2,
|
||||
Updated: types.NewAbsoluteTxPosition(ctx),
|
||||
Msg: []byte(`"migrate message 1"`),
|
||||
}, {
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 3,
|
||||
Updated: types.NewAbsoluteTxPosition(ctx),
|
||||
Msg: []byte(`"migrate message 2"`),
|
||||
}},
|
||||
expContent: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeInit,
|
||||
CodeID: firstCodeID,
|
||||
Msg: []byte(`"init message"`),
|
||||
}, {
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 2,
|
||||
Msg: []byte(`"migrate message 1"`),
|
||||
}, {
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 3,
|
||||
Msg: []byte(`"migrate message 2"`),
|
||||
}},
|
||||
},
|
||||
"unknown contract address": {
|
||||
srcQueryAddr: otherAddr,
|
||||
srcHistory: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeGenesis,
|
||||
CodeID: firstCodeID,
|
||||
Updated: types.NewAbsoluteTxPosition(ctx),
|
||||
Msg: []byte(`"init message"`),
|
||||
}},
|
||||
expContent: []types.ContractCodeHistoryEntry{},
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
_, _, myContractAddr := keyPubAddr()
|
||||
keeper.appendToContractHistory(ctx, myContractAddr, spec.srcHistory...)
|
||||
|
||||
var defaultQueryGasLimit sdk.Gas = 3000000
|
||||
q := NewLegacyQuerier(keeper, defaultQueryGasLimit)
|
||||
queryContractAddr := spec.srcQueryAddr
|
||||
if queryContractAddr == nil {
|
||||
queryContractAddr = myContractAddr
|
||||
}
|
||||
|
||||
// when
|
||||
query := []string{QueryContractHistory, queryContractAddr.String()}
|
||||
data := abci.RequestQuery{}
|
||||
resData, err := q(ctx, query, data)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
var got []types.ContractCodeHistoryEntry
|
||||
err = json.Unmarshal(resData, &got)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, spec.expContent, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyQueryCodeList(t *testing.T) {
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
specs := map[string]struct {
|
||||
codeIDs []uint64
|
||||
}{
|
||||
"none": {},
|
||||
"no gaps": {
|
||||
codeIDs: []uint64{1, 2, 3},
|
||||
},
|
||||
"with gaps": {
|
||||
codeIDs: []uint64{2, 4, 6},
|
||||
},
|
||||
}
|
||||
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
for _, codeID := range spec.codeIDs {
|
||||
require.NoError(t, keeper.importCode(ctx, codeID,
|
||||
types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)),
|
||||
wasmCode),
|
||||
)
|
||||
}
|
||||
var defaultQueryGasLimit sdk.Gas = 3000000
|
||||
q := NewLegacyQuerier(keeper, defaultQueryGasLimit)
|
||||
// when
|
||||
query := []string{QueryListCode}
|
||||
data := abci.RequestQuery{}
|
||||
resData, err := q(ctx, query, data)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
if len(spec.codeIDs) == 0 {
|
||||
require.Nil(t, resData)
|
||||
return
|
||||
}
|
||||
|
||||
var got []map[string]interface{}
|
||||
err = json.Unmarshal(resData, &got)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, len(spec.codeIDs))
|
||||
for i, exp := range spec.codeIDs {
|
||||
assert.EqualValues(t, exp, got[i]["id"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
72
x/wasm/keeper/metrics.go
Normal file
@ -0,0 +1,72 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
labelPinned = "pinned"
|
||||
labelMemory = "memory"
|
||||
labelFs = "fs"
|
||||
)
|
||||
|
||||
// metricSource source of wasmvm metrics
|
||||
type metricSource interface {
|
||||
GetMetrics() (*wasmvmtypes.Metrics, error)
|
||||
}
|
||||
|
||||
var _ prometheus.Collector = (*WasmVMMetricsCollector)(nil)
|
||||
|
||||
// WasmVMMetricsCollector custom metrics collector to be used with Prometheus
|
||||
type WasmVMMetricsCollector struct {
|
||||
source metricSource
|
||||
CacheHitsDescr *prometheus.Desc
|
||||
CacheMissesDescr *prometheus.Desc
|
||||
CacheElementsDescr *prometheus.Desc
|
||||
CacheSizeDescr *prometheus.Desc
|
||||
}
|
||||
|
||||
// NewWasmVMMetricsCollector constructor
|
||||
func NewWasmVMMetricsCollector(s metricSource) *WasmVMMetricsCollector {
|
||||
return &WasmVMMetricsCollector{
|
||||
source: s,
|
||||
CacheHitsDescr: prometheus.NewDesc("wasmvm_cache_hits_total", "Total number of cache hits", []string{"type"}, nil),
|
||||
CacheMissesDescr: prometheus.NewDesc("wasmvm_cache_misses_total", "Total number of cache misses", nil, nil),
|
||||
CacheElementsDescr: prometheus.NewDesc("wasmvm_cache_elements_total", "Total number of elements in the cache", []string{"type"}, nil),
|
||||
CacheSizeDescr: prometheus.NewDesc("wasmvm_cache_size_bytes", "Total number of elements in the cache", []string{"type"}, nil),
|
||||
}
|
||||
}
|
||||
|
||||
// Register registers all metrics
|
||||
func (p *WasmVMMetricsCollector) Register(r prometheus.Registerer) {
|
||||
r.MustRegister(p)
|
||||
}
|
||||
|
||||
// Describe sends the super-set of all possible descriptors of metrics
|
||||
func (p *WasmVMMetricsCollector) Describe(descs chan<- *prometheus.Desc) {
|
||||
descs <- p.CacheHitsDescr
|
||||
descs <- p.CacheMissesDescr
|
||||
descs <- p.CacheElementsDescr
|
||||
descs <- p.CacheSizeDescr
|
||||
}
|
||||
|
||||
// Collect is called by the Prometheus registry when collecting metrics.
|
||||
func (p *WasmVMMetricsCollector) Collect(c chan<- prometheus.Metric) {
|
||||
m, err := p.source.GetMetrics()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c <- prometheus.MustNewConstMetric(p.CacheHitsDescr, prometheus.CounterValue, float64(m.HitsPinnedMemoryCache), labelPinned)
|
||||
c <- prometheus.MustNewConstMetric(p.CacheHitsDescr, prometheus.CounterValue, float64(m.HitsMemoryCache), labelMemory)
|
||||
c <- prometheus.MustNewConstMetric(p.CacheHitsDescr, prometheus.CounterValue, float64(m.HitsFsCache), labelFs)
|
||||
c <- prometheus.MustNewConstMetric(p.CacheMissesDescr, prometheus.CounterValue, float64(m.Misses))
|
||||
c <- prometheus.MustNewConstMetric(p.CacheElementsDescr, prometheus.GaugeValue, float64(m.ElementsPinnedMemoryCache), labelPinned)
|
||||
c <- prometheus.MustNewConstMetric(p.CacheElementsDescr, prometheus.GaugeValue, float64(m.ElementsMemoryCache), labelMemory)
|
||||
c <- prometheus.MustNewConstMetric(p.CacheSizeDescr, prometheus.GaugeValue, float64(m.SizeMemoryCache), labelMemory)
|
||||
c <- prometheus.MustNewConstMetric(p.CacheSizeDescr, prometheus.GaugeValue, float64(m.SizePinnedMemoryCache), labelPinned)
|
||||
// Node about fs metrics:
|
||||
// The number of elements and the size of elements in the file system cache cannot easily be obtained.
|
||||
// We had to either scan the whole directory of potentially thousands of files or track the values when files are added or removed.
|
||||
// Such a tracking would need to be on disk such that the values are not cleared when the node is restarted.
|
||||
}
|
61
x/wasm/keeper/migrate_test.go
Normal file
@ -0,0 +1,61 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/address"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestMigrate1To2(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
wasmKeeper := keepers.WasmKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
creator := sdk.AccAddress(bytes.Repeat([]byte{1}, address.Len))
|
||||
keepers.Faucet.Fund(ctx, creator, deposit...)
|
||||
example := StoreHackatomExampleContract(t, ctx, keepers)
|
||||
|
||||
initMsg := HackatomExampleInitMsg{
|
||||
Verifier: RandomAccountAddress(t),
|
||||
Beneficiary: RandomAccountAddress(t),
|
||||
}
|
||||
initMsgBz, err := json.Marshal(initMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
em := sdk.NewEventManager()
|
||||
|
||||
// create with no balance is also legal
|
||||
gotContractAddr1, _, err := keepers.ContractKeeper.Instantiate(ctx.WithEventManager(em), example.CodeID, creator, nil, initMsgBz, "demo contract 1", nil)
|
||||
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
|
||||
gotContractAddr2, _, err := keepers.ContractKeeper.Instantiate(ctx.WithEventManager(em), example.CodeID, creator, nil, initMsgBz, "demo contract 1", nil)
|
||||
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
|
||||
gotContractAddr3, _, err := keepers.ContractKeeper.Instantiate(ctx.WithEventManager(em), example.CodeID, creator, nil, initMsgBz, "demo contract 1", nil)
|
||||
|
||||
info1 := wasmKeeper.GetContractInfo(ctx, gotContractAddr1)
|
||||
info2 := wasmKeeper.GetContractInfo(ctx, gotContractAddr2)
|
||||
info3 := wasmKeeper.GetContractInfo(ctx, gotContractAddr3)
|
||||
|
||||
// remove key
|
||||
ctx.KVStore(wasmKeeper.storeKey).Delete(types.GetContractByCreatorSecondaryIndexKey(creator, info1.Created.Bytes(), gotContractAddr1))
|
||||
ctx.KVStore(wasmKeeper.storeKey).Delete(types.GetContractByCreatorSecondaryIndexKey(creator, info2.Created.Bytes(), gotContractAddr2))
|
||||
ctx.KVStore(wasmKeeper.storeKey).Delete(types.GetContractByCreatorSecondaryIndexKey(creator, info3.Created.Bytes(), gotContractAddr3))
|
||||
|
||||
// migrator
|
||||
migrator := NewMigrator(*wasmKeeper)
|
||||
migrator.Migrate1to2(ctx)
|
||||
|
||||
// check new store
|
||||
var allContract []string
|
||||
wasmKeeper.IterateContractsByCreator(ctx, creator, func(addr sdk.AccAddress) bool {
|
||||
allContract = append(allContract, addr.String())
|
||||
return false
|
||||
})
|
||||
|
||||
require.Equal(t, []string{gotContractAddr1.String(), gotContractAddr2.String(), gotContractAddr3.String()}, allContract)
|
||||
}
|
27
x/wasm/keeper/migrations.go
Normal file
@ -0,0 +1,27 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// Migrator is a struct for handling in-place store migrations.
|
||||
type Migrator struct {
|
||||
keeper Keeper
|
||||
}
|
||||
|
||||
// NewMigrator returns a new Migrator.
|
||||
func NewMigrator(keeper Keeper) Migrator {
|
||||
return Migrator{keeper: keeper}
|
||||
}
|
||||
|
||||
// Migrate1to2 migrates from version 1 to 2.
|
||||
func (m Migrator) Migrate1to2(ctx sdk.Context) error {
|
||||
m.keeper.IterateContractInfo(ctx, func(contractAddr sdk.AccAddress, contractInfo types.ContractInfo) bool {
|
||||
creator := sdk.MustAccAddressFromBech32(contractInfo.Creator)
|
||||
m.keeper.addToContractCreatorSecondaryIndex(ctx, creator, contractInfo.Created, contractAddr)
|
||||
return false
|
||||
})
|
||||
return nil
|
||||
}
|
222
x/wasm/keeper/msg_dispatcher.go
Normal file
@ -0,0 +1,222 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// Messenger is an extension point for custom wasmd message handling
|
||||
type Messenger interface {
|
||||
// DispatchMsg encodes the wasmVM message and dispatches it.
|
||||
DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error)
|
||||
}
|
||||
|
||||
// replyer is a subset of keeper that can handle replies to submessages
|
||||
type replyer interface {
|
||||
reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error)
|
||||
}
|
||||
|
||||
// MessageDispatcher coordinates message sending and submessage reply/ state commits
|
||||
type MessageDispatcher struct {
|
||||
messenger Messenger
|
||||
keeper replyer
|
||||
}
|
||||
|
||||
// NewMessageDispatcher constructor
|
||||
func NewMessageDispatcher(messenger Messenger, keeper replyer) *MessageDispatcher {
|
||||
return &MessageDispatcher{messenger: messenger, keeper: keeper}
|
||||
}
|
||||
|
||||
// DispatchMessages sends all messages.
|
||||
func (d MessageDispatcher) DispatchMessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.CosmosMsg) error {
|
||||
for _, msg := range msgs {
|
||||
events, _, err := d.messenger.DispatchMsg(ctx, contractAddr, ibcPort, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// redispatch all events, (type sdk.EventTypeMessage will be filtered out in the handler)
|
||||
ctx.EventManager().EmitEvents(events)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dispatchMsgWithGasLimit sends a message with gas limit applied
|
||||
func (d MessageDispatcher) dispatchMsgWithGasLimit(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msg wasmvmtypes.CosmosMsg, gasLimit uint64) (events []sdk.Event, data [][]byte, err error) {
|
||||
limitedMeter := sdk.NewGasMeter(gasLimit)
|
||||
subCtx := ctx.WithGasMeter(limitedMeter)
|
||||
|
||||
// catch out of gas panic and just charge the entire gas limit
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// if it's not an OutOfGas error, raise it again
|
||||
if _, ok := r.(sdk.ErrorOutOfGas); !ok {
|
||||
// log it to get the original stack trace somewhere (as panic(r) keeps message but stacktrace to here
|
||||
moduleLogger(ctx).Info("SubMsg rethrowing panic: %#v", r)
|
||||
panic(r)
|
||||
}
|
||||
ctx.GasMeter().ConsumeGas(gasLimit, "Sub-Message OutOfGas panic")
|
||||
err = sdkerrors.Wrap(sdkerrors.ErrOutOfGas, "SubMsg hit gas limit")
|
||||
}
|
||||
}()
|
||||
events, data, err = d.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg)
|
||||
|
||||
// make sure we charge the parent what was spent
|
||||
spent := subCtx.GasMeter().GasConsumed()
|
||||
ctx.GasMeter().ConsumeGas(spent, "From limited Sub-Message")
|
||||
|
||||
return events, data, err
|
||||
}
|
||||
|
||||
// DispatchSubmessages builds a sandbox to execute these messages and returns the execution result to the contract
|
||||
// that dispatched them, both on success as well as failure
|
||||
func (d MessageDispatcher) DispatchSubmessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) {
|
||||
var rsp []byte
|
||||
for _, msg := range msgs {
|
||||
switch msg.ReplyOn {
|
||||
case wasmvmtypes.ReplySuccess, wasmvmtypes.ReplyError, wasmvmtypes.ReplyAlways, wasmvmtypes.ReplyNever:
|
||||
default:
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalid, "replyOn value")
|
||||
}
|
||||
// first, we build a sub-context which we can use inside the submessages
|
||||
subCtx, commit := ctx.CacheContext()
|
||||
em := sdk.NewEventManager()
|
||||
subCtx = subCtx.WithEventManager(em)
|
||||
|
||||
// check how much gas left locally, optionally wrap the gas meter
|
||||
gasRemaining := ctx.GasMeter().Limit() - ctx.GasMeter().GasConsumed()
|
||||
limitGas := msg.GasLimit != nil && (*msg.GasLimit < gasRemaining)
|
||||
|
||||
var err error
|
||||
var events []sdk.Event
|
||||
var data [][]byte
|
||||
if limitGas {
|
||||
events, data, err = d.dispatchMsgWithGasLimit(subCtx, contractAddr, ibcPort, msg.Msg, *msg.GasLimit)
|
||||
} else {
|
||||
events, data, err = d.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg.Msg)
|
||||
}
|
||||
|
||||
// if it succeeds, commit state changes from submessage, and pass on events to Event Manager
|
||||
var filteredEvents []sdk.Event
|
||||
if err == nil {
|
||||
commit()
|
||||
filteredEvents = filterEvents(append(em.Events(), events...))
|
||||
ctx.EventManager().EmitEvents(filteredEvents)
|
||||
if msg.Msg.Wasm == nil {
|
||||
filteredEvents = []sdk.Event{}
|
||||
} else {
|
||||
for _, e := range filteredEvents {
|
||||
attributes := e.Attributes
|
||||
sort.SliceStable(attributes, func(i, j int) bool {
|
||||
return bytes.Compare(attributes[i].Key, attributes[j].Key) < 0
|
||||
})
|
||||
}
|
||||
}
|
||||
} // on failure, revert state from sandbox, and ignore events (just skip doing the above)
|
||||
|
||||
// we only callback if requested. Short-circuit here the cases we don't want to
|
||||
if (msg.ReplyOn == wasmvmtypes.ReplySuccess || msg.ReplyOn == wasmvmtypes.ReplyNever) && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.ReplyOn == wasmvmtypes.ReplyNever || (msg.ReplyOn == wasmvmtypes.ReplyError && err == nil) {
|
||||
continue
|
||||
}
|
||||
|
||||
// otherwise, we create a SubMsgResult and pass it into the calling contract
|
||||
var result wasmvmtypes.SubMsgResult
|
||||
if err == nil {
|
||||
// just take the first one for now if there are multiple sub-sdk messages
|
||||
// and safely return nothing if no data
|
||||
var responseData []byte
|
||||
if len(data) > 0 {
|
||||
responseData = data[0]
|
||||
}
|
||||
result = wasmvmtypes.SubMsgResult{
|
||||
Ok: &wasmvmtypes.SubMsgResponse{
|
||||
Events: sdkEventsToWasmVMEvents(filteredEvents),
|
||||
Data: responseData,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Issue #759 - we don't return error string for worries of non-determinism
|
||||
moduleLogger(ctx).Info("Redacting submessage error", "cause", err)
|
||||
result = wasmvmtypes.SubMsgResult{
|
||||
Err: redactError(err).Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// now handle the reply, we use the parent context, and abort on error
|
||||
reply := wasmvmtypes.Reply{
|
||||
ID: msg.ID,
|
||||
Result: result,
|
||||
}
|
||||
|
||||
// we can ignore any result returned as there is nothing to do with the data
|
||||
// and the events are already in the ctx.EventManager()
|
||||
rspData, err := d.keeper.reply(ctx, contractAddr, reply)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, sdkerrors.Wrap(err, "reply")
|
||||
case rspData != nil:
|
||||
rsp = rspData
|
||||
}
|
||||
}
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
// Issue #759 - we don't return error string for worries of non-determinism
|
||||
func redactError(err error) error {
|
||||
// Do not redact system errors
|
||||
// SystemErrors must be created in x/wasm and we can ensure determinism
|
||||
if wasmvmtypes.ToSystemError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME: do we want to hardcode some constant string mappings here as well?
|
||||
// Or better document them? (SDK error string may change on a patch release to fix wording)
|
||||
// sdk/11 is out of gas
|
||||
// sdk/5 is insufficient funds (on bank send)
|
||||
// (we can theoretically redact less in the future, but this is a first step to safety)
|
||||
codespace, code, _ := sdkerrors.ABCIInfo(err, false)
|
||||
return fmt.Errorf("codespace: %s, code: %d", codespace, code)
|
||||
}
|
||||
|
||||
func filterEvents(events []sdk.Event) []sdk.Event {
|
||||
// pre-allocate space for efficiency
|
||||
res := make([]sdk.Event, 0, len(events))
|
||||
for _, ev := range events {
|
||||
if ev.Type != "message" {
|
||||
res = append(res, ev)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func sdkEventsToWasmVMEvents(events []sdk.Event) []wasmvmtypes.Event {
|
||||
res := make([]wasmvmtypes.Event, len(events))
|
||||
for i, ev := range events {
|
||||
res[i] = wasmvmtypes.Event{
|
||||
Type: ev.Type,
|
||||
Attributes: sdkAttributesToWasmVMAttributes(ev.Attributes),
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func sdkAttributesToWasmVMAttributes(attrs []abci.EventAttribute) []wasmvmtypes.EventAttribute {
|
||||
res := make([]wasmvmtypes.EventAttribute, len(attrs))
|
||||
for i, attr := range attrs {
|
||||
res[i] = wasmvmtypes.EventAttribute{
|
||||
Key: string(attr.Key),
|
||||
Value: string(attr.Value),
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
426
x/wasm/keeper/msg_dispatcher_test.go
Normal file
@ -0,0 +1,426 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting"
|
||||
)
|
||||
|
||||
func TestDispatchSubmessages(t *testing.T) {
|
||||
noReplyCalled := &mockReplyer{}
|
||||
var anyGasLimit uint64 = 1
|
||||
specs := map[string]struct {
|
||||
msgs []wasmvmtypes.SubMsg
|
||||
replyer *mockReplyer
|
||||
msgHandler *wasmtesting.MockMessageHandler
|
||||
expErr bool
|
||||
expData []byte
|
||||
expCommits []bool
|
||||
expEvents sdk.Events
|
||||
}{
|
||||
"no reply on error without error": {
|
||||
msgs: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyError}},
|
||||
replyer: noReplyCalled,
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, [][]byte{[]byte("myData")}, nil
|
||||
},
|
||||
},
|
||||
expCommits: []bool{true},
|
||||
},
|
||||
"no reply on success without success": {
|
||||
msgs: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplySuccess}},
|
||||
replyer: noReplyCalled,
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, nil, errors.New("test, ignore")
|
||||
},
|
||||
},
|
||||
expCommits: []bool{false},
|
||||
expErr: true,
|
||||
},
|
||||
"reply on success - received": {
|
||||
msgs: []wasmvmtypes.SubMsg{{
|
||||
ReplyOn: wasmvmtypes.ReplySuccess,
|
||||
}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
return []byte("myReplyData"), nil
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, [][]byte{[]byte("myData")}, nil
|
||||
},
|
||||
},
|
||||
expData: []byte("myReplyData"),
|
||||
expCommits: []bool{true},
|
||||
},
|
||||
"reply on error - handled": {
|
||||
msgs: []wasmvmtypes.SubMsg{{
|
||||
ReplyOn: wasmvmtypes.ReplyError,
|
||||
}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
return []byte("myReplyData"), nil
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, nil, errors.New("my error")
|
||||
},
|
||||
},
|
||||
expData: []byte("myReplyData"),
|
||||
expCommits: []bool{false},
|
||||
},
|
||||
"with reply events": {
|
||||
msgs: []wasmvmtypes.SubMsg{{
|
||||
ReplyOn: wasmvmtypes.ReplySuccess,
|
||||
}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent("wasm-reply"))
|
||||
return []byte("myReplyData"), nil
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
myEvents := []sdk.Event{{Type: "myEvent", Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}}}}
|
||||
return myEvents, [][]byte{[]byte("myData")}, nil
|
||||
},
|
||||
},
|
||||
expData: []byte("myReplyData"),
|
||||
expCommits: []bool{true},
|
||||
expEvents: []sdk.Event{
|
||||
{
|
||||
Type: "myEvent",
|
||||
Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}},
|
||||
},
|
||||
sdk.NewEvent("wasm-reply"),
|
||||
},
|
||||
},
|
||||
"with context events - released on commit": {
|
||||
msgs: []wasmvmtypes.SubMsg{{
|
||||
ReplyOn: wasmvmtypes.ReplyNever,
|
||||
}},
|
||||
replyer: &mockReplyer{},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
myEvents := []sdk.Event{{Type: "myEvent", Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}}}}
|
||||
ctx.EventManager().EmitEvents(myEvents)
|
||||
return nil, nil, nil
|
||||
},
|
||||
},
|
||||
expCommits: []bool{true},
|
||||
expEvents: []sdk.Event{{
|
||||
Type: "myEvent",
|
||||
Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}},
|
||||
}},
|
||||
},
|
||||
"with context events - discarded on failure": {
|
||||
msgs: []wasmvmtypes.SubMsg{{
|
||||
ReplyOn: wasmvmtypes.ReplyNever,
|
||||
}},
|
||||
replyer: &mockReplyer{},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
myEvents := []sdk.Event{{Type: "myEvent", Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}}}}
|
||||
ctx.EventManager().EmitEvents(myEvents)
|
||||
return nil, nil, errors.New("testing")
|
||||
},
|
||||
},
|
||||
expCommits: []bool{false},
|
||||
expErr: true,
|
||||
},
|
||||
"reply returns error": {
|
||||
msgs: []wasmvmtypes.SubMsg{{
|
||||
ReplyOn: wasmvmtypes.ReplySuccess,
|
||||
}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
return nil, errors.New("reply failed")
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, nil, nil
|
||||
},
|
||||
},
|
||||
expCommits: []bool{false},
|
||||
expErr: true,
|
||||
},
|
||||
"with gas limit - out of gas": {
|
||||
msgs: []wasmvmtypes.SubMsg{{
|
||||
GasLimit: &anyGasLimit,
|
||||
ReplyOn: wasmvmtypes.ReplyError,
|
||||
}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
return []byte("myReplyData"), nil
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
ctx.GasMeter().ConsumeGas(sdk.Gas(101), "testing")
|
||||
return nil, [][]byte{[]byte("someData")}, nil
|
||||
},
|
||||
},
|
||||
expData: []byte("myReplyData"),
|
||||
expCommits: []bool{false},
|
||||
},
|
||||
"with gas limit - within limit no error": {
|
||||
msgs: []wasmvmtypes.SubMsg{{
|
||||
GasLimit: &anyGasLimit,
|
||||
ReplyOn: wasmvmtypes.ReplyError,
|
||||
}},
|
||||
replyer: &mockReplyer{},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
ctx.GasMeter().ConsumeGas(sdk.Gas(1), "testing")
|
||||
return nil, [][]byte{[]byte("someData")}, nil
|
||||
},
|
||||
},
|
||||
expCommits: []bool{true},
|
||||
},
|
||||
"never reply - with nil response": {
|
||||
msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyNever}, {ID: 2, ReplyOn: wasmvmtypes.ReplyNever}},
|
||||
replyer: &mockReplyer{},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, [][]byte{nil}, nil
|
||||
},
|
||||
},
|
||||
expCommits: []bool{true, true},
|
||||
},
|
||||
"never reply - with any non nil response": {
|
||||
msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyNever}, {ID: 2, ReplyOn: wasmvmtypes.ReplyNever}},
|
||||
replyer: &mockReplyer{},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, [][]byte{{}}, nil
|
||||
},
|
||||
},
|
||||
expCommits: []bool{true, true},
|
||||
},
|
||||
"never reply - with error": {
|
||||
msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyNever}, {ID: 2, ReplyOn: wasmvmtypes.ReplyNever}},
|
||||
replyer: &mockReplyer{},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, [][]byte{{}}, errors.New("testing")
|
||||
},
|
||||
},
|
||||
expCommits: []bool{false, false},
|
||||
expErr: true,
|
||||
},
|
||||
"multiple msg - last reply returned": {
|
||||
msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyError}, {ID: 2, ReplyOn: wasmvmtypes.ReplyError}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
return []byte(fmt.Sprintf("myReplyData:%d", reply.ID)), nil
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, nil, errors.New("my error")
|
||||
},
|
||||
},
|
||||
expData: []byte("myReplyData:2"),
|
||||
expCommits: []bool{false, false},
|
||||
},
|
||||
"multiple msg - last non nil reply returned": {
|
||||
msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyError}, {ID: 2, ReplyOn: wasmvmtypes.ReplyError}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
if reply.ID == 2 {
|
||||
return nil, nil
|
||||
}
|
||||
return []byte("myReplyData:1"), nil
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, nil, errors.New("my error")
|
||||
},
|
||||
},
|
||||
expData: []byte("myReplyData:1"),
|
||||
expCommits: []bool{false, false},
|
||||
},
|
||||
"multiple msg - empty reply can overwrite result": {
|
||||
msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyError}, {ID: 2, ReplyOn: wasmvmtypes.ReplyError}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
if reply.ID == 2 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return []byte("myReplyData:1"), nil
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
return nil, nil, errors.New("my error")
|
||||
},
|
||||
},
|
||||
expData: []byte{},
|
||||
expCommits: []bool{false, false},
|
||||
},
|
||||
"message event filtered without reply": {
|
||||
msgs: []wasmvmtypes.SubMsg{{
|
||||
ReplyOn: wasmvmtypes.ReplyNever,
|
||||
}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
return nil, errors.New("should never be called")
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
myEvents := []sdk.Event{
|
||||
sdk.NewEvent("message"),
|
||||
sdk.NewEvent("execute", sdk.NewAttribute("foo", "bar")),
|
||||
}
|
||||
return myEvents, [][]byte{[]byte("myData")}, nil
|
||||
},
|
||||
},
|
||||
expData: nil,
|
||||
expCommits: []bool{true},
|
||||
expEvents: []sdk.Event{sdk.NewEvent("execute", sdk.NewAttribute("foo", "bar"))},
|
||||
},
|
||||
"wasm reply gets proper events": {
|
||||
// put fake wasmmsg in here to show where it comes from
|
||||
msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyAlways, Msg: wasmvmtypes.CosmosMsg{Wasm: &wasmvmtypes.WasmMsg{}}}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
if reply.Result.Err != "" {
|
||||
return nil, errors.New(reply.Result.Err)
|
||||
}
|
||||
res := reply.Result.Ok
|
||||
|
||||
// ensure the input events are what we expect
|
||||
// I didn't use require.Equal() to act more like a contract... but maybe that would be better
|
||||
if len(res.Events) != 2 {
|
||||
return nil, fmt.Errorf("event count: %#v", res.Events)
|
||||
}
|
||||
if res.Events[0].Type != "execute" {
|
||||
return nil, fmt.Errorf("event0: %#v", res.Events[0])
|
||||
}
|
||||
if res.Events[1].Type != "wasm" {
|
||||
return nil, fmt.Errorf("event1: %#v", res.Events[1])
|
||||
}
|
||||
|
||||
// let's add a custom event here and see if it makes it out
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent("wasm-reply"))
|
||||
|
||||
// update data from what we got in
|
||||
return res.Data, nil
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
events = []sdk.Event{
|
||||
sdk.NewEvent("message", sdk.NewAttribute("_contract_address", contractAddr.String())),
|
||||
// we don't know what the contarctAddr will be so we can't use it in the final tests
|
||||
sdk.NewEvent("execute", sdk.NewAttribute("_contract_address", "placeholder-random-addr")),
|
||||
sdk.NewEvent("wasm", sdk.NewAttribute("random", "data")),
|
||||
}
|
||||
return events, [][]byte{[]byte("subData")}, nil
|
||||
},
|
||||
},
|
||||
expData: []byte("subData"),
|
||||
expCommits: []bool{true},
|
||||
expEvents: []sdk.Event{
|
||||
sdk.NewEvent("execute", sdk.NewAttribute("_contract_address", "placeholder-random-addr")),
|
||||
sdk.NewEvent("wasm", sdk.NewAttribute("random", "data")),
|
||||
sdk.NewEvent("wasm-reply"),
|
||||
},
|
||||
},
|
||||
"non-wasm reply events get filtered": {
|
||||
// show events from a stargate message gets filtered out
|
||||
msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyAlways, Msg: wasmvmtypes.CosmosMsg{Stargate: &wasmvmtypes.StargateMsg{}}}},
|
||||
replyer: &mockReplyer{
|
||||
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
if reply.Result.Err != "" {
|
||||
return nil, errors.New(reply.Result.Err)
|
||||
}
|
||||
res := reply.Result.Ok
|
||||
|
||||
// ensure the input events are what we expect
|
||||
// I didn't use require.Equal() to act more like a contract... but maybe that would be better
|
||||
if len(res.Events) != 0 {
|
||||
return nil, errors.New("events not filtered out")
|
||||
}
|
||||
|
||||
// let's add a custom event here and see if it makes it out
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent("stargate-reply"))
|
||||
|
||||
// update data from what we got in
|
||||
return res.Data, nil
|
||||
},
|
||||
},
|
||||
msgHandler: &wasmtesting.MockMessageHandler{
|
||||
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
|
||||
events = []sdk.Event{
|
||||
// this is filtered out
|
||||
sdk.NewEvent("message", sdk.NewAttribute("stargate", "something-something")),
|
||||
// we still emit this to the client, but not the contract
|
||||
sdk.NewEvent("non-determinstic"),
|
||||
}
|
||||
return events, [][]byte{[]byte("subData")}, nil
|
||||
},
|
||||
},
|
||||
expData: []byte("subData"),
|
||||
expCommits: []bool{true},
|
||||
expEvents: []sdk.Event{
|
||||
sdk.NewEvent("non-determinstic"),
|
||||
// the event from reply is also exposed
|
||||
sdk.NewEvent("stargate-reply"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var mockStore wasmtesting.MockCommitMultiStore
|
||||
em := sdk.NewEventManager()
|
||||
ctx := sdk.Context{}.WithMultiStore(&mockStore).
|
||||
WithGasMeter(sdk.NewGasMeter(100)).
|
||||
WithEventManager(em).WithLogger(log.TestingLogger())
|
||||
d := NewMessageDispatcher(spec.msgHandler, spec.replyer)
|
||||
gotData, gotErr := d.DispatchSubmessages(ctx, RandomAccountAddress(t), "any_port", spec.msgs)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
assert.Empty(t, em.Events())
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, gotErr)
|
||||
assert.Equal(t, spec.expData, gotData)
|
||||
}
|
||||
assert.Equal(t, spec.expCommits, mockStore.Committed)
|
||||
if len(spec.expEvents) == 0 {
|
||||
assert.Empty(t, em.Events())
|
||||
} else {
|
||||
assert.Equal(t, spec.expEvents, em.Events())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockReplyer struct {
|
||||
replyFn func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error)
|
||||
}
|
||||
|
||||
func (m mockReplyer) reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) {
|
||||
if m.replyFn == nil {
|
||||
panic("not expected to be called")
|
||||
}
|
||||
return m.replyFn(ctx, contractAddress, reply)
|
||||
}
|
256
x/wasm/keeper/msg_server.go
Normal file
@ -0,0 +1,256 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
var _ types.MsgServer = msgServer{}
|
||||
|
||||
type msgServer struct {
|
||||
keeper types.ContractOpsKeeper
|
||||
}
|
||||
|
||||
func NewMsgServerImpl(k types.ContractOpsKeeper) types.MsgServer {
|
||||
return &msgServer{keeper: k}
|
||||
}
|
||||
|
||||
func (m msgServer) StoreCode(goCtx context.Context, msg *types.MsgStoreCode) (*types.MsgStoreCodeResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
senderAddr, err := sdk.AccAddressFromBech32(msg.Sender)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "sender")
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
|
||||
))
|
||||
|
||||
codeID, checksum, err := m.keeper.Create(ctx, senderAddr, msg.WASMByteCode, msg.InstantiatePermission)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.MsgStoreCodeResponse{
|
||||
CodeID: codeID,
|
||||
Checksum: checksum,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InstantiateContract instantiate a new contract with classic sequence based address generation
|
||||
func (m msgServer) InstantiateContract(goCtx context.Context, msg *types.MsgInstantiateContract) (*types.MsgInstantiateContractResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
|
||||
senderAddr, err := sdk.AccAddressFromBech32(msg.Sender)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "sender")
|
||||
}
|
||||
var adminAddr sdk.AccAddress
|
||||
if msg.Admin != "" {
|
||||
if adminAddr, err = sdk.AccAddressFromBech32(msg.Admin); err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "admin")
|
||||
}
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
|
||||
))
|
||||
|
||||
contractAddr, data, err := m.keeper.Instantiate(ctx, msg.CodeID, senderAddr, adminAddr, msg.Msg, msg.Label, msg.Funds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.MsgInstantiateContractResponse{
|
||||
Address: contractAddr.String(),
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InstantiateContract2 instantiate a new contract with predicatable address generated
|
||||
func (m msgServer) InstantiateContract2(goCtx context.Context, msg *types.MsgInstantiateContract2) (*types.MsgInstantiateContract2Response, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
|
||||
senderAddr, err := sdk.AccAddressFromBech32(msg.Sender)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "sender")
|
||||
}
|
||||
var adminAddr sdk.AccAddress
|
||||
if msg.Admin != "" {
|
||||
if adminAddr, err = sdk.AccAddressFromBech32(msg.Admin); err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "admin")
|
||||
}
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
|
||||
))
|
||||
contractAddr, data, err := m.keeper.Instantiate2(ctx, msg.CodeID, senderAddr, adminAddr, msg.Msg, msg.Label, msg.Funds, msg.Salt, msg.FixMsg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.MsgInstantiateContract2Response{
|
||||
Address: contractAddr.String(),
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m msgServer) ExecuteContract(goCtx context.Context, msg *types.MsgExecuteContract) (*types.MsgExecuteContractResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
senderAddr, err := sdk.AccAddressFromBech32(msg.Sender)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "sender")
|
||||
}
|
||||
contractAddr, err := sdk.AccAddressFromBech32(msg.Contract)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "contract")
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
|
||||
))
|
||||
|
||||
data, err := m.keeper.Execute(ctx, contractAddr, senderAddr, msg.Msg, msg.Funds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.MsgExecuteContractResponse{
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m msgServer) MigrateContract(goCtx context.Context, msg *types.MsgMigrateContract) (*types.MsgMigrateContractResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
senderAddr, err := sdk.AccAddressFromBech32(msg.Sender)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "sender")
|
||||
}
|
||||
contractAddr, err := sdk.AccAddressFromBech32(msg.Contract)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "contract")
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
|
||||
))
|
||||
|
||||
data, err := m.keeper.Migrate(ctx, contractAddr, senderAddr, msg.CodeID, msg.Msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.MsgMigrateContractResponse{
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m msgServer) UpdateAdmin(goCtx context.Context, msg *types.MsgUpdateAdmin) (*types.MsgUpdateAdminResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
senderAddr, err := sdk.AccAddressFromBech32(msg.Sender)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "sender")
|
||||
}
|
||||
contractAddr, err := sdk.AccAddressFromBech32(msg.Contract)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "contract")
|
||||
}
|
||||
newAdminAddr, err := sdk.AccAddressFromBech32(msg.NewAdmin)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "new admin")
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
|
||||
))
|
||||
|
||||
if err := m.keeper.UpdateContractAdmin(ctx, contractAddr, senderAddr, newAdminAddr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.MsgUpdateAdminResponse{}, nil
|
||||
}
|
||||
|
||||
func (m msgServer) ClearAdmin(goCtx context.Context, msg *types.MsgClearAdmin) (*types.MsgClearAdminResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
senderAddr, err := sdk.AccAddressFromBech32(msg.Sender)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "sender")
|
||||
}
|
||||
contractAddr, err := sdk.AccAddressFromBech32(msg.Contract)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "contract")
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
|
||||
))
|
||||
|
||||
if err := m.keeper.ClearContractAdmin(ctx, contractAddr, senderAddr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.MsgClearAdminResponse{}, nil
|
||||
}
|
||||
|
||||
func (m msgServer) UpdateInstantiateConfig(goCtx context.Context, msg *types.MsgUpdateInstantiateConfig) (*types.MsgUpdateInstantiateConfigResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
if err := m.keeper.SetAccessConfig(ctx, msg.CodeID, sdk.AccAddress(msg.Sender), *msg.NewInstantiatePermission); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
|
||||
))
|
||||
|
||||
return &types.MsgUpdateInstantiateConfigResponse{}, nil
|
||||
}
|
46
x/wasm/keeper/msg_server_integration_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"testing"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/testutil/testdata"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/app"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
//go:embed testdata/reflect.wasm
|
||||
var wasmContract []byte
|
||||
|
||||
func TestStoreCode(t *testing.T) {
|
||||
wasmApp := app.Setup(false)
|
||||
ctx := wasmApp.BaseApp.NewContext(false, tmproto.Header{})
|
||||
_, _, sender := testdata.KeyTestPubAddr()
|
||||
msg := types.MsgStoreCodeFixture(func(m *types.MsgStoreCode) {
|
||||
m.WASMByteCode = wasmContract
|
||||
m.Sender = sender.String()
|
||||
})
|
||||
|
||||
// when
|
||||
rsp, err := wasmApp.MsgServiceRouter().Handler(msg)(ctx, msg)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
var result types.MsgStoreCodeResponse
|
||||
require.NoError(t, wasmApp.AppCodec().Unmarshal(rsp.Data, &result))
|
||||
assert.Equal(t, uint64(1), result.CodeID)
|
||||
expHash := sha256.Sum256(wasmContract)
|
||||
assert.Equal(t, expHash[:], result.Checksum)
|
||||
// and
|
||||
info := wasmApp.WasmKeeper.GetCodeInfo(ctx, 1)
|
||||
assert.NotNil(t, info)
|
||||
assert.Equal(t, expHash[:], info.CodeHash)
|
||||
assert.Equal(t, sender.String(), info.Creator)
|
||||
assert.Equal(t, types.DefaultParams().InstantiateDefaultPermission.With(sender), info.InstantiateConfig)
|
||||
}
|
170
x/wasm/keeper/options.go
Normal file
@ -0,0 +1,170 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
![]() Sensitive package importCertain system packages contain functions which may be a possible source of non-determinism ## Sensitive package import
Certain system packages contain functions which may be a possible source of non-determinism
[Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/632)
|
||||
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
type optsFn func(*Keeper)
|
||||
|
||||
func (f optsFn) apply(keeper *Keeper) {
|
||||
f(keeper)
|
||||
}
|
||||
|
||||
// WithWasmEngine is an optional constructor parameter to replace the default wasmVM engine with the
|
||||
// given one.
|
||||
func WithWasmEngine(x types.WasmerEngine) Option {
|
||||
return optsFn(func(k *Keeper) {
|
||||
k.wasmVM = x
|
||||
})
|
||||
}
|
||||
|
||||
// WithMessageHandler is an optional constructor parameter to set a custom handler for wasmVM messages.
|
||||
// This option should not be combined with Option `WithMessageEncoders` or `WithMessageHandlerDecorator`
|
||||
func WithMessageHandler(x Messenger) Option {
|
||||
return optsFn(func(k *Keeper) {
|
||||
k.messenger = x
|
||||
})
|
||||
}
|
||||
|
||||
// WithMessageHandlerDecorator is an optional constructor parameter to decorate the wasm handler for wasmVM messages.
|
||||
// This option should not be combined with Option `WithMessageEncoders` or `WithMessageHandler`
|
||||
func WithMessageHandlerDecorator(d func(old Messenger) Messenger) Option {
|
||||
return optsFn(func(k *Keeper) {
|
||||
k.messenger = d(k.messenger)
|
||||
})
|
||||
}
|
||||
|
||||
// WithQueryHandler is an optional constructor parameter to set custom query handler for wasmVM requests.
|
||||
// This option should not be combined with Option `WithQueryPlugins` or `WithQueryHandlerDecorator`
|
||||
func WithQueryHandler(x WasmVMQueryHandler) Option {
|
||||
return optsFn(func(k *Keeper) {
|
||||
k.wasmVMQueryHandler = x
|
||||
})
|
||||
}
|
||||
|
||||
// WithQueryHandlerDecorator is an optional constructor parameter to decorate the default wasm query handler for wasmVM requests.
|
||||
// This option should not be combined with Option `WithQueryPlugins` or `WithQueryHandler`
|
||||
func WithQueryHandlerDecorator(d func(old WasmVMQueryHandler) WasmVMQueryHandler) Option {
|
||||
return optsFn(func(k *Keeper) {
|
||||
k.wasmVMQueryHandler = d(k.wasmVMQueryHandler)
|
||||
})
|
||||
}
|
||||
|
||||
// WithQueryPlugins is an optional constructor parameter to pass custom query plugins for wasmVM requests.
|
||||
// This option expects the default `QueryHandler` set and should not be combined with Option `WithQueryHandler` or `WithQueryHandlerDecorator`.
|
||||
func WithQueryPlugins(x *QueryPlugins) Option {
|
||||
return optsFn(func(k *Keeper) {
|
||||
q, ok := k.wasmVMQueryHandler.(QueryPlugins)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Unsupported query handler type: %T", k.wasmVMQueryHandler))
|
||||
}
|
||||
k.wasmVMQueryHandler = q.Merge(x)
|
||||
})
|
||||
}
|
||||
|
||||
// WithMessageEncoders is an optional constructor parameter to pass custom message encoder to the default wasm message handler.
|
||||
// This option expects the `DefaultMessageHandler` set and should not be combined with Option `WithMessageHandler` or `WithMessageHandlerDecorator`.
|
||||
func WithMessageEncoders(x *MessageEncoders) Option {
|
||||
return optsFn(func(k *Keeper) {
|
||||
q, ok := k.messenger.(*MessageHandlerChain)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Unsupported message handler type: %T", k.messenger))
|
||||
}
|
||||
s, ok := q.handlers[0].(SDKMessageHandler)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Unexpected message handler type: %T", q.handlers[0]))
|
||||
}
|
||||
e, ok := s.encoders.(MessageEncoders)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Unsupported encoder type: %T", s.encoders))
|
||||
}
|
||||
s.encoders = e.Merge(x)
|
||||
q.handlers[0] = s
|
||||
})
|
||||
}
|
||||
|
||||
// WithCoinTransferrer is an optional constructor parameter to set a custom coin transferrer
|
||||
func WithCoinTransferrer(x CoinTransferrer) Option {
|
||||
if x == nil {
|
||||
panic("must not be nil")
|
||||
}
|
||||
return optsFn(func(k *Keeper) {
|
||||
k.bank = x
|
||||
})
|
||||
}
|
||||
|
||||
// WithAccountPruner is an optional constructor parameter to set a custom type that handles balances and data cleanup
|
||||
// for accounts pruned on contract instantiate
|
||||
func WithAccountPruner(x AccountPruner) Option {
|
||||
if x == nil {
|
||||
panic("must not be nil")
|
||||
}
|
||||
return optsFn(func(k *Keeper) {
|
||||
k.accountPruner = x
|
||||
})
|
||||
}
|
||||
|
||||
func WithVMCacheMetrics(r prometheus.Registerer) Option {
|
||||
return optsFn(func(k *Keeper) {
|
||||
NewWasmVMMetricsCollector(k.wasmVM).Register(r)
|
||||
})
|
||||
}
|
||||
|
||||
// WithGasRegister set a new gas register to implement custom gas costs.
|
||||
// When the "gas multiplier" for wasmvm gas conversion is modified inside the new register,
|
||||
// make sure to also use `WithApiCosts` option for non default values
|
||||
func WithGasRegister(x GasRegister) Option {
|
||||
if x == nil {
|
||||
panic("must not be nil")
|
||||
}
|
||||
return optsFn(func(k *Keeper) {
|
||||
k.gasRegister = x
|
||||
})
|
||||
}
|
||||
|
||||
// WithAPICosts sets custom api costs. Amounts are in cosmwasm gas Not SDK gas.
|
||||
func WithAPICosts(human, canonical uint64) Option {
|
||||
return optsFn(func(_ *Keeper) {
|
||||
costHumanize = human
|
||||
costCanonical = canonical
|
||||
})
|
||||
}
|
||||
|
||||
// WithMaxQueryStackSize overwrites the default limit for maximum query stacks
|
||||
func WithMaxQueryStackSize(m uint32) Option {
|
||||
return optsFn(func(k *Keeper) {
|
||||
k.maxQueryStackSize = m
|
||||
})
|
||||
}
|
||||
|
||||
// WithAcceptedAccountTypesOnContractInstantiation sets the accepted account types. Account types of this list won't be overwritten or cause a failure
|
||||
// when they exist for an address on contract instantiation.
|
||||
//
|
||||
// Values should be references and contain the `*authtypes.BaseAccount` as default bank account type.
|
||||
func WithAcceptedAccountTypesOnContractInstantiation(accts ...authtypes.AccountI) Option {
|
||||
m := asTypeMap(accts)
|
||||
return optsFn(func(k *Keeper) {
|
||||
k.acceptedAccountTypes = m
|
||||
})
|
||||
}
|
||||
|
||||
func asTypeMap(accts []authtypes.AccountI) map[reflect.Type]struct{} {
|
||||
m := make(map[reflect.Type]struct{}, len(accts))
|
||||
for _, a := range accts {
|
||||
if a == nil {
|
||||
panic(types.ErrEmpty.Wrap("address"))
|
||||
}
|
||||
at := reflect.TypeOf(a)
|
||||
if _, exists := m[at]; exists {
|
||||
panic(types.ErrDuplicate.Wrapf("%T", a))
|
||||
}
|
||||
m[at] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
116
x/wasm/keeper/options_test.go
Normal file
@ -0,0 +1,116 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
|
||||
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
||||
distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper"
|
||||
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
||||
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestConstructorOptions(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
srcOpt Option
|
||||
verify func(*testing.T, Keeper)
|
||||
}{
|
||||
"wasm engine": {
|
||||
srcOpt: WithWasmEngine(&wasmtesting.MockWasmer{}),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
assert.IsType(t, &wasmtesting.MockWasmer{}, k.wasmVM)
|
||||
},
|
||||
},
|
||||
"message handler": {
|
||||
srcOpt: WithMessageHandler(&wasmtesting.MockMessageHandler{}),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
assert.IsType(t, &wasmtesting.MockMessageHandler{}, k.messenger)
|
||||
},
|
||||
},
|
||||
"query plugins": {
|
||||
srcOpt: WithQueryHandler(&wasmtesting.MockQueryHandler{}),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
assert.IsType(t, &wasmtesting.MockQueryHandler{}, k.wasmVMQueryHandler)
|
||||
},
|
||||
},
|
||||
"message handler decorator": {
|
||||
srcOpt: WithMessageHandlerDecorator(func(old Messenger) Messenger {
|
||||
require.IsType(t, &MessageHandlerChain{}, old)
|
||||
return &wasmtesting.MockMessageHandler{}
|
||||
}),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
assert.IsType(t, &wasmtesting.MockMessageHandler{}, k.messenger)
|
||||
},
|
||||
},
|
||||
"query plugins decorator": {
|
||||
srcOpt: WithQueryHandlerDecorator(func(old WasmVMQueryHandler) WasmVMQueryHandler {
|
||||
require.IsType(t, QueryPlugins{}, old)
|
||||
return &wasmtesting.MockQueryHandler{}
|
||||
}),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
assert.IsType(t, &wasmtesting.MockQueryHandler{}, k.wasmVMQueryHandler)
|
||||
},
|
||||
},
|
||||
"coin transferrer": {
|
||||
srcOpt: WithCoinTransferrer(&wasmtesting.MockCoinTransferrer{}),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
assert.IsType(t, &wasmtesting.MockCoinTransferrer{}, k.bank)
|
||||
},
|
||||
},
|
||||
"costs": {
|
||||
srcOpt: WithGasRegister(&wasmtesting.MockGasRegister{}),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
assert.IsType(t, &wasmtesting.MockGasRegister{}, k.gasRegister)
|
||||
},
|
||||
},
|
||||
"api costs": {
|
||||
srcOpt: WithAPICosts(1, 2),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
t.Cleanup(setApiDefaults)
|
||||
assert.Equal(t, uint64(1), costHumanize)
|
||||
assert.Equal(t, uint64(2), costCanonical)
|
||||
},
|
||||
},
|
||||
"max recursion query limit": {
|
||||
srcOpt: WithMaxQueryStackSize(1),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
assert.IsType(t, uint32(1), k.maxQueryStackSize)
|
||||
},
|
||||
},
|
||||
"accepted account types": {
|
||||
srcOpt: WithAcceptedAccountTypesOnContractInstantiation(&authtypes.BaseAccount{}, &vestingtypes.ContinuousVestingAccount{}),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
exp := map[reflect.Type]struct{}{
|
||||
reflect.TypeOf(&authtypes.BaseAccount{}): {},
|
||||
reflect.TypeOf(&vestingtypes.ContinuousVestingAccount{}): {},
|
||||
}
|
||||
assert.Equal(t, exp, k.acceptedAccountTypes)
|
||||
},
|
||||
},
|
||||
"account pruner": {
|
||||
srcOpt: WithAccountPruner(VestingCoinBurner{}),
|
||||
verify: func(t *testing.T, k Keeper) {
|
||||
assert.Equal(t, VestingCoinBurner{}, k.accountPruner)
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
k := NewKeeper(nil, nil, paramtypes.NewSubspace(nil, nil, nil, nil, ""), authkeeper.AccountKeeper{}, &bankkeeper.BaseKeeper{}, stakingkeeper.Keeper{}, distributionkeeper.Keeper{}, nil, nil, nil, nil, nil, nil, "tempDir", types.DefaultWasmConfig(), AvailableCapabilities, spec.srcOpt)
|
||||
spec.verify(t, k)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setApiDefaults() {
|
||||
costHumanize = DefaultGasCostHumanAddress * DefaultGasMultiplier
|
||||
costCanonical = DefaultGasCostCanonicalAddress * DefaultGasMultiplier
|
||||
}
|
326
x/wasm/keeper/proposal_handler.go
Normal file
@ -0,0 +1,326 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// NewWasmProposalHandler creates a new governance Handler for wasm proposals
|
||||
func NewWasmProposalHandler(k decoratedKeeper, enabledProposalTypes []types.ProposalType) govtypes.Handler {
|
||||
return NewWasmProposalHandlerX(NewGovPermissionKeeper(k), enabledProposalTypes)
|
||||
}
|
||||
|
||||
// NewWasmProposalHandlerX creates a new governance Handler for wasm proposals
|
||||
func NewWasmProposalHandlerX(k types.ContractOpsKeeper, enabledProposalTypes []types.ProposalType) govtypes.Handler {
|
||||
enabledTypes := make(map[string]struct{}, len(enabledProposalTypes))
|
||||
for i := range enabledProposalTypes {
|
||||
enabledTypes[string(enabledProposalTypes[i])] = struct{}{}
|
||||
}
|
||||
return func(ctx sdk.Context, content govtypes.Content) error {
|
||||
if content == nil {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "content must not be empty")
|
||||
}
|
||||
if _, ok := enabledTypes[content.ProposalType()]; !ok {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unsupported wasm proposal content type: %q", content.ProposalType())
|
||||
}
|
||||
switch c := content.(type) {
|
||||
case *types.StoreCodeProposal:
|
||||
return handleStoreCodeProposal(ctx, k, *c)
|
||||
case *types.InstantiateContractProposal:
|
||||
return handleInstantiateProposal(ctx, k, *c)
|
||||
case *types.InstantiateContract2Proposal:
|
||||
return handleInstantiate2Proposal(ctx, k, *c)
|
||||
case *types.MigrateContractProposal:
|
||||
return handleMigrateProposal(ctx, k, *c)
|
||||
case *types.SudoContractProposal:
|
||||
return handleSudoProposal(ctx, k, *c)
|
||||
case *types.ExecuteContractProposal:
|
||||
return handleExecuteProposal(ctx, k, *c)
|
||||
case *types.UpdateAdminProposal:
|
||||
return handleUpdateAdminProposal(ctx, k, *c)
|
||||
case *types.ClearAdminProposal:
|
||||
return handleClearAdminProposal(ctx, k, *c)
|
||||
case *types.PinCodesProposal:
|
||||
return handlePinCodesProposal(ctx, k, *c)
|
||||
case *types.UnpinCodesProposal:
|
||||
return handleUnpinCodesProposal(ctx, k, *c)
|
||||
case *types.UpdateInstantiateConfigProposal:
|
||||
return handleUpdateInstantiateConfigProposal(ctx, k, *c)
|
||||
case *types.StoreAndInstantiateContractProposal:
|
||||
return handleStoreAndInstantiateContractProposal(ctx, k, *c)
|
||||
default:
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized wasm proposal content type: %T", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleStoreCodeProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.StoreCodeProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "run as address")
|
||||
}
|
||||
codeID, checksum, err := k.Create(ctx, runAsAddr, p.WASMByteCode, p.InstantiatePermission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(p.CodeHash) != 0 && !bytes.Equal(checksum, p.CodeHash) {
|
||||
return fmt.Errorf("code-hash mismatch: %X, checksum: %X", p.CodeHash, checksum)
|
||||
}
|
||||
|
||||
// if code should not be pinned return earlier
|
||||
if p.UnpinCode {
|
||||
return nil
|
||||
}
|
||||
return k.PinCode(ctx, codeID)
|
||||
}
|
||||
|
||||
func handleInstantiateProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.InstantiateContractProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "run as address")
|
||||
}
|
||||
var adminAddr sdk.AccAddress
|
||||
if p.Admin != "" {
|
||||
if adminAddr, err = sdk.AccAddressFromBech32(p.Admin); err != nil {
|
||||
return sdkerrors.Wrap(err, "admin")
|
||||
}
|
||||
}
|
||||
|
||||
_, data, err := k.Instantiate(ctx, p.CodeID, runAsAddr, adminAddr, p.Msg, p.Label, p.Funds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
types.EventTypeGovContractResult,
|
||||
sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleInstantiate2Proposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.InstantiateContract2Proposal) error {
|
||||
// Validatebasic with proposal
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get runAsAddr as AccAddress
|
||||
runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "run as address")
|
||||
}
|
||||
|
||||
// Get admin address
|
||||
var adminAddr sdk.AccAddress
|
||||
if p.Admin != "" {
|
||||
if adminAddr, err = sdk.AccAddressFromBech32(p.Admin); err != nil {
|
||||
return sdkerrors.Wrap(err, "admin")
|
||||
}
|
||||
}
|
||||
|
||||
_, data, err := k.Instantiate2(ctx, p.CodeID, runAsAddr, adminAddr, p.Msg, p.Label, p.Funds, p.Salt, p.FixMsg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
types.EventTypeGovContractResult,
|
||||
sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleStoreAndInstantiateContractProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.StoreAndInstantiateContractProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "run as address")
|
||||
}
|
||||
var adminAddr sdk.AccAddress
|
||||
if p.Admin != "" {
|
||||
if adminAddr, err = sdk.AccAddressFromBech32(p.Admin); err != nil {
|
||||
return sdkerrors.Wrap(err, "admin")
|
||||
}
|
||||
}
|
||||
|
||||
codeID, checksum, err := k.Create(ctx, runAsAddr, p.WASMByteCode, p.InstantiatePermission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.CodeHash != nil && !bytes.Equal(checksum, p.CodeHash) {
|
||||
return sdkerrors.Wrap(fmt.Errorf("code-hash mismatch: %X, checksum: %X", p.CodeHash, checksum), "code-hash mismatch")
|
||||
}
|
||||
|
||||
if !p.UnpinCode {
|
||||
if err := k.PinCode(ctx, codeID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, data, err := k.Instantiate(ctx, codeID, runAsAddr, adminAddr, p.Msg, p.Label, p.Funds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
types.EventTypeGovContractResult,
|
||||
sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMigrateProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.MigrateContractProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contractAddr, err := sdk.AccAddressFromBech32(p.Contract)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "contract")
|
||||
}
|
||||
|
||||
// runAs is not used if this is permissioned, so just put any valid address there (second contractAddr)
|
||||
data, err := k.Migrate(ctx, contractAddr, contractAddr, p.CodeID, p.Msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
types.EventTypeGovContractResult,
|
||||
sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSudoProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.SudoContractProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contractAddr, err := sdk.AccAddressFromBech32(p.Contract)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "contract")
|
||||
}
|
||||
data, err := k.Sudo(ctx, contractAddr, p.Msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
types.EventTypeGovContractResult,
|
||||
sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleExecuteProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.ExecuteContractProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contractAddr, err := sdk.AccAddressFromBech32(p.Contract)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "contract")
|
||||
}
|
||||
runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "run as address")
|
||||
}
|
||||
data, err := k.Execute(ctx, contractAddr, runAsAddr, p.Msg, p.Funds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
types.EventTypeGovContractResult,
|
||||
sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleUpdateAdminProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.UpdateAdminProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
contractAddr, err := sdk.AccAddressFromBech32(p.Contract)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "contract")
|
||||
}
|
||||
newAdminAddr, err := sdk.AccAddressFromBech32(p.NewAdmin)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "run as address")
|
||||
}
|
||||
|
||||
return k.UpdateContractAdmin(ctx, contractAddr, nil, newAdminAddr)
|
||||
}
|
||||
|
||||
func handleClearAdminProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.ClearAdminProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contractAddr, err := sdk.AccAddressFromBech32(p.Contract)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(err, "contract")
|
||||
}
|
||||
if err := k.ClearContractAdmin(ctx, contractAddr, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handlePinCodesProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.PinCodesProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range p.CodeIDs {
|
||||
if err := k.PinCode(ctx, v); err != nil {
|
||||
return sdkerrors.Wrapf(err, "code id: %d", v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleUnpinCodesProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.UnpinCodesProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range p.CodeIDs {
|
||||
if err := k.UnpinCode(ctx, v); err != nil {
|
||||
return sdkerrors.Wrapf(err, "code id: %d", v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleUpdateInstantiateConfigProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.UpdateInstantiateConfigProposal) error {
|
||||
if err := p.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var emptyCaller sdk.AccAddress
|
||||
for _, accessConfigUpdate := range p.AccessConfigUpdates {
|
||||
if err := k.SetAccessConfig(ctx, accessConfigUpdate.CodeID, emptyCaller, accessConfigUpdate.InstantiatePermission); err != nil {
|
||||
return sdkerrors.Wrapf(err, "code id: %d", accessConfigUpdate.CodeID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
1001
x/wasm/keeper/proposal_integration_test.go
Normal file
346
x/wasm/keeper/querier.go
Normal file
@ -0,0 +1,346 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/types/query"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
var _ types.QueryServer = &grpcQuerier{}
|
||||
|
||||
type grpcQuerier struct {
|
||||
cdc codec.Codec
|
||||
storeKey sdk.StoreKey
|
||||
keeper types.ViewKeeper
|
||||
queryGasLimit sdk.Gas
|
||||
}
|
||||
|
||||
// NewGrpcQuerier constructor
|
||||
func NewGrpcQuerier(cdc codec.Codec, storeKey sdk.StoreKey, keeper types.ViewKeeper, queryGasLimit sdk.Gas) *grpcQuerier { //nolint:revive
|
||||
return &grpcQuerier{cdc: cdc, storeKey: storeKey, keeper: keeper, queryGasLimit: queryGasLimit}
|
||||
}
|
||||
|
||||
func (q grpcQuerier) ContractInfo(c context.Context, req *types.QueryContractInfoRequest) (*types.QueryContractInfoResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
contractAddr, err := sdk.AccAddressFromBech32(req.Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsp, err := queryContractInfo(sdk.UnwrapSDKContext(c), contractAddr, q.keeper)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case rsp == nil:
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
func (q grpcQuerier) ContractHistory(c context.Context, req *types.QueryContractHistoryRequest) (*types.QueryContractHistoryResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
contractAddr, err := sdk.AccAddressFromBech32(req.Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := sdk.UnwrapSDKContext(c)
|
||||
r := make([]types.ContractCodeHistoryEntry, 0)
|
||||
|
||||
prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.GetContractCodeHistoryElementPrefix(contractAddr))
|
||||
pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, value []byte, accumulate bool) (bool, error) {
|
||||
if accumulate {
|
||||
var e types.ContractCodeHistoryEntry
|
||||
if err := q.cdc.Unmarshal(value, &e); err != nil {
|
||||
return false, err
|
||||
}
|
||||
r = append(r, e)
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.QueryContractHistoryResponse{
|
||||
Entries: r,
|
||||
Pagination: pageRes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ContractsByCode lists all smart contracts for a code id
|
||||
func (q grpcQuerier) ContractsByCode(c context.Context, req *types.QueryContractsByCodeRequest) (*types.QueryContractsByCodeResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
if req.CodeId == 0 {
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalid, "code id")
|
||||
}
|
||||
ctx := sdk.UnwrapSDKContext(c)
|
||||
r := make([]string, 0)
|
||||
|
||||
prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.GetContractByCodeIDSecondaryIndexPrefix(req.CodeId))
|
||||
pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, value []byte, accumulate bool) (bool, error) {
|
||||
if accumulate {
|
||||
var contractAddr sdk.AccAddress = key[types.AbsoluteTxPositionLen:]
|
||||
r = append(r, contractAddr.String())
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.QueryContractsByCodeResponse{
|
||||
Contracts: r,
|
||||
Pagination: pageRes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q grpcQuerier) AllContractState(c context.Context, req *types.QueryAllContractStateRequest) (*types.QueryAllContractStateResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
contractAddr, err := sdk.AccAddressFromBech32(req.Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := sdk.UnwrapSDKContext(c)
|
||||
if !q.keeper.HasContractInfo(ctx, contractAddr) {
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
|
||||
r := make([]types.Model, 0)
|
||||
prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.GetContractStorePrefix(contractAddr))
|
||||
pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, value []byte, accumulate bool) (bool, error) {
|
||||
if accumulate {
|
||||
r = append(r, types.Model{
|
||||
Key: key,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.QueryAllContractStateResponse{
|
||||
Models: r,
|
||||
Pagination: pageRes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q grpcQuerier) RawContractState(c context.Context, req *types.QueryRawContractStateRequest) (*types.QueryRawContractStateResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
ctx := sdk.UnwrapSDKContext(c)
|
||||
|
||||
contractAddr, err := sdk.AccAddressFromBech32(req.Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !q.keeper.HasContractInfo(ctx, contractAddr) {
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
rsp := q.keeper.QueryRaw(ctx, contractAddr, req.QueryData)
|
||||
return &types.QueryRawContractStateResponse{Data: rsp}, nil
|
||||
}
|
||||
|
||||
func (q grpcQuerier) SmartContractState(c context.Context, req *types.QuerySmartContractStateRequest) (rsp *types.QuerySmartContractStateResponse, err error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
if err := req.QueryData.ValidateBasic(); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid query data")
|
||||
}
|
||||
contractAddr, err := sdk.AccAddressFromBech32(req.Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := sdk.UnwrapSDKContext(c).WithGasMeter(sdk.NewGasMeter(q.queryGasLimit))
|
||||
// recover from out-of-gas panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
switch rType := r.(type) {
|
||||
case sdk.ErrorOutOfGas:
|
||||
err = sdkerrors.Wrapf(sdkerrors.ErrOutOfGas,
|
||||
"out of gas in location: %v; gasWanted: %d, gasUsed: %d",
|
||||
rType.Descriptor, ctx.GasMeter().Limit(), ctx.GasMeter().GasConsumed(),
|
||||
)
|
||||
default:
|
||||
err = sdkerrors.ErrPanic
|
||||
}
|
||||
rsp = nil
|
||||
moduleLogger(ctx).
|
||||
Debug("smart query contract",
|
||||
"error", "recovering panic",
|
||||
"contract-address", req.Address,
|
||||
"stacktrace", string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
bz, err := q.keeper.QuerySmart(ctx, contractAddr, req.QueryData)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case bz == nil:
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
return &types.QuerySmartContractStateResponse{Data: bz}, nil
|
||||
}
|
||||
|
||||
func (q grpcQuerier) Code(c context.Context, req *types.QueryCodeRequest) (*types.QueryCodeResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
if req.CodeId == 0 {
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalid, "code id")
|
||||
}
|
||||
rsp, err := queryCode(sdk.UnwrapSDKContext(c), req.CodeId, q.keeper)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case rsp == nil:
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
return &types.QueryCodeResponse{
|
||||
CodeInfoResponse: rsp.CodeInfoResponse,
|
||||
Data: rsp.Data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q grpcQuerier) Codes(c context.Context, req *types.QueryCodesRequest) (*types.QueryCodesResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
ctx := sdk.UnwrapSDKContext(c)
|
||||
r := make([]types.CodeInfoResponse, 0)
|
||||
prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.CodeKeyPrefix)
|
||||
pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, value []byte, accumulate bool) (bool, error) {
|
||||
if accumulate {
|
||||
var c types.CodeInfo
|
||||
if err := q.cdc.Unmarshal(value, &c); err != nil {
|
||||
return false, err
|
||||
}
|
||||
r = append(r, types.CodeInfoResponse{
|
||||
CodeID: binary.BigEndian.Uint64(key),
|
||||
Creator: c.Creator,
|
||||
DataHash: c.CodeHash,
|
||||
InstantiatePermission: c.InstantiateConfig,
|
||||
})
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.QueryCodesResponse{CodeInfos: r, Pagination: pageRes}, nil
|
||||
}
|
||||
|
||||
func queryContractInfo(ctx sdk.Context, addr sdk.AccAddress, keeper types.ViewKeeper) (*types.QueryContractInfoResponse, error) {
|
||||
info := keeper.GetContractInfo(ctx, addr)
|
||||
if info == nil {
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
return &types.QueryContractInfoResponse{
|
||||
Address: addr.String(),
|
||||
ContractInfo: *info,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func queryCode(ctx sdk.Context, codeID uint64, keeper types.ViewKeeper) (*types.QueryCodeResponse, error) {
|
||||
if codeID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
res := keeper.GetCodeInfo(ctx, codeID)
|
||||
if res == nil {
|
||||
// nil, nil leads to 404 in rest handler
|
||||
return nil, nil
|
||||
}
|
||||
info := types.CodeInfoResponse{
|
||||
CodeID: codeID,
|
||||
Creator: res.Creator,
|
||||
DataHash: res.CodeHash,
|
||||
InstantiatePermission: res.InstantiateConfig,
|
||||
}
|
||||
|
||||
code, err := keeper.GetByteCode(ctx, codeID)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "loading wasm code")
|
||||
}
|
||||
|
||||
return &types.QueryCodeResponse{CodeInfoResponse: &info, Data: code}, nil
|
||||
}
|
||||
|
||||
func (q grpcQuerier) PinnedCodes(c context.Context, req *types.QueryPinnedCodesRequest) (*types.QueryPinnedCodesResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
ctx := sdk.UnwrapSDKContext(c)
|
||||
r := make([]uint64, 0)
|
||||
|
||||
prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.PinnedCodeIndexPrefix)
|
||||
pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, _ []byte, accumulate bool) (bool, error) {
|
||||
if accumulate {
|
||||
r = append(r, sdk.BigEndianToUint64(key))
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.QueryPinnedCodesResponse{
|
||||
CodeIDs: r,
|
||||
Pagination: pageRes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Params returns params of the module.
|
||||
func (q grpcQuerier) Params(c context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) {
|
||||
ctx := sdk.UnwrapSDKContext(c)
|
||||
params := q.keeper.GetParams(ctx)
|
||||
return &types.QueryParamsResponse{Params: params}, nil
|
||||
}
|
||||
|
||||
func (q grpcQuerier) ContractsByCreator(c context.Context, req *types.QueryContractsByCreatorRequest) (*types.QueryContractsByCreatorResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
ctx := sdk.UnwrapSDKContext(c)
|
||||
contracts := make([]string, 0)
|
||||
|
||||
creatorAddress, err := sdk.AccAddressFromBech32(req.CreatorAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.GetContractsByCreatorPrefix(creatorAddress))
|
||||
pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, _ []byte, accumulate bool) (bool, error) {
|
||||
if accumulate {
|
||||
accAddres := sdk.AccAddress(key[types.AbsoluteTxPositionLen:])
|
||||
contracts = append(contracts, accAddres.String())
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.QueryContractsByCreatorResponse{
|
||||
ContractAddresses: contracts,
|
||||
Pagination: pageRes,
|
||||
}, nil
|
||||
}
|
917
x/wasm/keeper/querier_test.go
Normal file
@ -0,0 +1,917 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
wasmvm "github.com/CosmWasm/wasmvm"
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkErrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/types/query"
|
||||
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestQueryAllContractState(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers)
|
||||
contractAddr := exampleContract.Contract
|
||||
contractModel := []types.Model{
|
||||
{Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)},
|
||||
{Key: []byte("foo"), Value: []byte(`"bar"`)},
|
||||
}
|
||||
require.NoError(t, keeper.importContractState(ctx, contractAddr, contractModel))
|
||||
|
||||
q := Querier(keeper)
|
||||
specs := map[string]struct {
|
||||
srcQuery *types.QueryAllContractStateRequest
|
||||
expModelContains []types.Model
|
||||
expModelContainsNot []types.Model
|
||||
expErr *sdkErrors.Error
|
||||
}{
|
||||
"query all": {
|
||||
srcQuery: &types.QueryAllContractStateRequest{Address: contractAddr.String()},
|
||||
expModelContains: contractModel,
|
||||
},
|
||||
"query all with unknown address": {
|
||||
srcQuery: &types.QueryAllContractStateRequest{Address: RandomBech32AccountAddress(t)},
|
||||
expErr: types.ErrNotFound,
|
||||
},
|
||||
"with pagination offset": {
|
||||
srcQuery: &types.QueryAllContractStateRequest{
|
||||
Address: contractAddr.String(),
|
||||
Pagination: &query.PageRequest{
|
||||
Offset: 1,
|
||||
},
|
||||
},
|
||||
expModelContains: []types.Model{
|
||||
{Key: []byte("foo"), Value: []byte(`"bar"`)},
|
||||
},
|
||||
expModelContainsNot: []types.Model{
|
||||
{Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)},
|
||||
},
|
||||
},
|
||||
"with pagination limit": {
|
||||
srcQuery: &types.QueryAllContractStateRequest{
|
||||
Address: contractAddr.String(),
|
||||
Pagination: &query.PageRequest{
|
||||
Limit: 1,
|
||||
},
|
||||
},
|
||||
expModelContains: []types.Model{
|
||||
{Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)},
|
||||
},
|
||||
expModelContainsNot: []types.Model{
|
||||
{Key: []byte("foo"), Value: []byte(`"bar"`)},
|
||||
},
|
||||
},
|
||||
"with pagination next key": {
|
||||
srcQuery: &types.QueryAllContractStateRequest{
|
||||
Address: contractAddr.String(),
|
||||
Pagination: &query.PageRequest{
|
||||
Key: fromBase64("Y29uZmln"),
|
||||
},
|
||||
},
|
||||
expModelContains: []types.Model{
|
||||
{Key: []byte("foo"), Value: []byte(`"bar"`)},
|
||||
},
|
||||
expModelContainsNot: []types.Model{
|
||||
{Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)},
|
||||
},
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
got, err := q.AllContractState(sdk.WrapSDKContext(ctx), spec.srcQuery)
|
||||
require.True(t, spec.expErr.Is(err), err)
|
||||
if spec.expErr != nil {
|
||||
return
|
||||
}
|
||||
for _, exp := range spec.expModelContains {
|
||||
assert.Contains(t, got.Models, exp)
|
||||
}
|
||||
for _, exp := range spec.expModelContainsNot {
|
||||
assert.NotContains(t, got.Models, exp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuerySmartContractState(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers)
|
||||
contractAddr := exampleContract.Contract.String()
|
||||
|
||||
q := Querier(keeper)
|
||||
specs := map[string]struct {
|
||||
srcAddr sdk.AccAddress
|
||||
srcQuery *types.QuerySmartContractStateRequest
|
||||
expResp string
|
||||
expErr error
|
||||
}{
|
||||
"query smart": {
|
||||
srcQuery: &types.QuerySmartContractStateRequest{Address: contractAddr, QueryData: []byte(`{"verifier":{}}`)},
|
||||
expResp: fmt.Sprintf(`{"verifier":"%s"}`, exampleContract.VerifierAddr.String()),
|
||||
},
|
||||
"query smart invalid request": {
|
||||
srcQuery: &types.QuerySmartContractStateRequest{Address: contractAddr, QueryData: []byte(`{"raw":{"key":"config"}}`)},
|
||||
expErr: types.ErrQueryFailed,
|
||||
},
|
||||
"query smart with invalid json": {
|
||||
srcQuery: &types.QuerySmartContractStateRequest{Address: contractAddr, QueryData: []byte(`not a json string`)},
|
||||
expErr: status.Error(codes.InvalidArgument, "invalid query data"),
|
||||
},
|
||||
"query smart with unknown address": {
|
||||
srcQuery: &types.QuerySmartContractStateRequest{Address: RandomBech32AccountAddress(t), QueryData: []byte(`{"verifier":{}}`)},
|
||||
expErr: types.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
got, err := q.SmartContractState(sdk.WrapSDKContext(ctx), spec.srcQuery)
|
||||
require.True(t, errors.Is(err, spec.expErr), "but got %+v", err)
|
||||
if spec.expErr != nil {
|
||||
return
|
||||
}
|
||||
assert.JSONEq(t, string(got.Data), spec.expResp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuerySmartContractPanics(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
contractAddr := BuildContractAddressClassic(1, 1)
|
||||
keepers.WasmKeeper.storeCodeInfo(ctx, 1, types.CodeInfo{})
|
||||
keepers.WasmKeeper.storeContractInfo(ctx, contractAddr, &types.ContractInfo{
|
||||
CodeID: 1,
|
||||
Created: types.NewAbsoluteTxPosition(ctx),
|
||||
})
|
||||
ctx = ctx.WithGasMeter(sdk.NewGasMeter(DefaultInstanceCost)).WithLogger(log.TestingLogger())
|
||||
|
||||
specs := map[string]struct {
|
||||
doInContract func()
|
||||
expErr *sdkErrors.Error
|
||||
}{
|
||||
"out of gas": {
|
||||
doInContract: func() {
|
||||
ctx.GasMeter().ConsumeGas(ctx.GasMeter().Limit()+1, "test - consume more than limit")
|
||||
},
|
||||
expErr: sdkErrors.ErrOutOfGas,
|
||||
},
|
||||
"other panic": {
|
||||
doInContract: func() {
|
||||
panic("my panic")
|
||||
},
|
||||
expErr: sdkErrors.ErrPanic,
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
keepers.WasmKeeper.wasmVM = &wasmtesting.MockWasmer{QueryFn: func(checksum wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) {
|
||||
spec.doInContract()
|
||||
return nil, 0, nil
|
||||
}}
|
||||
// when
|
||||
q := Querier(keepers.WasmKeeper)
|
||||
got, err := q.SmartContractState(sdk.WrapSDKContext(ctx), &types.QuerySmartContractStateRequest{
|
||||
Address: contractAddr.String(),
|
||||
QueryData: types.RawContractMessage("{}"),
|
||||
})
|
||||
require.True(t, spec.expErr.Is(err), "got error: %+v", err)
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRawContractState(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers)
|
||||
contractAddr := exampleContract.Contract.String()
|
||||
contractModel := []types.Model{
|
||||
{Key: []byte("foo"), Value: []byte(`"bar"`)},
|
||||
{Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)},
|
||||
}
|
||||
require.NoError(t, keeper.importContractState(ctx, exampleContract.Contract, contractModel))
|
||||
|
||||
q := Querier(keeper)
|
||||
specs := map[string]struct {
|
||||
srcQuery *types.QueryRawContractStateRequest
|
||||
expData []byte
|
||||
expErr *sdkErrors.Error
|
||||
}{
|
||||
"query raw key": {
|
||||
srcQuery: &types.QueryRawContractStateRequest{Address: contractAddr, QueryData: []byte("foo")},
|
||||
expData: []byte(`"bar"`),
|
||||
},
|
||||
"query raw contract binary key": {
|
||||
srcQuery: &types.QueryRawContractStateRequest{Address: contractAddr, QueryData: []byte{0x0, 0x1}},
|
||||
expData: []byte(`{"count":8}`),
|
||||
},
|
||||
"query non-existent raw key": {
|
||||
srcQuery: &types.QueryRawContractStateRequest{Address: contractAddr, QueryData: []byte("not existing key")},
|
||||
expData: nil,
|
||||
},
|
||||
"query empty raw key": {
|
||||
srcQuery: &types.QueryRawContractStateRequest{Address: contractAddr, QueryData: []byte("")},
|
||||
expData: nil,
|
||||
},
|
||||
"query nil raw key": {
|
||||
srcQuery: &types.QueryRawContractStateRequest{Address: contractAddr},
|
||||
expData: nil,
|
||||
},
|
||||
"query raw with unknown address": {
|
||||
srcQuery: &types.QueryRawContractStateRequest{Address: RandomBech32AccountAddress(t), QueryData: []byte("foo")},
|
||||
expErr: types.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
got, err := q.RawContractState(sdk.WrapSDKContext(ctx), spec.srcQuery)
|
||||
require.True(t, spec.expErr.Is(err), err)
|
||||
if spec.expErr != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, spec.expData, got.Data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryContractListByCodeOrdering(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000000))
|
||||
topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 500))
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
anyAddr := keepers.Faucet.NewFundedRandomAccount(ctx, topUp...)
|
||||
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, wasmCode, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, bob := keyPubAddr()
|
||||
initMsg := HackatomExampleInitMsg{
|
||||
Verifier: anyAddr,
|
||||
Beneficiary: bob,
|
||||
}
|
||||
initMsgBz, err := json.Marshal(initMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// manage some realistic block settings
|
||||
var h int64 = 10
|
||||
setBlock := func(ctx sdk.Context, height int64) sdk.Context {
|
||||
ctx = ctx.WithBlockHeight(height)
|
||||
meter := sdk.NewGasMeter(1000000)
|
||||
ctx = ctx.WithGasMeter(meter)
|
||||
ctx = ctx.WithBlockGasMeter(meter)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// create 10 contracts with real block/gas setup
|
||||
for i := 0; i < 10; i++ {
|
||||
// 3 tx per block, so we ensure both comparisons work
|
||||
if i%3 == 0 {
|
||||
ctx = setBlock(ctx, h)
|
||||
h++
|
||||
}
|
||||
_, _, err = keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, fmt.Sprintf("contract %d", i), topUp)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// query and check the results are properly sorted
|
||||
q := Querier(keeper)
|
||||
res, err := q.ContractsByCode(sdk.WrapSDKContext(ctx), &types.QueryContractsByCodeRequest{CodeId: codeID})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 10, len(res.Contracts))
|
||||
|
||||
for _, contractAddr := range res.Contracts {
|
||||
assert.NotEmpty(t, contractAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryContractHistory(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
var (
|
||||
myContractBech32Addr = RandomBech32AccountAddress(t)
|
||||
otherBech32Addr = RandomBech32AccountAddress(t)
|
||||
)
|
||||
|
||||
specs := map[string]struct {
|
||||
srcHistory []types.ContractCodeHistoryEntry
|
||||
req types.QueryContractHistoryRequest
|
||||
expContent []types.ContractCodeHistoryEntry
|
||||
}{
|
||||
"response with internal fields cleared": {
|
||||
srcHistory: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeGenesis,
|
||||
CodeID: firstCodeID,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2},
|
||||
Msg: []byte(`"init message"`),
|
||||
}},
|
||||
req: types.QueryContractHistoryRequest{Address: myContractBech32Addr},
|
||||
expContent: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeGenesis,
|
||||
CodeID: firstCodeID,
|
||||
Msg: []byte(`"init message"`),
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2},
|
||||
}},
|
||||
},
|
||||
"response with multiple entries": {
|
||||
srcHistory: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeInit,
|
||||
CodeID: firstCodeID,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2},
|
||||
Msg: []byte(`"init message"`),
|
||||
}, {
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 2,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 3, TxIndex: 4},
|
||||
Msg: []byte(`"migrate message 1"`),
|
||||
}, {
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 3,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 5, TxIndex: 6},
|
||||
Msg: []byte(`"migrate message 2"`),
|
||||
}},
|
||||
req: types.QueryContractHistoryRequest{Address: myContractBech32Addr},
|
||||
expContent: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeInit,
|
||||
CodeID: firstCodeID,
|
||||
Msg: []byte(`"init message"`),
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2},
|
||||
}, {
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 2,
|
||||
Msg: []byte(`"migrate message 1"`),
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 3, TxIndex: 4},
|
||||
}, {
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 3,
|
||||
Msg: []byte(`"migrate message 2"`),
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 5, TxIndex: 6},
|
||||
}},
|
||||
},
|
||||
"with pagination offset": {
|
||||
srcHistory: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeInit,
|
||||
CodeID: firstCodeID,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2},
|
||||
Msg: []byte(`"init message"`),
|
||||
}, {
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 2,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 3, TxIndex: 4},
|
||||
Msg: []byte(`"migrate message 1"`),
|
||||
}},
|
||||
req: types.QueryContractHistoryRequest{
|
||||
Address: myContractBech32Addr,
|
||||
Pagination: &query.PageRequest{
|
||||
Offset: 1,
|
||||
},
|
||||
},
|
||||
expContent: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 2,
|
||||
Msg: []byte(`"migrate message 1"`),
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 3, TxIndex: 4},
|
||||
}},
|
||||
},
|
||||
"with pagination limit": {
|
||||
srcHistory: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeInit,
|
||||
CodeID: firstCodeID,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2},
|
||||
Msg: []byte(`"init message"`),
|
||||
}, {
|
||||
Operation: types.ContractCodeHistoryOperationTypeMigrate,
|
||||
CodeID: 2,
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 3, TxIndex: 4},
|
||||
Msg: []byte(`"migrate message 1"`),
|
||||
}},
|
||||
req: types.QueryContractHistoryRequest{
|
||||
Address: myContractBech32Addr,
|
||||
Pagination: &query.PageRequest{
|
||||
Limit: 1,
|
||||
},
|
||||
},
|
||||
expContent: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeInit,
|
||||
CodeID: firstCodeID,
|
||||
Msg: []byte(`"init message"`),
|
||||
Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2},
|
||||
}},
|
||||
},
|
||||
"unknown contract address": {
|
||||
req: types.QueryContractHistoryRequest{Address: otherBech32Addr},
|
||||
srcHistory: []types.ContractCodeHistoryEntry{{
|
||||
Operation: types.ContractCodeHistoryOperationTypeGenesis,
|
||||
CodeID: firstCodeID,
|
||||
Updated: types.NewAbsoluteTxPosition(ctx),
|
||||
Msg: []byte(`"init message"`),
|
||||
}},
|
||||
expContent: nil,
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
xCtx, _ := ctx.CacheContext()
|
||||
|
||||
cAddr, _ := sdk.AccAddressFromBech32(myContractBech32Addr)
|
||||
keeper.appendToContractHistory(xCtx, cAddr, spec.srcHistory...)
|
||||
|
||||
// when
|
||||
q := Querier(keeper)
|
||||
got, err := q.ContractHistory(sdk.WrapSDKContext(xCtx), &spec.req)
|
||||
|
||||
// then
|
||||
if spec.expContent == nil {
|
||||
require.Error(t, types.ErrEmpty)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, spec.expContent, got.Entries)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryCodeList(t *testing.T) {
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
specs := map[string]struct {
|
||||
storedCodeIDs []uint64
|
||||
req types.QueryCodesRequest
|
||||
expCodeIDs []uint64
|
||||
}{
|
||||
"none": {},
|
||||
"no gaps": {
|
||||
storedCodeIDs: []uint64{1, 2, 3},
|
||||
expCodeIDs: []uint64{1, 2, 3},
|
||||
},
|
||||
"with gaps": {
|
||||
storedCodeIDs: []uint64{2, 4, 6},
|
||||
expCodeIDs: []uint64{2, 4, 6},
|
||||
},
|
||||
"with pagination offset": {
|
||||
storedCodeIDs: []uint64{1, 2, 3},
|
||||
req: types.QueryCodesRequest{
|
||||
Pagination: &query.PageRequest{
|
||||
Offset: 1,
|
||||
},
|
||||
},
|
||||
expCodeIDs: []uint64{2, 3},
|
||||
},
|
||||
"with pagination limit": {
|
||||
storedCodeIDs: []uint64{1, 2, 3},
|
||||
req: types.QueryCodesRequest{
|
||||
Pagination: &query.PageRequest{
|
||||
Limit: 2,
|
||||
},
|
||||
},
|
||||
expCodeIDs: []uint64{1, 2},
|
||||
},
|
||||
"with pagination next key": {
|
||||
storedCodeIDs: []uint64{1, 2, 3},
|
||||
req: types.QueryCodesRequest{
|
||||
Pagination: &query.PageRequest{
|
||||
Key: fromBase64("AAAAAAAAAAI="),
|
||||
},
|
||||
},
|
||||
expCodeIDs: []uint64{2, 3},
|
||||
},
|
||||
}
|
||||
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
xCtx, _ := ctx.CacheContext()
|
||||
|
||||
for _, codeID := range spec.storedCodeIDs {
|
||||
require.NoError(t, keeper.importCode(xCtx, codeID,
|
||||
types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)),
|
||||
wasmCode),
|
||||
)
|
||||
}
|
||||
// when
|
||||
q := Querier(keeper)
|
||||
got, err := q.Codes(sdk.WrapSDKContext(xCtx), &spec.req)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got.CodeInfos)
|
||||
require.Len(t, got.CodeInfos, len(spec.expCodeIDs))
|
||||
for i, exp := range spec.expCodeIDs {
|
||||
assert.EqualValues(t, exp, got.CodeInfos[i].CodeID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryContractInfo(t *testing.T) {
|
||||
var (
|
||||
contractAddr = RandomAccountAddress(t)
|
||||
anyDate = time.Now().UTC()
|
||||
)
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
// register an example extension. must be protobuf
|
||||
keepers.EncodingConfig.InterfaceRegistry.RegisterImplementations(
|
||||
(*types.ContractInfoExtension)(nil),
|
||||
&govtypes.Proposal{},
|
||||
)
|
||||
govtypes.RegisterInterfaces(keepers.EncodingConfig.InterfaceRegistry)
|
||||
|
||||
k := keepers.WasmKeeper
|
||||
querier := NewGrpcQuerier(k.cdc, k.storeKey, k, k.queryGasLimit)
|
||||
myExtension := func(info *types.ContractInfo) {
|
||||
// abuse gov proposal as a random protobuf extension with an Any type
|
||||
myExt, err := govtypes.NewProposal(&govtypes.TextProposal{Title: "foo", Description: "bar"}, 1, anyDate, anyDate)
|
||||
require.NoError(t, err)
|
||||
myExt.TotalDeposit = nil
|
||||
info.SetExtension(&myExt)
|
||||
}
|
||||
specs := map[string]struct {
|
||||
src *types.QueryContractInfoRequest
|
||||
stored types.ContractInfo
|
||||
expRsp *types.QueryContractInfoResponse
|
||||
expErr bool
|
||||
}{
|
||||
"found": {
|
||||
src: &types.QueryContractInfoRequest{Address: contractAddr.String()},
|
||||
stored: types.ContractInfoFixture(),
|
||||
expRsp: &types.QueryContractInfoResponse{
|
||||
Address: contractAddr.String(),
|
||||
ContractInfo: types.ContractInfoFixture(),
|
||||
},
|
||||
},
|
||||
"with extension": {
|
||||
src: &types.QueryContractInfoRequest{Address: contractAddr.String()},
|
||||
stored: types.ContractInfoFixture(myExtension),
|
||||
expRsp: &types.QueryContractInfoResponse{
|
||||
Address: contractAddr.String(),
|
||||
ContractInfo: types.ContractInfoFixture(myExtension),
|
||||
},
|
||||
},
|
||||
"not found": {
|
||||
src: &types.QueryContractInfoRequest{Address: RandomBech32AccountAddress(t)},
|
||||
stored: types.ContractInfoFixture(),
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
xCtx, _ := ctx.CacheContext()
|
||||
k.storeContractInfo(xCtx, contractAddr, &spec.stored)
|
||||
// when
|
||||
gotRsp, gotErr := querier.ContractInfo(sdk.WrapSDKContext(xCtx), spec.src)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
assert.Equal(t, spec.expRsp, gotRsp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryPinnedCodes(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
exampleContract1 := InstantiateHackatomExampleContract(t, ctx, keepers)
|
||||
exampleContract2 := InstantiateIBCReflectContract(t, ctx, keepers)
|
||||
require.NoError(t, keeper.pinCode(ctx, exampleContract1.CodeID))
|
||||
require.NoError(t, keeper.pinCode(ctx, exampleContract2.CodeID))
|
||||
|
||||
q := Querier(keeper)
|
||||
specs := map[string]struct {
|
||||
srcQuery *types.QueryPinnedCodesRequest
|
||||
expCodeIDs []uint64
|
||||
expErr *sdkErrors.Error
|
||||
}{
|
||||
"query all": {
|
||||
srcQuery: &types.QueryPinnedCodesRequest{},
|
||||
expCodeIDs: []uint64{exampleContract1.CodeID, exampleContract2.CodeID},
|
||||
},
|
||||
"with pagination offset": {
|
||||
srcQuery: &types.QueryPinnedCodesRequest{
|
||||
Pagination: &query.PageRequest{
|
||||
Offset: 1,
|
||||
},
|
||||
},
|
||||
expCodeIDs: []uint64{exampleContract2.CodeID},
|
||||
},
|
||||
"with pagination limit": {
|
||||
srcQuery: &types.QueryPinnedCodesRequest{
|
||||
Pagination: &query.PageRequest{
|
||||
Limit: 1,
|
||||
},
|
||||
},
|
||||
expCodeIDs: []uint64{exampleContract1.CodeID},
|
||||
},
|
||||
"with pagination next key": {
|
||||
srcQuery: &types.QueryPinnedCodesRequest{
|
||||
Pagination: &query.PageRequest{
|
||||
Key: fromBase64("AAAAAAAAAAM="),
|
||||
},
|
||||
},
|
||||
expCodeIDs: []uint64{exampleContract2.CodeID},
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
got, err := q.PinnedCodes(sdk.WrapSDKContext(ctx), spec.srcQuery)
|
||||
require.True(t, spec.expErr.Is(err), err)
|
||||
if spec.expErr != nil {
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, spec.expCodeIDs, got.CodeIDs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryParams(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
q := Querier(keeper)
|
||||
|
||||
paramsResponse, err := q.Params(sdk.WrapSDKContext(ctx), &types.QueryParamsRequest{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, paramsResponse)
|
||||
|
||||
defaultParams := types.DefaultParams()
|
||||
|
||||
require.Equal(t, paramsResponse.Params.CodeUploadAccess, defaultParams.CodeUploadAccess)
|
||||
require.Equal(t, paramsResponse.Params.InstantiateDefaultPermission, defaultParams.InstantiateDefaultPermission)
|
||||
|
||||
keeper.SetParams(ctx, types.Params{
|
||||
CodeUploadAccess: types.AllowNobody,
|
||||
InstantiateDefaultPermission: types.AccessTypeNobody,
|
||||
})
|
||||
|
||||
paramsResponse, err = q.Params(sdk.WrapSDKContext(ctx), &types.QueryParamsRequest{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, paramsResponse)
|
||||
|
||||
require.Equal(t, paramsResponse.Params.CodeUploadAccess, types.AllowNobody)
|
||||
require.Equal(t, paramsResponse.Params.InstantiateDefaultPermission, types.AccessTypeNobody)
|
||||
}
|
||||
|
||||
func TestQueryCodeInfo(t *testing.T) {
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
anyAddress, err := sdk.AccAddressFromBech32("cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz")
|
||||
require.NoError(t, err)
|
||||
specs := map[string]struct {
|
||||
codeId uint64
|
||||
accessConfig types.AccessConfig
|
||||
}{
|
||||
"everybody": {
|
||||
codeId: 1,
|
||||
accessConfig: types.AllowEverybody,
|
||||
},
|
||||
"nobody": {
|
||||
codeId: 10,
|
||||
accessConfig: types.AllowNobody,
|
||||
},
|
||||
"with_address": {
|
||||
codeId: 20,
|
||||
accessConfig: types.AccessTypeOnlyAddress.With(anyAddress),
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
codeInfo := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode))
|
||||
codeInfo.InstantiateConfig = spec.accessConfig
|
||||
require.NoError(t, keeper.importCode(ctx, spec.codeId,
|
||||
codeInfo,
|
||||
wasmCode),
|
||||
)
|
||||
|
||||
q := Querier(keeper)
|
||||
got, err := q.Code(sdk.WrapSDKContext(ctx), &types.QueryCodeRequest{
|
||||
CodeId: spec.codeId,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
expectedResponse := &types.QueryCodeResponse{
|
||||
CodeInfoResponse: &types.CodeInfoResponse{
|
||||
CodeID: spec.codeId,
|
||||
Creator: codeInfo.Creator,
|
||||
DataHash: codeInfo.CodeHash,
|
||||
InstantiatePermission: spec.accessConfig,
|
||||
},
|
||||
Data: wasmCode,
|
||||
}
|
||||
require.NotNil(t, got.CodeInfoResponse)
|
||||
require.EqualValues(t, expectedResponse, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryCodeInfoList(t *testing.T) {
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
anyAddress, err := sdk.AccAddressFromBech32("cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz")
|
||||
require.NoError(t, err)
|
||||
codeInfoWithConfig := func(accessConfig types.AccessConfig) types.CodeInfo {
|
||||
codeInfo := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode))
|
||||
codeInfo.InstantiateConfig = accessConfig
|
||||
return codeInfo
|
||||
}
|
||||
|
||||
codes := []struct {
|
||||
name string
|
||||
codeId uint64
|
||||
codeInfo types.CodeInfo
|
||||
}{
|
||||
{
|
||||
name: "everybody",
|
||||
codeId: 1,
|
||||
codeInfo: codeInfoWithConfig(types.AllowEverybody),
|
||||
},
|
||||
{
|
||||
codeId: 10,
|
||||
name: "nobody",
|
||||
codeInfo: codeInfoWithConfig(types.AllowNobody),
|
||||
},
|
||||
{
|
||||
name: "with_address",
|
||||
codeId: 20,
|
||||
codeInfo: codeInfoWithConfig(types.AccessTypeOnlyAddress.With(anyAddress)),
|
||||
},
|
||||
}
|
||||
|
||||
allCodesResponse := make([]types.CodeInfoResponse, 0)
|
||||
for _, code := range codes {
|
||||
t.Run(fmt.Sprintf("import_%s", code.name), func(t *testing.T) {
|
||||
require.NoError(t, keeper.importCode(ctx, code.codeId,
|
||||
code.codeInfo,
|
||||
wasmCode),
|
||||
)
|
||||
})
|
||||
|
||||
allCodesResponse = append(allCodesResponse, types.CodeInfoResponse{
|
||||
CodeID: code.codeId,
|
||||
Creator: code.codeInfo.Creator,
|
||||
DataHash: code.codeInfo.CodeHash,
|
||||
InstantiatePermission: code.codeInfo.InstantiateConfig,
|
||||
})
|
||||
}
|
||||
q := Querier(keeper)
|
||||
got, err := q.Codes(sdk.WrapSDKContext(ctx), &types.QueryCodesRequest{
|
||||
Pagination: &query.PageRequest{
|
||||
Limit: 3,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got.CodeInfos, 3)
|
||||
require.EqualValues(t, allCodesResponse, got.CodeInfos)
|
||||
}
|
||||
|
||||
func TestQueryContractsByCreatorList(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000000))
|
||||
topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 500))
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
anyAddr := keepers.Faucet.NewFundedRandomAccount(ctx, topUp...)
|
||||
|
||||
wasmCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
|
||||
codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, wasmCode, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, bob := keyPubAddr()
|
||||
initMsg := HackatomExampleInitMsg{
|
||||
Verifier: anyAddr,
|
||||
Beneficiary: bob,
|
||||
}
|
||||
initMsgBz, err := json.Marshal(initMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// manage some realistic block settings
|
||||
var h int64 = 10
|
||||
setBlock := func(ctx sdk.Context, height int64) sdk.Context {
|
||||
ctx = ctx.WithBlockHeight(height)
|
||||
meter := sdk.NewGasMeter(1000000)
|
||||
ctx = ctx.WithGasMeter(meter)
|
||||
ctx = ctx.WithBlockGasMeter(meter)
|
||||
return ctx
|
||||
}
|
||||
|
||||
var allExpecedContracts []string
|
||||
// create 10 contracts with real block/gas setup
|
||||
for i := 0; i < 10; i++ {
|
||||
ctx = setBlock(ctx, h)
|
||||
h++
|
||||
contract, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, fmt.Sprintf("contract %d", i), topUp)
|
||||
allExpecedContracts = append(allExpecedContracts, contract.String())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
specs := map[string]struct {
|
||||
srcQuery *types.QueryContractsByCreatorRequest
|
||||
expContractAddr []string
|
||||
expErr error
|
||||
}{
|
||||
"query all": {
|
||||
srcQuery: &types.QueryContractsByCreatorRequest{
|
||||
CreatorAddress: creator.String(),
|
||||
},
|
||||
expContractAddr: allExpecedContracts,
|
||||
expErr: nil,
|
||||
},
|
||||
"with pagination offset": {
|
||||
srcQuery: &types.QueryContractsByCreatorRequest{
|
||||
CreatorAddress: creator.String(),
|
||||
Pagination: &query.PageRequest{
|
||||
Offset: 1,
|
||||
},
|
||||
},
|
||||
expContractAddr: allExpecedContracts[1:],
|
||||
expErr: nil,
|
||||
},
|
||||
"with pagination limit": {
|
||||
srcQuery: &types.QueryContractsByCreatorRequest{
|
||||
CreatorAddress: creator.String(),
|
||||
Pagination: &query.PageRequest{
|
||||
Limit: 1,
|
||||
},
|
||||
},
|
||||
expContractAddr: allExpecedContracts[0:1],
|
||||
expErr: nil,
|
||||
},
|
||||
"nil creator": {
|
||||
srcQuery: &types.QueryContractsByCreatorRequest{
|
||||
Pagination: &query.PageRequest{},
|
||||
},
|
||||
expContractAddr: allExpecedContracts,
|
||||
expErr: errors.New("empty address string is not allowed"),
|
||||
},
|
||||
"nil req": {
|
||||
srcQuery: nil,
|
||||
expContractAddr: allExpecedContracts,
|
||||
expErr: status.Error(codes.InvalidArgument, "empty request"),
|
||||
},
|
||||
}
|
||||
|
||||
q := Querier(keepers.WasmKeeper)
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
got, err := q.ContractsByCreator(sdk.WrapSDKContext(ctx), spec.srcQuery)
|
||||
|
||||
if spec.expErr != nil {
|
||||
require.Equal(t, spec.expErr, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, spec.expContractAddr, got.ContractAddresses)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func fromBase64(s string) []byte {
|
||||
r, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
614
x/wasm/keeper/query_plugins.go
Normal file
@ -0,0 +1,614 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/baseapp"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
)
|
||||
|
||||
type QueryHandler struct {
|
||||
Ctx sdk.Context
|
||||
Plugins WasmVMQueryHandler
|
||||
Caller sdk.AccAddress
|
||||
gasRegister GasRegister
|
||||
}
|
||||
|
||||
func NewQueryHandler(ctx sdk.Context, vmQueryHandler WasmVMQueryHandler, caller sdk.AccAddress, gasRegister GasRegister) QueryHandler {
|
||||
return QueryHandler{
|
||||
Ctx: ctx,
|
||||
Plugins: vmQueryHandler,
|
||||
Caller: caller,
|
||||
gasRegister: gasRegister,
|
||||
}
|
||||
}
|
||||
|
||||
type GRPCQueryRouter interface {
|
||||
Route(path string) baseapp.GRPCQueryHandler
|
||||
}
|
||||
|
||||
// -- end baseapp interfaces --
|
||||
|
||||
var _ wasmvmtypes.Querier = QueryHandler{}
|
||||
|
||||
func (q QueryHandler) Query(request wasmvmtypes.QueryRequest, gasLimit uint64) ([]byte, error) {
|
||||
// set a limit for a subCtx
|
||||
sdkGas := q.gasRegister.FromWasmVMGas(gasLimit)
|
||||
// discard all changes/ events in subCtx by not committing the cached context
|
||||
subCtx, _ := q.Ctx.WithGasMeter(sdk.NewGasMeter(sdkGas)).CacheContext()
|
||||
|
||||
// make sure we charge the higher level context even on panic
|
||||
defer func() {
|
||||
q.Ctx.GasMeter().ConsumeGas(subCtx.GasMeter().GasConsumed(), "contract sub-query")
|
||||
}()
|
||||
|
||||
res, err := q.Plugins.HandleQuery(subCtx, q.Caller, request)
|
||||
if err == nil {
|
||||
// short-circuit, the rest is dealing with handling existing errors
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// special mappings to wasmvm system error (which are not redacted)
|
||||
var wasmvmErr types.WasmVMErrorable
|
||||
if ok := errors.As(err, &wasmvmErr); ok {
|
||||
err = wasmvmErr.ToWasmVMError()
|
||||
}
|
||||
|
||||
// Issue #759 - we don't return error string for worries of non-determinism
|
||||
return nil, redactError(err)
|
||||
}
|
||||
|
||||
func (q QueryHandler) GasConsumed() uint64 {
|
||||
return q.Ctx.GasMeter().GasConsumed()
|
||||
}
|
||||
|
||||
type CustomQuerier func(ctx sdk.Context, request json.RawMessage) ([]byte, error)
|
||||
|
||||
type QueryPlugins struct {
|
||||
Bank func(ctx sdk.Context, request *wasmvmtypes.BankQuery) ([]byte, error)
|
||||
Custom CustomQuerier
|
||||
IBC func(ctx sdk.Context, caller sdk.AccAddress, request *wasmvmtypes.IBCQuery) ([]byte, error)
|
||||
Staking func(ctx sdk.Context, request *wasmvmtypes.StakingQuery) ([]byte, error)
|
||||
Stargate func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error)
|
||||
Wasm func(ctx sdk.Context, request *wasmvmtypes.WasmQuery) ([]byte, error)
|
||||
}
|
||||
|
||||
type contractMetaDataSource interface {
|
||||
GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo
|
||||
}
|
||||
|
||||
type wasmQueryKeeper interface {
|
||||
contractMetaDataSource
|
||||
GetCodeInfo(ctx sdk.Context, codeID uint64) *types.CodeInfo
|
||||
QueryRaw(ctx sdk.Context, contractAddress sdk.AccAddress, key []byte) []byte
|
||||
QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error)
|
||||
IsPinnedCode(ctx sdk.Context, codeID uint64) bool
|
||||
}
|
||||
|
||||
func DefaultQueryPlugins(
|
||||
bank types.BankViewKeeper,
|
||||
staking types.StakingKeeper,
|
||||
distKeeper types.DistributionKeeper,
|
||||
channelKeeper types.ChannelKeeper,
|
||||
wasm wasmQueryKeeper,
|
||||
) QueryPlugins {
|
||||
return QueryPlugins{
|
||||
Bank: BankQuerier(bank),
|
||||
Custom: NoCustomQuerier,
|
||||
IBC: IBCQuerier(wasm, channelKeeper),
|
||||
Staking: StakingQuerier(staking, distKeeper),
|
||||
Stargate: RejectStargateQuerier(),
|
||||
Wasm: WasmQuerier(wasm),
|
||||
}
|
||||
}
|
||||
|
||||
func (e QueryPlugins) Merge(o *QueryPlugins) QueryPlugins {
|
||||
// only update if this is non-nil and then only set values
|
||||
if o == nil {
|
||||
return e
|
||||
}
|
||||
if o.Bank != nil {
|
||||
e.Bank = o.Bank
|
||||
}
|
||||
if o.Custom != nil {
|
||||
e.Custom = o.Custom
|
||||
}
|
||||
if o.IBC != nil {
|
||||
e.IBC = o.IBC
|
||||
}
|
||||
if o.Staking != nil {
|
||||
e.Staking = o.Staking
|
||||
}
|
||||
if o.Stargate != nil {
|
||||
e.Stargate = o.Stargate
|
||||
}
|
||||
if o.Wasm != nil {
|
||||
e.Wasm = o.Wasm
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// HandleQuery executes the requested query
|
||||
func (e QueryPlugins) HandleQuery(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) {
|
||||
// do the query
|
||||
if request.Bank != nil {
|
||||
return e.Bank(ctx, request.Bank)
|
||||
}
|
||||
if request.Custom != nil {
|
||||
return e.Custom(ctx, request.Custom)
|
||||
}
|
||||
if request.IBC != nil {
|
||||
return e.IBC(ctx, caller, request.IBC)
|
||||
}
|
||||
if request.Staking != nil {
|
||||
return e.Staking(ctx, request.Staking)
|
||||
}
|
||||
if request.Stargate != nil {
|
||||
return e.Stargate(ctx, request.Stargate)
|
||||
}
|
||||
if request.Wasm != nil {
|
||||
return e.Wasm(ctx, request.Wasm)
|
||||
}
|
||||
return nil, wasmvmtypes.Unknown{}
|
||||
}
|
||||
|
||||
func BankQuerier(bankKeeper types.BankViewKeeper) func(ctx sdk.Context, request *wasmvmtypes.BankQuery) ([]byte, error) {
|
||||
return func(ctx sdk.Context, request *wasmvmtypes.BankQuery) ([]byte, error) {
|
||||
if request.AllBalances != nil {
|
||||
addr, err := sdk.AccAddressFromBech32(request.AllBalances.Address)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.AllBalances.Address)
|
||||
}
|
||||
coins := bankKeeper.GetAllBalances(ctx, addr)
|
||||
res := wasmvmtypes.AllBalancesResponse{
|
||||
Amount: ConvertSdkCoinsToWasmCoins(coins),
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
if request.Balance != nil {
|
||||
addr, err := sdk.AccAddressFromBech32(request.Balance.Address)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Balance.Address)
|
||||
}
|
||||
coin := bankKeeper.GetBalance(ctx, addr, request.Balance.Denom)
|
||||
res := wasmvmtypes.BalanceResponse{
|
||||
Amount: wasmvmtypes.Coin{
|
||||
Denom: coin.Denom,
|
||||
Amount: coin.Amount.String(),
|
||||
},
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
if request.Supply != nil {
|
||||
coin := bankKeeper.GetSupply(ctx, request.Supply.Denom)
|
||||
res := wasmvmtypes.SupplyResponse{
|
||||
Amount: wasmvmtypes.Coin{
|
||||
Denom: coin.Denom,
|
||||
Amount: coin.Amount.String(),
|
||||
},
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown BankQuery variant"}
|
||||
}
|
||||
}
|
||||
|
||||
func NoCustomQuerier(sdk.Context, json.RawMessage) ([]byte, error) {
|
||||
return nil, wasmvmtypes.UnsupportedRequest{Kind: "custom"}
|
||||
}
|
||||
|
||||
func IBCQuerier(wasm contractMetaDataSource, channelKeeper types.ChannelKeeper) func(ctx sdk.Context, caller sdk.AccAddress, request *wasmvmtypes.IBCQuery) ([]byte, error) {
|
||||
return func(ctx sdk.Context, caller sdk.AccAddress, request *wasmvmtypes.IBCQuery) ([]byte, error) {
|
||||
if request.PortID != nil {
|
||||
contractInfo := wasm.GetContractInfo(ctx, caller)
|
||||
res := wasmvmtypes.PortIDResponse{
|
||||
PortID: contractInfo.IBCPortID,
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
if request.ListChannels != nil {
|
||||
portID := request.ListChannels.PortID
|
||||
channels := make(wasmvmtypes.IBCChannels, 0)
|
||||
channelKeeper.IterateChannels(ctx, func(ch channeltypes.IdentifiedChannel) bool {
|
||||
// it must match the port and be in open state
|
||||
if (portID == "" || portID == ch.PortId) && ch.State == channeltypes.OPEN {
|
||||
newChan := wasmvmtypes.IBCChannel{
|
||||
Endpoint: wasmvmtypes.IBCEndpoint{
|
||||
PortID: ch.PortId,
|
||||
ChannelID: ch.ChannelId,
|
||||
},
|
||||
CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{
|
||||
PortID: ch.Counterparty.PortId,
|
||||
ChannelID: ch.Counterparty.ChannelId,
|
||||
},
|
||||
Order: ch.Ordering.String(),
|
||||
Version: ch.Version,
|
||||
ConnectionID: ch.ConnectionHops[0],
|
||||
}
|
||||
channels = append(channels, newChan)
|
||||
}
|
||||
return false
|
||||
})
|
||||
res := wasmvmtypes.ListChannelsResponse{
|
||||
Channels: channels,
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
if request.Channel != nil {
|
||||
channelID := request.Channel.ChannelID
|
||||
portID := request.Channel.PortID
|
||||
if portID == "" {
|
||||
contractInfo := wasm.GetContractInfo(ctx, caller)
|
||||
portID = contractInfo.IBCPortID
|
||||
}
|
||||
got, found := channelKeeper.GetChannel(ctx, portID, channelID)
|
||||
var channel *wasmvmtypes.IBCChannel
|
||||
// it must be in open state
|
||||
if found && got.State == channeltypes.OPEN {
|
||||
channel = &wasmvmtypes.IBCChannel{
|
||||
Endpoint: wasmvmtypes.IBCEndpoint{
|
||||
PortID: portID,
|
||||
ChannelID: channelID,
|
||||
},
|
||||
CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{
|
||||
PortID: got.Counterparty.PortId,
|
||||
ChannelID: got.Counterparty.ChannelId,
|
||||
},
|
||||
Order: got.Ordering.String(),
|
||||
Version: got.Version,
|
||||
ConnectionID: got.ConnectionHops[0],
|
||||
}
|
||||
}
|
||||
res := wasmvmtypes.ChannelResponse{
|
||||
Channel: channel,
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown IBCQuery variant"}
|
||||
}
|
||||
}
|
||||
|
||||
// RejectStargateQuerier rejects all stargate queries
|
||||
func RejectStargateQuerier() func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) {
|
||||
return func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) {
|
||||
return nil, wasmvmtypes.UnsupportedRequest{Kind: "Stargate queries are disabled"}
|
||||
}
|
||||
}
|
||||
|
||||
// AcceptedStargateQueries define accepted Stargate queries as a map with path as key and response type as value.
|
||||
// For example:
|
||||
// acceptList["/cosmos.auth.v1beta1.Query/Account"]= &authtypes.QueryAccountResponse{}
|
||||
type AcceptedStargateQueries map[string]codec.ProtoMarshaler
|
||||
|
||||
// AcceptListStargateQuerier supports a preconfigured set of stargate queries only.
|
||||
// All arguments must be non nil.
|
||||
//
|
||||
// Warning: Chains need to test and maintain their accept list carefully.
|
||||
// There were critical consensus breaking issues in the past with non-deterministic behaviour in the SDK.
|
||||
//
|
||||
// This queries can be set via WithQueryPlugins option in the wasm keeper constructor:
|
||||
// WithQueryPlugins(&QueryPlugins{Stargate: AcceptListStargateQuerier(acceptList, queryRouter, codec)})
|
||||
func AcceptListStargateQuerier(acceptList AcceptedStargateQueries, queryRouter GRPCQueryRouter, codec codec.Codec) func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) {
|
||||
return func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) {
|
||||
protoResponse, accepted := acceptList[request.Path]
|
||||
if !accepted {
|
||||
return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("'%s' path is not allowed from the contract", request.Path)}
|
||||
}
|
||||
|
||||
route := queryRouter.Route(request.Path)
|
||||
if route == nil {
|
||||
return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("No route to query '%s'", request.Path)}
|
||||
}
|
||||
|
||||
res, err := route(ctx, abci.RequestQuery{
|
||||
Data: request.Data,
|
||||
Path: request.Path,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ConvertProtoToJSONMarshal(codec, protoResponse, res.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func StakingQuerier(keeper types.StakingKeeper, distKeeper types.DistributionKeeper) func(ctx sdk.Context, request *wasmvmtypes.StakingQuery) ([]byte, error) {
|
||||
return func(ctx sdk.Context, request *wasmvmtypes.StakingQuery) ([]byte, error) {
|
||||
if request.BondedDenom != nil {
|
||||
denom := keeper.BondDenom(ctx)
|
||||
res := wasmvmtypes.BondedDenomResponse{
|
||||
Denom: denom,
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
if request.AllValidators != nil {
|
||||
validators := keeper.GetBondedValidatorsByPower(ctx)
|
||||
// validators := keeper.GetAllValidators(ctx)
|
||||
wasmVals := make([]wasmvmtypes.Validator, len(validators))
|
||||
for i, v := range validators {
|
||||
wasmVals[i] = wasmvmtypes.Validator{
|
||||
Address: v.OperatorAddress,
|
||||
Commission: v.Commission.Rate.String(),
|
||||
MaxCommission: v.Commission.MaxRate.String(),
|
||||
MaxChangeRate: v.Commission.MaxChangeRate.String(),
|
||||
}
|
||||
}
|
||||
res := wasmvmtypes.AllValidatorsResponse{
|
||||
Validators: wasmVals,
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
if request.Validator != nil {
|
||||
valAddr, err := sdk.ValAddressFromBech32(request.Validator.Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, found := keeper.GetValidator(ctx, valAddr)
|
||||
res := wasmvmtypes.ValidatorResponse{}
|
||||
if found {
|
||||
res.Validator = &wasmvmtypes.Validator{
|
||||
Address: v.OperatorAddress,
|
||||
Commission: v.Commission.Rate.String(),
|
||||
MaxCommission: v.Commission.MaxRate.String(),
|
||||
MaxChangeRate: v.Commission.MaxChangeRate.String(),
|
||||
}
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
if request.AllDelegations != nil {
|
||||
delegator, err := sdk.AccAddressFromBech32(request.AllDelegations.Delegator)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.AllDelegations.Delegator)
|
||||
}
|
||||
sdkDels := keeper.GetAllDelegatorDelegations(ctx, delegator)
|
||||
delegations, err := sdkToDelegations(ctx, keeper, sdkDels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := wasmvmtypes.AllDelegationsResponse{
|
||||
Delegations: delegations,
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
if request.Delegation != nil {
|
||||
delegator, err := sdk.AccAddressFromBech32(request.Delegation.Delegator)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Delegation.Delegator)
|
||||
}
|
||||
validator, err := sdk.ValAddressFromBech32(request.Delegation.Validator)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Delegation.Validator)
|
||||
}
|
||||
|
||||
var res wasmvmtypes.DelegationResponse
|
||||
d, found := keeper.GetDelegation(ctx, delegator, validator)
|
||||
if found {
|
||||
res.Delegation, err = sdkToFullDelegation(ctx, keeper, distKeeper, d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown Staking variant"}
|
||||
}
|
||||
}
|
||||
|
||||
func sdkToDelegations(ctx sdk.Context, keeper types.StakingKeeper, delegations []stakingtypes.Delegation) (wasmvmtypes.Delegations, error) {
|
||||
result := make([]wasmvmtypes.Delegation, len(delegations))
|
||||
bondDenom := keeper.BondDenom(ctx)
|
||||
|
||||
for i, d := range delegations {
|
||||
delAddr, err := sdk.AccAddressFromBech32(d.DelegatorAddress)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "delegator address")
|
||||
}
|
||||
valAddr, err := sdk.ValAddressFromBech32(d.ValidatorAddress)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "validator address")
|
||||
}
|
||||
|
||||
// shares to amount logic comes from here:
|
||||
// https://github.com/cosmos/cosmos-sdk/blob/v0.38.3/x/staking/keeper/querier.go#L404
|
||||
val, found := keeper.GetValidator(ctx, valAddr)
|
||||
if !found {
|
||||
return nil, sdkerrors.Wrap(stakingtypes.ErrNoValidatorFound, "can't load validator for delegation")
|
||||
}
|
||||
amount := sdk.NewCoin(bondDenom, val.TokensFromShares(d.Shares).TruncateInt())
|
||||
|
||||
result[i] = wasmvmtypes.Delegation{
|
||||
Delegator: delAddr.String(),
|
||||
Validator: valAddr.String(),
|
||||
Amount: ConvertSdkCoinToWasmCoin(amount),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func sdkToFullDelegation(ctx sdk.Context, keeper types.StakingKeeper, distKeeper types.DistributionKeeper, delegation stakingtypes.Delegation) (*wasmvmtypes.FullDelegation, error) {
|
||||
delAddr, err := sdk.AccAddressFromBech32(delegation.DelegatorAddress)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "delegator address")
|
||||
}
|
||||
valAddr, err := sdk.ValAddressFromBech32(delegation.ValidatorAddress)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "validator address")
|
||||
}
|
||||
val, found := keeper.GetValidator(ctx, valAddr)
|
||||
if !found {
|
||||
return nil, sdkerrors.Wrap(stakingtypes.ErrNoValidatorFound, "can't load validator for delegation")
|
||||
}
|
||||
bondDenom := keeper.BondDenom(ctx)
|
||||
amount := sdk.NewCoin(bondDenom, val.TokensFromShares(delegation.Shares).TruncateInt())
|
||||
|
||||
delegationCoins := ConvertSdkCoinToWasmCoin(amount)
|
||||
|
||||
// FIXME: this is very rough but better than nothing...
|
||||
// https://github.com/CosmWasm/wasmd/issues/282
|
||||
// if this (val, delegate) pair is receiving a redelegation, it cannot redelegate more
|
||||
// otherwise, it can redelegate the full amount
|
||||
// (there are cases of partial funds redelegated, but this is a start)
|
||||
redelegateCoins := wasmvmtypes.NewCoin(0, bondDenom)
|
||||
if !keeper.HasReceivingRedelegation(ctx, delAddr, valAddr) {
|
||||
redelegateCoins = delegationCoins
|
||||
}
|
||||
|
||||
// FIXME: make a cleaner way to do this (modify the sdk)
|
||||
// we need the info from `distKeeper.calculateDelegationRewards()`, but it is not public
|
||||
// neither is `queryDelegationRewards(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper)`
|
||||
// so we go through the front door of the querier....
|
||||
accRewards, err := getAccumulatedRewards(ctx, distKeeper, delegation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wasmvmtypes.FullDelegation{
|
||||
Delegator: delAddr.String(),
|
||||
Validator: valAddr.String(),
|
||||
Amount: delegationCoins,
|
||||
AccumulatedRewards: accRewards,
|
||||
CanRedelegate: redelegateCoins,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FIXME: simplify this enormously when
|
||||
// https://github.com/cosmos/cosmos-sdk/issues/7466 is merged
|
||||
func getAccumulatedRewards(ctx sdk.Context, distKeeper types.DistributionKeeper, delegation stakingtypes.Delegation) ([]wasmvmtypes.Coin, error) {
|
||||
// Try to get *delegator* reward info!
|
||||
params := distributiontypes.QueryDelegationRewardsRequest{
|
||||
DelegatorAddress: delegation.DelegatorAddress,
|
||||
ValidatorAddress: delegation.ValidatorAddress,
|
||||
}
|
||||
cache, _ := ctx.CacheContext()
|
||||
qres, err := distKeeper.DelegationRewards(sdk.WrapSDKContext(cache), ¶ms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// now we have it, convert it into wasmvm types
|
||||
rewards := make([]wasmvmtypes.Coin, len(qres.Rewards))
|
||||
for i, r := range qres.Rewards {
|
||||
rewards[i] = wasmvmtypes.Coin{
|
||||
Denom: r.Denom,
|
||||
Amount: r.Amount.TruncateInt().String(),
|
||||
}
|
||||
}
|
||||
return rewards, nil
|
||||
}
|
||||
|
||||
func WasmQuerier(k wasmQueryKeeper) func(ctx sdk.Context, request *wasmvmtypes.WasmQuery) ([]byte, error) {
|
||||
return func(ctx sdk.Context, request *wasmvmtypes.WasmQuery) ([]byte, error) {
|
||||
switch {
|
||||
case request.Smart != nil:
|
||||
addr, err := sdk.AccAddressFromBech32(request.Smart.ContractAddr)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Smart.ContractAddr)
|
||||
}
|
||||
msg := types.RawContractMessage(request.Smart.Msg)
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "json msg")
|
||||
}
|
||||
return k.QuerySmart(ctx, addr, msg)
|
||||
case request.Raw != nil:
|
||||
addr, err := sdk.AccAddressFromBech32(request.Raw.ContractAddr)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Raw.ContractAddr)
|
||||
}
|
||||
return k.QueryRaw(ctx, addr, request.Raw.Key), nil
|
||||
case request.ContractInfo != nil:
|
||||
contractAddr := request.ContractInfo.ContractAddr
|
||||
addr, err := sdk.AccAddressFromBech32(contractAddr)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, contractAddr)
|
||||
}
|
||||
info := k.GetContractInfo(ctx, addr)
|
||||
if info == nil {
|
||||
return nil, types.ErrNoSuchContractFn(contractAddr).
|
||||
Wrapf("address %s", contractAddr)
|
||||
}
|
||||
res := wasmvmtypes.ContractInfoResponse{
|
||||
CodeID: info.CodeID,
|
||||
Creator: info.Creator,
|
||||
Admin: info.Admin,
|
||||
Pinned: k.IsPinnedCode(ctx, info.CodeID),
|
||||
IBCPort: info.IBCPortID,
|
||||
}
|
||||
return json.Marshal(res)
|
||||
case request.CodeInfo != nil:
|
||||
if request.CodeInfo.CodeID == 0 {
|
||||
return nil, types.ErrEmpty.Wrap("code id")
|
||||
}
|
||||
info := k.GetCodeInfo(ctx, request.CodeInfo.CodeID)
|
||||
if info == nil {
|
||||
return nil, types.ErrNoSuchCodeFn(request.CodeInfo.CodeID).
|
||||
Wrapf("code id %d", request.CodeInfo.CodeID)
|
||||
}
|
||||
|
||||
res := wasmvmtypes.CodeInfoResponse{
|
||||
CodeID: request.CodeInfo.CodeID,
|
||||
Creator: info.Creator,
|
||||
Checksum: info.CodeHash,
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown WasmQuery variant"}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertSdkCoinsToWasmCoins covert sdk type to wasmvm coins type
|
||||
func ConvertSdkCoinsToWasmCoins(coins []sdk.Coin) wasmvmtypes.Coins {
|
||||
converted := make(wasmvmtypes.Coins, len(coins))
|
||||
for i, c := range coins {
|
||||
converted[i] = ConvertSdkCoinToWasmCoin(c)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
// ConvertSdkCoinToWasmCoin covert sdk type to wasmvm coin type
|
||||
func ConvertSdkCoinToWasmCoin(coin sdk.Coin) wasmvmtypes.Coin {
|
||||
return wasmvmtypes.Coin{
|
||||
Denom: coin.Denom,
|
||||
Amount: coin.Amount.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertProtoToJSONMarshal unmarshals the given bytes into a proto message and then marshals it to json.
|
||||
// This is done so that clients calling stargate queries do not need to define their own proto unmarshalers,
|
||||
// being able to use response directly by json marshalling, which is supported in cosmwasm.
|
||||
func ConvertProtoToJSONMarshal(cdc codec.Codec, protoResponse codec.ProtoMarshaler, bz []byte) ([]byte, error) {
|
||||
// unmarshal binary into stargate response data structure
|
||||
err := cdc.Unmarshal(bz, protoResponse)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "to proto")
|
||||
}
|
||||
|
||||
bz, err = cdc.MarshalJSON(protoResponse)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "to json")
|
||||
}
|
||||
|
||||
return bz, nil
|
||||
}
|
||||
|
||||
var _ WasmVMQueryHandler = WasmVMQueryHandlerFn(nil)
|
||||
|
||||
// WasmVMQueryHandlerFn is a helper to construct a function based query handler.
|
||||
type WasmVMQueryHandlerFn func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error)
|
||||
|
||||
// HandleQuery delegates call into wrapped WasmVMQueryHandlerFn
|
||||
func (w WasmVMQueryHandlerFn) HandleQuery(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) {
|
||||
return w(ctx, caller, request)
|
||||
}
|
825
x/wasm/keeper/query_plugins_test.go
Normal file
@ -0,0 +1,825 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
|
||||
"github.com/cosmos/cosmos-sdk/store"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/types/query"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/cerc-io/laconicd/app"
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestIBCQuerier(t *testing.T) {
|
||||
myExampleChannels := []channeltypes.IdentifiedChannel{
|
||||
// this is returned
|
||||
{
|
||||
State: channeltypes.OPEN,
|
||||
Ordering: channeltypes.ORDERED,
|
||||
Counterparty: channeltypes.Counterparty{
|
||||
PortId: "counterPartyPortID",
|
||||
ChannelId: "counterPartyChannelID",
|
||||
},
|
||||
ConnectionHops: []string{"one"},
|
||||
Version: "v1",
|
||||
PortId: "myPortID",
|
||||
ChannelId: "myChannelID",
|
||||
},
|
||||
// this is filtered out
|
||||
{
|
||||
State: channeltypes.INIT,
|
||||
Ordering: channeltypes.UNORDERED,
|
||||
Counterparty: channeltypes.Counterparty{
|
||||
PortId: "foobar",
|
||||
},
|
||||
ConnectionHops: []string{"one"},
|
||||
Version: "initversion",
|
||||
PortId: "initPortID",
|
||||
ChannelId: "initChannelID",
|
||||
},
|
||||
// this is returned
|
||||
{
|
||||
State: channeltypes.OPEN,
|
||||
Ordering: channeltypes.UNORDERED,
|
||||
Counterparty: channeltypes.Counterparty{
|
||||
PortId: "otherCounterPartyPortID",
|
||||
ChannelId: "otherCounterPartyChannelID",
|
||||
},
|
||||
ConnectionHops: []string{"other", "second"},
|
||||
Version: "otherVersion",
|
||||
PortId: "otherPortID",
|
||||
ChannelId: "otherChannelID",
|
||||
},
|
||||
// this is filtered out
|
||||
{
|
||||
State: channeltypes.CLOSED,
|
||||
Ordering: channeltypes.ORDERED,
|
||||
Counterparty: channeltypes.Counterparty{
|
||||
PortId: "super",
|
||||
ChannelId: "duper",
|
||||
},
|
||||
ConnectionHops: []string{"no-more"},
|
||||
Version: "closedVersion",
|
||||
PortId: "closedPortID",
|
||||
ChannelId: "closedChannelID",
|
||||
},
|
||||
}
|
||||
specs := map[string]struct {
|
||||
srcQuery *wasmvmtypes.IBCQuery
|
||||
wasmKeeper *mockWasmQueryKeeper
|
||||
channelKeeper *wasmtesting.MockChannelKeeper
|
||||
expJsonResult string
|
||||
expErr *sdkerrors.Error
|
||||
}{
|
||||
"query port id": {
|
||||
srcQuery: &wasmvmtypes.IBCQuery{
|
||||
PortID: &wasmvmtypes.PortIDQuery{},
|
||||
},
|
||||
wasmKeeper: &mockWasmQueryKeeper{
|
||||
GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo {
|
||||
return &types.ContractInfo{IBCPortID: "myIBCPortID"}
|
||||
},
|
||||
},
|
||||
channelKeeper: &wasmtesting.MockChannelKeeper{},
|
||||
expJsonResult: `{"port_id":"myIBCPortID"}`,
|
||||
},
|
||||
"query list channels - all": {
|
||||
srcQuery: &wasmvmtypes.IBCQuery{
|
||||
ListChannels: &wasmvmtypes.ListChannelsQuery{},
|
||||
},
|
||||
channelKeeper: &wasmtesting.MockChannelKeeper{
|
||||
IterateChannelsFn: wasmtesting.MockChannelKeeperIterator(myExampleChannels),
|
||||
},
|
||||
expJsonResult: `{
|
||||
"channels": [
|
||||
{
|
||||
"endpoint": {
|
||||
"port_id": "myPortID",
|
||||
"channel_id": "myChannelID"
|
||||
},
|
||||
"counterparty_endpoint": {
|
||||
"port_id": "counterPartyPortID",
|
||||
"channel_id": "counterPartyChannelID"
|
||||
},
|
||||
"order": "ORDER_ORDERED",
|
||||
"version": "v1",
|
||||
"connection_id": "one"
|
||||
},
|
||||
{
|
||||
"endpoint": {
|
||||
"port_id": "otherPortID",
|
||||
"channel_id": "otherChannelID"
|
||||
},
|
||||
"counterparty_endpoint": {
|
||||
"port_id": "otherCounterPartyPortID",
|
||||
"channel_id": "otherCounterPartyChannelID"
|
||||
},
|
||||
"order": "ORDER_UNORDERED",
|
||||
"version": "otherVersion",
|
||||
"connection_id": "other"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
"query list channels - filtered": {
|
||||
srcQuery: &wasmvmtypes.IBCQuery{
|
||||
ListChannels: &wasmvmtypes.ListChannelsQuery{
|
||||
PortID: "otherPortID",
|
||||
},
|
||||
},
|
||||
channelKeeper: &wasmtesting.MockChannelKeeper{
|
||||
IterateChannelsFn: wasmtesting.MockChannelKeeperIterator(myExampleChannels),
|
||||
},
|
||||
expJsonResult: `{
|
||||
"channels": [
|
||||
{
|
||||
"endpoint": {
|
||||
"port_id": "otherPortID",
|
||||
"channel_id": "otherChannelID"
|
||||
},
|
||||
"counterparty_endpoint": {
|
||||
"port_id": "otherCounterPartyPortID",
|
||||
"channel_id": "otherCounterPartyChannelID"
|
||||
},
|
||||
"order": "ORDER_UNORDERED",
|
||||
"version": "otherVersion",
|
||||
"connection_id": "other"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
"query list channels - filtered empty": {
|
||||
srcQuery: &wasmvmtypes.IBCQuery{
|
||||
ListChannels: &wasmvmtypes.ListChannelsQuery{
|
||||
PortID: "none-existing",
|
||||
},
|
||||
},
|
||||
channelKeeper: &wasmtesting.MockChannelKeeper{
|
||||
IterateChannelsFn: wasmtesting.MockChannelKeeperIterator(myExampleChannels),
|
||||
},
|
||||
expJsonResult: `{"channels": []}`,
|
||||
},
|
||||
"query channel": {
|
||||
srcQuery: &wasmvmtypes.IBCQuery{
|
||||
Channel: &wasmvmtypes.ChannelQuery{
|
||||
PortID: "myQueryPortID",
|
||||
ChannelID: "myQueryChannelID",
|
||||
},
|
||||
},
|
||||
channelKeeper: &wasmtesting.MockChannelKeeper{
|
||||
GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) {
|
||||
return channeltypes.Channel{
|
||||
State: channeltypes.OPEN,
|
||||
Ordering: channeltypes.UNORDERED,
|
||||
Counterparty: channeltypes.Counterparty{
|
||||
PortId: "counterPartyPortID",
|
||||
ChannelId: "otherCounterPartyChannelID",
|
||||
},
|
||||
ConnectionHops: []string{"one"},
|
||||
Version: "version",
|
||||
}, true
|
||||
},
|
||||
},
|
||||
expJsonResult: `{
|
||||
"channel": {
|
||||
"endpoint": {
|
||||
"port_id": "myQueryPortID",
|
||||
"channel_id": "myQueryChannelID"
|
||||
},
|
||||
"counterparty_endpoint": {
|
||||
"port_id": "counterPartyPortID",
|
||||
"channel_id": "otherCounterPartyChannelID"
|
||||
},
|
||||
"order": "ORDER_UNORDERED",
|
||||
"version": "version",
|
||||
"connection_id": "one"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
"query channel - without port set": {
|
||||
srcQuery: &wasmvmtypes.IBCQuery{
|
||||
Channel: &wasmvmtypes.ChannelQuery{
|
||||
ChannelID: "myQueryChannelID",
|
||||
},
|
||||
},
|
||||
wasmKeeper: &mockWasmQueryKeeper{
|
||||
GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo {
|
||||
return &types.ContractInfo{IBCPortID: "myLoadedPortID"}
|
||||
},
|
||||
},
|
||||
channelKeeper: &wasmtesting.MockChannelKeeper{
|
||||
GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) {
|
||||
return channeltypes.Channel{
|
||||
State: channeltypes.OPEN,
|
||||
Ordering: channeltypes.UNORDERED,
|
||||
Counterparty: channeltypes.Counterparty{
|
||||
PortId: "counterPartyPortID",
|
||||
ChannelId: "otherCounterPartyChannelID",
|
||||
},
|
||||
ConnectionHops: []string{"one"},
|
||||
Version: "version",
|
||||
}, true
|
||||
},
|
||||
},
|
||||
expJsonResult: `{
|
||||
"channel": {
|
||||
"endpoint": {
|
||||
"port_id": "myLoadedPortID",
|
||||
"channel_id": "myQueryChannelID"
|
||||
},
|
||||
"counterparty_endpoint": {
|
||||
"port_id": "counterPartyPortID",
|
||||
"channel_id": "otherCounterPartyChannelID"
|
||||
},
|
||||
"order": "ORDER_UNORDERED",
|
||||
"version": "version",
|
||||
"connection_id": "one"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
"query channel in init state": {
|
||||
srcQuery: &wasmvmtypes.IBCQuery{
|
||||
Channel: &wasmvmtypes.ChannelQuery{
|
||||
PortID: "myQueryPortID",
|
||||
ChannelID: "myQueryChannelID",
|
||||
},
|
||||
},
|
||||
channelKeeper: &wasmtesting.MockChannelKeeper{
|
||||
GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) {
|
||||
return channeltypes.Channel{
|
||||
State: channeltypes.INIT,
|
||||
Ordering: channeltypes.UNORDERED,
|
||||
Counterparty: channeltypes.Counterparty{
|
||||
PortId: "foobar",
|
||||
},
|
||||
ConnectionHops: []string{"one"},
|
||||
Version: "initversion",
|
||||
}, true
|
||||
},
|
||||
},
|
||||
expJsonResult: "{}",
|
||||
},
|
||||
"query channel in closed state": {
|
||||
srcQuery: &wasmvmtypes.IBCQuery{
|
||||
Channel: &wasmvmtypes.ChannelQuery{
|
||||
PortID: "myQueryPortID",
|
||||
ChannelID: "myQueryChannelID",
|
||||
},
|
||||
},
|
||||
channelKeeper: &wasmtesting.MockChannelKeeper{
|
||||
GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) {
|
||||
return channeltypes.Channel{
|
||||
State: channeltypes.CLOSED,
|
||||
Ordering: channeltypes.ORDERED,
|
||||
Counterparty: channeltypes.Counterparty{
|
||||
PortId: "super",
|
||||
ChannelId: "duper",
|
||||
},
|
||||
ConnectionHops: []string{"no-more"},
|
||||
Version: "closedVersion",
|
||||
}, true
|
||||
},
|
||||
},
|
||||
expJsonResult: "{}",
|
||||
},
|
||||
"query channel - empty result": {
|
||||
srcQuery: &wasmvmtypes.IBCQuery{
|
||||
Channel: &wasmvmtypes.ChannelQuery{
|
||||
PortID: "myQueryPortID",
|
||||
ChannelID: "myQueryChannelID",
|
||||
},
|
||||
},
|
||||
channelKeeper: &wasmtesting.MockChannelKeeper{
|
||||
GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) {
|
||||
return channeltypes.Channel{}, false
|
||||
},
|
||||
},
|
||||
expJsonResult: "{}",
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := keeper.IBCQuerier(spec.wasmKeeper, spec.channelKeeper)
|
||||
gotResult, gotErr := h(sdk.Context{}, keeper.RandomAccountAddress(t), spec.srcQuery)
|
||||
require.True(t, spec.expErr.Is(gotErr), "exp %v but got %#+v", spec.expErr, gotErr)
|
||||
if spec.expErr != nil {
|
||||
return
|
||||
}
|
||||
assert.JSONEq(t, spec.expJsonResult, string(gotResult), string(gotResult))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBankQuerierBalance(t *testing.T) {
|
||||
mock := bankKeeperMock{GetBalanceFn: func(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin {
|
||||
return sdk.NewCoin(denom, sdk.NewInt(1))
|
||||
}}
|
||||
|
||||
ctx := sdk.Context{}
|
||||
q := keeper.BankQuerier(mock)
|
||||
gotBz, gotErr := q(ctx, &wasmvmtypes.BankQuery{
|
||||
Balance: &wasmvmtypes.BalanceQuery{
|
||||
Address: keeper.RandomBech32AccountAddress(t),
|
||||
Denom: "ALX",
|
||||
},
|
||||
})
|
||||
require.NoError(t, gotErr)
|
||||
var got wasmvmtypes.BalanceResponse
|
||||
require.NoError(t, json.Unmarshal(gotBz, &got))
|
||||
exp := wasmvmtypes.BalanceResponse{
|
||||
Amount: wasmvmtypes.Coin{
|
||||
Denom: "ALX",
|
||||
Amount: "1",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, exp, got)
|
||||
}
|
||||
|
||||
func TestContractInfoWasmQuerier(t *testing.T) {
|
||||
myValidContractAddr := keeper.RandomBech32AccountAddress(t)
|
||||
myCreatorAddr := keeper.RandomBech32AccountAddress(t)
|
||||
myAdminAddr := keeper.RandomBech32AccountAddress(t)
|
||||
var ctx sdk.Context
|
||||
|
||||
specs := map[string]struct {
|
||||
req *wasmvmtypes.WasmQuery
|
||||
mock mockWasmQueryKeeper
|
||||
expRes wasmvmtypes.ContractInfoResponse
|
||||
expErr bool
|
||||
}{
|
||||
"all good": {
|
||||
req: &wasmvmtypes.WasmQuery{
|
||||
ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: myValidContractAddr},
|
||||
},
|
||||
mock: mockWasmQueryKeeper{
|
||||
GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo {
|
||||
val := types.ContractInfoFixture(func(i *types.ContractInfo) {
|
||||
i.Admin, i.Creator, i.IBCPortID = myAdminAddr, myCreatorAddr, "myIBCPort"
|
||||
})
|
||||
return &val
|
||||
},
|
||||
IsPinnedCodeFn: func(ctx sdk.Context, codeID uint64) bool { return true },
|
||||
},
|
||||
expRes: wasmvmtypes.ContractInfoResponse{
|
||||
CodeID: 1,
|
||||
Creator: myCreatorAddr,
|
||||
Admin: myAdminAddr,
|
||||
Pinned: true,
|
||||
IBCPort: "myIBCPort",
|
||||
},
|
||||
},
|
||||
"invalid addr": {
|
||||
req: &wasmvmtypes.WasmQuery{
|
||||
ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: "not a valid addr"},
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
"unknown addr": {
|
||||
req: &wasmvmtypes.WasmQuery{
|
||||
ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: myValidContractAddr},
|
||||
},
|
||||
mock: mockWasmQueryKeeper{GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo {
|
||||
return nil
|
||||
}},
|
||||
expErr: true,
|
||||
},
|
||||
"not pinned": {
|
||||
req: &wasmvmtypes.WasmQuery{
|
||||
ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: myValidContractAddr},
|
||||
},
|
||||
mock: mockWasmQueryKeeper{
|
||||
GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo {
|
||||
val := types.ContractInfoFixture(func(i *types.ContractInfo) {
|
||||
i.Admin, i.Creator = myAdminAddr, myCreatorAddr
|
||||
})
|
||||
return &val
|
||||
},
|
||||
IsPinnedCodeFn: func(ctx sdk.Context, codeID uint64) bool { return false },
|
||||
},
|
||||
expRes: wasmvmtypes.ContractInfoResponse{
|
||||
CodeID: 1,
|
||||
Creator: myCreatorAddr,
|
||||
Admin: myAdminAddr,
|
||||
Pinned: false,
|
||||
},
|
||||
},
|
||||
"without admin": {
|
||||
req: &wasmvmtypes.WasmQuery{
|
||||
ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: myValidContractAddr},
|
||||
},
|
||||
mock: mockWasmQueryKeeper{
|
||||
GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo {
|
||||
val := types.ContractInfoFixture(func(i *types.ContractInfo) {
|
||||
i.Creator = myCreatorAddr
|
||||
})
|
||||
return &val
|
||||
},
|
||||
IsPinnedCodeFn: func(ctx sdk.Context, codeID uint64) bool { return true },
|
||||
},
|
||||
expRes: wasmvmtypes.ContractInfoResponse{
|
||||
CodeID: 1,
|
||||
Creator: myCreatorAddr,
|
||||
Pinned: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
q := keeper.WasmQuerier(spec.mock)
|
||||
gotBz, gotErr := q(ctx, spec.req)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
var gotRes wasmvmtypes.ContractInfoResponse
|
||||
require.NoError(t, json.Unmarshal(gotBz, &gotRes))
|
||||
assert.Equal(t, spec.expRes, gotRes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodeInfoWasmQuerier(t *testing.T) {
|
||||
myCreatorAddr := keeper.RandomBech32AccountAddress(t)
|
||||
var ctx sdk.Context
|
||||
|
||||
myRawChecksum := []byte("myHash78901234567890123456789012")
|
||||
specs := map[string]struct {
|
||||
req *wasmvmtypes.WasmQuery
|
||||
mock mockWasmQueryKeeper
|
||||
expRes wasmvmtypes.CodeInfoResponse
|
||||
expErr bool
|
||||
}{
|
||||
"all good": {
|
||||
req: &wasmvmtypes.WasmQuery{
|
||||
CodeInfo: &wasmvmtypes.CodeInfoQuery{CodeID: 1},
|
||||
},
|
||||
mock: mockWasmQueryKeeper{
|
||||
GetCodeInfoFn: func(ctx sdk.Context, codeID uint64) *types.CodeInfo {
|
||||
return &types.CodeInfo{
|
||||
CodeHash: myRawChecksum,
|
||||
Creator: myCreatorAddr,
|
||||
InstantiateConfig: types.AccessConfig{
|
||||
Permission: types.AccessTypeNobody,
|
||||
Addresses: []string{myCreatorAddr},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
expRes: wasmvmtypes.CodeInfoResponse{
|
||||
CodeID: 1,
|
||||
Creator: myCreatorAddr,
|
||||
Checksum: myRawChecksum,
|
||||
},
|
||||
},
|
||||
"empty code id": {
|
||||
req: &wasmvmtypes.WasmQuery{
|
||||
CodeInfo: &wasmvmtypes.CodeInfoQuery{},
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
"unknown code id": {
|
||||
req: &wasmvmtypes.WasmQuery{
|
||||
CodeInfo: &wasmvmtypes.CodeInfoQuery{CodeID: 1},
|
||||
},
|
||||
mock: mockWasmQueryKeeper{
|
||||
GetCodeInfoFn: func(ctx sdk.Context, codeID uint64) *types.CodeInfo {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
q := keeper.WasmQuerier(spec.mock)
|
||||
gotBz, gotErr := q(ctx, spec.req)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
var gotRes wasmvmtypes.CodeInfoResponse
|
||||
require.NoError(t, json.Unmarshal(gotBz, &gotRes), string(gotBz))
|
||||
assert.Equal(t, spec.expRes, gotRes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryErrors(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
src error
|
||||
expErr error
|
||||
}{
|
||||
"no error": {},
|
||||
"no such contract": {
|
||||
src: types.ErrNoSuchContractFn("contract-addr"),
|
||||
expErr: wasmvmtypes.NoSuchContract{Addr: "contract-addr"},
|
||||
},
|
||||
"no such contract - wrapped": {
|
||||
src: sdkerrors.Wrap(types.ErrNoSuchContractFn("contract-addr"), "my additional data"),
|
||||
expErr: wasmvmtypes.NoSuchContract{Addr: "contract-addr"},
|
||||
},
|
||||
"no such code": {
|
||||
src: types.ErrNoSuchCodeFn(123),
|
||||
expErr: wasmvmtypes.NoSuchCode{CodeID: 123},
|
||||
},
|
||||
"no such code - wrapped": {
|
||||
src: sdkerrors.Wrap(types.ErrNoSuchCodeFn(123), "my additional data"),
|
||||
expErr: wasmvmtypes.NoSuchCode{CodeID: 123},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mock := keeper.WasmVMQueryHandlerFn(func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) {
|
||||
return nil, spec.src
|
||||
})
|
||||
ctx := sdk.Context{}.WithGasMeter(sdk.NewInfiniteGasMeter()).WithMultiStore(store.NewCommitMultiStore(dbm.NewMemDB()))
|
||||
q := keeper.NewQueryHandler(ctx, mock, sdk.AccAddress{}, keeper.NewDefaultWasmGasRegister())
|
||||
_, gotErr := q.Query(wasmvmtypes.QueryRequest{}, 1)
|
||||
assert.Equal(t, spec.expErr, gotErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptListStargateQuerier(t *testing.T) {
|
||||
wasmApp := app.SetupWithEmptyStore(t)
|
||||
ctx := wasmApp.NewUncachedContext(false, tmproto.Header{ChainID: "foo", Height: 1, Time: time.Now()})
|
||||
wasmApp.StakingKeeper.SetParams(ctx, stakingtypes.DefaultParams())
|
||||
|
||||
addrs := app.AddTestAddrs(wasmApp, ctx, 2, sdk.NewInt(1_000_000))
|
||||
accepted := keeper.AcceptedStargateQueries{
|
||||
"/cosmos.auth.v1beta1.Query/Account": &authtypes.QueryAccountResponse{},
|
||||
"/no/route/to/this": &authtypes.QueryAccountResponse{},
|
||||
}
|
||||
|
||||
marshal := func(pb proto.Message) []byte {
|
||||
b, err := proto.Marshal(pb)
|
||||
require.NoError(t, err)
|
||||
return b
|
||||
}
|
||||
|
||||
specs := map[string]struct {
|
||||
req *wasmvmtypes.StargateQuery
|
||||
expErr bool
|
||||
expResp string
|
||||
}{
|
||||
"in accept list - success result": {
|
||||
req: &wasmvmtypes.StargateQuery{
|
||||
Path: "/cosmos.auth.v1beta1.Query/Account",
|
||||
Data: marshal(&authtypes.QueryAccountRequest{Address: addrs[0].String()}),
|
||||
},
|
||||
expResp: fmt.Sprintf(`{"account":{"@type":"/cosmos.auth.v1beta1.BaseAccount","address":%q,"pub_key":null,"account_number":"1","sequence":"0"}}`, addrs[0].String()),
|
||||
},
|
||||
"in accept list - error result": {
|
||||
req: &wasmvmtypes.StargateQuery{
|
||||
Path: "/cosmos.auth.v1beta1.Query/Account",
|
||||
Data: marshal(&authtypes.QueryAccountRequest{Address: sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()).String()}),
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
"not in accept list": {
|
||||
req: &wasmvmtypes.StargateQuery{
|
||||
Path: "/cosmos.bank.v1beta1.Query/AllBalances",
|
||||
Data: marshal(&banktypes.QueryAllBalancesRequest{Address: addrs[0].String()}),
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
"unknown route": {
|
||||
req: &wasmvmtypes.StargateQuery{
|
||||
Path: "/no/route/to/this",
|
||||
Data: marshal(&banktypes.QueryAllBalancesRequest{Address: addrs[0].String()}),
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
q := keeper.AcceptListStargateQuerier(accepted, wasmApp.GRPCQueryRouter(), wasmApp.AppCodec())
|
||||
gotBz, gotErr := q(ctx, spec.req)
|
||||
if spec.expErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
assert.JSONEq(t, spec.expResp, string(gotBz), string(gotBz))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockWasmQueryKeeper struct {
|
||||
GetContractInfoFn func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo
|
||||
QueryRawFn func(ctx sdk.Context, contractAddress sdk.AccAddress, key []byte) []byte
|
||||
QuerySmartFn func(ctx sdk.Context, contractAddr sdk.AccAddress, req types.RawContractMessage) ([]byte, error)
|
||||
IsPinnedCodeFn func(ctx sdk.Context, codeID uint64) bool
|
||||
GetCodeInfoFn func(ctx sdk.Context, codeID uint64) *types.CodeInfo
|
||||
}
|
||||
|
||||
func (m mockWasmQueryKeeper) GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo {
|
||||
if m.GetContractInfoFn == nil {
|
||||
panic("not expected to be called")
|
||||
}
|
||||
return m.GetContractInfoFn(ctx, contractAddress)
|
||||
}
|
||||
|
||||
func (m mockWasmQueryKeeper) QueryRaw(ctx sdk.Context, contractAddress sdk.AccAddress, key []byte) []byte {
|
||||
if m.QueryRawFn == nil {
|
||||
panic("not expected to be called")
|
||||
}
|
||||
return m.QueryRawFn(ctx, contractAddress, key)
|
||||
}
|
||||
|
||||
func (m mockWasmQueryKeeper) QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error) {
|
||||
if m.QuerySmartFn == nil {
|
||||
panic("not expected to be called")
|
||||
}
|
||||
return m.QuerySmartFn(ctx, contractAddr, req)
|
||||
}
|
||||
|
||||
func (m mockWasmQueryKeeper) IsPinnedCode(ctx sdk.Context, codeID uint64) bool {
|
||||
if m.IsPinnedCodeFn == nil {
|
||||
panic("not expected to be called")
|
||||
}
|
||||
return m.IsPinnedCodeFn(ctx, codeID)
|
||||
}
|
||||
|
||||
func (m mockWasmQueryKeeper) GetCodeInfo(ctx sdk.Context, codeID uint64) *types.CodeInfo {
|
||||
if m.GetCodeInfoFn == nil {
|
||||
panic("not expected to be called")
|
||||
}
|
||||
return m.GetCodeInfoFn(ctx, codeID)
|
||||
}
|
||||
|
||||
type bankKeeperMock struct {
|
||||
GetSupplyFn func(ctx sdk.Context, denom string) sdk.Coin
|
||||
GetBalanceFn func(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin
|
||||
GetAllBalancesFn func(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
|
||||
}
|
||||
|
||||
func (m bankKeeperMock) GetSupply(ctx sdk.Context, denom string) sdk.Coin {
|
||||
if m.GetSupplyFn == nil {
|
||||
panic("not expected to be called")
|
||||
}
|
||||
return m.GetSupplyFn(ctx, denom)
|
||||
}
|
||||
|
||||
func (m bankKeeperMock) GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin {
|
||||
if m.GetBalanceFn == nil {
|
||||
panic("not expected to be called")
|
||||
}
|
||||
return m.GetBalanceFn(ctx, addr, denom)
|
||||
}
|
||||
|
||||
func (m bankKeeperMock) GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins {
|
||||
if m.GetAllBalancesFn == nil {
|
||||
panic("not expected to be called")
|
||||
}
|
||||
return m.GetAllBalancesFn(ctx, addr)
|
||||
}
|
||||
|
||||
func TestConvertProtoToJSONMarshal(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
queryPath string
|
||||
protoResponseStruct codec.ProtoMarshaler
|
||||
originalResponse string
|
||||
expectedProtoResponse codec.ProtoMarshaler
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "successful conversion from proto response to json marshalled response",
|
||||
queryPath: "/cosmos.bank.v1beta1.Query/AllBalances",
|
||||
originalResponse: "0a090a036261721202333012050a03666f6f",
|
||||
protoResponseStruct: &banktypes.QueryAllBalancesResponse{},
|
||||
expectedProtoResponse: &banktypes.QueryAllBalancesResponse{
|
||||
Balances: sdk.NewCoins(sdk.NewCoin("bar", sdk.NewInt(30))),
|
||||
Pagination: &query.PageResponse{
|
||||
NextKey: []byte("foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid proto response struct",
|
||||
queryPath: "/cosmos.bank.v1beta1.Query/AllBalances",
|
||||
originalResponse: "0a090a036261721202333012050a03666f6f",
|
||||
protoResponseStruct: &authtypes.QueryAccountResponse{},
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Case %s", tc.name), func(t *testing.T) {
|
||||
originalVersionBz, err := hex.DecodeString(tc.originalResponse)
|
||||
require.NoError(t, err)
|
||||
appCodec := app.MakeEncodingConfig().Marshaler
|
||||
|
||||
jsonMarshalledResponse, err := keeper.ConvertProtoToJSONMarshal(appCodec, tc.protoResponseStruct, originalVersionBz)
|
||||
if tc.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// check response by json marshalling proto response into json response manually
|
||||
jsonMarshalExpectedResponse, err := appCodec.MarshalJSON(tc.expectedProtoResponse)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, string(jsonMarshalledResponse), string(jsonMarshalExpectedResponse))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeterministicJsonMarshal tests that we get deterministic JSON marshalled response upon
|
||||
// proto struct update in the state machine.
|
||||
func TestDeterministicJsonMarshal(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
originalResponse string
|
||||
updatedResponse string
|
||||
queryPath string
|
||||
responseProtoStruct codec.ProtoMarshaler
|
||||
expectedProto func() codec.ProtoMarshaler
|
||||
}{
|
||||
/**
|
||||
*
|
||||
* Origin Response
|
||||
* 0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f7331346c3268686a6e676c3939367772703935673867646a6871653038326375367a7732706c686b
|
||||
*
|
||||
* Updated Response
|
||||
* 0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f7331646a783375676866736d6b6135386676673076616a6e6533766c72776b7a6a346e6377747271122d636f736d6f7331646a783375676866736d6b6135386676673076616a6e6533766c72776b7a6a346e6377747271
|
||||
// Origin proto
|
||||
message QueryAccountResponse {
|
||||
// account defines the account of the corresponding address.
|
||||
google.protobuf.Any account = 1 [(cosmos_proto.accepts_interface) = "AccountI"];
|
||||
}
|
||||
// Updated proto
|
||||
message QueryAccountResponse {
|
||||
// account defines the account of the corresponding address.
|
||||
google.protobuf.Any account = 1 [(cosmos_proto.accepts_interface) = "AccountI"];
|
||||
// address is the address to query for.
|
||||
string address = 2;
|
||||
}
|
||||
*/
|
||||
{
|
||||
"Query Account",
|
||||
"0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f733166387578756c746e3873717a687a6e72737a3371373778776171756867727367366a79766679",
|
||||
"0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f733166387578756c746e3873717a687a6e72737a3371373778776171756867727367366a79766679122d636f736d6f733166387578756c746e3873717a687a6e72737a3371373778776171756867727367366a79766679",
|
||||
"/cosmos.auth.v1beta1.Query/Account",
|
||||
&authtypes.QueryAccountResponse{},
|
||||
func() codec.ProtoMarshaler {
|
||||
account := authtypes.BaseAccount{
|
||||
Address: "cosmos1f8uxultn8sqzhznrsz3q77xwaquhgrsg6jyvfy",
|
||||
}
|
||||
accountResponse, err := codectypes.NewAnyWithValue(&account)
|
||||
require.NoError(t, err)
|
||||
return &authtypes.QueryAccountResponse{
|
||||
Account: accountResponse,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Case %s", tc.name), func(t *testing.T) {
|
||||
appCodec := app.MakeEncodingConfig().Marshaler
|
||||
|
||||
originVersionBz, err := hex.DecodeString(tc.originalResponse)
|
||||
require.NoError(t, err)
|
||||
jsonMarshalledOriginalBz, err := keeper.ConvertProtoToJSONMarshal(appCodec, tc.responseProtoStruct, originVersionBz)
|
||||
require.NoError(t, err)
|
||||
|
||||
newVersionBz, err := hex.DecodeString(tc.updatedResponse)
|
||||
require.NoError(t, err)
|
||||
jsonMarshalledUpdatedBz, err := keeper.ConvertProtoToJSONMarshal(appCodec, tc.responseProtoStruct, newVersionBz)
|
||||
require.NoError(t, err)
|
||||
|
||||
// json marshalled bytes should be the same since we use the same proto struct for unmarshalling
|
||||
require.Equal(t, jsonMarshalledOriginalBz, jsonMarshalledUpdatedBz)
|
||||
|
||||
// raw build also make same result
|
||||
jsonMarshalExpectedResponse, err := appCodec.MarshalJSON(tc.expectedProto())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, jsonMarshalledUpdatedBz, jsonMarshalExpectedResponse)
|
||||
})
|
||||
}
|
||||
}
|
306
x/wasm/keeper/recurse_test.go
Normal file
@ -0,0 +1,306 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
type Recurse struct {
|
||||
Depth uint32 `json:"depth"`
|
||||
Work uint32 `json:"work"`
|
||||
}
|
||||
|
||||
type recurseWrapper struct {
|
||||
Recurse Recurse `json:"recurse"`
|
||||
}
|
||||
|
||||
func buildRecurseQuery(t *testing.T, msg Recurse) []byte {
|
||||
wrapper := recurseWrapper{Recurse: msg}
|
||||
bz, err := json.Marshal(wrapper)
|
||||
require.NoError(t, err)
|
||||
return bz
|
||||
}
|
||||
|
||||
type recurseResponse struct {
|
||||
Hashed []byte `json:"hashed"`
|
||||
}
|
||||
|
||||
// number os wasm queries called from a contract
|
||||
var totalWasmQueryCounter int
|
||||
|
||||
func initRecurseContract(t *testing.T) (contract sdk.AccAddress, creator sdk.AccAddress, ctx sdk.Context, keeper *Keeper) {
|
||||
countingQuerierDec := func(realWasmQuerier WasmVMQueryHandler) WasmVMQueryHandler {
|
||||
return WasmVMQueryHandlerFn(func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) {
|
||||
totalWasmQueryCounter++
|
||||
return realWasmQuerier.HandleQuery(ctx, caller, request)
|
||||
})
|
||||
}
|
||||
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithQueryHandlerDecorator(countingQuerierDec))
|
||||
keeper = keepers.WasmKeeper
|
||||
exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers)
|
||||
return exampleContract.Contract, exampleContract.CreatorAddr, ctx, keeper
|
||||
}
|
||||
|
||||
func TestGasCostOnQuery(t *testing.T) {
|
||||
const (
|
||||
GasNoWork uint64 = 63_950
|
||||
// Note: about 100 SDK gas (10k wasmer gas) for each round of sha256
|
||||
GasWork50 uint64 = 64_218 // this is a little shy of 50k gas - to keep an eye on the limit
|
||||
|
||||
GasReturnUnhashed uint64 = 32
|
||||
GasReturnHashed uint64 = 27
|
||||
)
|
||||
|
||||
cases := map[string]struct {
|
||||
gasLimit uint64
|
||||
msg Recurse
|
||||
expectedGas uint64
|
||||
}{
|
||||
"no recursion, no work": {
|
||||
gasLimit: 400_000,
|
||||
msg: Recurse{},
|
||||
expectedGas: GasNoWork,
|
||||
},
|
||||
"no recursion, some work": {
|
||||
gasLimit: 400_000,
|
||||
msg: Recurse{
|
||||
Work: 50, // 50 rounds of sha256 inside the contract
|
||||
},
|
||||
expectedGas: GasWork50,
|
||||
},
|
||||
"recursion 1, no work": {
|
||||
gasLimit: 400_000,
|
||||
msg: Recurse{
|
||||
Depth: 1,
|
||||
},
|
||||
expectedGas: 2*GasNoWork + GasReturnUnhashed,
|
||||
},
|
||||
"recursion 1, some work": {
|
||||
gasLimit: 400_000,
|
||||
msg: Recurse{
|
||||
Depth: 1,
|
||||
Work: 50,
|
||||
},
|
||||
expectedGas: 2*GasWork50 + GasReturnHashed,
|
||||
},
|
||||
"recursion 4, some work": {
|
||||
gasLimit: 400_000,
|
||||
msg: Recurse{
|
||||
Depth: 4,
|
||||
Work: 50,
|
||||
},
|
||||
expectedGas: 5*GasWork50 + 4*GasReturnHashed,
|
||||
},
|
||||
}
|
||||
|
||||
contractAddr, _, ctx, keeper := initRecurseContract(t)
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// external limit has no effect (we get a panic if this is enforced)
|
||||
keeper.queryGasLimit = 1000
|
||||
|
||||
// make sure we set a limit before calling
|
||||
ctx = ctx.WithGasMeter(sdk.NewGasMeter(tc.gasLimit))
|
||||
require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed())
|
||||
|
||||
// do the query
|
||||
recurse := tc.msg
|
||||
msg := buildRecurseQuery(t, recurse)
|
||||
data, err := keeper.QuerySmart(ctx, contractAddr, msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// check the gas is what we expected
|
||||
if types.EnableGasVerification {
|
||||
assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed())
|
||||
}
|
||||
// assert result is 32 byte sha256 hash (if hashed), or contractAddr if not
|
||||
var resp recurseResponse
|
||||
err = json.Unmarshal(data, &resp)
|
||||
require.NoError(t, err)
|
||||
if recurse.Work == 0 {
|
||||
assert.Equal(t, len(contractAddr.String()), len(resp.Hashed))
|
||||
} else {
|
||||
assert.Equal(t, 32, len(resp.Hashed))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGasOnExternalQuery(t *testing.T) {
|
||||
const (
|
||||
GasWork50 uint64 = DefaultInstanceCost + 8_464
|
||||
)
|
||||
|
||||
cases := map[string]struct {
|
||||
gasLimit uint64
|
||||
msg Recurse
|
||||
expOutOfGas bool
|
||||
}{
|
||||
"no recursion, plenty gas": {
|
||||
gasLimit: 400_000,
|
||||
msg: Recurse{
|
||||
Work: 50, // 50 rounds of sha256 inside the contract
|
||||
},
|
||||
},
|
||||
"recursion 4, plenty gas": {
|
||||
// this uses 244708 gas
|
||||
gasLimit: 400_000,
|
||||
msg: Recurse{
|
||||
Depth: 4,
|
||||
Work: 50,
|
||||
},
|
||||
},
|
||||
"no recursion, external gas limit": {
|
||||
gasLimit: 5000, // this is not enough
|
||||
msg: Recurse{
|
||||
Work: 50,
|
||||
},
|
||||
expOutOfGas: true,
|
||||
},
|
||||
"recursion 4, external gas limit": {
|
||||
// this uses 244708 gas but give less
|
||||
gasLimit: 4 * GasWork50,
|
||||
msg: Recurse{
|
||||
Depth: 4,
|
||||
Work: 50,
|
||||
},
|
||||
expOutOfGas: true,
|
||||
},
|
||||
}
|
||||
|
||||
contractAddr, _, ctx, keeper := initRecurseContract(t)
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
recurse := tc.msg
|
||||
msg := buildRecurseQuery(t, recurse)
|
||||
|
||||
querier := NewGrpcQuerier(keeper.cdc, keeper.storeKey, keeper, tc.gasLimit)
|
||||
req := &types.QuerySmartContractStateRequest{Address: contractAddr.String(), QueryData: msg}
|
||||
_, gotErr := querier.SmartContractState(sdk.WrapSDKContext(ctx), req)
|
||||
if tc.expOutOfGas {
|
||||
require.Error(t, gotErr, sdkerrors.ErrOutOfGas)
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitRecursiveQueryGas(t *testing.T) {
|
||||
// The point of this test from https://github.com/CosmWasm/cosmwasm/issues/456
|
||||
// Basically, if I burn 90% of gas in CPU loop, then query out (to my self)
|
||||
// the sub-query will have all the original gas (minus the 40k instance charge)
|
||||
// and can burn 90% and call a sub-contract again...
|
||||
// This attack would allow us to use far more than the provided gas before
|
||||
// eventually hitting an OutOfGas panic.
|
||||
|
||||
const (
|
||||
// Note: about 100 SDK gas (10k wasmer gas) for each round of sha256
|
||||
GasWork2k uint64 = 77_206 // = NewContractInstanceCosts + x // we have 6x gas used in cpu than in the instance
|
||||
// This is overhead for calling into a sub-contract
|
||||
GasReturnHashed uint64 = 27
|
||||
)
|
||||
|
||||
cases := map[string]struct {
|
||||
gasLimit uint64
|
||||
msg Recurse
|
||||
expectQueriesFromContract int
|
||||
expectedGas uint64
|
||||
expectOutOfGas bool
|
||||
expectError string
|
||||
}{
|
||||
"no recursion, lots of work": {
|
||||
gasLimit: 4_000_000,
|
||||
msg: Recurse{
|
||||
Depth: 0,
|
||||
Work: 2000,
|
||||
},
|
||||
expectQueriesFromContract: 0,
|
||||
expectedGas: GasWork2k,
|
||||
},
|
||||
"recursion 5, lots of work": {
|
||||
gasLimit: 4_000_000,
|
||||
msg: Recurse{
|
||||
Depth: 5,
|
||||
Work: 2000,
|
||||
},
|
||||
expectQueriesFromContract: 5,
|
||||
// FIXME: why -1 ... confused a bit by calculations, seems like rounding issues
|
||||
expectedGas: GasWork2k + 5*(GasWork2k+GasReturnHashed),
|
||||
},
|
||||
// this is where we expect an error...
|
||||
// it has enough gas to run 5 times and die on the 6th (5th time dispatching to sub-contract)
|
||||
// however, if we don't charge the cpu gas before sub-dispatching, we can recurse over 20 times
|
||||
"deep recursion, should die on 5th level": {
|
||||
gasLimit: 400_000,
|
||||
msg: Recurse{
|
||||
Depth: 50,
|
||||
Work: 2000,
|
||||
},
|
||||
expectQueriesFromContract: 5,
|
||||
expectOutOfGas: true,
|
||||
},
|
||||
"very deep recursion, hits recursion limit": {
|
||||
gasLimit: 10_000_000,
|
||||
msg: Recurse{
|
||||
Depth: 100,
|
||||
Work: 2000,
|
||||
},
|
||||
expectQueriesFromContract: 10,
|
||||
expectOutOfGas: false,
|
||||
expectError: "query wasm contract failed", // Error we get from the contract instance doing the failing query, not wasmd
|
||||
expectedGas: 10*(GasWork2k+GasReturnHashed) - 247,
|
||||
},
|
||||
}
|
||||
|
||||
contractAddr, _, ctx, keeper := initRecurseContract(t)
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// reset the counter before test
|
||||
totalWasmQueryCounter = 0
|
||||
|
||||
// make sure we set a limit before calling
|
||||
ctx = ctx.WithGasMeter(sdk.NewGasMeter(tc.gasLimit))
|
||||
require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed())
|
||||
|
||||
// prepare the query
|
||||
recurse := tc.msg
|
||||
msg := buildRecurseQuery(t, recurse)
|
||||
|
||||
// if we expect out of gas, make sure this panics
|
||||
if tc.expectOutOfGas {
|
||||
require.Panics(t, func() {
|
||||
_, err := keeper.QuerySmart(ctx, contractAddr, msg)
|
||||
t.Logf("Got error not panic: %#v", err)
|
||||
})
|
||||
assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, we expect a successful call
|
||||
_, err := keeper.QuerySmart(ctx, contractAddr, msg)
|
||||
if tc.expectError != "" {
|
||||
require.ErrorContains(t, err, tc.expectError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
if types.EnableGasVerification {
|
||||
assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed())
|
||||
}
|
||||
assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter)
|
||||
})
|
||||
}
|
||||
}
|
665
x/wasm/keeper/reflect_test.go
Normal file
@ -0,0 +1,665 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
|
||||
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/testdata"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// ReflectInitMsg is {}
|
||||
|
||||
func buildReflectQuery(t *testing.T, query *testdata.ReflectQueryMsg) []byte {
|
||||
bz, err := json.Marshal(query)
|
||||
require.NoError(t, err)
|
||||
return bz
|
||||
}
|
||||
|
||||
func mustParse(t *testing.T, data []byte, res interface{}) {
|
||||
err := json.Unmarshal(data, res)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
const ReflectFeatures = "staking,mask,stargate,cosmwasm_1_1"
|
||||
|
||||
func TestReflectContractSend(t *testing.T) {
|
||||
cdc := MakeEncodingConfig(t).Marshaler
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)))
|
||||
accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.ContractKeeper, keepers.BankKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
_, _, bob := keyPubAddr()
|
||||
|
||||
// upload reflect code
|
||||
reflectID, _, err := keeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), reflectID)
|
||||
|
||||
// upload hackatom escrow code
|
||||
escrowCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
escrowID, _, err := keeper.Create(ctx, creator, escrowCode, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(2), escrowID)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
reflectStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000))
|
||||
reflectAddr, _, err := keeper.Instantiate(ctx, reflectID, creator, nil, []byte("{}"), "reflect contract 2", reflectStart)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, reflectAddr)
|
||||
|
||||
// now we set contract as verifier of an escrow
|
||||
initMsg := HackatomExampleInitMsg{
|
||||
Verifier: reflectAddr,
|
||||
Beneficiary: bob,
|
||||
}
|
||||
initMsgBz, err := json.Marshal(initMsg)
|
||||
require.NoError(t, err)
|
||||
escrowStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 25000))
|
||||
escrowAddr, _, err := keeper.Instantiate(ctx, escrowID, creator, nil, initMsgBz, "escrow contract 2", escrowStart)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, escrowAddr)
|
||||
|
||||
// let's make sure all balances make sense
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, creator, sdk.NewCoins(sdk.NewInt64Coin("denom", 35000))) // 100k - 40k - 25k
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, reflectAddr, reflectStart)
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, escrowAddr, escrowStart)
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, bob, nil)
|
||||
|
||||
// now for the trick.... we reflect a message through the reflect to call the escrow
|
||||
// we also send an additional 14k tokens there.
|
||||
// this should reduce the reflect balance by 14k (to 26k)
|
||||
// this 14k is added to the escrow, then the entire balance is sent to bob (total: 39k)
|
||||
approveMsg := []byte(`{"release":{}}`)
|
||||
msgs := []wasmvmtypes.CosmosMsg{{
|
||||
Wasm: &wasmvmtypes.WasmMsg{
|
||||
Execute: &wasmvmtypes.ExecuteMsg{
|
||||
ContractAddr: escrowAddr.String(),
|
||||
Msg: approveMsg,
|
||||
Funds: []wasmvmtypes.Coin{{
|
||||
Denom: "denom",
|
||||
Amount: "14000",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}}
|
||||
reflectSend := testdata.ReflectHandleMsg{
|
||||
Reflect: &testdata.ReflectPayload{
|
||||
Msgs: msgs,
|
||||
},
|
||||
}
|
||||
reflectSendBz, err := json.Marshal(reflectSend)
|
||||
require.NoError(t, err)
|
||||
_, err = keeper.Execute(ctx, reflectAddr, creator, reflectSendBz, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// did this work???
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, creator, sdk.NewCoins(sdk.NewInt64Coin("denom", 35000))) // same as before
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, reflectAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 26000))) // 40k - 14k (from send)
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, escrowAddr, sdk.Coins{}) // emptied reserved
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, bob, sdk.NewCoins(sdk.NewInt64Coin("denom", 39000))) // all escrow of 25k + 14k
|
||||
}
|
||||
|
||||
func TestReflectCustomMsg(t *testing.T) {
|
||||
cdc := MakeEncodingConfig(t).Marshaler
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins()))
|
||||
accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.ContractKeeper, keepers.BankKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
bob := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
_, _, fred := keyPubAddr()
|
||||
|
||||
// upload code
|
||||
codeID, _, err := keeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), codeID)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000))
|
||||
contractAddr, _, err := keeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, contractAddr)
|
||||
|
||||
// set owner to bob
|
||||
transfer := testdata.ReflectHandleMsg{
|
||||
ChangeOwner: &testdata.OwnerPayload{
|
||||
Owner: bob,
|
||||
},
|
||||
}
|
||||
transferBz, err := json.Marshal(transfer)
|
||||
require.NoError(t, err)
|
||||
_, err = keeper.Execute(ctx, contractAddr, creator, transferBz, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// check some account values
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, contractStart)
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, bob, deposit)
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, fred, nil)
|
||||
|
||||
// bob can send contract's tokens to fred (using SendMsg)
|
||||
msgs := []wasmvmtypes.CosmosMsg{{
|
||||
Bank: &wasmvmtypes.BankMsg{
|
||||
Send: &wasmvmtypes.SendMsg{
|
||||
ToAddress: fred.String(),
|
||||
Amount: []wasmvmtypes.Coin{{
|
||||
Denom: "denom",
|
||||
Amount: "15000",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}}
|
||||
reflectSend := testdata.ReflectHandleMsg{
|
||||
Reflect: &testdata.ReflectPayload{
|
||||
Msgs: msgs,
|
||||
},
|
||||
}
|
||||
reflectSendBz, err := json.Marshal(reflectSend)
|
||||
require.NoError(t, err)
|
||||
_, err = keeper.Execute(ctx, contractAddr, bob, reflectSendBz, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fred got coins
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, fred, sdk.NewCoins(sdk.NewInt64Coin("denom", 15000)))
|
||||
// contract lost them
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 25000)))
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, bob, deposit)
|
||||
|
||||
// construct an opaque message
|
||||
var sdkSendMsg sdk.Msg = &banktypes.MsgSend{
|
||||
FromAddress: contractAddr.String(),
|
||||
ToAddress: fred.String(),
|
||||
Amount: sdk.NewCoins(sdk.NewInt64Coin("denom", 23000)),
|
||||
}
|
||||
opaque, err := toReflectRawMsg(cdc, sdkSendMsg)
|
||||
require.NoError(t, err)
|
||||
reflectOpaque := testdata.ReflectHandleMsg{
|
||||
Reflect: &testdata.ReflectPayload{
|
||||
Msgs: []wasmvmtypes.CosmosMsg{opaque},
|
||||
},
|
||||
}
|
||||
reflectOpaqueBz, err := json.Marshal(reflectOpaque)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = keeper.Execute(ctx, contractAddr, bob, reflectOpaqueBz, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fred got more coins
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, fred, sdk.NewCoins(sdk.NewInt64Coin("denom", 38000)))
|
||||
// contract lost them
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 2000)))
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, bob, deposit)
|
||||
}
|
||||
|
||||
func TestMaskReflectCustomQuery(t *testing.T) {
|
||||
cdc := MakeEncodingConfig(t).Marshaler
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins()))
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
|
||||
// upload code
|
||||
codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), codeID)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000))
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, contractAddr)
|
||||
|
||||
// let's perform a normal query of state
|
||||
ownerQuery := testdata.ReflectQueryMsg{
|
||||
Owner: &struct{}{},
|
||||
}
|
||||
ownerQueryBz, err := json.Marshal(ownerQuery)
|
||||
require.NoError(t, err)
|
||||
ownerRes, err := keeper.QuerySmart(ctx, contractAddr, ownerQueryBz)
|
||||
require.NoError(t, err)
|
||||
var res testdata.OwnerResponse
|
||||
err = json.Unmarshal(ownerRes, &res)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, res.Owner, creator.String())
|
||||
|
||||
// and now making use of the custom querier callbacks
|
||||
customQuery := testdata.ReflectQueryMsg{
|
||||
Capitalized: &testdata.Text{
|
||||
Text: "all Caps noW",
|
||||
},
|
||||
}
|
||||
customQueryBz, err := json.Marshal(customQuery)
|
||||
require.NoError(t, err)
|
||||
custom, err := keeper.QuerySmart(ctx, contractAddr, customQueryBz)
|
||||
require.NoError(t, err)
|
||||
var resp capitalizedResponse
|
||||
err = json.Unmarshal(custom, &resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, resp.Text, "ALL CAPS NOW")
|
||||
}
|
||||
|
||||
func TestReflectStargateQuery(t *testing.T) {
|
||||
cdc := MakeEncodingConfig(t).Marshaler
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins()))
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
funds := sdk.NewCoins(sdk.NewInt64Coin("denom", 320000))
|
||||
contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000))
|
||||
expectedBalance := funds.Sub(contractStart)
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, funds...)
|
||||
|
||||
// upload code
|
||||
codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), codeID)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, contractAddr)
|
||||
|
||||
// first, normal query for the bank balance (to make sure our query is proper)
|
||||
bankQuery := wasmvmtypes.QueryRequest{
|
||||
Bank: &wasmvmtypes.BankQuery{
|
||||
AllBalances: &wasmvmtypes.AllBalancesQuery{
|
||||
Address: creator.String(),
|
||||
},
|
||||
},
|
||||
}
|
||||
simpleQueryBz, err := json.Marshal(testdata.ReflectQueryMsg{
|
||||
Chain: &testdata.ChainQuery{Request: &bankQuery},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
simpleRes, err := keeper.QuerySmart(ctx, contractAddr, simpleQueryBz)
|
||||
require.NoError(t, err)
|
||||
var simpleChain testdata.ChainResponse
|
||||
mustParse(t, simpleRes, &simpleChain)
|
||||
var simpleBalance wasmvmtypes.AllBalancesResponse
|
||||
mustParse(t, simpleChain.Data, &simpleBalance)
|
||||
require.Equal(t, len(expectedBalance), len(simpleBalance.Amount))
|
||||
assert.Equal(t, simpleBalance.Amount[0].Amount, expectedBalance[0].Amount.String())
|
||||
assert.Equal(t, simpleBalance.Amount[0].Denom, expectedBalance[0].Denom)
|
||||
}
|
||||
|
||||
func TestReflectTotalSupplyQuery(t *testing.T) {
|
||||
cdc := MakeEncodingConfig(t).Marshaler
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins()))
|
||||
keeper := keepers.WasmKeeper
|
||||
// upload code
|
||||
codeID := StoreReflectContract(t, ctx, keepers).CodeID
|
||||
// creator instantiates a contract and gives it tokens
|
||||
creator := RandomAccountAddress(t)
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "testing", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
currentStakeSupply := keepers.BankKeeper.GetSupply(ctx, "stake")
|
||||
require.NotEmpty(t, currentStakeSupply.Amount) // ensure we have real data
|
||||
specs := map[string]struct {
|
||||
denom string
|
||||
expAmount wasmvmtypes.Coin
|
||||
}{
|
||||
"known denom": {
|
||||
denom: "stake",
|
||||
expAmount: ConvertSdkCoinToWasmCoin(currentStakeSupply),
|
||||
},
|
||||
"unknown denom": {
|
||||
denom: "unknown",
|
||||
expAmount: wasmvmtypes.Coin{Denom: "unknown", Amount: "0"},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// when
|
||||
queryBz := mustMarshal(t, testdata.ReflectQueryMsg{
|
||||
Chain: &testdata.ChainQuery{
|
||||
Request: &wasmvmtypes.QueryRequest{
|
||||
Bank: &wasmvmtypes.BankQuery{
|
||||
Supply: &wasmvmtypes.SupplyQuery{spec.denom},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
simpleRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
var rsp testdata.ChainResponse
|
||||
mustParse(t, simpleRes, &rsp)
|
||||
var supplyRsp wasmvmtypes.SupplyResponse
|
||||
mustParse(t, rsp.Data, &supplyRsp)
|
||||
assert.Equal(t, spec.expAmount, supplyRsp.Amount, spec.expAmount)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReflectInvalidStargateQuery(t *testing.T) {
|
||||
cdc := MakeEncodingConfig(t).Marshaler
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins()))
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
funds := sdk.NewCoins(sdk.NewInt64Coin("denom", 320000))
|
||||
contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000))
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, funds...)
|
||||
|
||||
// upload code
|
||||
codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), codeID)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, contractAddr)
|
||||
|
||||
// now, try to build a protobuf query
|
||||
protoQuery := banktypes.QueryAllBalancesRequest{
|
||||
Address: creator.String(),
|
||||
}
|
||||
protoQueryBin, err := proto.Marshal(&protoQuery)
|
||||
protoRequest := wasmvmtypes.QueryRequest{
|
||||
Stargate: &wasmvmtypes.StargateQuery{
|
||||
Path: "/cosmos.bank.v1beta1.Query/AllBalances",
|
||||
Data: protoQueryBin,
|
||||
},
|
||||
}
|
||||
protoQueryBz, err := json.Marshal(testdata.ReflectQueryMsg{
|
||||
Chain: &testdata.ChainQuery{Request: &protoRequest},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// make a query on the chain, should not be whitelisted
|
||||
_, err = keeper.QuerySmart(ctx, contractAddr, protoQueryBz)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "Unsupported query")
|
||||
|
||||
// now, try to build a protobuf query
|
||||
protoRequest = wasmvmtypes.QueryRequest{
|
||||
Stargate: &wasmvmtypes.StargateQuery{
|
||||
Path: "/cosmos.tx.v1beta1.Service/GetTx",
|
||||
Data: []byte{},
|
||||
},
|
||||
}
|
||||
protoQueryBz, err = json.Marshal(testdata.ReflectQueryMsg{
|
||||
Chain: &testdata.ChainQuery{Request: &protoRequest},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// make a query on the chain, should be blacklisted
|
||||
_, err = keeper.QuerySmart(ctx, contractAddr, protoQueryBz)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "Unsupported query")
|
||||
|
||||
// and another one
|
||||
protoRequest = wasmvmtypes.QueryRequest{
|
||||
Stargate: &wasmvmtypes.StargateQuery{
|
||||
Path: "/cosmos.base.tendermint.v1beta1.Service/GetNodeInfo",
|
||||
Data: []byte{},
|
||||
},
|
||||
}
|
||||
protoQueryBz, err = json.Marshal(testdata.ReflectQueryMsg{
|
||||
Chain: &testdata.ChainQuery{Request: &protoRequest},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// make a query on the chain, should be blacklisted
|
||||
_, err = keeper.QuerySmart(ctx, contractAddr, protoQueryBz)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "Unsupported query")
|
||||
}
|
||||
|
||||
type reflectState struct {
|
||||
Owner string `json:"owner"`
|
||||
}
|
||||
|
||||
func TestMaskReflectWasmQueries(t *testing.T) {
|
||||
cdc := MakeEncodingConfig(t).Marshaler
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins()))
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
|
||||
// upload reflect code
|
||||
reflectID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), reflectID)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
reflectStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000))
|
||||
reflectAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, reflectID, creator, nil, []byte("{}"), "reflect contract 2", reflectStart)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, reflectAddr)
|
||||
|
||||
// for control, let's make some queries directly on the reflect
|
||||
ownerQuery := buildReflectQuery(t, &testdata.ReflectQueryMsg{Owner: &struct{}{}})
|
||||
res, err := keeper.QuerySmart(ctx, reflectAddr, ownerQuery)
|
||||
require.NoError(t, err)
|
||||
var ownerRes testdata.OwnerResponse
|
||||
mustParse(t, res, &ownerRes)
|
||||
require.Equal(t, ownerRes.Owner, creator.String())
|
||||
|
||||
// and a raw query: cosmwasm_storage::Singleton uses 2 byte big-endian length-prefixed to store data
|
||||
configKey := append([]byte{0, 6}, []byte("config")...)
|
||||
raw := keeper.QueryRaw(ctx, reflectAddr, configKey)
|
||||
var stateRes reflectState
|
||||
mustParse(t, raw, &stateRes)
|
||||
require.Equal(t, stateRes.Owner, creator.String())
|
||||
|
||||
// now, let's reflect a smart query into the x/wasm handlers and see if we get the same result
|
||||
reflectOwnerQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Wasm: &wasmvmtypes.WasmQuery{
|
||||
Smart: &wasmvmtypes.SmartQuery{
|
||||
ContractAddr: reflectAddr.String(),
|
||||
Msg: ownerQuery,
|
||||
},
|
||||
}}}}
|
||||
reflectOwnerBin := buildReflectQuery(t, &reflectOwnerQuery)
|
||||
res, err = keeper.QuerySmart(ctx, reflectAddr, reflectOwnerBin)
|
||||
require.NoError(t, err)
|
||||
// first we pull out the data from chain response, before parsing the original response
|
||||
var reflectRes testdata.ChainResponse
|
||||
mustParse(t, res, &reflectRes)
|
||||
var reflectOwnerRes testdata.OwnerResponse
|
||||
mustParse(t, reflectRes.Data, &reflectOwnerRes)
|
||||
require.Equal(t, reflectOwnerRes.Owner, creator.String())
|
||||
|
||||
// and with queryRaw
|
||||
reflectStateQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Wasm: &wasmvmtypes.WasmQuery{
|
||||
Raw: &wasmvmtypes.RawQuery{
|
||||
ContractAddr: reflectAddr.String(),
|
||||
Key: configKey,
|
||||
},
|
||||
}}}}
|
||||
reflectStateBin := buildReflectQuery(t, &reflectStateQuery)
|
||||
res, err = keeper.QuerySmart(ctx, reflectAddr, reflectStateBin)
|
||||
require.NoError(t, err)
|
||||
// first we pull out the data from chain response, before parsing the original response
|
||||
var reflectRawRes testdata.ChainResponse
|
||||
mustParse(t, res, &reflectRawRes)
|
||||
// now, with the raw data, we can parse it into state
|
||||
var reflectStateRes reflectState
|
||||
mustParse(t, reflectRawRes.Data, &reflectStateRes)
|
||||
require.Equal(t, reflectStateRes.Owner, creator.String())
|
||||
}
|
||||
|
||||
func TestWasmRawQueryWithNil(t *testing.T) {
|
||||
cdc := MakeEncodingConfig(t).Marshaler
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins()))
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
|
||||
// upload reflect code
|
||||
reflectID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), reflectID)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
reflectStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000))
|
||||
reflectAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, reflectID, creator, nil, []byte("{}"), "reflect contract 2", reflectStart)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, reflectAddr)
|
||||
|
||||
// control: query directly
|
||||
missingKey := []byte{0, 1, 2, 3, 4}
|
||||
raw := keeper.QueryRaw(ctx, reflectAddr, missingKey)
|
||||
require.Nil(t, raw)
|
||||
|
||||
// and with queryRaw
|
||||
reflectQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Wasm: &wasmvmtypes.WasmQuery{
|
||||
Raw: &wasmvmtypes.RawQuery{
|
||||
ContractAddr: reflectAddr.String(),
|
||||
Key: missingKey,
|
||||
},
|
||||
}}}}
|
||||
reflectStateBin := buildReflectQuery(t, &reflectQuery)
|
||||
res, err := keeper.QuerySmart(ctx, reflectAddr, reflectStateBin)
|
||||
require.NoError(t, err)
|
||||
|
||||
// first we pull out the data from chain response, before parsing the original response
|
||||
var reflectRawRes testdata.ChainResponse
|
||||
mustParse(t, res, &reflectRawRes)
|
||||
// and make sure there is no data
|
||||
require.Empty(t, reflectRawRes.Data)
|
||||
// we get an empty byte slice not nil (if anyone care in go-land)
|
||||
require.Equal(t, []byte{}, reflectRawRes.Data)
|
||||
}
|
||||
|
||||
func checkAccount(t *testing.T, ctx sdk.Context, accKeeper authkeeper.AccountKeeper, bankKeeper bankkeeper.Keeper, addr sdk.AccAddress, expected sdk.Coins) {
|
||||
acct := accKeeper.GetAccount(ctx, addr)
|
||||
if expected == nil {
|
||||
assert.Nil(t, acct)
|
||||
} else {
|
||||
assert.NotNil(t, acct)
|
||||
if expected.Empty() {
|
||||
// there is confusion between nil and empty slice... let's just treat them the same
|
||||
assert.True(t, bankKeeper.GetAllBalances(ctx, acct.GetAddress()).Empty())
|
||||
} else {
|
||||
assert.Equal(t, bankKeeper.GetAllBalances(ctx, acct.GetAddress()), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**** Code to support custom messages *****/
|
||||
|
||||
type reflectCustomMsg struct {
|
||||
Debug string `json:"debug,omitempty"`
|
||||
Raw []byte `json:"raw,omitempty"`
|
||||
}
|
||||
|
||||
// toReflectRawMsg encodes an sdk msg using any type with json encoding.
|
||||
// Then wraps it as an opaque message
|
||||
func toReflectRawMsg(cdc codec.Codec, msg sdk.Msg) (wasmvmtypes.CosmosMsg, error) {
|
||||
any, err := codectypes.NewAnyWithValue(msg)
|
||||
if err != nil {
|
||||
return wasmvmtypes.CosmosMsg{}, err
|
||||
}
|
||||
rawBz, err := cdc.MarshalJSON(any)
|
||||
if err != nil {
|
||||
return wasmvmtypes.CosmosMsg{}, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||
}
|
||||
customMsg, err := json.Marshal(reflectCustomMsg{
|
||||
Raw: rawBz,
|
||||
})
|
||||
res := wasmvmtypes.CosmosMsg{
|
||||
Custom: customMsg,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// reflectEncoders needs to be registered in test setup to handle custom message callbacks
|
||||
func reflectEncoders(cdc codec.Codec) *MessageEncoders {
|
||||
return &MessageEncoders{
|
||||
Custom: fromReflectRawMsg(cdc),
|
||||
}
|
||||
}
|
||||
|
||||
// fromReflectRawMsg decodes msg.Data to an sdk.Msg using proto Any and json encoding.
|
||||
// this needs to be registered on the Encoders
|
||||
func fromReflectRawMsg(cdc codec.Codec) CustomEncoder {
|
||||
return func(_sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) {
|
||||
var custom reflectCustomMsg
|
||||
err := json.Unmarshal(msg, &custom)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||
}
|
||||
if custom.Raw != nil {
|
||||
var any codectypes.Any
|
||||
if err := cdc.UnmarshalJSON(custom.Raw, &any); err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||
}
|
||||
var msg sdk.Msg
|
||||
if err := cdc.UnpackAny(&any, &msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []sdk.Msg{msg}, nil
|
||||
}
|
||||
if custom.Debug != "" {
|
||||
return nil, sdkerrors.Wrapf(types.ErrInvalidMsg, "Custom Debug: %s", custom.Debug)
|
||||
}
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown Custom message variant")
|
||||
}
|
||||
}
|
||||
|
||||
type reflectCustomQuery struct {
|
||||
Ping *struct{} `json:"ping,omitempty"`
|
||||
Capitalized *testdata.Text `json:"capitalized,omitempty"`
|
||||
}
|
||||
|
||||
// this is from the go code back to the contract (capitalized or ping)
|
||||
type customQueryResponse struct {
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// these are the return values from contract -> go depending on type of query
|
||||
type ownerResponse struct {
|
||||
Owner string `json:"owner"`
|
||||
}
|
||||
|
||||
type capitalizedResponse struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type chainResponse struct {
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
// reflectPlugins needs to be registered in test setup to handle custom query callbacks
|
||||
func reflectPlugins() *QueryPlugins {
|
||||
return &QueryPlugins{
|
||||
Custom: performCustomQuery,
|
||||
}
|
||||
}
|
||||
|
||||
func performCustomQuery(_ sdk.Context, request json.RawMessage) ([]byte, error) {
|
||||
var custom reflectCustomQuery
|
||||
err := json.Unmarshal(request, &custom)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||
}
|
||||
if custom.Capitalized != nil {
|
||||
msg := strings.ToUpper(custom.Capitalized.Text)
|
||||
return json.Marshal(customQueryResponse{Msg: msg})
|
||||
}
|
||||
if custom.Ping != nil {
|
||||
return json.Marshal(customQueryResponse{Msg: "pong"})
|
||||
}
|
||||
return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown Custom query variant")
|
||||
}
|
203
x/wasm/keeper/relay.go
Normal file
@ -0,0 +1,203 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
"github.com/cosmos/cosmos-sdk/telemetry"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
var _ types.IBCContractKeeper = (*Keeper)(nil)
|
||||
|
||||
// OnOpenChannel calls the contract to participate in the IBC channel handshake step.
|
||||
// In the IBC protocol this is either the `Channel Open Init` event on the initiating chain or
|
||||
// `Channel Open Try` on the counterparty chain.
|
||||
// Protocol version and channel ordering should be verified for example.
|
||||
// See https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#channel-lifecycle-management
|
||||
func (k Keeper) OnOpenChannel(
|
||||
ctx sdk.Context,
|
||||
contractAddr sdk.AccAddress,
|
||||
msg wasmvmtypes.IBCChannelOpenMsg,
|
||||
) (string, error) {
|
||||
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-open-channel")
|
||||
_, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
env := types.NewEnv(ctx, contractAddr)
|
||||
querier := k.newQueryHandler(ctx, contractAddr)
|
||||
|
||||
gas := k.runtimeGasForContract(ctx)
|
||||
res, gasUsed, execErr := k.wasmVM.IBCChannelOpen(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization)
|
||||
k.consumeRuntimeGas(ctx, gasUsed)
|
||||
if execErr != nil {
|
||||
return "", sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error())
|
||||
}
|
||||
if res != nil {
|
||||
return res.Version, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// OnConnectChannel calls the contract to let it know the IBC channel was established.
|
||||
// In the IBC protocol this is either the `Channel Open Ack` event on the initiating chain or
|
||||
// `Channel Open Confirm` on the counterparty chain.
|
||||
//
|
||||
// There is an open issue with the [cosmos-sdk](https://github.com/cosmos/cosmos-sdk/issues/8334)
|
||||
// that the counterparty channelID is empty on the initiating chain
|
||||
// See https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#channel-lifecycle-management
|
||||
func (k Keeper) OnConnectChannel(
|
||||
ctx sdk.Context,
|
||||
contractAddr sdk.AccAddress,
|
||||
msg wasmvmtypes.IBCChannelConnectMsg,
|
||||
) error {
|
||||
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-connect-channel")
|
||||
contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
env := types.NewEnv(ctx, contractAddr)
|
||||
querier := k.newQueryHandler(ctx, contractAddr)
|
||||
|
||||
gas := k.runtimeGasForContract(ctx)
|
||||
res, gasUsed, execErr := k.wasmVM.IBCChannelConnect(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization)
|
||||
k.consumeRuntimeGas(ctx, gasUsed)
|
||||
if execErr != nil {
|
||||
return sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error())
|
||||
}
|
||||
|
||||
return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res)
|
||||
}
|
||||
|
||||
// OnCloseChannel calls the contract to let it know the IBC channel is closed.
|
||||
// Calling modules MAY atomically execute appropriate application logic in conjunction with calling chanCloseConfirm.
|
||||
//
|
||||
// Once closed, channels cannot be reopened and identifiers cannot be reused. Identifier reuse is prevented because
|
||||
// we want to prevent potential replay of previously sent packets
|
||||
// See https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#channel-lifecycle-management
|
||||
func (k Keeper) OnCloseChannel(
|
||||
ctx sdk.Context,
|
||||
contractAddr sdk.AccAddress,
|
||||
msg wasmvmtypes.IBCChannelCloseMsg,
|
||||
) error {
|
||||
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-close-channel")
|
||||
|
||||
contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params := types.NewEnv(ctx, contractAddr)
|
||||
querier := k.newQueryHandler(ctx, contractAddr)
|
||||
|
||||
gas := k.runtimeGasForContract(ctx)
|
||||
res, gasUsed, execErr := k.wasmVM.IBCChannelClose(codeInfo.CodeHash, params, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization)
|
||||
k.consumeRuntimeGas(ctx, gasUsed)
|
||||
if execErr != nil {
|
||||
return sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error())
|
||||
}
|
||||
|
||||
return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res)
|
||||
}
|
||||
|
||||
// OnRecvPacket calls the contract to process the incoming IBC packet. The contract fully owns the data processing and
|
||||
// returns the acknowledgement data for the chain level. This allows custom applications and protocols on top
|
||||
// of IBC. Although it is recommended to use the standard acknowledgement envelope defined in
|
||||
// https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#acknowledgement-envelope
|
||||
//
|
||||
// For more information see: https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#packet-flow--handling
|
||||
func (k Keeper) OnRecvPacket(
|
||||
ctx sdk.Context,
|
||||
contractAddr sdk.AccAddress,
|
||||
msg wasmvmtypes.IBCPacketReceiveMsg,
|
||||
) ([]byte, error) {
|
||||
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-recv-packet")
|
||||
contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
env := types.NewEnv(ctx, contractAddr)
|
||||
querier := k.newQueryHandler(ctx, contractAddr)
|
||||
|
||||
gas := k.runtimeGasForContract(ctx)
|
||||
res, gasUsed, execErr := k.wasmVM.IBCPacketReceive(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization)
|
||||
k.consumeRuntimeGas(ctx, gasUsed)
|
||||
if execErr != nil {
|
||||
return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error())
|
||||
}
|
||||
if res.Err != "" { // handle error case as before https://github.com/CosmWasm/wasmvm/commit/c300106fe5c9426a495f8e10821e00a9330c56c6
|
||||
return nil, sdkerrors.Wrap(types.ErrExecuteFailed, res.Err)
|
||||
}
|
||||
// note submessage reply results can overwrite the `Acknowledgement` data
|
||||
return k.handleContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res.Ok.Messages, res.Ok.Attributes, res.Ok.Acknowledgement, res.Ok.Events)
|
||||
}
|
||||
|
||||
// OnAckPacket calls the contract to handle the "acknowledgement" data which can contain success or failure of a packet
|
||||
// acknowledgement written on the receiving chain for example. This is application level data and fully owned by the
|
||||
// contract. The use of the standard acknowledgement envelope is recommended: https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#acknowledgement-envelope
|
||||
//
|
||||
// On application errors the contract can revert an operation like returning tokens as in ibc-transfer.
|
||||
//
|
||||
// For more information see: https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#packet-flow--handling
|
||||
func (k Keeper) OnAckPacket(
|
||||
ctx sdk.Context,
|
||||
contractAddr sdk.AccAddress,
|
||||
msg wasmvmtypes.IBCPacketAckMsg,
|
||||
) error {
|
||||
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-ack-packet")
|
||||
contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
env := types.NewEnv(ctx, contractAddr)
|
||||
querier := k.newQueryHandler(ctx, contractAddr)
|
||||
|
||||
gas := k.runtimeGasForContract(ctx)
|
||||
res, gasUsed, execErr := k.wasmVM.IBCPacketAck(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization)
|
||||
k.consumeRuntimeGas(ctx, gasUsed)
|
||||
if execErr != nil {
|
||||
return sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error())
|
||||
}
|
||||
return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res)
|
||||
}
|
||||
|
||||
// OnTimeoutPacket calls the contract to let it know the packet was never received on the destination chain within
|
||||
// the timeout boundaries.
|
||||
// The contract should handle this on the application level and undo the original operation
|
||||
func (k Keeper) OnTimeoutPacket(
|
||||
ctx sdk.Context,
|
||||
contractAddr sdk.AccAddress,
|
||||
msg wasmvmtypes.IBCPacketTimeoutMsg,
|
||||
) error {
|
||||
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-timeout-packet")
|
||||
|
||||
contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
env := types.NewEnv(ctx, contractAddr)
|
||||
querier := k.newQueryHandler(ctx, contractAddr)
|
||||
|
||||
gas := k.runtimeGasForContract(ctx)
|
||||
res, gasUsed, execErr := k.wasmVM.IBCPacketTimeout(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization)
|
||||
k.consumeRuntimeGas(ctx, gasUsed)
|
||||
if execErr != nil {
|
||||
return sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error())
|
||||
}
|
||||
|
||||
return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res)
|
||||
}
|
||||
|
||||
func (k Keeper) handleIBCBasicContractResponse(ctx sdk.Context, addr sdk.AccAddress, id string, res *wasmvmtypes.IBCBasicResponse) error {
|
||||
_, err := k.handleContractResponse(ctx, addr, id, res.Messages, res.Attributes, nil, res.Events)
|
||||
return err
|
||||
}
|
703
x/wasm/keeper/relay_test.go
Normal file
@ -0,0 +1,703 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
wasmvm "github.com/CosmWasm/wasmvm"
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
func TestOnOpenChannel(t *testing.T) {
|
||||
var m wasmtesting.MockWasmer
|
||||
wasmtesting.MakeIBCInstantiable(&m)
|
||||
messenger := &wasmtesting.MockMessageHandler{}
|
||||
parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger))
|
||||
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
|
||||
const myContractGas = 40
|
||||
|
||||
specs := map[string]struct {
|
||||
contractAddr sdk.AccAddress
|
||||
contractGas sdk.Gas
|
||||
contractErr error
|
||||
expGas uint64
|
||||
expErr bool
|
||||
}{
|
||||
"consume contract gas": {
|
||||
contractAddr: example.Contract,
|
||||
contractGas: myContractGas,
|
||||
expGas: myContractGas,
|
||||
},
|
||||
"consume max gas": {
|
||||
contractAddr: example.Contract,
|
||||
contractGas: math.MaxUint64 / DefaultGasMultiplier,
|
||||
expGas: math.MaxUint64 / DefaultGasMultiplier,
|
||||
},
|
||||
"consume gas on error": {
|
||||
contractAddr: example.Contract,
|
||||
contractGas: myContractGas,
|
||||
contractErr: errors.New("test, ignore"),
|
||||
expErr: true,
|
||||
},
|
||||
"unknown contract address": {
|
||||
contractAddr: RandomAccountAddress(t),
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
myChannel := wasmvmtypes.IBCChannel{Version: "my test channel"}
|
||||
myMsg := wasmvmtypes.IBCChannelOpenMsg{OpenTry: &wasmvmtypes.IBCOpenTry{Channel: myChannel, CounterpartyVersion: "foo"}}
|
||||
m.IBCChannelOpenFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) {
|
||||
assert.Equal(t, myMsg, msg)
|
||||
return &wasmvmtypes.IBC3ChannelOpenResponse{}, spec.contractGas * DefaultGasMultiplier, spec.contractErr
|
||||
}
|
||||
|
||||
ctx, _ := parentCtx.CacheContext()
|
||||
before := ctx.GasMeter().GasConsumed()
|
||||
|
||||
// when
|
||||
msg := wasmvmtypes.IBCChannelOpenMsg{
|
||||
OpenTry: &wasmvmtypes.IBCOpenTry{
|
||||
Channel: myChannel,
|
||||
CounterpartyVersion: "foo",
|
||||
},
|
||||
}
|
||||
_, err := keepers.WasmKeeper.OnOpenChannel(ctx, spec.contractAddr, msg)
|
||||
|
||||
// then
|
||||
if spec.expErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
// verify gas consumed
|
||||
const storageCosts = sdk.Gas(2903)
|
||||
assert.Equal(t, spec.expGas, ctx.GasMeter().GasConsumed()-before-storageCosts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnConnectChannel(t *testing.T) {
|
||||
var m wasmtesting.MockWasmer
|
||||
wasmtesting.MakeIBCInstantiable(&m)
|
||||
messenger := &wasmtesting.MockMessageHandler{}
|
||||
parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger))
|
||||
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
|
||||
const myContractGas = 40
|
||||
|
||||
specs := map[string]struct {
|
||||
contractAddr sdk.AccAddress
|
||||
contractResp *wasmvmtypes.IBCBasicResponse
|
||||
contractErr error
|
||||
overwriteMessenger *wasmtesting.MockMessageHandler
|
||||
expContractGas sdk.Gas
|
||||
expErr bool
|
||||
expEventTypes []string
|
||||
}{
|
||||
"consume contract gas": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{},
|
||||
},
|
||||
"consume gas on error, ignore events + messages": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}},
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
contractErr: errors.New("test, ignore"),
|
||||
expErr: true,
|
||||
},
|
||||
"dispatch contract messages on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}},
|
||||
},
|
||||
},
|
||||
"emit contract events on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 10,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
expEventTypes: []string{types.WasmModuleEventType},
|
||||
},
|
||||
"messenger errors returned, events stored": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 10,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}},
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
overwriteMessenger: wasmtesting.NewErroringMessageHandler(),
|
||||
expErr: true,
|
||||
expEventTypes: []string{types.WasmModuleEventType},
|
||||
},
|
||||
"unknown contract address": {
|
||||
contractAddr: RandomAccountAddress(t),
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
myChannel := wasmvmtypes.IBCChannel{Version: "my test channel"}
|
||||
myMsg := wasmvmtypes.IBCChannelConnectMsg{OpenConfirm: &wasmvmtypes.IBCOpenConfirm{Channel: myChannel}}
|
||||
m.IBCChannelConnectFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelConnectMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) {
|
||||
assert.Equal(t, msg, myMsg)
|
||||
return spec.contractResp, myContractGas * DefaultGasMultiplier, spec.contractErr
|
||||
}
|
||||
|
||||
ctx, _ := parentCtx.CacheContext()
|
||||
ctx = ctx.WithEventManager(sdk.NewEventManager())
|
||||
|
||||
before := ctx.GasMeter().GasConsumed()
|
||||
msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler()
|
||||
*messenger = *msger
|
||||
if spec.overwriteMessenger != nil {
|
||||
*messenger = *spec.overwriteMessenger
|
||||
}
|
||||
|
||||
// when
|
||||
msg := wasmvmtypes.IBCChannelConnectMsg{
|
||||
OpenConfirm: &wasmvmtypes.IBCOpenConfirm{
|
||||
Channel: myChannel,
|
||||
},
|
||||
}
|
||||
err := keepers.WasmKeeper.OnConnectChannel(ctx, spec.contractAddr, msg)
|
||||
|
||||
// then
|
||||
if spec.expErr {
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, capturedMsgs) // no messages captured on error
|
||||
assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events()))
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
// verify gas consumed
|
||||
const storageCosts = sdk.Gas(2903)
|
||||
assert.Equal(t, spec.expContractGas, ctx.GasMeter().GasConsumed()-before-storageCosts)
|
||||
// verify msgs dispatched
|
||||
require.Len(t, *capturedMsgs, len(spec.contractResp.Messages))
|
||||
for i, m := range spec.contractResp.Messages {
|
||||
assert.Equal(t, (*capturedMsgs)[i], m.Msg)
|
||||
}
|
||||
assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnCloseChannel(t *testing.T) {
|
||||
var m wasmtesting.MockWasmer
|
||||
wasmtesting.MakeIBCInstantiable(&m)
|
||||
messenger := &wasmtesting.MockMessageHandler{}
|
||||
parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger))
|
||||
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
|
||||
const myContractGas = 40
|
||||
|
||||
specs := map[string]struct {
|
||||
contractAddr sdk.AccAddress
|
||||
contractResp *wasmvmtypes.IBCBasicResponse
|
||||
contractErr error
|
||||
overwriteMessenger *wasmtesting.MockMessageHandler
|
||||
expContractGas sdk.Gas
|
||||
expErr bool
|
||||
expEventTypes []string
|
||||
}{
|
||||
"consume contract gas": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{},
|
||||
},
|
||||
"consume gas on error, ignore events + messages": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}},
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
contractErr: errors.New("test, ignore"),
|
||||
expErr: true,
|
||||
},
|
||||
"dispatch contract messages on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}},
|
||||
},
|
||||
},
|
||||
"emit contract events on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 10,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
expEventTypes: []string{types.WasmModuleEventType},
|
||||
},
|
||||
"messenger errors returned, events stored": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 10,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}},
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
overwriteMessenger: wasmtesting.NewErroringMessageHandler(),
|
||||
expErr: true,
|
||||
expEventTypes: []string{types.WasmModuleEventType},
|
||||
},
|
||||
"unknown contract address": {
|
||||
contractAddr: RandomAccountAddress(t),
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
myChannel := wasmvmtypes.IBCChannel{Version: "my test channel"}
|
||||
myMsg := wasmvmtypes.IBCChannelCloseMsg{CloseInit: &wasmvmtypes.IBCCloseInit{Channel: myChannel}}
|
||||
m.IBCChannelCloseFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) {
|
||||
assert.Equal(t, msg, myMsg)
|
||||
return spec.contractResp, myContractGas * DefaultGasMultiplier, spec.contractErr
|
||||
}
|
||||
|
||||
ctx, _ := parentCtx.CacheContext()
|
||||
before := ctx.GasMeter().GasConsumed()
|
||||
msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler()
|
||||
*messenger = *msger
|
||||
|
||||
if spec.overwriteMessenger != nil {
|
||||
*messenger = *spec.overwriteMessenger
|
||||
}
|
||||
|
||||
// when
|
||||
msg := wasmvmtypes.IBCChannelCloseMsg{
|
||||
CloseInit: &wasmvmtypes.IBCCloseInit{
|
||||
Channel: myChannel,
|
||||
},
|
||||
}
|
||||
err := keepers.WasmKeeper.OnCloseChannel(ctx, spec.contractAddr, msg)
|
||||
|
||||
// then
|
||||
if spec.expErr {
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, capturedMsgs) // no messages captured on error
|
||||
assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events()))
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
// verify gas consumed
|
||||
const storageCosts = sdk.Gas(2903)
|
||||
assert.Equal(t, spec.expContractGas, ctx.GasMeter().GasConsumed()-before-storageCosts)
|
||||
// verify msgs dispatched
|
||||
require.Len(t, *capturedMsgs, len(spec.contractResp.Messages))
|
||||
for i, m := range spec.contractResp.Messages {
|
||||
assert.Equal(t, (*capturedMsgs)[i], m.Msg)
|
||||
}
|
||||
assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnRecvPacket(t *testing.T) {
|
||||
var m wasmtesting.MockWasmer
|
||||
wasmtesting.MakeIBCInstantiable(&m)
|
||||
messenger := &wasmtesting.MockMessageHandler{}
|
||||
parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger))
|
||||
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
|
||||
const myContractGas = 40
|
||||
const storageCosts = sdk.Gas(2903)
|
||||
|
||||
specs := map[string]struct {
|
||||
contractAddr sdk.AccAddress
|
||||
contractResp *wasmvmtypes.IBCReceiveResponse
|
||||
contractErr error
|
||||
overwriteMessenger *wasmtesting.MockMessageHandler
|
||||
mockReplyFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error)
|
||||
expContractGas sdk.Gas
|
||||
expAck []byte
|
||||
expErr bool
|
||||
expEventTypes []string
|
||||
}{
|
||||
"consume contract gas": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCReceiveResponse{
|
||||
Acknowledgement: []byte("myAck"),
|
||||
},
|
||||
expAck: []byte("myAck"),
|
||||
},
|
||||
"can return empty ack": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCReceiveResponse{},
|
||||
},
|
||||
"consume gas on error, ignore events + messages": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCReceiveResponse{
|
||||
Acknowledgement: []byte("myAck"),
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}},
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
contractErr: errors.New("test, ignore"),
|
||||
expErr: true,
|
||||
},
|
||||
"dispatch contract messages on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCReceiveResponse{
|
||||
Acknowledgement: []byte("myAck"),
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}},
|
||||
},
|
||||
expAck: []byte("myAck"),
|
||||
},
|
||||
"emit contract attributes on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 10,
|
||||
contractResp: &wasmvmtypes.IBCReceiveResponse{
|
||||
Acknowledgement: []byte("myAck"),
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
expEventTypes: []string{types.WasmModuleEventType},
|
||||
expAck: []byte("myAck"),
|
||||
},
|
||||
"emit contract events on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 46, // charge or custom event as well
|
||||
contractResp: &wasmvmtypes.IBCReceiveResponse{
|
||||
Acknowledgement: []byte("myAck"),
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
Events: []wasmvmtypes.Event{{
|
||||
Type: "custom",
|
||||
Attributes: []wasmvmtypes.EventAttribute{{
|
||||
Key: "message",
|
||||
Value: "to rudi",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expEventTypes: []string{types.WasmModuleEventType, "wasm-custom"},
|
||||
expAck: []byte("myAck"),
|
||||
},
|
||||
"messenger errors returned, events stored": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 10,
|
||||
contractResp: &wasmvmtypes.IBCReceiveResponse{
|
||||
Acknowledgement: []byte("myAck"),
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}},
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
overwriteMessenger: wasmtesting.NewErroringMessageHandler(),
|
||||
expErr: true,
|
||||
expEventTypes: []string{types.WasmModuleEventType},
|
||||
},
|
||||
"submessage reply can overwrite ack data": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + storageCosts,
|
||||
contractResp: &wasmvmtypes.IBCReceiveResponse{
|
||||
Acknowledgement: []byte("myAck"),
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyAlways, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}},
|
||||
},
|
||||
mockReplyFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) {
|
||||
return &wasmvmtypes.Response{Data: []byte("myBetterAck")}, 0, nil
|
||||
},
|
||||
expAck: []byte("myBetterAck"),
|
||||
expEventTypes: []string{types.EventTypeReply},
|
||||
},
|
||||
"unknown contract address": {
|
||||
contractAddr: RandomAccountAddress(t),
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
myPacket := wasmvmtypes.IBCPacket{Data: []byte("my data")}
|
||||
|
||||
m.IBCPacketReceiveFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) {
|
||||
assert.Equal(t, myPacket, msg.Packet)
|
||||
return &wasmvmtypes.IBCReceiveResult{Ok: spec.contractResp}, myContractGas * DefaultGasMultiplier, spec.contractErr
|
||||
}
|
||||
if spec.mockReplyFn != nil {
|
||||
m.ReplyFn = spec.mockReplyFn
|
||||
h, ok := keepers.WasmKeeper.wasmVMResponseHandler.(*DefaultWasmVMContractResponseHandler)
|
||||
require.True(t, ok)
|
||||
h.md = NewMessageDispatcher(messenger, keepers.WasmKeeper)
|
||||
}
|
||||
|
||||
ctx, _ := parentCtx.CacheContext()
|
||||
before := ctx.GasMeter().GasConsumed()
|
||||
|
||||
msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler()
|
||||
*messenger = *msger
|
||||
|
||||
if spec.overwriteMessenger != nil {
|
||||
*messenger = *spec.overwriteMessenger
|
||||
}
|
||||
|
||||
// when
|
||||
msg := wasmvmtypes.IBCPacketReceiveMsg{Packet: myPacket}
|
||||
gotAck, err := keepers.WasmKeeper.OnRecvPacket(ctx, spec.contractAddr, msg)
|
||||
|
||||
// then
|
||||
if spec.expErr {
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, capturedMsgs) // no messages captured on error
|
||||
assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events()))
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, spec.expAck, gotAck)
|
||||
|
||||
// verify gas consumed
|
||||
const storageCosts = sdk.Gas(2903)
|
||||
assert.Equal(t, spec.expContractGas, ctx.GasMeter().GasConsumed()-before-storageCosts)
|
||||
// verify msgs dispatched
|
||||
require.Len(t, *capturedMsgs, len(spec.contractResp.Messages))
|
||||
for i, m := range spec.contractResp.Messages {
|
||||
assert.Equal(t, (*capturedMsgs)[i], m.Msg)
|
||||
}
|
||||
assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnAckPacket(t *testing.T) {
|
||||
var m wasmtesting.MockWasmer
|
||||
wasmtesting.MakeIBCInstantiable(&m)
|
||||
messenger := &wasmtesting.MockMessageHandler{}
|
||||
parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger))
|
||||
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
|
||||
const myContractGas = 40
|
||||
|
||||
specs := map[string]struct {
|
||||
contractAddr sdk.AccAddress
|
||||
contractResp *wasmvmtypes.IBCBasicResponse
|
||||
contractErr error
|
||||
overwriteMessenger *wasmtesting.MockMessageHandler
|
||||
expContractGas sdk.Gas
|
||||
expErr bool
|
||||
expEventTypes []string
|
||||
}{
|
||||
"consume contract gas": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{},
|
||||
},
|
||||
"consume gas on error, ignore events + messages": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}},
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
contractErr: errors.New("test, ignore"),
|
||||
expErr: true,
|
||||
},
|
||||
"dispatch contract messages on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}},
|
||||
},
|
||||
},
|
||||
"emit contract events on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 10,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
expEventTypes: []string{types.WasmModuleEventType},
|
||||
},
|
||||
"messenger errors returned, events stored": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 10,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}},
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
overwriteMessenger: wasmtesting.NewErroringMessageHandler(),
|
||||
expErr: true,
|
||||
expEventTypes: []string{types.WasmModuleEventType},
|
||||
},
|
||||
"unknown contract address": {
|
||||
contractAddr: RandomAccountAddress(t),
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
myAck := wasmvmtypes.IBCPacketAckMsg{Acknowledgement: wasmvmtypes.IBCAcknowledgement{Data: []byte("myAck")}}
|
||||
m.IBCPacketAckFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketAckMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) {
|
||||
assert.Equal(t, myAck, msg)
|
||||
return spec.contractResp, myContractGas * DefaultGasMultiplier, spec.contractErr
|
||||
}
|
||||
|
||||
ctx, _ := parentCtx.CacheContext()
|
||||
before := ctx.GasMeter().GasConsumed()
|
||||
msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler()
|
||||
*messenger = *msger
|
||||
|
||||
if spec.overwriteMessenger != nil {
|
||||
*messenger = *spec.overwriteMessenger
|
||||
}
|
||||
|
||||
// when
|
||||
err := keepers.WasmKeeper.OnAckPacket(ctx, spec.contractAddr, myAck)
|
||||
|
||||
// then
|
||||
|
||||
if spec.expErr {
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, capturedMsgs) // no messages captured on error
|
||||
assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events()))
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
// verify gas consumed
|
||||
const storageCosts = sdk.Gas(2903)
|
||||
assert.Equal(t, spec.expContractGas, ctx.GasMeter().GasConsumed()-before-storageCosts)
|
||||
// verify msgs dispatched
|
||||
require.Len(t, *capturedMsgs, len(spec.contractResp.Messages))
|
||||
for i, m := range spec.contractResp.Messages {
|
||||
assert.Equal(t, (*capturedMsgs)[i], m.Msg)
|
||||
}
|
||||
assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnTimeoutPacket(t *testing.T) {
|
||||
var m wasmtesting.MockWasmer
|
||||
wasmtesting.MakeIBCInstantiable(&m)
|
||||
messenger := &wasmtesting.MockMessageHandler{}
|
||||
parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger))
|
||||
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
|
||||
const myContractGas = 40
|
||||
|
||||
specs := map[string]struct {
|
||||
contractAddr sdk.AccAddress
|
||||
contractResp *wasmvmtypes.IBCBasicResponse
|
||||
contractErr error
|
||||
overwriteMessenger *wasmtesting.MockMessageHandler
|
||||
expContractGas sdk.Gas
|
||||
expErr bool
|
||||
expEventTypes []string
|
||||
}{
|
||||
"consume contract gas": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{},
|
||||
},
|
||||
"consume gas on error, ignore events + messages": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}},
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
contractErr: errors.New("test, ignore"),
|
||||
expErr: true,
|
||||
},
|
||||
"dispatch contract messages on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}},
|
||||
},
|
||||
},
|
||||
"emit contract attributes on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 10,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
expEventTypes: []string{types.WasmModuleEventType},
|
||||
},
|
||||
"emit contract events on success": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 46, // cost for custom events
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
Events: []wasmvmtypes.Event{{
|
||||
Type: "custom",
|
||||
Attributes: []wasmvmtypes.EventAttribute{{
|
||||
Key: "message",
|
||||
Value: "to rudi",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expEventTypes: []string{types.WasmModuleEventType, "wasm-custom"},
|
||||
},
|
||||
"messenger errors returned, events stored before": {
|
||||
contractAddr: example.Contract,
|
||||
expContractGas: myContractGas + 10,
|
||||
contractResp: &wasmvmtypes.IBCBasicResponse{
|
||||
Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}},
|
||||
Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}},
|
||||
},
|
||||
overwriteMessenger: wasmtesting.NewErroringMessageHandler(),
|
||||
expErr: true,
|
||||
expEventTypes: []string{types.WasmModuleEventType},
|
||||
},
|
||||
"unknown contract address": {
|
||||
contractAddr: RandomAccountAddress(t),
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
myPacket := wasmvmtypes.IBCPacket{Data: []byte("my test packet")}
|
||||
m.IBCPacketTimeoutFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) {
|
||||
assert.Equal(t, myPacket, msg.Packet)
|
||||
return spec.contractResp, myContractGas * DefaultGasMultiplier, spec.contractErr
|
||||
}
|
||||
|
||||
ctx, _ := parentCtx.CacheContext()
|
||||
before := ctx.GasMeter().GasConsumed()
|
||||
msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler()
|
||||
*messenger = *msger
|
||||
|
||||
if spec.overwriteMessenger != nil {
|
||||
*messenger = *spec.overwriteMessenger
|
||||
}
|
||||
|
||||
// when
|
||||
msg := wasmvmtypes.IBCPacketTimeoutMsg{Packet: myPacket}
|
||||
err := keepers.WasmKeeper.OnTimeoutPacket(ctx, spec.contractAddr, msg)
|
||||
|
||||
// then
|
||||
if spec.expErr {
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, capturedMsgs) // no messages captured on error
|
||||
assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events()))
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
// verify gas consumed
|
||||
const storageCosts = sdk.Gas(2903)
|
||||
assert.Equal(t, spec.expContractGas, ctx.GasMeter().GasConsumed()-before-storageCosts)
|
||||
// verify msgs dispatched
|
||||
require.Len(t, *capturedMsgs, len(spec.contractResp.Messages))
|
||||
for i, m := range spec.contractResp.Messages {
|
||||
assert.Equal(t, (*capturedMsgs)[i], m.Msg)
|
||||
}
|
||||
assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stripTypes(events sdk.Events) []string {
|
||||
var r []string
|
||||
for _, e := range events {
|
||||
r = append(r, e.Type)
|
||||
}
|
||||
return r
|
||||
}
|
156
x/wasm/keeper/snapshotter.go
Normal file
@ -0,0 +1,156 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"io"
|
||||
|
||||
snapshot "github.com/cosmos/cosmos-sdk/snapshots/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
protoio "github.com/gogo/protobuf/io"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/ioutils"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
var _ snapshot.ExtensionSnapshotter = &WasmSnapshotter{}
|
||||
|
||||
// SnapshotFormat format 1 is just gzipped wasm byte code for each item payload. No protobuf envelope, no metadata.
|
||||
const SnapshotFormat = 1
|
||||
|
||||
type WasmSnapshotter struct {
|
||||
wasm *Keeper
|
||||
cms sdk.MultiStore
|
||||
}
|
||||
|
||||
func NewWasmSnapshotter(cms sdk.MultiStore, wasm *Keeper) *WasmSnapshotter {
|
||||
return &WasmSnapshotter{
|
||||
wasm: wasm,
|
||||
cms: cms,
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WasmSnapshotter) SnapshotName() string {
|
||||
return types.ModuleName
|
||||
}
|
||||
|
||||
func (ws *WasmSnapshotter) SnapshotFormat() uint32 {
|
||||
return SnapshotFormat
|
||||
}
|
||||
|
||||
func (ws *WasmSnapshotter) SupportedFormats() []uint32 {
|
||||
// If we support older formats, add them here and handle them in Restore
|
||||
return []uint32{SnapshotFormat}
|
||||
}
|
||||
|
||||
func (ws *WasmSnapshotter) Snapshot(height uint64, protoWriter protoio.Writer) error {
|
||||
cacheMS, err := ws.cms.CacheMultiStoreWithVersion(int64(height))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := sdk.NewContext(cacheMS, tmproto.Header{}, false, log.NewNopLogger())
|
||||
seenBefore := make(map[string]bool)
|
||||
var rerr error
|
||||
|
||||
ws.wasm.IterateCodeInfos(ctx, func(id uint64, info types.CodeInfo) bool {
|
||||
// Many code ids may point to the same code hash... only sync it once
|
||||
hexHash := hex.EncodeToString(info.CodeHash)
|
||||
// if seenBefore, just skip this one and move to the next
|
||||
if seenBefore[hexHash] {
|
||||
return false
|
||||
}
|
||||
seenBefore[hexHash] = true
|
||||
|
||||
// load code and abort on error
|
||||
wasmBytes, err := ws.wasm.GetByteCode(ctx, id)
|
||||
if err != nil {
|
||||
rerr = err
|
||||
return true
|
||||
}
|
||||
|
||||
compressedWasm, err := ioutils.GzipIt(wasmBytes)
|
||||
if err != nil {
|
||||
rerr = err
|
||||
return true
|
||||
}
|
||||
|
||||
err = snapshot.WriteExtensionItem(protoWriter, compressedWasm)
|
||||
if err != nil {
|
||||
rerr = err
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return rerr
|
||||
}
|
||||
|
||||
func (ws *WasmSnapshotter) Restore(
|
||||
height uint64, format uint32, protoReader protoio.Reader,
|
||||
) (snapshot.SnapshotItem, error) {
|
||||
if format == SnapshotFormat {
|
||||
return ws.processAllItems(height, protoReader, restoreV1, finalizeV1)
|
||||
}
|
||||
return snapshot.SnapshotItem{}, snapshot.ErrUnknownFormat
|
||||
}
|
||||
|
||||
func restoreV1(ctx sdk.Context, k *Keeper, compressedCode []byte) error {
|
||||
if !ioutils.IsGzip(compressedCode) {
|
||||
return types.ErrInvalid.Wrap("not a gzip")
|
||||
}
|
||||
wasmCode, err := ioutils.Uncompress(compressedCode, uint64(types.MaxWasmSize))
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(types.ErrCreateFailed, err.Error())
|
||||
}
|
||||
|
||||
// FIXME: check which codeIDs the checksum matches??
|
||||
_, err = k.wasmVM.Create(wasmCode)
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(types.ErrCreateFailed, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func finalizeV1(ctx sdk.Context, k *Keeper) error {
|
||||
// FIXME: ensure all codes have been uploaded?
|
||||
return k.InitializePinnedCodes(ctx)
|
||||
}
|
||||
|
||||
func (ws *WasmSnapshotter) processAllItems(
|
||||
height uint64,
|
||||
protoReader protoio.Reader,
|
||||
cb func(sdk.Context, *Keeper, []byte) error,
|
||||
finalize func(sdk.Context, *Keeper) error,
|
||||
) (snapshot.SnapshotItem, error) {
|
||||
ctx := sdk.NewContext(ws.cms, tmproto.Header{Height: int64(height)}, false, log.NewNopLogger())
|
||||
|
||||
// keep the last item here... if we break, it will either be empty (if we hit io.EOF)
|
||||
// or contain the last item (if we hit payload == nil)
|
||||
var item snapshot.SnapshotItem
|
||||
for {
|
||||
item = snapshot.SnapshotItem{}
|
||||
err := protoReader.ReadMsg(&item)
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return snapshot.SnapshotItem{}, sdkerrors.Wrap(err, "invalid protobuf message")
|
||||
}
|
||||
|
||||
// if it is not another ExtensionPayload message, then it is not for us.
|
||||
// we should return it an let the manager handle this one
|
||||
payload := item.GetExtensionPayload()
|
||||
if payload == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if err := cb(ctx, ws.wasm, payload.Payload); err != nil {
|
||||
return snapshot.SnapshotItem{}, sdkerrors.Wrap(err, "processing snapshot item")
|
||||
}
|
||||
}
|
||||
|
||||
return item, finalize(ctx, ws.wasm)
|
||||
}
|
124
x/wasm/keeper/snapshotter_integration_test.go
Normal file
@ -0,0 +1,124 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
"github.com/cerc-io/laconicd/app"
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper"
|
||||
)
|
||||
|
||||
func TestSnapshotter(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
wasmFiles []string
|
||||
}{
|
||||
"single contract": {
|
||||
wasmFiles: []string{"./testdata/reflect.wasm"},
|
||||
},
|
||||
"multiple contract": {
|
||||
wasmFiles: []string{"./testdata/reflect.wasm", "./testdata/burner.wasm", "./testdata/reflect.wasm"},
|
||||
},
|
||||
"duplicate contracts": {
|
||||
wasmFiles: []string{"./testdata/reflect.wasm", "./testdata/reflect.wasm"},
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// setup source app
|
||||
srcWasmApp, genesisAddr := newWasmExampleApp(t)
|
||||
|
||||
// store wasm codes on chain
|
||||
ctx := srcWasmApp.NewUncachedContext(false, tmproto.Header{
|
||||
ChainID: "foo",
|
||||
Height: srcWasmApp.LastBlockHeight() + 1,
|
||||
Time: time.Now(),
|
||||
})
|
||||
wasmKeeper := app.NewTestSupport(t, srcWasmApp).WasmKeeper()
|
||||
contractKeeper := keeper.NewDefaultPermissionKeeper(&wasmKeeper)
|
||||
|
||||
srcCodeIDToChecksum := make(map[uint64][]byte, len(spec.wasmFiles))
|
||||
for i, v := range spec.wasmFiles {
|
||||
wasmCode, err := os.ReadFile(v)
|
||||
require.NoError(t, err)
|
||||
codeID, checksum, err := contractKeeper.Create(ctx, genesisAddr, wasmCode, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(i+1), codeID)
|
||||
srcCodeIDToChecksum[codeID] = checksum
|
||||
}
|
||||
// create snapshot
|
||||
srcWasmApp.Commit()
|
||||
snapshotHeight := uint64(srcWasmApp.LastBlockHeight())
|
||||
snapshot, err := srcWasmApp.SnapshotManager().Create(snapshotHeight)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, snapshot)
|
||||
|
||||
// when snapshot imported into dest app instance
|
||||
destWasmApp := app.SetupWithEmptyStore(t)
|
||||
require.NoError(t, destWasmApp.SnapshotManager().Restore(*snapshot))
|
||||
for i := uint32(0); i < snapshot.Chunks; i++ {
|
||||
chunkBz, err := srcWasmApp.SnapshotManager().LoadChunk(snapshot.Height, snapshot.Format, i)
|
||||
require.NoError(t, err)
|
||||
end, err := destWasmApp.SnapshotManager().RestoreChunk(chunkBz)
|
||||
require.NoError(t, err)
|
||||
if end {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// then all wasm contracts are imported
|
||||
wasmKeeper = app.NewTestSupport(t, destWasmApp).WasmKeeper()
|
||||
ctx = destWasmApp.NewUncachedContext(false, tmproto.Header{
|
||||
ChainID: "foo",
|
||||
Height: destWasmApp.LastBlockHeight() + 1,
|
||||
Time: time.Now(),
|
||||
})
|
||||
|
||||
destCodeIDToChecksum := make(map[uint64][]byte, len(spec.wasmFiles))
|
||||
wasmKeeper.IterateCodeInfos(ctx, func(id uint64, info types.CodeInfo) bool {
|
||||
bz, err := wasmKeeper.GetByteCode(ctx, id)
|
||||
require.NoError(t, err)
|
||||
hash := sha256.Sum256(bz)
|
||||
destCodeIDToChecksum[id] = hash[:]
|
||||
assert.Equal(t, hash[:], info.CodeHash)
|
||||
return false
|
||||
})
|
||||
assert.Equal(t, srcCodeIDToChecksum, destCodeIDToChecksum)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newWasmExampleApp(t *testing.T) (*app.WasmApp, sdk.AccAddress) {
|
||||
senderPrivKey := ed25519.GenPrivKey()
|
||||
pubKey, err := cryptocodec.ToTmPubKeyInterface(senderPrivKey.PubKey())
|
||||
require.NoError(t, err)
|
||||
|
||||
senderAddr := senderPrivKey.PubKey().Address().Bytes()
|
||||
acc := authtypes.NewBaseAccount(senderAddr, senderPrivKey.PubKey(), 0, 0)
|
||||
amount, ok := sdk.NewIntFromString("10000000000000000000")
|
||||
require.True(t, ok)
|
||||
|
||||
balance := banktypes.Balance{
|
||||
Address: acc.GetAddress().String(),
|
||||
Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, amount)),
|
||||
}
|
||||
validator := tmtypes.NewValidator(pubKey, 1)
|
||||
valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{validator})
|
||||
wasmApp := app.SetupWithGenesisValSet(t, valSet, []authtypes.GenesisAccount{acc}, "testing", nil, balance)
|
||||
|
||||
return wasmApp, senderAddr
|
||||
}
|
748
x/wasm/keeper/staking_test.go
Normal file
@ -0,0 +1,748 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
|
||||
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
||||
distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper"
|
||||
distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/staking"
|
||||
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
|
||||
"github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/testdata"
|
||||
wasmtypes "github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
type StakingInitMsg struct {
|
||||
Name string `json:"name"`
|
||||
Symbol string `json:"symbol"`
|
||||
Decimals uint8 `json:"decimals"`
|
||||
Validator sdk.ValAddress `json:"validator"`
|
||||
ExitTax sdk.Dec `json:"exit_tax"`
|
||||
// MinWithdrawal is uint128 encoded as a string (use sdk.Int?)
|
||||
MinWithdrawl string `json:"min_withdrawal"`
|
||||
}
|
||||
|
||||
// StakingHandleMsg is used to encode handle messages
|
||||
type StakingHandleMsg struct {
|
||||
Transfer *transferPayload `json:"transfer,omitempty"`
|
||||
Bond *struct{} `json:"bond,omitempty"`
|
||||
Unbond *unbondPayload `json:"unbond,omitempty"`
|
||||
Claim *struct{} `json:"claim,omitempty"`
|
||||
Reinvest *struct{} `json:"reinvest,omitempty"`
|
||||
Change *testdata.OwnerPayload `json:"change_owner,omitempty"`
|
||||
}
|
||||
|
||||
type transferPayload struct {
|
||||
Recipient sdk.Address `json:"recipient"`
|
||||
// uint128 encoded as string
|
||||
Amount string `json:"amount"`
|
||||
}
|
||||
|
||||
type unbondPayload struct {
|
||||
// uint128 encoded as string
|
||||
Amount string `json:"amount"`
|
||||
}
|
||||
|
||||
// StakingQueryMsg is used to encode query messages
|
||||
type StakingQueryMsg struct {
|
||||
Balance *addressQuery `json:"balance,omitempty"`
|
||||
Claims *addressQuery `json:"claims,omitempty"`
|
||||
TokenInfo *struct{} `json:"token_info,omitempty"`
|
||||
Investment *struct{} `json:"investment,omitempty"`
|
||||
}
|
||||
|
||||
type addressQuery struct {
|
||||
Address sdk.AccAddress `json:"address"`
|
||||
}
|
||||
|
||||
type BalanceResponse struct {
|
||||
Balance string `json:"balance,omitempty"`
|
||||
}
|
||||
|
||||
type ClaimsResponse struct {
|
||||
Claims string `json:"claims,omitempty"`
|
||||
}
|
||||
|
||||
type TokenInfoResponse struct {
|
||||
Name string `json:"name"`
|
||||
Symbol string `json:"symbol"`
|
||||
Decimals uint8 `json:"decimals"`
|
||||
}
|
||||
|
||||
type InvestmentResponse struct {
|
||||
TokenSupply string `json:"token_supply"`
|
||||
StakedTokens sdk.Coin `json:"staked_tokens"`
|
||||
NominalValue sdk.Dec `json:"nominal_value"`
|
||||
Owner sdk.AccAddress `json:"owner"`
|
||||
Validator sdk.ValAddress `json:"validator"`
|
||||
ExitTax sdk.Dec `json:"exit_tax"`
|
||||
// MinWithdrawl is uint128 encoded as a string (use sdk.Int?)
|
||||
MinWithdrawl string `json:"min_withdrawal"`
|
||||
}
|
||||
|
||||
func TestInitializeStaking(t *testing.T) {
|
||||
ctx, k := CreateTestInput(t, false, AvailableCapabilities)
|
||||
accKeeper, stakingKeeper, keeper, bankKeeper := k.AccountKeeper, k.StakingKeeper, k.ContractKeeper, k.BankKeeper
|
||||
|
||||
valAddr := addValidator(t, ctx, stakingKeeper, k.Faucet, sdk.NewInt64Coin("stake", 1234567))
|
||||
ctx = nextBlock(ctx, stakingKeeper)
|
||||
v, found := stakingKeeper.GetValidator(ctx, valAddr)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, v.GetDelegatorShares(), sdk.NewDec(1234567))
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000), sdk.NewInt64Coin("stake", 500000))
|
||||
creator := k.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
|
||||
// upload staking derivates code
|
||||
stakingCode, err := os.ReadFile("./testdata/staking.wasm")
|
||||
require.NoError(t, err)
|
||||
stakingID, _, err := keeper.Create(ctx, creator, stakingCode, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), stakingID)
|
||||
|
||||
// register to a valid address
|
||||
initMsg := StakingInitMsg{
|
||||
Name: "Staking Derivatives",
|
||||
Symbol: "DRV",
|
||||
Decimals: 0,
|
||||
Validator: valAddr,
|
||||
ExitTax: sdk.MustNewDecFromStr("0.10"),
|
||||
MinWithdrawl: "100",
|
||||
}
|
||||
initBz, err := json.Marshal(&initMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
stakingAddr, _, err := k.ContractKeeper.Instantiate(ctx, stakingID, creator, nil, initBz, "staking derivates - DRV", nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, stakingAddr)
|
||||
|
||||
// nothing spent here
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, creator, deposit)
|
||||
|
||||
// try to register with a validator not on the list and it fails
|
||||
_, _, bob := keyPubAddr()
|
||||
badInitMsg := StakingInitMsg{
|
||||
Name: "Missing Validator",
|
||||
Symbol: "MISS",
|
||||
Decimals: 0,
|
||||
Validator: sdk.ValAddress(bob),
|
||||
ExitTax: sdk.MustNewDecFromStr("0.10"),
|
||||
MinWithdrawl: "100",
|
||||
}
|
||||
badBz, err := json.Marshal(&badInitMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = k.ContractKeeper.Instantiate(ctx, stakingID, creator, nil, badBz, "missing validator", nil)
|
||||
require.Error(t, err)
|
||||
|
||||
// no changes to bonding shares
|
||||
val, _ := stakingKeeper.GetValidator(ctx, valAddr)
|
||||
assert.Equal(t, val.GetDelegatorShares(), sdk.NewDec(1234567))
|
||||
}
|
||||
|
||||
type initInfo struct {
|
||||
valAddr sdk.ValAddress
|
||||
creator sdk.AccAddress
|
||||
contractAddr sdk.AccAddress
|
||||
|
||||
ctx sdk.Context
|
||||
accKeeper authkeeper.AccountKeeper
|
||||
stakingKeeper stakingkeeper.Keeper
|
||||
distKeeper distributionkeeper.Keeper
|
||||
wasmKeeper Keeper
|
||||
contractKeeper wasmtypes.ContractOpsKeeper
|
||||
bankKeeper bankkeeper.Keeper
|
||||
faucet *TestFaucet
|
||||
}
|
||||
|
||||
func initializeStaking(t *testing.T) initInfo {
|
||||
ctx, k := CreateTestInput(t, false, AvailableCapabilities)
|
||||
accKeeper, stakingKeeper, keeper, bankKeeper := k.AccountKeeper, k.StakingKeeper, k.WasmKeeper, k.BankKeeper
|
||||
|
||||
valAddr := addValidator(t, ctx, stakingKeeper, k.Faucet, sdk.NewInt64Coin("stake", 1000000))
|
||||
ctx = nextBlock(ctx, stakingKeeper)
|
||||
|
||||
// set some baseline - this seems to be needed
|
||||
k.DistKeeper.SetValidatorHistoricalRewards(ctx, valAddr, 0, distributiontypes.ValidatorHistoricalRewards{
|
||||
CumulativeRewardRatio: sdk.DecCoins{},
|
||||
ReferenceCount: 1,
|
||||
})
|
||||
|
||||
v, found := stakingKeeper.GetValidator(ctx, valAddr)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, v.GetDelegatorShares(), sdk.NewDec(1000000))
|
||||
assert.Equal(t, v.Status, stakingtypes.Bonded)
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000), sdk.NewInt64Coin("stake", 500000))
|
||||
creator := k.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
|
||||
// upload staking derivates code
|
||||
stakingCode, err := os.ReadFile("./testdata/staking.wasm")
|
||||
require.NoError(t, err)
|
||||
stakingID, _, err := k.ContractKeeper.Create(ctx, creator, stakingCode, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), stakingID)
|
||||
|
||||
// register to a valid address
|
||||
initMsg := StakingInitMsg{
|
||||
Name: "Staking Derivatives",
|
||||
Symbol: "DRV",
|
||||
Decimals: 0,
|
||||
Validator: valAddr,
|
||||
ExitTax: sdk.MustNewDecFromStr("0.10"),
|
||||
MinWithdrawl: "100",
|
||||
}
|
||||
initBz, err := json.Marshal(&initMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
stakingAddr, _, err := k.ContractKeeper.Instantiate(ctx, stakingID, creator, nil, initBz, "staking derivates - DRV", nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, stakingAddr)
|
||||
|
||||
return initInfo{
|
||||
valAddr: valAddr,
|
||||
creator: creator,
|
||||
contractAddr: stakingAddr,
|
||||
ctx: ctx,
|
||||
accKeeper: accKeeper,
|
||||
stakingKeeper: stakingKeeper,
|
||||
wasmKeeper: *keeper,
|
||||
distKeeper: k.DistKeeper,
|
||||
bankKeeper: bankKeeper,
|
||||
contractKeeper: k.ContractKeeper,
|
||||
faucet: k.Faucet,
|
||||
}
|
||||
}
|
||||
|
||||
func TestBonding(t *testing.T) {
|
||||
initInfo := initializeStaking(t)
|
||||
ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr
|
||||
keeper, stakingKeeper, accKeeper, bankKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper, initInfo.bankKeeper
|
||||
|
||||
// initial checks of bonding state
|
||||
val, found := stakingKeeper.GetValidator(ctx, valAddr)
|
||||
require.True(t, found)
|
||||
initPower := val.GetDelegatorShares()
|
||||
|
||||
// bob has 160k, putting 80k into the contract
|
||||
full := sdk.NewCoins(sdk.NewInt64Coin("stake", 160000))
|
||||
funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 80000))
|
||||
bob := initInfo.faucet.NewFundedRandomAccount(ctx, full...)
|
||||
|
||||
// check contract state before
|
||||
assertBalance(t, ctx, keeper, contractAddr, bob, "0")
|
||||
assertClaims(t, ctx, keeper, contractAddr, bob, "0")
|
||||
assertSupply(t, ctx, keeper, contractAddr, "0", sdk.NewInt64Coin("stake", 0))
|
||||
|
||||
bond := StakingHandleMsg{
|
||||
Bond: &struct{}{},
|
||||
}
|
||||
bondBz, err := json.Marshal(bond)
|
||||
require.NoError(t, err)
|
||||
_, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, bondBz, funds)
|
||||
require.NoError(t, err)
|
||||
|
||||
// check some account values - the money is on neither account (cuz it is bonded)
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.Coins{})
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, bob, funds)
|
||||
|
||||
// make sure the proper number of tokens have been bonded
|
||||
val, _ = stakingKeeper.GetValidator(ctx, valAddr)
|
||||
finalPower := val.GetDelegatorShares()
|
||||
assert.Equal(t, sdk.NewInt(80000), finalPower.Sub(initPower).TruncateInt())
|
||||
|
||||
// check the delegation itself
|
||||
d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr)
|
||||
require.True(t, found)
|
||||
assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("80000"))
|
||||
|
||||
// check we have the desired balance
|
||||
assertBalance(t, ctx, keeper, contractAddr, bob, "80000")
|
||||
assertClaims(t, ctx, keeper, contractAddr, bob, "0")
|
||||
assertSupply(t, ctx, keeper, contractAddr, "80000", sdk.NewInt64Coin("stake", 80000))
|
||||
}
|
||||
|
||||
func TestUnbonding(t *testing.T) {
|
||||
initInfo := initializeStaking(t)
|
||||
ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr
|
||||
keeper, stakingKeeper, accKeeper, bankKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper, initInfo.bankKeeper
|
||||
|
||||
// initial checks of bonding state
|
||||
val, found := stakingKeeper.GetValidator(ctx, valAddr)
|
||||
require.True(t, found)
|
||||
initPower := val.GetDelegatorShares()
|
||||
|
||||
// bob has 160k, putting 80k into the contract
|
||||
full := sdk.NewCoins(sdk.NewInt64Coin("stake", 160000))
|
||||
funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 80000))
|
||||
bob := initInfo.faucet.NewFundedRandomAccount(ctx, full...)
|
||||
|
||||
bond := StakingHandleMsg{
|
||||
Bond: &struct{}{},
|
||||
}
|
||||
bondBz, err := json.Marshal(bond)
|
||||
require.NoError(t, err)
|
||||
_, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, bondBz, funds)
|
||||
require.NoError(t, err)
|
||||
|
||||
// update height a bit
|
||||
ctx = nextBlock(ctx, stakingKeeper)
|
||||
|
||||
// now unbond 30k - note that 3k (10%) goes to the owner as a tax, 27k unbonded and available as claims
|
||||
unbond := StakingHandleMsg{
|
||||
Unbond: &unbondPayload{
|
||||
Amount: "30000",
|
||||
},
|
||||
}
|
||||
unbondBz, err := json.Marshal(unbond)
|
||||
require.NoError(t, err)
|
||||
_, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, unbondBz, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// check some account values - the money is on neither account (cuz it is bonded)
|
||||
// Note: why is this immediate? just test setup?
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.Coins{})
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, bob, funds)
|
||||
|
||||
// make sure the proper number of tokens have been bonded (80k - 27k = 53k)
|
||||
val, _ = stakingKeeper.GetValidator(ctx, valAddr)
|
||||
finalPower := val.GetDelegatorShares()
|
||||
assert.Equal(t, sdk.NewInt(53000), finalPower.Sub(initPower).TruncateInt(), finalPower.String())
|
||||
|
||||
// check the delegation itself
|
||||
d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr)
|
||||
require.True(t, found)
|
||||
assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("53000"))
|
||||
|
||||
// check there is unbonding in progress
|
||||
un, found := stakingKeeper.GetUnbondingDelegation(ctx, contractAddr, valAddr)
|
||||
require.True(t, found)
|
||||
require.Equal(t, 1, len(un.Entries))
|
||||
assert.Equal(t, "27000", un.Entries[0].Balance.String())
|
||||
|
||||
// check we have the desired balance
|
||||
assertBalance(t, ctx, keeper, contractAddr, bob, "50000")
|
||||
assertBalance(t, ctx, keeper, contractAddr, initInfo.creator, "3000")
|
||||
assertClaims(t, ctx, keeper, contractAddr, bob, "27000")
|
||||
assertSupply(t, ctx, keeper, contractAddr, "53000", sdk.NewInt64Coin("stake", 53000))
|
||||
}
|
||||
|
||||
func TestReinvest(t *testing.T) {
|
||||
initInfo := initializeStaking(t)
|
||||
ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr
|
||||
keeper, stakingKeeper, accKeeper, bankKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper, initInfo.bankKeeper
|
||||
distKeeper := initInfo.distKeeper
|
||||
|
||||
// initial checks of bonding state
|
||||
val, found := stakingKeeper.GetValidator(ctx, valAddr)
|
||||
require.True(t, found)
|
||||
initPower := val.GetDelegatorShares()
|
||||
assert.Equal(t, val.Tokens, sdk.NewInt(1000000), "%s", val.Tokens)
|
||||
|
||||
// full is 2x funds, 1x goes to the contract, other stays on his wallet
|
||||
full := sdk.NewCoins(sdk.NewInt64Coin("stake", 400000))
|
||||
funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 200000))
|
||||
bob := initInfo.faucet.NewFundedRandomAccount(ctx, full...)
|
||||
|
||||
// we will stake 200k to a validator with 1M self-bond
|
||||
// this means we should get 1/6 of the rewards
|
||||
bond := StakingHandleMsg{
|
||||
Bond: &struct{}{},
|
||||
}
|
||||
bondBz, err := json.Marshal(bond)
|
||||
require.NoError(t, err)
|
||||
_, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, bondBz, funds)
|
||||
require.NoError(t, err)
|
||||
|
||||
// update height a bit to solidify the delegation
|
||||
ctx = nextBlock(ctx, stakingKeeper)
|
||||
// we get 1/6, our share should be 40k minus 10% commission = 36k
|
||||
setValidatorRewards(ctx, stakingKeeper, distKeeper, valAddr, "240000")
|
||||
|
||||
// this should withdraw our outstanding 36k of rewards and reinvest them in the same delegation
|
||||
reinvest := StakingHandleMsg{
|
||||
Reinvest: &struct{}{},
|
||||
}
|
||||
reinvestBz, err := json.Marshal(reinvest)
|
||||
require.NoError(t, err)
|
||||
_, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, reinvestBz, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// check some account values - the money is on neither account (cuz it is bonded)
|
||||
// Note: why is this immediate? just test setup?
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.Coins{})
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, bob, funds)
|
||||
|
||||
// check the delegation itself
|
||||
d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr)
|
||||
require.True(t, found)
|
||||
// we started with 200k and added 36k
|
||||
assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("236000"))
|
||||
|
||||
// make sure the proper number of tokens have been bonded (80k + 40k = 120k)
|
||||
val, _ = stakingKeeper.GetValidator(ctx, valAddr)
|
||||
finalPower := val.GetDelegatorShares()
|
||||
assert.Equal(t, sdk.NewInt(236000), finalPower.Sub(initPower).TruncateInt(), finalPower.String())
|
||||
|
||||
// check there is no unbonding in progress
|
||||
un, found := stakingKeeper.GetUnbondingDelegation(ctx, contractAddr, valAddr)
|
||||
assert.False(t, found, "%#v", un)
|
||||
|
||||
// check we have the desired balance
|
||||
assertBalance(t, ctx, keeper, contractAddr, bob, "200000")
|
||||
assertBalance(t, ctx, keeper, contractAddr, initInfo.creator, "0")
|
||||
assertClaims(t, ctx, keeper, contractAddr, bob, "0")
|
||||
assertSupply(t, ctx, keeper, contractAddr, "200000", sdk.NewInt64Coin("stake", 236000))
|
||||
}
|
||||
|
||||
func TestQueryStakingInfo(t *testing.T) {
|
||||
// STEP 1: take a lot of setup from TestReinvest so we have non-zero info
|
||||
initInfo := initializeStaking(t)
|
||||
ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr
|
||||
keeper, stakingKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper
|
||||
distKeeper := initInfo.distKeeper
|
||||
|
||||
// initial checks of bonding state
|
||||
val, found := stakingKeeper.GetValidator(ctx, valAddr)
|
||||
require.True(t, found)
|
||||
assert.Equal(t, sdk.NewInt(1000000), val.Tokens)
|
||||
|
||||
// full is 2x funds, 1x goes to the contract, other stays on his wallet
|
||||
full := sdk.NewCoins(sdk.NewInt64Coin("stake", 400000))
|
||||
funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 200000))
|
||||
bob := initInfo.faucet.NewFundedRandomAccount(ctx, full...)
|
||||
|
||||
// we will stake 200k to a validator with 1M self-bond
|
||||
// this means we should get 1/6 of the rewards
|
||||
bond := StakingHandleMsg{
|
||||
Bond: &struct{}{},
|
||||
}
|
||||
bondBz, err := json.Marshal(bond)
|
||||
require.NoError(t, err)
|
||||
_, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, bondBz, funds)
|
||||
require.NoError(t, err)
|
||||
|
||||
// update height a bit to solidify the delegation
|
||||
ctx = nextBlock(ctx, stakingKeeper)
|
||||
// we get 1/6, our share should be 40k minus 10% commission = 36k
|
||||
setValidatorRewards(ctx, stakingKeeper, distKeeper, valAddr, "240000")
|
||||
|
||||
// see what the current rewards are
|
||||
origReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr)
|
||||
|
||||
// STEP 2: Prepare the mask contract
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
creator := initInfo.faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
|
||||
// upload mask code
|
||||
maskID, _, err := initInfo.contractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(2), maskID)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
maskAddr, _, err := initInfo.contractKeeper.Instantiate(ctx, maskID, creator, nil, []byte("{}"), "mask contract 2", nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, maskAddr)
|
||||
|
||||
// STEP 3: now, let's reflect some queries.
|
||||
// let's get the bonded denom
|
||||
reflectBondedQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{
|
||||
BondedDenom: &struct{}{},
|
||||
}}}}
|
||||
reflectBondedBin := buildReflectQuery(t, &reflectBondedQuery)
|
||||
res, err := keeper.QuerySmart(ctx, maskAddr, reflectBondedBin)
|
||||
require.NoError(t, err)
|
||||
// first we pull out the data from chain response, before parsing the original response
|
||||
var reflectRes testdata.ChainResponse
|
||||
mustParse(t, res, &reflectRes)
|
||||
var bondedRes wasmvmtypes.BondedDenomResponse
|
||||
mustParse(t, reflectRes.Data, &bondedRes)
|
||||
assert.Equal(t, "stake", bondedRes.Denom)
|
||||
|
||||
// now, let's reflect a smart query into the x/wasm handlers and see if we get the same result
|
||||
reflectAllValidatorsQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{
|
||||
AllValidators: &wasmvmtypes.AllValidatorsQuery{},
|
||||
}}}}
|
||||
reflectAllValidatorsBin := buildReflectQuery(t, &reflectAllValidatorsQuery)
|
||||
res, err = keeper.QuerySmart(ctx, maskAddr, reflectAllValidatorsBin)
|
||||
require.NoError(t, err)
|
||||
// first we pull out the data from chain response, before parsing the original response
|
||||
mustParse(t, res, &reflectRes)
|
||||
var allValidatorsRes wasmvmtypes.AllValidatorsResponse
|
||||
mustParse(t, reflectRes.Data, &allValidatorsRes)
|
||||
require.Len(t, allValidatorsRes.Validators, 1)
|
||||
valInfo := allValidatorsRes.Validators[0]
|
||||
// Note: this ValAddress not AccAddress, may change with #264
|
||||
require.Equal(t, valAddr.String(), valInfo.Address)
|
||||
require.Contains(t, valInfo.Commission, "0.100")
|
||||
require.Contains(t, valInfo.MaxCommission, "0.200")
|
||||
require.Contains(t, valInfo.MaxChangeRate, "0.010")
|
||||
|
||||
// find a validator
|
||||
reflectValidatorQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{
|
||||
Validator: &wasmvmtypes.ValidatorQuery{
|
||||
Address: valAddr.String(),
|
||||
},
|
||||
}}}}
|
||||
reflectValidatorBin := buildReflectQuery(t, &reflectValidatorQuery)
|
||||
res, err = keeper.QuerySmart(ctx, maskAddr, reflectValidatorBin)
|
||||
require.NoError(t, err)
|
||||
// first we pull out the data from chain response, before parsing the original response
|
||||
mustParse(t, res, &reflectRes)
|
||||
var validatorRes wasmvmtypes.ValidatorResponse
|
||||
mustParse(t, reflectRes.Data, &validatorRes)
|
||||
require.NotNil(t, validatorRes.Validator)
|
||||
valInfo = *validatorRes.Validator
|
||||
// Note: this ValAddress not AccAddress, may change with #264
|
||||
require.Equal(t, valAddr.String(), valInfo.Address)
|
||||
require.Contains(t, valInfo.Commission, "0.100")
|
||||
require.Contains(t, valInfo.MaxCommission, "0.200")
|
||||
require.Contains(t, valInfo.MaxChangeRate, "0.010")
|
||||
|
||||
// missing validator
|
||||
noVal := sdk.ValAddress(secp256k1.GenPrivKey().PubKey().Address())
|
||||
reflectNoValidatorQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{
|
||||
Validator: &wasmvmtypes.ValidatorQuery{
|
||||
Address: noVal.String(),
|
||||
},
|
||||
}}}}
|
||||
reflectNoValidatorBin := buildReflectQuery(t, &reflectNoValidatorQuery)
|
||||
res, err = keeper.QuerySmart(ctx, maskAddr, reflectNoValidatorBin)
|
||||
require.NoError(t, err)
|
||||
// first we pull out the data from chain response, before parsing the original response
|
||||
mustParse(t, res, &reflectRes)
|
||||
var noValidatorRes wasmvmtypes.ValidatorResponse
|
||||
mustParse(t, reflectRes.Data, &noValidatorRes)
|
||||
require.Nil(t, noValidatorRes.Validator)
|
||||
|
||||
// test to get all my delegations
|
||||
reflectAllDelegationsQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{
|
||||
AllDelegations: &wasmvmtypes.AllDelegationsQuery{
|
||||
Delegator: contractAddr.String(),
|
||||
},
|
||||
}}}}
|
||||
reflectAllDelegationsBin := buildReflectQuery(t, &reflectAllDelegationsQuery)
|
||||
res, err = keeper.QuerySmart(ctx, maskAddr, reflectAllDelegationsBin)
|
||||
require.NoError(t, err)
|
||||
// first we pull out the data from chain response, before parsing the original response
|
||||
mustParse(t, res, &reflectRes)
|
||||
var allDelegationsRes wasmvmtypes.AllDelegationsResponse
|
||||
mustParse(t, reflectRes.Data, &allDelegationsRes)
|
||||
require.Len(t, allDelegationsRes.Delegations, 1)
|
||||
delInfo := allDelegationsRes.Delegations[0]
|
||||
// Note: this ValAddress not AccAddress, may change with #264
|
||||
require.Equal(t, valAddr.String(), delInfo.Validator)
|
||||
// note this is not bob (who staked to the contract), but the contract itself
|
||||
require.Equal(t, contractAddr.String(), delInfo.Delegator)
|
||||
// this is a different Coin type, with String not BigInt, compare field by field
|
||||
require.Equal(t, funds[0].Denom, delInfo.Amount.Denom)
|
||||
require.Equal(t, funds[0].Amount.String(), delInfo.Amount.Amount)
|
||||
|
||||
// test to get one delegations
|
||||
reflectDelegationQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{
|
||||
Delegation: &wasmvmtypes.DelegationQuery{
|
||||
Validator: valAddr.String(),
|
||||
Delegator: contractAddr.String(),
|
||||
},
|
||||
}}}}
|
||||
reflectDelegationBin := buildReflectQuery(t, &reflectDelegationQuery)
|
||||
res, err = keeper.QuerySmart(ctx, maskAddr, reflectDelegationBin)
|
||||
require.NoError(t, err)
|
||||
// first we pull out the data from chain response, before parsing the original response
|
||||
mustParse(t, res, &reflectRes)
|
||||
var delegationRes wasmvmtypes.DelegationResponse
|
||||
mustParse(t, reflectRes.Data, &delegationRes)
|
||||
assert.NotEmpty(t, delegationRes.Delegation)
|
||||
delInfo2 := delegationRes.Delegation
|
||||
// Note: this ValAddress not AccAddress, may change with #264
|
||||
require.Equal(t, valAddr.String(), delInfo2.Validator)
|
||||
// note this is not bob (who staked to the contract), but the contract itself
|
||||
require.Equal(t, contractAddr.String(), delInfo2.Delegator)
|
||||
// this is a different Coin type, with String not BigInt, compare field by field
|
||||
require.Equal(t, funds[0].Denom, delInfo2.Amount.Denom)
|
||||
require.Equal(t, funds[0].Amount.String(), delInfo2.Amount.Amount)
|
||||
|
||||
require.Equal(t, wasmvmtypes.NewCoin(200000, "stake"), delInfo2.CanRedelegate)
|
||||
require.Len(t, delInfo2.AccumulatedRewards, 1)
|
||||
// see bonding above to see how we calculate 36000 (240000 / 6 - 10% commission)
|
||||
require.Equal(t, wasmvmtypes.NewCoin(36000, "stake"), delInfo2.AccumulatedRewards[0])
|
||||
|
||||
// ensure rewards did not change when querying (neither amount nor period)
|
||||
finalReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr)
|
||||
require.Equal(t, origReward, finalReward)
|
||||
}
|
||||
|
||||
func TestQueryStakingPlugin(t *testing.T) {
|
||||
// STEP 1: take a lot of setup from TestReinvest so we have non-zero info
|
||||
initInfo := initializeStaking(t)
|
||||
ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr
|
||||
stakingKeeper := initInfo.stakingKeeper
|
||||
distKeeper := initInfo.distKeeper
|
||||
|
||||
// initial checks of bonding state
|
||||
val, found := stakingKeeper.GetValidator(ctx, valAddr)
|
||||
require.True(t, found)
|
||||
assert.Equal(t, sdk.NewInt(1000000), val.Tokens)
|
||||
|
||||
// full is 2x funds, 1x goes to the contract, other stays on his wallet
|
||||
full := sdk.NewCoins(sdk.NewInt64Coin("stake", 400000))
|
||||
funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 200000))
|
||||
bob := initInfo.faucet.NewFundedRandomAccount(ctx, full...)
|
||||
|
||||
// we will stake 200k to a validator with 1M self-bond
|
||||
// this means we should get 1/6 of the rewards
|
||||
bond := StakingHandleMsg{
|
||||
Bond: &struct{}{},
|
||||
}
|
||||
bondBz, err := json.Marshal(bond)
|
||||
require.NoError(t, err)
|
||||
_, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, bondBz, funds)
|
||||
require.NoError(t, err)
|
||||
|
||||
// update height a bit to solidify the delegation
|
||||
ctx = nextBlock(ctx, stakingKeeper)
|
||||
// we get 1/6, our share should be 40k minus 10% commission = 36k
|
||||
setValidatorRewards(ctx, stakingKeeper, distKeeper, valAddr, "240000")
|
||||
|
||||
// see what the current rewards are
|
||||
origReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr)
|
||||
|
||||
// Step 2: Try out the query plugins
|
||||
query := wasmvmtypes.StakingQuery{
|
||||
Delegation: &wasmvmtypes.DelegationQuery{
|
||||
Delegator: contractAddr.String(),
|
||||
Validator: valAddr.String(),
|
||||
},
|
||||
}
|
||||
raw, err := StakingQuerier(stakingKeeper, distKeeper)(ctx, &query)
|
||||
require.NoError(t, err)
|
||||
var res wasmvmtypes.DelegationResponse
|
||||
mustParse(t, raw, &res)
|
||||
assert.NotEmpty(t, res.Delegation)
|
||||
delInfo := res.Delegation
|
||||
// Note: this ValAddress not AccAddress, may change with #264
|
||||
require.Equal(t, valAddr.String(), delInfo.Validator)
|
||||
// note this is not bob (who staked to the contract), but the contract itself
|
||||
require.Equal(t, contractAddr.String(), delInfo.Delegator)
|
||||
// this is a different Coin type, with String not BigInt, compare field by field
|
||||
require.Equal(t, funds[0].Denom, delInfo.Amount.Denom)
|
||||
require.Equal(t, funds[0].Amount.String(), delInfo.Amount.Amount)
|
||||
|
||||
require.Equal(t, wasmvmtypes.NewCoin(200000, "stake"), delInfo.CanRedelegate)
|
||||
require.Len(t, delInfo.AccumulatedRewards, 1)
|
||||
// see bonding above to see how we calculate 36000 (240000 / 6 - 10% commission)
|
||||
require.Equal(t, wasmvmtypes.NewCoin(36000, "stake"), delInfo.AccumulatedRewards[0])
|
||||
|
||||
// ensure rewards did not change when querying (neither amount nor period)
|
||||
finalReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr)
|
||||
require.Equal(t, origReward, finalReward)
|
||||
}
|
||||
|
||||
// adds a few validators and returns a list of validators that are registered
|
||||
func addValidator(t *testing.T, ctx sdk.Context, stakingKeeper stakingkeeper.Keeper, faucet *TestFaucet, value sdk.Coin) sdk.ValAddress {
|
||||
owner := faucet.NewFundedRandomAccount(ctx, value)
|
||||
|
||||
privKey := secp256k1.GenPrivKey()
|
||||
pubKey := privKey.PubKey()
|
||||
addr := sdk.ValAddress(pubKey.Address())
|
||||
|
||||
pkAny, err := codectypes.NewAnyWithValue(pubKey)
|
||||
require.NoError(t, err)
|
||||
msg := stakingtypes.MsgCreateValidator{
|
||||
Description: types.Description{
|
||||
Moniker: "Validator power",
|
||||
},
|
||||
Commission: types.CommissionRates{
|
||||
Rate: sdk.MustNewDecFromStr("0.1"),
|
||||
MaxRate: sdk.MustNewDecFromStr("0.2"),
|
||||
MaxChangeRate: sdk.MustNewDecFromStr("0.01"),
|
||||
},
|
||||
MinSelfDelegation: sdk.OneInt(),
|
||||
DelegatorAddress: owner.String(),
|
||||
ValidatorAddress: addr.String(),
|
||||
Pubkey: pkAny,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
h := staking.NewHandler(stakingKeeper)
|
||||
_, err = h(ctx, &msg)
|
||||
require.NoError(t, err)
|
||||
return addr
|
||||
}
|
||||
|
||||
// this will commit the current set, update the block height and set historic info
|
||||
// basically, letting two blocks pass
|
||||
func nextBlock(ctx sdk.Context, stakingKeeper stakingkeeper.Keeper) sdk.Context {
|
||||
staking.EndBlocker(ctx, stakingKeeper)
|
||||
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
|
||||
staking.BeginBlocker(ctx, stakingKeeper)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func setValidatorRewards(ctx sdk.Context, stakingKeeper stakingkeeper.Keeper, distKeeper distributionkeeper.Keeper, valAddr sdk.ValAddress, reward string) {
|
||||
// allocate some rewards
|
||||
vali := stakingKeeper.Validator(ctx, valAddr)
|
||||
amount, err := sdk.NewDecFromStr(reward)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
payout := sdk.DecCoins{{Denom: "stake", Amount: amount}}
|
||||
distKeeper.AllocateTokensToValidator(ctx, vali, payout)
|
||||
}
|
||||
|
||||
func assertBalance(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, addr sdk.AccAddress, expected string) {
|
||||
query := StakingQueryMsg{
|
||||
Balance: &addressQuery{
|
||||
Address: addr,
|
||||
},
|
||||
}
|
||||
queryBz, err := json.Marshal(query)
|
||||
require.NoError(t, err)
|
||||
res, err := keeper.QuerySmart(ctx, contract, queryBz)
|
||||
require.NoError(t, err)
|
||||
var balance BalanceResponse
|
||||
err = json.Unmarshal(res, &balance)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, balance.Balance)
|
||||
}
|
||||
|
||||
func assertClaims(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, addr sdk.AccAddress, expected string) {
|
||||
query := StakingQueryMsg{
|
||||
Claims: &addressQuery{
|
||||
Address: addr,
|
||||
},
|
||||
}
|
||||
queryBz, err := json.Marshal(query)
|
||||
require.NoError(t, err)
|
||||
res, err := keeper.QuerySmart(ctx, contract, queryBz)
|
||||
require.NoError(t, err)
|
||||
var claims ClaimsResponse
|
||||
err = json.Unmarshal(res, &claims)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, claims.Claims)
|
||||
}
|
||||
|
||||
func assertSupply(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, expectedIssued string, expectedBonded sdk.Coin) {
|
||||
query := StakingQueryMsg{Investment: &struct{}{}}
|
||||
queryBz, err := json.Marshal(query)
|
||||
require.NoError(t, err)
|
||||
res, err := keeper.QuerySmart(ctx, contract, queryBz)
|
||||
require.NoError(t, err)
|
||||
var invest InvestmentResponse
|
||||
err = json.Unmarshal(res, &invest)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedIssued, invest.TokenSupply)
|
||||
assert.Equal(t, expectedBonded, invest.StakedTokens)
|
||||
}
|
552
x/wasm/keeper/submsg_test.go
Normal file
@ -0,0 +1,552 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/testdata"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
// test handing of submessages, very closely related to the reflect_test
|
||||
|
||||
// Try a simple send, no gas limit to for a sanity check before trying table tests
|
||||
func TestDispatchSubMsgSuccessCase(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures)
|
||||
accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.WasmKeeper, keepers.BankKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000))
|
||||
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
creatorBalance := deposit.Sub(contractStart)
|
||||
_, _, fred := keyPubAddr()
|
||||
|
||||
// upload code
|
||||
codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), codeID)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, contractAddr)
|
||||
|
||||
// check some account values
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, contractStart)
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, creator, creatorBalance)
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, fred, nil)
|
||||
|
||||
// creator can send contract's tokens to fred (using SendMsg)
|
||||
msg := wasmvmtypes.CosmosMsg{
|
||||
Bank: &wasmvmtypes.BankMsg{
|
||||
Send: &wasmvmtypes.SendMsg{
|
||||
ToAddress: fred.String(),
|
||||
Amount: []wasmvmtypes.Coin{{
|
||||
Denom: "denom",
|
||||
Amount: "15000",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
reflectSend := testdata.ReflectHandleMsg{
|
||||
ReflectSubMsg: &testdata.ReflectSubPayload{
|
||||
Msgs: []wasmvmtypes.SubMsg{{
|
||||
ID: 7,
|
||||
Msg: msg,
|
||||
ReplyOn: wasmvmtypes.ReplyAlways,
|
||||
}},
|
||||
},
|
||||
}
|
||||
reflectSendBz, err := json.Marshal(reflectSend)
|
||||
require.NoError(t, err)
|
||||
_, err = keepers.ContractKeeper.Execute(ctx, contractAddr, creator, reflectSendBz, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fred got coins
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, fred, sdk.NewCoins(sdk.NewInt64Coin("denom", 15000)))
|
||||
// contract lost them
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 25000)))
|
||||
checkAccount(t, ctx, accKeeper, bankKeeper, creator, creatorBalance)
|
||||
|
||||
// query the reflect state to ensure the result was stored
|
||||
query := testdata.ReflectQueryMsg{
|
||||
SubMsgResult: &testdata.SubCall{ID: 7},
|
||||
}
|
||||
queryBz, err := json.Marshal(query)
|
||||
require.NoError(t, err)
|
||||
queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz)
|
||||
require.NoError(t, err)
|
||||
|
||||
var res wasmvmtypes.Reply
|
||||
err = json.Unmarshal(queryRes, &res)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(7), res.ID)
|
||||
assert.Empty(t, res.Result.Err)
|
||||
require.NotNil(t, res.Result.Ok)
|
||||
sub := res.Result.Ok
|
||||
assert.Empty(t, sub.Data)
|
||||
// as of v0.28.0 we strip out all events that don't come from wasm contracts. can't trust the sdk.
|
||||
require.Len(t, sub.Events, 0)
|
||||
}
|
||||
|
||||
func TestDispatchSubMsgErrorHandling(t *testing.T) {
|
||||
fundedDenom := "funds"
|
||||
fundedAmount := 1_000_000
|
||||
ctxGasLimit := uint64(1_000_000)
|
||||
subGasLimit := uint64(300_000)
|
||||
|
||||
// prep - create one chain and upload the code
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures)
|
||||
ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
|
||||
ctx = ctx.WithBlockGasMeter(sdk.NewInfiniteGasMeter())
|
||||
keeper := keepers.WasmKeeper
|
||||
contractStart := sdk.NewCoins(sdk.NewInt64Coin(fundedDenom, int64(fundedAmount)))
|
||||
uploader := keepers.Faucet.NewFundedRandomAccount(ctx, contractStart.Add(contractStart...)...)
|
||||
|
||||
// upload code
|
||||
reflectID, _, err := keepers.ContractKeeper.Create(ctx, uploader, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create hackatom contract for testing (for infinite loop)
|
||||
hackatomCode, err := os.ReadFile("./testdata/hackatom.wasm")
|
||||
require.NoError(t, err)
|
||||
hackatomID, _, err := keepers.ContractKeeper.Create(ctx, uploader, hackatomCode, nil)
|
||||
require.NoError(t, err)
|
||||
_, _, bob := keyPubAddr()
|
||||
_, _, fred := keyPubAddr()
|
||||
initMsg := HackatomExampleInitMsg{
|
||||
Verifier: fred,
|
||||
Beneficiary: bob,
|
||||
}
|
||||
initMsgBz, err := json.Marshal(initMsg)
|
||||
require.NoError(t, err)
|
||||
hackatomAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, hackatomID, uploader, nil, initMsgBz, "hackatom demo", contractStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
validBankSend := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg {
|
||||
return wasmvmtypes.CosmosMsg{
|
||||
Bank: &wasmvmtypes.BankMsg{
|
||||
Send: &wasmvmtypes.SendMsg{
|
||||
ToAddress: emptyAccount,
|
||||
Amount: []wasmvmtypes.Coin{{
|
||||
Denom: fundedDenom,
|
||||
Amount: strconv.Itoa(fundedAmount / 2),
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
invalidBankSend := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg {
|
||||
return wasmvmtypes.CosmosMsg{
|
||||
Bank: &wasmvmtypes.BankMsg{
|
||||
Send: &wasmvmtypes.SendMsg{
|
||||
ToAddress: emptyAccount,
|
||||
Amount: []wasmvmtypes.Coin{{
|
||||
Denom: fundedDenom,
|
||||
Amount: strconv.Itoa(fundedAmount * 2),
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
infiniteLoop := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg {
|
||||
return wasmvmtypes.CosmosMsg{
|
||||
Wasm: &wasmvmtypes.WasmMsg{
|
||||
Execute: &wasmvmtypes.ExecuteMsg{
|
||||
ContractAddr: hackatomAddr.String(),
|
||||
Msg: []byte(`{"cpu_loop":{}}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
instantiateContract := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg {
|
||||
return wasmvmtypes.CosmosMsg{
|
||||
Wasm: &wasmvmtypes.WasmMsg{
|
||||
Instantiate: &wasmvmtypes.InstantiateMsg{
|
||||
CodeID: reflectID,
|
||||
Msg: []byte("{}"),
|
||||
Label: "subcall reflect",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type assertion func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult)
|
||||
|
||||
assertReturnedEvents := func(expectedEvents int) assertion {
|
||||
return func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) {
|
||||
require.Len(t, response.Ok.Events, expectedEvents)
|
||||
}
|
||||
}
|
||||
|
||||
assertGasUsed := func(minGas, maxGas uint64) assertion {
|
||||
return func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) {
|
||||
gasUsed := ctx.GasMeter().GasConsumed()
|
||||
assert.True(t, gasUsed >= minGas, "Used %d gas (less than expected %d)", gasUsed, minGas)
|
||||
assert.True(t, gasUsed <= maxGas, "Used %d gas (more than expected %d)", gasUsed, maxGas)
|
||||
}
|
||||
}
|
||||
|
||||
assertErrorString := func(shouldContain string) assertion {
|
||||
return func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) {
|
||||
assert.Contains(t, response.Err, shouldContain)
|
||||
}
|
||||
}
|
||||
|
||||
assertGotContractAddr := func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) {
|
||||
// should get the events emitted on new contract
|
||||
event := response.Ok.Events[0]
|
||||
require.Equal(t, event.Type, "instantiate")
|
||||
assert.Equal(t, event.Attributes[0].Key, "_contract_address")
|
||||
eventAddr := event.Attributes[0].Value
|
||||
assert.NotEqual(t, contract, eventAddr)
|
||||
|
||||
var res types.MsgInstantiateContractResponse
|
||||
keepers.EncodingConfig.Marshaler.MustUnmarshal(response.Ok.Data, &res)
|
||||
assert.Equal(t, eventAddr, res.Address)
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
submsgID uint64
|
||||
// we will generate message from the
|
||||
msg func(contract, emptyAccount string) wasmvmtypes.CosmosMsg
|
||||
gasLimit *uint64
|
||||
|
||||
// true if we expect this to throw out of gas panic
|
||||
isOutOfGasPanic bool
|
||||
// true if we expect this execute to return an error (can be false when submessage errors)
|
||||
executeError bool
|
||||
// true if we expect submessage to return an error (but execute to return success)
|
||||
subMsgError bool
|
||||
// make assertions after dispatch
|
||||
resultAssertions []assertion
|
||||
}{
|
||||
"send tokens": {
|
||||
submsgID: 5,
|
||||
msg: validBankSend,
|
||||
resultAssertions: []assertion{assertReturnedEvents(0), assertGasUsed(95000, 96000)},
|
||||
},
|
||||
"not enough tokens": {
|
||||
submsgID: 6,
|
||||
msg: invalidBankSend,
|
||||
subMsgError: true,
|
||||
// uses less gas than the send tokens (cost of bank transfer)
|
||||
resultAssertions: []assertion{assertGasUsed(76000, 79000), assertErrorString("codespace: sdk, code: 5")},
|
||||
},
|
||||
"out of gas panic with no gas limit": {
|
||||
submsgID: 7,
|
||||
msg: infiniteLoop,
|
||||
isOutOfGasPanic: true,
|
||||
},
|
||||
|
||||
"send tokens with limit": {
|
||||
submsgID: 15,
|
||||
msg: validBankSend,
|
||||
gasLimit: &subGasLimit,
|
||||
// uses same gas as call without limit (note we do not charge the 40k on reply)
|
||||
resultAssertions: []assertion{assertReturnedEvents(0), assertGasUsed(95000, 96000)},
|
||||
},
|
||||
"not enough tokens with limit": {
|
||||
submsgID: 16,
|
||||
msg: invalidBankSend,
|
||||
subMsgError: true,
|
||||
gasLimit: &subGasLimit,
|
||||
// uses same gas as call without limit (note we do not charge the 40k on reply)
|
||||
resultAssertions: []assertion{assertGasUsed(77800, 77900), assertErrorString("codespace: sdk, code: 5")},
|
||||
},
|
||||
"out of gas caught with gas limit": {
|
||||
submsgID: 17,
|
||||
msg: infiniteLoop,
|
||||
subMsgError: true,
|
||||
gasLimit: &subGasLimit,
|
||||
// uses all the subGasLimit, plus the 52k or so for the main contract
|
||||
resultAssertions: []assertion{assertGasUsed(subGasLimit+73000, subGasLimit+74000), assertErrorString("codespace: sdk, code: 11")},
|
||||
},
|
||||
"instantiate contract gets address in data and events": {
|
||||
submsgID: 21,
|
||||
msg: instantiateContract,
|
||||
resultAssertions: []assertion{assertReturnedEvents(1), assertGotContractAddr},
|
||||
},
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, contractStart...)
|
||||
_, _, empty := keyPubAddr()
|
||||
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, reflectID, creator, nil, []byte("{}"), fmt.Sprintf("contract %s", name), contractStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
msg := tc.msg(contractAddr.String(), empty.String())
|
||||
reflectSend := testdata.ReflectHandleMsg{
|
||||
ReflectSubMsg: &testdata.ReflectSubPayload{
|
||||
Msgs: []wasmvmtypes.SubMsg{{
|
||||
ID: tc.submsgID,
|
||||
Msg: msg,
|
||||
GasLimit: tc.gasLimit,
|
||||
ReplyOn: wasmvmtypes.ReplyAlways,
|
||||
}},
|
||||
},
|
||||
}
|
||||
reflectSendBz, err := json.Marshal(reflectSend)
|
||||
require.NoError(t, err)
|
||||
|
||||
execCtx := ctx.WithGasMeter(sdk.NewGasMeter(ctxGasLimit))
|
||||
defer func() {
|
||||
if tc.isOutOfGasPanic {
|
||||
r := recover()
|
||||
require.NotNil(t, r, "expected panic")
|
||||
if _, ok := r.(sdk.ErrorOutOfGas); !ok {
|
||||
t.Fatalf("Expected OutOfGas panic, got: %#v\n", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
_, err = keepers.ContractKeeper.Execute(execCtx, contractAddr, creator, reflectSendBz, nil)
|
||||
|
||||
if tc.executeError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
// query the reply
|
||||
query := testdata.ReflectQueryMsg{
|
||||
SubMsgResult: &testdata.SubCall{ID: tc.submsgID},
|
||||
}
|
||||
queryBz, err := json.Marshal(query)
|
||||
require.NoError(t, err)
|
||||
queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz)
|
||||
require.NoError(t, err)
|
||||
var res wasmvmtypes.Reply
|
||||
err = json.Unmarshal(queryRes, &res)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.submsgID, res.ID)
|
||||
|
||||
if tc.subMsgError {
|
||||
require.NotEmpty(t, res.Result.Err)
|
||||
require.Nil(t, res.Result.Ok)
|
||||
} else {
|
||||
require.Empty(t, res.Result.Err)
|
||||
require.NotNil(t, res.Result.Ok)
|
||||
}
|
||||
|
||||
for _, assertion := range tc.resultAssertions {
|
||||
assertion(t, execCtx, contractAddr.String(), empty.String(), res.Result)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test an error case, where the Encoded doesn't return any sdk.Msg and we trigger(ed) a null pointer exception.
|
||||
// This occurs with the IBC encoder. Test this.
|
||||
func TestDispatchSubMsgEncodeToNoSdkMsg(t *testing.T) {
|
||||
// fake out the bank handle to return success with no data
|
||||
nilEncoder := func(sender sdk.AccAddress, msg *wasmvmtypes.BankMsg) ([]sdk.Msg, error) {
|
||||
return nil, nil
|
||||
}
|
||||
customEncoders := &MessageEncoders{
|
||||
Bank: nilEncoder,
|
||||
}
|
||||
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageHandler(NewSDKMessageHandler(nil, customEncoders)))
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000))
|
||||
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
_, _, fred := keyPubAddr()
|
||||
|
||||
// upload code
|
||||
codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, contractAddr)
|
||||
|
||||
// creator can send contract's tokens to fred (using SendMsg)
|
||||
msg := wasmvmtypes.CosmosMsg{
|
||||
Bank: &wasmvmtypes.BankMsg{
|
||||
Send: &wasmvmtypes.SendMsg{
|
||||
ToAddress: fred.String(),
|
||||
Amount: []wasmvmtypes.Coin{{
|
||||
Denom: "denom",
|
||||
Amount: "15000",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
reflectSend := testdata.ReflectHandleMsg{
|
||||
ReflectSubMsg: &testdata.ReflectSubPayload{
|
||||
Msgs: []wasmvmtypes.SubMsg{{
|
||||
ID: 7,
|
||||
Msg: msg,
|
||||
ReplyOn: wasmvmtypes.ReplyAlways,
|
||||
}},
|
||||
},
|
||||
}
|
||||
reflectSendBz, err := json.Marshal(reflectSend)
|
||||
require.NoError(t, err)
|
||||
_, err = keepers.ContractKeeper.Execute(ctx, contractAddr, creator, reflectSendBz, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// query the reflect state to ensure the result was stored
|
||||
query := testdata.ReflectQueryMsg{
|
||||
SubMsgResult: &testdata.SubCall{ID: 7},
|
||||
}
|
||||
queryBz, err := json.Marshal(query)
|
||||
require.NoError(t, err)
|
||||
queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz)
|
||||
require.NoError(t, err)
|
||||
|
||||
var res wasmvmtypes.Reply
|
||||
err = json.Unmarshal(queryRes, &res)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(7), res.ID)
|
||||
assert.Empty(t, res.Result.Err)
|
||||
require.NotNil(t, res.Result.Ok)
|
||||
sub := res.Result.Ok
|
||||
assert.Empty(t, sub.Data)
|
||||
require.Len(t, sub.Events, 0)
|
||||
}
|
||||
|
||||
// Try a simple send, no gas limit to for a sanity check before trying table tests
|
||||
func TestDispatchSubMsgConditionalReplyOn(t *testing.T) {
|
||||
ctx, keepers := CreateTestInput(t, false, ReflectFeatures)
|
||||
keeper := keepers.WasmKeeper
|
||||
|
||||
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
|
||||
contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000))
|
||||
|
||||
creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...)
|
||||
_, _, fred := keyPubAddr()
|
||||
|
||||
// upload code
|
||||
codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// creator instantiates a contract and gives it tokens
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
goodSend := wasmvmtypes.CosmosMsg{
|
||||
Bank: &wasmvmtypes.BankMsg{
|
||||
Send: &wasmvmtypes.SendMsg{
|
||||
ToAddress: fred.String(),
|
||||
Amount: []wasmvmtypes.Coin{{
|
||||
Denom: "denom",
|
||||
Amount: "1000",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
failSend := wasmvmtypes.CosmosMsg{
|
||||
Bank: &wasmvmtypes.BankMsg{
|
||||
Send: &wasmvmtypes.SendMsg{
|
||||
ToAddress: fred.String(),
|
||||
Amount: []wasmvmtypes.Coin{{
|
||||
Denom: "no-such-token",
|
||||
Amount: "777777",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
// true for wasmvmtypes.ReplySuccess, false for wasmvmtypes.ReplyError
|
||||
replyOnSuccess bool
|
||||
msg wasmvmtypes.CosmosMsg
|
||||
// true if the call should return an error (it wasn't handled)
|
||||
expectError bool
|
||||
// true if the reflect contract wrote the response (success or error) - it was captured
|
||||
writeResult bool
|
||||
}{
|
||||
"all good, reply success": {
|
||||
replyOnSuccess: true,
|
||||
msg: goodSend,
|
||||
expectError: false,
|
||||
writeResult: true,
|
||||
},
|
||||
"all good, reply error": {
|
||||
replyOnSuccess: false,
|
||||
msg: goodSend,
|
||||
expectError: false,
|
||||
writeResult: false,
|
||||
},
|
||||
"bad msg, reply success": {
|
||||
replyOnSuccess: true,
|
||||
msg: failSend,
|
||||
expectError: true,
|
||||
writeResult: false,
|
||||
},
|
||||
"bad msg, reply error": {
|
||||
replyOnSuccess: false,
|
||||
msg: failSend,
|
||||
expectError: false,
|
||||
writeResult: true,
|
||||
},
|
||||
}
|
||||
|
||||
var id uint64 = 0
|
||||
for name, tc := range cases {
|
||||
id++
|
||||
t.Run(name, func(t *testing.T) {
|
||||
subMsg := wasmvmtypes.SubMsg{
|
||||
ID: id,
|
||||
Msg: tc.msg,
|
||||
ReplyOn: wasmvmtypes.ReplySuccess,
|
||||
}
|
||||
if !tc.replyOnSuccess {
|
||||
subMsg.ReplyOn = wasmvmtypes.ReplyError
|
||||
}
|
||||
|
||||
reflectSend := testdata.ReflectHandleMsg{
|
||||
ReflectSubMsg: &testdata.ReflectSubPayload{
|
||||
Msgs: []wasmvmtypes.SubMsg{subMsg},
|
||||
},
|
||||
}
|
||||
reflectSendBz, err := json.Marshal(reflectSend)
|
||||
require.NoError(t, err)
|
||||
_, err = keepers.ContractKeeper.Execute(ctx, contractAddr, creator, reflectSendBz, nil)
|
||||
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// query the reflect state to check if the result was stored
|
||||
query := testdata.ReflectQueryMsg{
|
||||
SubMsgResult: &testdata.SubCall{ID: id},
|
||||
}
|
||||
queryBz, err := json.Marshal(query)
|
||||
require.NoError(t, err)
|
||||
queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz)
|
||||
if tc.writeResult {
|
||||
// we got some data for this call
|
||||
require.NoError(t, err)
|
||||
var res wasmvmtypes.Reply
|
||||
err = json.Unmarshal(queryRes, &res)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, id, res.ID)
|
||||
} else {
|
||||
// nothing should be there -> error
|
||||
require.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
759
x/wasm/keeper/test_common.go
Normal file
@ -0,0 +1,759 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/baseapp"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/std"
|
||||
"github.com/cosmos/cosmos-sdk/store"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/address"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/types/module"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
|
||||
authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
|
||||
"github.com/cosmos/cosmos-sdk/x/bank"
|
||||
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/capability"
|
||||
capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper"
|
||||
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/crisis"
|
||||
crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/distribution"
|
||||
distrclient "github.com/cosmos/cosmos-sdk/x/distribution/client"
|
||||
distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper"
|
||||
distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/evidence"
|
||||
evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/feegrant"
|
||||
"github.com/cosmos/cosmos-sdk/x/gov"
|
||||
govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper"
|
||||
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/mint"
|
||||
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/params"
|
||||
paramsclient "github.com/cosmos/cosmos-sdk/x/params/client"
|
||||
paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper"
|
||||
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
||||
paramproposal "github.com/cosmos/cosmos-sdk/x/params/types/proposal"
|
||||
"github.com/cosmos/cosmos-sdk/x/slashing"
|
||||
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/staking"
|
||||
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/upgrade"
|
||||
upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client"
|
||||
upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper"
|
||||
upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
|
||||
"github.com/cosmos/ibc-go/v4/modules/apps/transfer"
|
||||
ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
|
||||
ibc "github.com/cosmos/ibc-go/v4/modules/core"
|
||||
ibchost "github.com/cosmos/ibc-go/v4/modules/core/24-host"
|
||||
ibckeeper "github.com/cosmos/ibc-go/v4/modules/core/keeper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/rand"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
wasmappparams "github.com/cerc-io/laconicd/app/params"
|
||||
"github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting"
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
var moduleBasics = module.NewBasicManager(
|
||||
auth.AppModuleBasic{},
|
||||
bank.AppModuleBasic{},
|
||||
capability.AppModuleBasic{},
|
||||
staking.AppModuleBasic{},
|
||||
mint.AppModuleBasic{},
|
||||
distribution.AppModuleBasic{},
|
||||
gov.NewAppModuleBasic(
|
||||
paramsclient.ProposalHandler, distrclient.ProposalHandler, upgradeclient.ProposalHandler,
|
||||
),
|
||||
params.AppModuleBasic{},
|
||||
crisis.AppModuleBasic{},
|
||||
slashing.AppModuleBasic{},
|
||||
ibc.AppModuleBasic{},
|
||||
upgrade.AppModuleBasic{},
|
||||
evidence.AppModuleBasic{},
|
||||
transfer.AppModuleBasic{},
|
||||
vesting.AppModuleBasic{},
|
||||
)
|
||||
|
||||
func MakeTestCodec(t testing.TB) codec.Codec {
|
||||
return MakeEncodingConfig(t).Marshaler
|
||||
}
|
||||
|
||||
func MakeEncodingConfig(_ testing.TB) wasmappparams.EncodingConfig {
|
||||
encodingConfig := wasmappparams.MakeEncodingConfig()
|
||||
amino := encodingConfig.Amino
|
||||
interfaceRegistry := encodingConfig.InterfaceRegistry
|
||||
|
||||
std.RegisterInterfaces(interfaceRegistry)
|
||||
std.RegisterLegacyAminoCodec(amino)
|
||||
|
||||
moduleBasics.RegisterLegacyAminoCodec(amino)
|
||||
moduleBasics.RegisterInterfaces(interfaceRegistry)
|
||||
// add wasmd types
|
||||
types.RegisterInterfaces(interfaceRegistry)
|
||||
types.RegisterLegacyAminoCodec(amino)
|
||||
|
||||
return encodingConfig
|
||||
}
|
||||
|
||||
var TestingStakeParams = stakingtypes.Params{
|
||||
UnbondingTime: 100,
|
||||
MaxValidators: 10,
|
||||
MaxEntries: 10,
|
||||
HistoricalEntries: 10,
|
||||
BondDenom: "stake",
|
||||
}
|
||||
|
||||
type TestFaucet struct {
|
||||
t testing.TB
|
||||
bankKeeper bankkeeper.Keeper
|
||||
sender sdk.AccAddress
|
||||
balance sdk.Coins
|
||||
minterModuleName string
|
||||
}
|
||||
|
||||
func NewTestFaucet(t testing.TB, ctx sdk.Context, bankKeeper bankkeeper.Keeper, minterModuleName string, initialAmount ...sdk.Coin) *TestFaucet {
|
||||
require.NotEmpty(t, initialAmount)
|
||||
r := &TestFaucet{t: t, bankKeeper: bankKeeper, minterModuleName: minterModuleName}
|
||||
_, _, addr := keyPubAddr()
|
||||
r.sender = addr
|
||||
r.Mint(ctx, addr, initialAmount...)
|
||||
r.balance = initialAmount
|
||||
return r
|
||||
}
|
||||
|
||||
func (f *TestFaucet) Mint(parentCtx sdk.Context, addr sdk.AccAddress, amounts ...sdk.Coin) {
|
||||
require.NotEmpty(f.t, amounts)
|
||||
ctx := parentCtx.WithEventManager(sdk.NewEventManager()) // discard all faucet related events
|
||||
err := f.bankKeeper.MintCoins(ctx, f.minterModuleName, amounts)
|
||||
require.NoError(f.t, err)
|
||||
err = f.bankKeeper.SendCoinsFromModuleToAccount(ctx, f.minterModuleName, addr, amounts)
|
||||
require.NoError(f.t, err)
|
||||
f.balance = f.balance.Add(amounts...)
|
||||
}
|
||||
|
||||
func (f *TestFaucet) Fund(parentCtx sdk.Context, receiver sdk.AccAddress, amounts ...sdk.Coin) {
|
||||
require.NotEmpty(f.t, amounts)
|
||||
// ensure faucet is always filled
|
||||
if !f.balance.IsAllGTE(amounts) {
|
||||
f.Mint(parentCtx, f.sender, amounts...)
|
||||
}
|
||||
ctx := parentCtx.WithEventManager(sdk.NewEventManager()) // discard all faucet related events
|
||||
err := f.bankKeeper.SendCoins(ctx, f.sender, receiver, amounts)
|
||||
require.NoError(f.t, err)
|
||||
f.balance = f.balance.Sub(amounts)
|
||||
}
|
||||
|
||||
func (f *TestFaucet) NewFundedRandomAccount(ctx sdk.Context, amounts ...sdk.Coin) sdk.AccAddress {
|
||||
_, _, addr := keyPubAddr()
|
||||
f.Fund(ctx, addr, amounts...)
|
||||
return addr
|
||||
}
|
||||
|
||||
type TestKeepers struct {
|
||||
AccountKeeper authkeeper.AccountKeeper
|
||||
StakingKeeper stakingkeeper.Keeper
|
||||
DistKeeper distributionkeeper.Keeper
|
||||
BankKeeper bankkeeper.Keeper
|
||||
GovKeeper govkeeper.Keeper
|
||||
ContractKeeper types.ContractOpsKeeper
|
||||
WasmKeeper *Keeper
|
||||
IBCKeeper *ibckeeper.Keeper
|
||||
Router *baseapp.Router
|
||||
EncodingConfig wasmappparams.EncodingConfig
|
||||
Faucet *TestFaucet
|
||||
MultiStore sdk.CommitMultiStore
|
||||
ScopedWasmKeeper capabilitykeeper.ScopedKeeper
|
||||
}
|
||||
|
||||
// CreateDefaultTestInput common settings for CreateTestInput
|
||||
func CreateDefaultTestInput(t testing.TB) (sdk.Context, TestKeepers) {
|
||||
return CreateTestInput(t, false, "staking")
|
||||
}
|
||||
|
||||
// CreateTestInput encoders can be nil to accept the defaults, or set it to override some of the message handlers (like default)
|
||||
func CreateTestInput(t testing.TB, isCheckTx bool, availableCapabilities string, opts ...Option) (sdk.Context, TestKeepers) {
|
||||
// Load default wasm config
|
||||
return createTestInput(t, isCheckTx, availableCapabilities, types.DefaultWasmConfig(), dbm.NewMemDB(), opts...)
|
||||
}
|
||||
|
||||
// encoders can be nil to accept the defaults, or set it to override some of the message handlers (like default)
|
||||
func createTestInput(
|
||||
t testing.TB,
|
||||
isCheckTx bool,
|
||||
availableCapabilities string,
|
||||
wasmConfig types.WasmConfig,
|
||||
db dbm.DB,
|
||||
opts ...Option,
|
||||
) (sdk.Context, TestKeepers) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
keys := sdk.NewKVStoreKeys(
|
||||
authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey,
|
||||
minttypes.StoreKey, distributiontypes.StoreKey, slashingtypes.StoreKey,
|
||||
govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey,
|
||||
evidencetypes.StoreKey, ibctransfertypes.StoreKey,
|
||||
capabilitytypes.StoreKey, feegrant.StoreKey, authzkeeper.StoreKey,
|
||||
types.StoreKey,
|
||||
)
|
||||
ms := store.NewCommitMultiStore(db)
|
||||
for _, v := range keys {
|
||||
ms.MountStoreWithDB(v, sdk.StoreTypeIAVL, db)
|
||||
}
|
||||
tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey)
|
||||
for _, v := range tkeys {
|
||||
ms.MountStoreWithDB(v, sdk.StoreTypeTransient, db)
|
||||
}
|
||||
|
||||
memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey)
|
||||
for _, v := range memKeys {
|
||||
ms.MountStoreWithDB(v, sdk.StoreTypeMemory, db)
|
||||
}
|
||||
|
||||
require.NoError(t, ms.LoadLatestVersion())
|
||||
|
||||
ctx := sdk.NewContext(ms, tmproto.Header{
|
||||
Height: 1234567,
|
||||
Time: time.Date(2020, time.April, 22, 12, 0, 0, 0, time.UTC),
|
||||
}, isCheckTx, log.NewNopLogger())
|
||||
ctx = types.WithTXCounter(ctx, 0)
|
||||
|
||||
encodingConfig := MakeEncodingConfig(t)
|
||||
appCodec, legacyAmino := encodingConfig.Marshaler, encodingConfig.Amino
|
||||
|
||||
paramsKeeper := paramskeeper.NewKeeper(
|
||||
appCodec,
|
||||
legacyAmino,
|
||||
keys[paramstypes.StoreKey],
|
||||
tkeys[paramstypes.TStoreKey],
|
||||
)
|
||||
for _, m := range []string{
|
||||
authtypes.ModuleName,
|
||||
banktypes.ModuleName,
|
||||
stakingtypes.ModuleName,
|
||||
minttypes.ModuleName,
|
||||
distributiontypes.ModuleName,
|
||||
slashingtypes.ModuleName,
|
||||
crisistypes.ModuleName,
|
||||
ibctransfertypes.ModuleName,
|
||||
capabilitytypes.ModuleName,
|
||||
ibchost.ModuleName,
|
||||
govtypes.ModuleName,
|
||||
types.ModuleName,
|
||||
} {
|
||||
paramsKeeper.Subspace(m)
|
||||
}
|
||||
subspace := func(m string) paramstypes.Subspace {
|
||||
r, ok := paramsKeeper.GetSubspace(m)
|
||||
require.True(t, ok)
|
||||
return r
|
||||
}
|
||||
maccPerms := map[string][]string{ // module account permissions
|
||||
authtypes.FeeCollectorName: nil,
|
||||
distributiontypes.ModuleName: nil,
|
||||
minttypes.ModuleName: {authtypes.Minter},
|
||||
stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking},
|
||||
stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking},
|
||||
govtypes.ModuleName: {authtypes.Burner},
|
||||
ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner},
|
||||
types.ModuleName: {authtypes.Burner},
|
||||
}
|
||||
accountKeeper := authkeeper.NewAccountKeeper(
|
||||
appCodec,
|
||||
keys[authtypes.StoreKey], // target store
|
||||
subspace(authtypes.ModuleName),
|
||||
authtypes.ProtoBaseAccount, // prototype
|
||||
maccPerms,
|
||||
)
|
||||
blockedAddrs := make(map[string]bool)
|
||||
for acc := range maccPerms {
|
||||
blockedAddrs[authtypes.NewModuleAddress(acc).String()] = true
|
||||
}
|
||||
|
||||
bankKeeper := bankkeeper.NewBaseKeeper(
|
||||
appCodec,
|
||||
keys[banktypes.StoreKey],
|
||||
accountKeeper,
|
||||
subspace(banktypes.ModuleName),
|
||||
blockedAddrs,
|
||||
)
|
||||
bankKeeper.SetParams(ctx, banktypes.DefaultParams())
|
||||
|
||||
stakingKeeper := stakingkeeper.NewKeeper(
|
||||
appCodec,
|
||||
keys[stakingtypes.StoreKey],
|
||||
accountKeeper,
|
||||
bankKeeper,
|
||||
subspace(stakingtypes.ModuleName),
|
||||
)
|
||||
stakingKeeper.SetParams(ctx, TestingStakeParams)
|
||||
|
||||
distKeeper := distributionkeeper.NewKeeper(
|
||||
appCodec,
|
||||
keys[distributiontypes.StoreKey],
|
||||
subspace(distributiontypes.ModuleName),
|
||||
accountKeeper,
|
||||
bankKeeper,
|
||||
stakingKeeper,
|
||||
authtypes.FeeCollectorName,
|
||||
nil,
|
||||
)
|
||||
distKeeper.SetParams(ctx, distributiontypes.DefaultParams())
|
||||
stakingKeeper.SetHooks(distKeeper.Hooks())
|
||||
|
||||
// set genesis items required for distribution
|
||||
distKeeper.SetFeePool(ctx, distributiontypes.InitialFeePool())
|
||||
|
||||
upgradeKeeper := upgradekeeper.NewKeeper(
|
||||
map[int64]bool{},
|
||||
keys[upgradetypes.StoreKey],
|
||||
appCodec,
|
||||
tempDir,
|
||||
nil,
|
||||
)
|
||||
|
||||
faucet := NewTestFaucet(t, ctx, bankKeeper, minttypes.ModuleName, sdk.NewCoin("stake", sdk.NewInt(100_000_000_000)))
|
||||
|
||||
// set some funds ot pay out validatores, based on code from:
|
||||
// https://github.com/cosmos/cosmos-sdk/blob/fea231556aee4d549d7551a6190389c4328194eb/x/distribution/keeper/keeper_test.go#L50-L57
|
||||
distrAcc := distKeeper.GetDistributionAccount(ctx)
|
||||
faucet.Fund(ctx, distrAcc.GetAddress(), sdk.NewCoin("stake", sdk.NewInt(2000000)))
|
||||
accountKeeper.SetModuleAccount(ctx, distrAcc)
|
||||
|
||||
capabilityKeeper := capabilitykeeper.NewKeeper(
|
||||
appCodec,
|
||||
keys[capabilitytypes.StoreKey],
|
||||
memKeys[capabilitytypes.MemStoreKey],
|
||||
)
|
||||
scopedIBCKeeper := capabilityKeeper.ScopeToModule(ibchost.ModuleName)
|
||||
scopedWasmKeeper := capabilityKeeper.ScopeToModule(types.ModuleName)
|
||||
|
||||
ibcKeeper := ibckeeper.NewKeeper(
|
||||
appCodec,
|
||||
keys[ibchost.StoreKey],
|
||||
subspace(ibchost.ModuleName),
|
||||
stakingKeeper,
|
||||
upgradeKeeper,
|
||||
scopedIBCKeeper,
|
||||
)
|
||||
|
||||
router := baseapp.NewRouter()
|
||||
bh := bank.NewHandler(bankKeeper)
|
||||
router.AddRoute(sdk.NewRoute(banktypes.RouterKey, bh))
|
||||
sh := staking.NewHandler(stakingKeeper)
|
||||
router.AddRoute(sdk.NewRoute(stakingtypes.RouterKey, sh))
|
||||
dh := distribution.NewHandler(distKeeper)
|
||||
router.AddRoute(sdk.NewRoute(distributiontypes.RouterKey, dh))
|
||||
|
||||
querier := baseapp.NewGRPCQueryRouter()
|
||||
querier.SetInterfaceRegistry(encodingConfig.InterfaceRegistry)
|
||||
msgRouter := baseapp.NewMsgServiceRouter()
|
||||
msgRouter.SetInterfaceRegistry(encodingConfig.InterfaceRegistry)
|
||||
|
||||
cfg := sdk.GetConfig()
|
||||
cfg.SetAddressVerifier(types.VerifyAddressLen())
|
||||
|
||||
keeper := NewKeeper(
|
||||
appCodec,
|
||||
keys[types.StoreKey],
|
||||
subspace(types.ModuleName),
|
||||
accountKeeper,
|
||||
bankKeeper,
|
||||
stakingKeeper,
|
||||
distKeeper,
|
||||
ibcKeeper.ChannelKeeper,
|
||||
&ibcKeeper.PortKeeper,
|
||||
scopedWasmKeeper,
|
||||
wasmtesting.MockIBCTransferKeeper{},
|
||||
msgRouter,
|
||||
querier,
|
||||
tempDir,
|
||||
wasmConfig,
|
||||
availableCapabilities,
|
||||
opts...,
|
||||
)
|
||||
keeper.SetParams(ctx, types.DefaultParams())
|
||||
// add wasm handler so we can loop-back (contracts calling contracts)
|
||||
contractKeeper := NewDefaultPermissionKeeper(&keeper)
|
||||
router.AddRoute(sdk.NewRoute(types.RouterKey, TestHandler(contractKeeper)))
|
||||
|
||||
am := module.NewManager( // minimal module set that we use for message/ query tests
|
||||
bank.NewAppModule(appCodec, bankKeeper, accountKeeper),
|
||||
staking.NewAppModule(appCodec, stakingKeeper, accountKeeper, bankKeeper),
|
||||
distribution.NewAppModule(appCodec, distKeeper, accountKeeper, bankKeeper, stakingKeeper),
|
||||
)
|
||||
am.RegisterServices(module.NewConfigurator(appCodec, msgRouter, querier))
|
||||
types.RegisterMsgServer(msgRouter, NewMsgServerImpl(NewDefaultPermissionKeeper(keeper)))
|
||||
types.RegisterQueryServer(querier, NewGrpcQuerier(appCodec, keys[types.ModuleName], keeper, keeper.queryGasLimit))
|
||||
|
||||
govRouter := govtypes.NewRouter().
|
||||
AddRoute(govtypes.RouterKey, govtypes.ProposalHandler).
|
||||
AddRoute(paramproposal.RouterKey, params.NewParamChangeProposalHandler(paramsKeeper)).
|
||||
AddRoute(distributiontypes.RouterKey, distribution.NewCommunityPoolSpendProposalHandler(distKeeper)).
|
||||
AddRoute(types.RouterKey, NewWasmProposalHandler(&keeper, types.EnableAllProposals))
|
||||
|
||||
govKeeper := govkeeper.NewKeeper(
|
||||
appCodec,
|
||||
keys[govtypes.StoreKey],
|
||||
subspace(govtypes.ModuleName).WithKeyTable(govtypes.ParamKeyTable()),
|
||||
accountKeeper,
|
||||
bankKeeper,
|
||||
stakingKeeper,
|
||||
govRouter,
|
||||
)
|
||||
|
||||
govKeeper.SetProposalID(ctx, govtypes.DefaultStartingProposalID)
|
||||
govKeeper.SetDepositParams(ctx, govtypes.DefaultDepositParams())
|
||||
govKeeper.SetVotingParams(ctx, govtypes.DefaultVotingParams())
|
||||
govKeeper.SetTallyParams(ctx, govtypes.DefaultTallyParams())
|
||||
|
||||
keepers := TestKeepers{
|
||||
AccountKeeper: accountKeeper,
|
||||
StakingKeeper: stakingKeeper,
|
||||
DistKeeper: distKeeper,
|
||||
ContractKeeper: contractKeeper,
|
||||
WasmKeeper: &keeper,
|
||||
BankKeeper: bankKeeper,
|
||||
GovKeeper: govKeeper,
|
||||
IBCKeeper: ibcKeeper,
|
||||
Router: router,
|
||||
EncodingConfig: encodingConfig,
|
||||
Faucet: faucet,
|
||||
MultiStore: ms,
|
||||
ScopedWasmKeeper: scopedWasmKeeper,
|
||||
}
|
||||
return ctx, keepers
|
||||
}
|
||||
|
||||
// TestHandler returns a wasm handler for tests (to avoid circular imports)
|
||||
func TestHandler(k types.ContractOpsKeeper) sdk.Handler {
|
||||
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
|
||||
ctx = ctx.WithEventManager(sdk.NewEventManager())
|
||||
switch msg := msg.(type) {
|
||||
case *types.MsgStoreCode:
|
||||
return handleStoreCode(ctx, k, msg)
|
||||
case *types.MsgInstantiateContract:
|
||||
return handleInstantiate(ctx, k, msg)
|
||||
case *types.MsgExecuteContract:
|
||||
return handleExecute(ctx, k, msg)
|
||||
default:
|
||||
errMsg := fmt.Sprintf("unrecognized wasm message type: %T", msg)
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleStoreCode(ctx sdk.Context, k types.ContractOpsKeeper, msg *types.MsgStoreCode) (*sdk.Result, error) {
|
||||
senderAddr, err := sdk.AccAddressFromBech32(msg.Sender)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "sender")
|
||||
}
|
||||
codeID, _, err := k.Create(ctx, senderAddr, msg.WASMByteCode, msg.InstantiatePermission)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sdk.Result{
|
||||
Data: []byte(fmt.Sprintf("%d", codeID)),
|
||||
Events: ctx.EventManager().ABCIEvents(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func handleInstantiate(ctx sdk.Context, k types.ContractOpsKeeper, msg *types.MsgInstantiateContract) (*sdk.Result, error) {
|
||||
senderAddr, err := sdk.AccAddressFromBech32(msg.Sender)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "sender")
|
||||
}
|
||||
var adminAddr sdk.AccAddress
|
||||
if msg.Admin != "" {
|
||||
if adminAddr, err = sdk.AccAddressFromBech32(msg.Admin); err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "admin")
|
||||
}
|
||||
}
|
||||
|
||||
contractAddr, _, err := k.Instantiate(ctx, msg.CodeID, senderAddr, adminAddr, msg.Msg, msg.Label, msg.Funds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sdk.Result{
|
||||
Data: contractAddr,
|
||||
Events: ctx.EventManager().Events().ToABCIEvents(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func handleExecute(ctx sdk.Context, k types.ContractOpsKeeper, msg *types.MsgExecuteContract) (*sdk.Result, error) {
|
||||
senderAddr, err := sdk.AccAddressFromBech32(msg.Sender)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "sender")
|
||||
}
|
||||
contractAddr, err := sdk.AccAddressFromBech32(msg.Contract)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "admin")
|
||||
}
|
||||
data, err := k.Execute(ctx, contractAddr, senderAddr, msg.Msg, msg.Funds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sdk.Result{
|
||||
Data: data,
|
||||
Events: ctx.EventManager().Events().ToABCIEvents(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func RandomAccountAddress(_ testing.TB) sdk.AccAddress {
|
||||
_, _, addr := keyPubAddr()
|
||||
return addr
|
||||
}
|
||||
|
||||
// DeterministicAccountAddress creates a test address with v repeated to valid address size
|
||||
func DeterministicAccountAddress(_ testing.TB, v byte) sdk.AccAddress {
|
||||
return bytes.Repeat([]byte{v}, address.Len)
|
||||
}
|
||||
|
||||
func RandomBech32AccountAddress(t testing.TB) string {
|
||||
return RandomAccountAddress(t).String()
|
||||
}
|
||||
|
||||
type ExampleContract struct {
|
||||
InitialAmount sdk.Coins
|
||||
Creator crypto.PrivKey
|
||||
CreatorAddr sdk.AccAddress
|
||||
CodeID uint64
|
||||
Checksum []byte
|
||||
}
|
||||
|
||||
func StoreHackatomExampleContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) ExampleContract {
|
||||
return StoreExampleContract(t, ctx, keepers, "./testdata/hackatom.wasm")
|
||||
}
|
||||
|
||||
func StoreBurnerExampleContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) ExampleContract {
|
||||
return StoreExampleContract(t, ctx, keepers, "./testdata/burner.wasm")
|
||||
}
|
||||
|
||||
func StoreIBCReflectContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) ExampleContract {
|
||||
return StoreExampleContract(t, ctx, keepers, "./testdata/ibc_reflect.wasm")
|
||||
}
|
||||
|
||||
func StoreReflectContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) ExampleContract {
|
||||
return StoreExampleContract(t, ctx, keepers, "./testdata/reflect.wasm")
|
||||
}
|
||||
|
||||
func StoreExampleContract(t testing.TB, ctx sdk.Context, keepers TestKeepers, wasmFile string) ExampleContract {
|
||||
anyAmount := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000))
|
||||
creator, _, creatorAddr := keyPubAddr()
|
||||
fundAccounts(t, ctx, keepers.AccountKeeper, keepers.BankKeeper, creatorAddr, anyAmount)
|
||||
|
||||
wasmCode, err := os.ReadFile(wasmFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
codeID, _, err := keepers.ContractKeeper.Create(ctx, creatorAddr, wasmCode, nil)
|
||||
require.NoError(t, err)
|
||||
hash := keepers.WasmKeeper.GetCodeInfo(ctx, codeID).CodeHash
|
||||
return ExampleContract{anyAmount, creator, creatorAddr, codeID, hash}
|
||||
}
|
||||
|
||||
var wasmIdent = []byte("\x00\x61\x73\x6D")
|
||||
|
||||
type ExampleContractInstance struct {
|
||||
ExampleContract
|
||||
Contract sdk.AccAddress
|
||||
}
|
||||
|
||||
// SeedNewContractInstance sets the mock wasmerEngine in keeper and calls store + instantiate to init the contract's metadata
|
||||
func SeedNewContractInstance(t testing.TB, ctx sdk.Context, keepers TestKeepers, mock types.WasmerEngine) ExampleContractInstance {
|
||||
t.Helper()
|
||||
exampleContract := StoreRandomContract(t, ctx, keepers, mock)
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, exampleContract.CodeID, exampleContract.CreatorAddr, exampleContract.CreatorAddr, []byte(`{}`), "", nil)
|
||||
require.NoError(t, err)
|
||||
return ExampleContractInstance{
|
||||
ExampleContract: exampleContract,
|
||||
Contract: contractAddr,
|
||||
}
|
||||
}
|
||||
|
||||
// StoreRandomContract sets the mock wasmerEngine in keeper and calls store
|
||||
func StoreRandomContract(t testing.TB, ctx sdk.Context, keepers TestKeepers, mock types.WasmerEngine) ExampleContract {
|
||||
return StoreRandomContractWithAccessConfig(t, ctx, keepers, mock, nil)
|
||||
}
|
||||
|
||||
func StoreRandomContractWithAccessConfig(
|
||||
t testing.TB, ctx sdk.Context,
|
||||
keepers TestKeepers,
|
||||
mock types.WasmerEngine,
|
||||
cfg *types.AccessConfig,
|
||||
) ExampleContract {
|
||||
t.Helper()
|
||||
anyAmount := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000))
|
||||
creator, _, creatorAddr := keyPubAddr()
|
||||
fundAccounts(t, ctx, keepers.AccountKeeper, keepers.BankKeeper, creatorAddr, anyAmount)
|
||||
keepers.WasmKeeper.wasmVM = mock
|
||||
wasmCode := append(wasmIdent, rand.Bytes(10)...) //nolint:gocritic
|
||||
codeID, checksum, err := keepers.ContractKeeper.Create(ctx, creatorAddr, wasmCode, cfg)
|
||||
require.NoError(t, err)
|
||||
exampleContract := ExampleContract{InitialAmount: anyAmount, Creator: creator, CreatorAddr: creatorAddr, CodeID: codeID, Checksum: checksum}
|
||||
return exampleContract
|
||||
}
|
||||
|
||||
type HackatomExampleInstance struct {
|
||||
ExampleContract
|
||||
Contract sdk.AccAddress
|
||||
Verifier crypto.PrivKey
|
||||
VerifierAddr sdk.AccAddress
|
||||
Beneficiary crypto.PrivKey
|
||||
BeneficiaryAddr sdk.AccAddress
|
||||
Label string
|
||||
Deposit sdk.Coins
|
||||
}
|
||||
|
||||
// InstantiateHackatomExampleContract load and instantiate the "./testdata/hackatom.wasm" contract
|
||||
func InstantiateHackatomExampleContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) HackatomExampleInstance {
|
||||
contract := StoreHackatomExampleContract(t, ctx, keepers)
|
||||
|
||||
verifier, _, verifierAddr := keyPubAddr()
|
||||
fundAccounts(t, ctx, keepers.AccountKeeper, keepers.BankKeeper, verifierAddr, contract.InitialAmount)
|
||||
|
||||
beneficiary, _, beneficiaryAddr := keyPubAddr()
|
||||
initMsgBz := HackatomExampleInitMsg{
|
||||
Verifier: verifierAddr,
|
||||
Beneficiary: beneficiaryAddr,
|
||||
}.GetBytes(t)
|
||||
initialAmount := sdk.NewCoins(sdk.NewInt64Coin("denom", 100))
|
||||
|
||||
adminAddr := contract.CreatorAddr
|
||||
label := "demo contract to query"
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, contract.CodeID, contract.CreatorAddr, adminAddr, initMsgBz, label, initialAmount)
|
||||
require.NoError(t, err)
|
||||
return HackatomExampleInstance{
|
||||
ExampleContract: contract,
|
||||
Contract: contractAddr,
|
||||
Verifier: verifier,
|
||||
VerifierAddr: verifierAddr,
|
||||
Beneficiary: beneficiary,
|
||||
BeneficiaryAddr: beneficiaryAddr,
|
||||
Label: label,
|
||||
Deposit: initialAmount,
|
||||
}
|
||||
}
|
||||
|
||||
type ExampleInstance struct {
|
||||
ExampleContract
|
||||
Contract sdk.AccAddress
|
||||
Label string
|
||||
Deposit sdk.Coins
|
||||
}
|
||||
|
||||
// InstantiateReflectExampleContract load and instantiate the "./testdata/reflect.wasm" contract
|
||||
func InstantiateReflectExampleContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) ExampleInstance {
|
||||
example := StoreReflectContract(t, ctx, keepers)
|
||||
initialAmount := sdk.NewCoins(sdk.NewInt64Coin("denom", 100))
|
||||
label := "demo contract to query"
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, example.CodeID, example.CreatorAddr, example.CreatorAddr, []byte("{}"), label, initialAmount)
|
||||
|
||||
require.NoError(t, err)
|
||||
return ExampleInstance{
|
||||
ExampleContract: example,
|
||||
Contract: contractAddr,
|
||||
Label: label,
|
||||
Deposit: initialAmount,
|
||||
}
|
||||
}
|
||||
|
||||
type HackatomExampleInitMsg struct {
|
||||
Verifier sdk.AccAddress `json:"verifier"`
|
||||
Beneficiary sdk.AccAddress `json:"beneficiary"`
|
||||
}
|
||||
|
||||
func (m HackatomExampleInitMsg) GetBytes(t testing.TB) []byte {
|
||||
initMsgBz, err := json.Marshal(m)
|
||||
require.NoError(t, err)
|
||||
return initMsgBz
|
||||
}
|
||||
|
||||
type IBCReflectExampleInstance struct {
|
||||
Contract sdk.AccAddress
|
||||
Admin sdk.AccAddress
|
||||
CodeID uint64
|
||||
ReflectCodeID uint64
|
||||
}
|
||||
|
||||
// InstantiateIBCReflectContract load and instantiate the "./testdata/ibc_reflect.wasm" contract
|
||||
func InstantiateIBCReflectContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) IBCReflectExampleInstance {
|
||||
reflectID := StoreReflectContract(t, ctx, keepers).CodeID
|
||||
ibcReflectID := StoreIBCReflectContract(t, ctx, keepers).CodeID
|
||||
|
||||
initMsgBz := IBCReflectInitMsg{
|
||||
ReflectCodeID: reflectID,
|
||||
}.GetBytes(t)
|
||||
adminAddr := RandomAccountAddress(t)
|
||||
|
||||
contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, ibcReflectID, adminAddr, adminAddr, initMsgBz, "ibc-reflect-factory", nil)
|
||||
require.NoError(t, err)
|
||||
return IBCReflectExampleInstance{
|
||||
Admin: adminAddr,
|
||||
Contract: contractAddr,
|
||||
CodeID: ibcReflectID,
|
||||
ReflectCodeID: reflectID,
|
||||
}
|
||||
}
|
||||
|
||||
type IBCReflectInitMsg struct {
|
||||
ReflectCodeID uint64 `json:"reflect_code_id"`
|
||||
}
|
||||
|
||||
func (m IBCReflectInitMsg) GetBytes(t testing.TB) []byte {
|
||||
initMsgBz, err := json.Marshal(m)
|
||||
require.NoError(t, err)
|
||||
return initMsgBz
|
||||
}
|
||||
|
||||
type BurnerExampleInitMsg struct {
|
||||
Payout sdk.AccAddress `json:"payout"`
|
||||
}
|
||||
|
||||
func (m BurnerExampleInitMsg) GetBytes(t testing.TB) []byte {
|
||||
initMsgBz, err := json.Marshal(m)
|
||||
require.NoError(t, err)
|
||||
return initMsgBz
|
||||
}
|
||||
|
||||
func fundAccounts(t testing.TB, ctx sdk.Context, am authkeeper.AccountKeeper, bank bankkeeper.Keeper, addr sdk.AccAddress, coins sdk.Coins) {
|
||||
acc := am.NewAccountWithAddress(ctx, addr)
|
||||
am.SetAccount(ctx, acc)
|
||||
NewTestFaucet(t, ctx, bank, minttypes.ModuleName, coins...).Fund(ctx, addr, coins...)
|
||||
}
|
||||
|
||||
var keyCounter uint64
|
||||
|
||||
// we need to make this deterministic (same every test run), as encoded address size and thus gas cost,
|
||||
// depends on the actual bytes (due to ugly CanonicalAddress encoding)
|
||||
func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.AccAddress) {
|
||||
keyCounter++
|
||||
seed := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(seed, keyCounter)
|
||||
|
||||
key := ed25519.GenPrivKeyFromSecret(seed)
|
||||
pub := key.PubKey()
|
||||
addr := sdk.AccAddress(pub.Address())
|
||||
return key, pub, addr
|
||||
}
|
76
x/wasm/keeper/test_fuzz.go
Normal file
@ -0,0 +1,76 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
tmBytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
|
||||
"github.com/cerc-io/laconicd/x/wasm/types"
|
||||
)
|
||||
|
||||
var ModelFuzzers = []interface{}{FuzzAddr, FuzzAddrString, FuzzAbsoluteTxPosition, FuzzContractInfo, FuzzStateModel, FuzzAccessType, FuzzAccessConfig, FuzzContractCodeHistory}
|
||||
|
||||
func FuzzAddr(m *sdk.AccAddress, c fuzz.Continue) {
|
||||
*m = make([]byte, 20)
|
||||
c.Read(*m)
|
||||
}
|
||||
|
||||
func FuzzAddrString(m *string, c fuzz.Continue) {
|
||||
var x sdk.AccAddress
|
||||
FuzzAddr(&x, c)
|
||||
*m = x.String()
|
||||
}
|
||||
|
||||
func FuzzAbsoluteTxPosition(m *types.AbsoluteTxPosition, c fuzz.Continue) {
|
||||
m.BlockHeight = c.RandUint64()
|
||||
m.TxIndex = c.RandUint64()
|
||||
}
|
||||
|
||||
func FuzzContractInfo(m *types.ContractInfo, c fuzz.Continue) {
|
||||
m.CodeID = c.RandUint64()
|
||||
FuzzAddrString(&m.Creator, c)
|
||||
FuzzAddrString(&m.Admin, c)
|
||||
m.Label = c.RandString()
|
||||
c.Fuzz(&m.Created)
|
||||
}
|
||||
|
||||
func FuzzContractCodeHistory(m *types.ContractCodeHistoryEntry, c fuzz.Continue) {
|
||||
const maxMsgSize = 128
|
||||
m.CodeID = c.RandUint64()
|
||||
msg := make([]byte, c.RandUint64()%maxMsgSize)
|
||||
c.Read(msg)
|
||||
var err error
|
||||
if m.Msg, err = json.Marshal(msg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Fuzz(&m.Updated)
|
||||
m.Operation = types.AllCodeHistoryTypes[c.Int()%len(types.AllCodeHistoryTypes)]
|
||||
}
|
||||
|
||||
func FuzzStateModel(m *types.Model, c fuzz.Continue) {
|
||||
m.Key = tmBytes.HexBytes(c.RandString())
|
||||
if len(m.Key) == 0 {
|
||||
m.Key = tmBytes.HexBytes("non empty key")
|
||||
}
|
||||
c.Fuzz(&m.Value)
|
||||
}
|
||||
|
||||
func FuzzAccessType(m *types.AccessType, c fuzz.Continue) {
|
||||
pos := c.Int() % len(types.AllAccessTypes)
|
||||
for _, v := range types.AllAccessTypes {
|
||||
if pos == 0 {
|
||||
*m = v
|
||||
return
|
||||
}
|
||||
pos--
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzAccessConfig(m *types.AccessConfig, c fuzz.Continue) {
|
||||
FuzzAccessType(&m.Permission, c)
|
||||
var add sdk.AccAddress
|
||||
FuzzAddr(&add, c)
|
||||
*m = m.Permission.With(add)
|
||||
}
|
BIN
x/wasm/keeper/testdata/broken_crc.gzip
vendored
Normal file
BIN
x/wasm/keeper/testdata/burner.wasm
vendored
Normal file
23
x/wasm/keeper/testdata/download_releases.sh
vendored
Executable file
@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
set -o errexit -o nounset -o pipefail
|
||||
command -v shellcheck > /dev/null && shellcheck "$0"
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: ./download_releases.sh RELEASE_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tag="$1"
|
||||
|
||||
for contract in burner hackatom ibc_reflect ibc_reflect_send reflect staking; do
|
||||
url="https://github.com/CosmWasm/cosmwasm/releases/download/$tag/${contract}.wasm"
|
||||
echo "Downloading $url ..."
|
||||
wget -O "${contract}.wasm" "$url"
|
||||
done
|
||||
|
||||
# create the zip variant
|
||||
gzip -k hackatom.wasm
|
||||
mv hackatom.wasm.gz hackatom.wasm.gzip
|
||||
|
||||
rm -f version.txt
|
||||
echo "$tag" >version.txt
|
219
x/wasm/keeper/testdata/genesis.json
vendored
Normal file
@ -0,0 +1,219 @@
|
||||
{
|
||||
"genesis_time": "2020-07-13T07:49:08.2945876Z",
|
||||
"chain_id": "testing",
|
||||
"consensus_params": {
|
||||
"block": {
|
||||
"max_bytes": "22020096",
|
||||
"max_gas": "-1",
|
||||
"time_iota_ms": "1000"
|
||||
},
|
||||
"evidence": {
|
||||
"max_age_num_blocks": "100000",
|
||||
"max_age_duration": "172800000000000"
|
||||
},
|
||||
"validator": {
|
||||
"pub_key_types": [
|
||||
"ed25519"
|
||||
]
|
||||
}
|
||||
},
|
||||
"app_hash": "",
|
||||
"app_state": {
|
||||
"upgrade": {},
|
||||
"evidence": {
|
||||
"params": {
|
||||
"max_evidence_age": "120000000000"
|
||||
},
|
||||
"evidence": []
|
||||
},
|
||||
"supply": {
|
||||
"supply": []
|
||||
},
|
||||
"mint": {
|
||||
"minter": {
|
||||
"inflation": "0.130000000000000000",
|
||||
"annual_provisions": "0.000000000000000000"
|
||||
},
|
||||
"params": {
|
||||
"mint_denom": "ustake",
|
||||
"inflation_rate_change": "0.130000000000000000",
|
||||
"inflation_max": "0.200000000000000000",
|
||||
"inflation_min": "0.070000000000000000",
|
||||
"goal_bonded": "0.670000000000000000",
|
||||
"blocks_per_year": "6311520"
|
||||
}
|
||||
},
|
||||
"gov": {
|
||||
"starting_proposal_id": "1",
|
||||
"deposits": null,
|
||||
"votes": null,
|
||||
"proposals": null,
|
||||
"deposit_params": {
|
||||
"min_deposit": [
|
||||
{
|
||||
"denom": "ustake",
|
||||
"amount": "1"
|
||||
}
|
||||
],
|
||||
"max_deposit_period": "172800000000000"
|
||||
},
|
||||
"voting_params": {
|
||||
"voting_period": "60000000000",
|
||||
"voting_period_desc": "1minute"
|
||||
},
|
||||
"tally_params": {
|
||||
"quorum": "0.000000000000000001",
|
||||
"threshold": "0.000000000000000001",
|
||||
"veto": "0.334000000000000000"
|
||||
}
|
||||
},
|
||||
"slashing": {
|
||||
"params": {
|
||||
"signed_blocks_window": "100",
|
||||
"min_signed_per_window": "0.500000000000000000",
|
||||
"downtime_jail_duration": "600000000000",
|
||||
"slash_fraction_double_sign": "0.050000000000000000",
|
||||
"slash_fraction_downtime": "0.010000000000000000"
|
||||
},
|
||||
"signing_infos": {},
|
||||
"missed_blocks": {}
|
||||
},
|
||||
"wasm": {
|
||||
"params": {
|
||||
"upload_access": {
|
||||
"type": 3,
|
||||
"address": ""
|
||||
},
|
||||
"instantiate_default_permission": 3
|
||||
},
|
||||
"codes": null,
|
||||
"contracts": null,
|
||||
"sequences": null
|
||||
},
|
||||
"bank": {
|
||||
"send_enabled": true
|
||||
},
|
||||
"distribution": {
|
||||
"params": {
|
||||
"community_tax": "0.020000000000000000",
|
||||
"base_proposer_reward": "0.010000000000000000",
|
||||
"bonus_proposer_reward": "0.040000000000000000",
|
||||
"withdraw_addr_enabled": true
|
||||
},
|
||||
"fee_pool": {
|
||||
"community_pool": []
|
||||
},
|
||||
"delegator_withdraw_infos": [],
|
||||
"previous_proposer": "",
|
||||
"outstanding_rewards": [],
|
||||
"validator_accumulated_commissions": [],
|
||||
"validator_historical_rewards": [],
|
||||
"validator_current_rewards": [],
|
||||
"delegator_starting_infos": [],
|
||||
"validator_slash_events": []
|
||||
},
|
||||
"crisis": {
|
||||
"constant_fee": {
|
||||
"denom": "ustake",
|
||||
"amount": "1000"
|
||||
}
|
||||
},
|
||||
"genutil": {
|
||||
"gentxs": [
|
||||
{
|
||||
"type": "cosmos-sdk/StdTx",
|
||||
"value": {
|
||||
"msg": [
|
||||
{
|
||||
"type": "cosmos-sdk/MsgCreateValidator",
|
||||
"value": {
|
||||
"description": {
|
||||
"moniker": "testing",
|
||||
"identity": "",
|
||||
"website": "",
|
||||
"security_contact": "",
|
||||
"details": ""
|
||||
},
|
||||
"commission": {
|
||||
"rate": "0.100000000000000000",
|
||||
"max_rate": "0.200000000000000000",
|
||||
"max_change_rate": "0.010000000000000000"
|
||||
},
|
||||
"min_self_delegation": "1",
|
||||
"delegator_address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"validator_address": "cosmosvaloper1ve557a5g9yw2g2z57js3pdmcvd5my6g88d76lj",
|
||||
"pubkey": "cosmosvalconspub1zcjduepqddfln4tujr2p8actpgqz4h2xnls9y7tu9c9tu5lqkdglmdjalzuqah4neg",
|
||||
"value": {
|
||||
"denom": "ustake",
|
||||
"amount": "250000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"fee": {
|
||||
"amount": [],
|
||||
"gas": "200000"
|
||||
},
|
||||
"signatures": [
|
||||
{
|
||||
"pub_key": {
|
||||
"type": "tendermint/PubKeySecp256k1",
|
||||
"value": "A//cqZxkpH1re0VrHBtH308nb5t8K+Y/hF0GeRdRBmaJ"
|
||||
},
|
||||
"signature": "5QEEIuUVQTEBMuAtOOHnnKo6rPsIbmfzUxUqRnDFERVqwVr1Kg+ex4f/UGIK0yrOAvOG8zDADwFP4yF8lw+o5g=="
|
||||
}
|
||||
],
|
||||
"memo": "836fc54e9cad58f4ed6420223ec6290f75342afa@172.17.0.2:26656"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"auth": {
|
||||
"params": {
|
||||
"max_memo_characters": "256",
|
||||
"tx_sig_limit": "7",
|
||||
"tx_size_cost_per_byte": "10",
|
||||
"sig_verify_cost_ed25519": "590",
|
||||
"sig_verify_cost_secp256k1": "1000"
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"type": "cosmos-sdk/Account",
|
||||
"value": {
|
||||
"address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np",
|
||||
"coins": [
|
||||
{
|
||||
"denom": "ucosm",
|
||||
"amount": "1000000000"
|
||||
},
|
||||
{
|
||||
"denom": "ustake",
|
||||
"amount": "1000000000"
|
||||
}
|
||||
],
|
||||
"public_key": "",
|
||||
"account_number": 0,
|
||||
"sequence": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": null,
|
||||
"staking": {
|
||||
"params": {
|
||||
"unbonding_time": "1814400000000000",
|
||||
"max_validators": 100,
|
||||
"max_entries": 7,
|
||||
"historical_entries": 0,
|
||||
"bond_denom": "ustake"
|
||||
},
|
||||
"last_total_power": "0",
|
||||
"last_validator_powers": null,
|
||||
"validators": null,
|
||||
"delegations": null,
|
||||
"unbonding_delegations": null,
|
||||
"redelegations": null,
|
||||
"exported": false
|
||||
}
|
||||
}
|
||||
}
|
Variable
$X
is likely modified and later used on error. In some cases this could result in panics due to a nil dereferenceVariable
proposalTitle
is likely modified and later used on error. In some cases this could result in panics due to a nil dereferenceShow more details