forked from cerc-io/laconicd-deprecated
Compare commits
3 Commits
main
...
aleem/25-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34812d5e27 | ||
|
|
290c9fbf0c | ||
|
|
1cb6e7acbc |
109
proto/wasm/v1/authz.proto
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientCtx, proposalTitle, proposalDescr, deposit, nil
|
||||||
|
}
|
||||||
158
x/wasm/client/cli/gov_tx_test.go
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
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())
|
||||||
|
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())
|
||||||
|
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())
|
||||||
|
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())
|
||||||
|
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())
|
||||||
|
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
1188
x/wasm/keeper/keeper.go
Normal file
File diff suppressed because it is too large
Load Diff
69
x/wasm/keeper/keeper_cgo.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
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
2410
x/wasm/keeper/keeper_test.go
Normal file
File diff suppressed because it is too large
Load Diff
154
x/wasm/keeper/legacy_querier.go
Normal file
154
x/wasm/keeper/legacy_querier.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"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() {
|
||||||
|
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
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
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
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
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
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
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
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
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
170
x/wasm/keeper/options.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
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
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
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
1001
x/wasm/keeper/proposal_integration_test.go
Normal file
File diff suppressed because it is too large
Load Diff
346
x/wasm/keeper/querier.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
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
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
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
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
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
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
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
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
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
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
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
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
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/broken_crc.gzip
vendored
Normal file
Binary file not shown.
BIN
x/wasm/keeper/testdata/burner.wasm
vendored
Normal file
BIN
x/wasm/keeper/testdata/burner.wasm
vendored
Normal file
Binary file not shown.
23
x/wasm/keeper/testdata/download_releases.sh
vendored
Executable 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
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user