forked from cerc-io/laconicd-deprecated
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34812d5e27 | ||
|
|
290c9fbf0c | ||
|
|
1cb6e7acbc | ||
|
|
c227a38e9a | ||
|
|
15c9ade06d | ||
|
|
4b318822d9 | ||
|
|
c61f268329 | ||
|
|
facd64a14b | ||
|
|
6945792aea | ||
|
|
5af22471a8 | ||
|
|
5e7a890adb |
@@ -1,4 +1,4 @@
|
||||
name: Publish onn release
|
||||
name: Publish on release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.txt
|
||||
fail_ci_if_error: true
|
||||
fail_ci_if_error: false
|
||||
if: env.GIT_DIFF
|
||||
|
||||
test-importer:
|
||||
@@ -81,6 +81,53 @@ jobs:
|
||||
make test-rpc
|
||||
if: env.GIT_DIFF
|
||||
|
||||
sdk_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout laconic-sdk
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: "./laconic-sdk/"
|
||||
repository: cerc-io/laconic-sdk
|
||||
fetch-depth: 0
|
||||
ref: jest_timeout
|
||||
- name: Environment
|
||||
run: ls -tlh && env
|
||||
- name: build containers scripts
|
||||
working-directory: tests/sdk_tests
|
||||
run: ./build-laconicd-container.sh && ./build-sdk-test-container.sh
|
||||
- name: start containers
|
||||
working-directory: tests/sdk_tests
|
||||
run: docker compose up -d
|
||||
- name: run-tests.sh
|
||||
working-directory: tests/sdk_tests
|
||||
run: ./run-tests.sh
|
||||
- name: reset containers for auction tests
|
||||
working-directory: tests/sdk_tests
|
||||
if: always()
|
||||
run: docker compose down
|
||||
- name: start auction containers
|
||||
working-directory: tests/sdk_tests
|
||||
run: docker compose -f docker-compose-auctions.yml up -d
|
||||
- name: run-acution-tests.sh
|
||||
working-directory: tests/sdk_tests
|
||||
run: ./run-auction-tests.sh
|
||||
- name: reset containers for nameservice tests
|
||||
working-directory: tests/sdk_tests
|
||||
if: always()
|
||||
run: docker compose -f docker-compose-auctions.yml down
|
||||
- name: start auction containers
|
||||
working-directory: tests/sdk_tests
|
||||
run: docker compose -f docker-compose-nameservice.yml up -d
|
||||
- name: run-nameservice-expiry-tests.sh
|
||||
working-directory: tests/sdk_tests
|
||||
run: ./run-nameservice-expiry-tests.sh
|
||||
- name: reset containers for nameservice tests
|
||||
working-directory: tests/sdk_tests
|
||||
if: always()
|
||||
run: docker compose -f docker-compose-nameservice.yml down
|
||||
|
||||
# integration_tests:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
|
||||
@@ -50,6 +50,7 @@ coverage.txt
|
||||
sim_log_file
|
||||
tests/**/tmp/*
|
||||
yarn.lock
|
||||
x/auction/client/testutil/bidder-bafyre*
|
||||
|
||||
# Vagrant
|
||||
.vagrant/
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ RUN make build
|
||||
FROM alpine:3.17.0
|
||||
|
||||
# Install ca-certificates
|
||||
RUN apk add --update ca-certificates jq
|
||||
RUN apk add --update ca-certificates jq curl
|
||||
WORKDIR /
|
||||
|
||||
# Copy over binaries from the build-env
|
||||
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
# Security
|
||||
|
||||
As part of our vulnerability disclosure policy, we operate a security vulnerability program through [Immunefi](https://immunefi.com/). This document serves as a complementary guideline for reporting vulnerabilities and how the disclosure process is managed. Please refer to the official Evmos [bug bounty program](https://immunefi.com/bounty/evmos/) for up-to-date information.
|
||||
|
||||
## Guidelines
|
||||
|
||||
We require that all researchers:
|
||||
|
||||
- Use the Evmos [bug bounty program](https://immunefi.com/bounty/evmos/) on Immunefi to disclose all vulnerabilities, and avoid posting vulnerability information in public places, including GitHub, Discord, Telegram, Twitter or other non-private channels.
|
||||
- Make every effort to avoid privacy violations, degradation of user experience, disruption to production systems, and destruction of data.
|
||||
- Keep any information about vulnerabilities that you’ve discovered confidential between yourself and the engineering team until the issue has been resolved and disclosed
|
||||
- Avoid posting personally identifiable information, privately or publicly
|
||||
|
||||
If you follow these guidelines when reporting an issue to us, we commit to:
|
||||
|
||||
- Not pursue or support any legal action related to your research on this vulnerability
|
||||
- Work with you to understand, resolve and ultimately disclose the issue in a timely fashion
|
||||
|
||||
## Disclosure Process
|
||||
|
||||
Evmos uses the following disclosure process:
|
||||
|
||||
1. Once a security report is received via the Immunefi Bug Bounty program, the team works to verify the issue and confirm its severity level using [CVSS](https://nvd.nist.gov/vuln-metrics/cvss) or [Immunefi’s Vulnerability Severity Classification System v2.2](https://immunefi.com/immunefi-vulnerability-severity-classification-system-v2-2/).
|
||||
1. Two people from the affected project will review, replicate and acknowledge the report within 48-96 hours of the alert according to the table below:
|
||||
| Security Level | Hours to First Response (ACK) from Escalation |
|
||||
| -------------------- | --------------------------------------------- |
|
||||
| Critical | 48 |
|
||||
| High | 96 |
|
||||
| Medium | 96 |
|
||||
| Low or Informational | 96 |
|
||||
| None | 96 |
|
||||
|
||||
2. If the report is not applicable or reproducible, the Security Lead (or Security Secondary) will revert to the reporter to request more info or close the report.
|
||||
3. The report is confirmed by the Security Lead to the reporter.
|
||||
2. The team determines the vulnerability’s potential impact on Evmos.
|
||||
1. Vulnerabilities with `Informational` and `Low` categorization will result in creating a public issue.
|
||||
2. Vulnerabilities with `Medium` categorization will result in the creation of an internal ticket and patch of the code.
|
||||
3. Vulnerabilities with `High` or `Critical` will result in the [creation of a new Security Advisory](https://docs.github.com/en/code-security/repository-security-advisories/creating-a-repository-security-advisory)
|
||||
|
||||
Once the vulnerability severity is defined, the following steps apply:
|
||||
|
||||
- For `High` and `Critical`:
|
||||
1. Patches are prepared for supported releases of Evmos in a [temporary private fork](https://docs.github.com/en/code-security/repository-security-advisories/collaborating-in-a-temporary-private-fork-to-resolve-a-repository-security-vulnerability) of the repository.
|
||||
2. Only relevant parties will be notified about an upcoming upgrade. These being validators, the core developer team, and users directly affected by the vulnerability.
|
||||
3. 24 hours following this notification, relevant releases with the patch will be made public.
|
||||
4. The nodes and validators update their Evmos and Ethermint dependencies to use these releases.
|
||||
5. A week (or less) after the security vulnerability has been patched on Evmos, we will disclose that the mentioned release contained a security fix.
|
||||
6. After an additional 2 weeks, we will publish a public announcement of the vulnerability. We also publish a security Advisory on GitHub and publish a [CVE](https://en.wikipedia.org/wiki/Common_Vulnerabilities_and_Exposures)
|
||||
|
||||
- For `Informational` , `Low` and `Medium` severities:
|
||||
1. `Medium` and `Low` severity bug reports are included in a public issue and will be incorporated in the current sprint and patched in the next release. `Informational` reports are additionally categorized as with low or medium priority and might not be included in the next release.
|
||||
2. One week after the releases go out, we will publish a post with further details on the vulnerability as well as our response to it.
|
||||
|
||||
This process can take some time. Every effort will be made to handle the bug in as timely a manner as possible, however, it's important that we follow the process described above to ensure that disclosures are handled consistently and to keep Ethermint and its downstream dependent projects, including but not limited to Evmos, as secure as possible.
|
||||
|
||||
### Payment Process
|
||||
|
||||
The payment process will be executed according to Evmos’s Immunefi Bug Bounty program Rules.
|
||||
|
||||
### Contact
|
||||
|
||||
The Evmos Security Team is constantly being monitored. If you need to reach out to the team directly, please reach out via email: [security@evmos.org](mailto:security@evmos.org)
|
||||
@@ -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" ];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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\"" ];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
# Validator Upgrade Guide for laconic_81337-5 Testnet v0.6.0 -> v0.7.0
|
||||
|
||||
This guide assumes you have followed the instructions to crete a systemd service or docker container validator node v0.6.0 and you perform the upgrade on a machine running v0.6.0
|
||||
|
||||
It is highly recommended to make the backup of your datadir after you stop v0.6.0 and before you start v0.7.0. Unless you changed your datadir, it should be located in `~/.laconicd`
|
||||
|
||||
## Systemd service
|
||||
Skip this section if you use docker
|
||||
|
||||
This is very similar to building v0.6.0. We keep v 0.6.0 running until v0.7.0 is built and only after the successful build we should replace 0.6.0 binary with v0.7.0. This is to avoid jailing your validator for long downtime.
|
||||
The general upgrade plan is the following:
|
||||
1. Install Go v1.19.5 (we used 1.18 for v0.6.0)
|
||||
2. Remove old copy of the github repository and build directory
|
||||
3. Download the latest laconicd repository and checkout v0.7.0
|
||||
4. Build laconicd binary (but not install in this moment)
|
||||
5. Stop laconicd systemd service
|
||||
6. Install recently built new version of laconicd
|
||||
7. Start laconicd service
|
||||
|
||||
>***You have ~10 minutes after step 5 to complete steps 6 and 7 before your validator is jailed for downtime. Getting jailed for downtime is not a disaster, however would require manual unjailing.***
|
||||
|
||||
|
||||
|
||||
### Install Go 1.19
|
||||
|
||||
```sh
|
||||
# Update Ubuntu
|
||||
sudo apt update
|
||||
sudo apt upgrade -y
|
||||
|
||||
# Install required software packages
|
||||
sudo apt install git curl build-essential make jq -y
|
||||
|
||||
# Remove any existing installation of `go`
|
||||
sudo rm -rf /usr/local/go
|
||||
|
||||
# Install Go version 1.19.5
|
||||
curl https://dl.google.com/go/go1.19.5.linux-amd64.tar.gz | sudo tar -C/usr/local -zxvf -
|
||||
|
||||
```
|
||||
|
||||
Check the version of go installed
|
||||
|
||||
```sh
|
||||
go version
|
||||
|
||||
# Should return something like: go version go1.19.5 linux/amd64
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Remove old copy of `laconicd` build
|
||||
|
||||
>Attention should be paid that the directory mentioned below is `~/laconicd` and NOT `~/.laconicd`. The latter is the data directory containing all your node data and configuration and it must be kept during the upgrade.
|
||||
|
||||
```sh
|
||||
# Remove the previous build directory
|
||||
cd ~
|
||||
rm -rf laconicd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Download `laconicd` repository
|
||||
```sh
|
||||
git clone https://github.com/cerc-io/laconicd.git
|
||||
cd laconicd
|
||||
|
||||
# Checkout 0.7.0 branch
|
||||
git fetch --all
|
||||
git checkout v0.7.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Build the new version of `laconicd`
|
||||
|
||||
```sh
|
||||
# Build laconic (but not install at this moment)
|
||||
make VERSION=v0.7.0 build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Stop `laconicd` systemd service
|
||||
|
||||
```sh
|
||||
sudo systemctl stop laconicd
|
||||
```
|
||||
|
||||
>***Make sure the service is stopped***
|
||||
|
||||
```sh
|
||||
sudo systemctl status laconicd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Install new `laconicd` version
|
||||
|
||||
```sh
|
||||
make VERSION=v0.7.0 install
|
||||
```
|
||||
|
||||
Verify your installation
|
||||
|
||||
```sh
|
||||
laconicd version
|
||||
```
|
||||
This should return `0.7.0`
|
||||
|
||||
---
|
||||
|
||||
### Start `laconicd` systemd service
|
||||
|
||||
```sh
|
||||
sudo systemctl start laconicd
|
||||
```
|
||||
|
||||
Verify that the node joined the network and produces new blocks
|
||||
|
||||
```sh
|
||||
journalctl -f -u laconicd
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Docker container
|
||||
|
||||
Upgrade plan:
|
||||
1. Stop running v0.6.0 container
|
||||
2. Delete v0.6.0 container
|
||||
3. Create v0.7.0 container
|
||||
4. Start v0.7.0 container
|
||||
|
||||
>***You have ~10 minutes to complete the upgrade procedure before your validator is jailed for downtime. Getting jailed for downtime is not a disaster, however would require manual unjailing.***
|
||||
|
||||
### Stop running v0.6.0 container
|
||||
```sh
|
||||
docker stop laconic-testnet-5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Delete v0.6.0 container
|
||||
```sh
|
||||
docker rm laconic-testnet-5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create v0.7.0 container
|
||||
```sh
|
||||
docker create --name laconic-testnet-5 \
|
||||
--restart always \
|
||||
-v ~/.laconicd:/root/.laconicd \
|
||||
-p 26656:26656 \
|
||||
-p 127.0.0.1:26657:26657 \
|
||||
-p 127.0.0.1:26660:26660 \
|
||||
git.vdb.to/cerc-io/laconicd/laconicd:v0.7.0 \
|
||||
laconicd start --gql-playground --gql-server --log_level=warn
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Start v0.7.0 container
|
||||
```sh
|
||||
docker start laconic-testnet-5
|
||||
```
|
||||
|
||||
Verify that the node joined the network and produces new blocks
|
||||
|
||||
```sh
|
||||
docker logs -f laconic-testnet-5
|
||||
```
|
||||
|
||||
---
|
||||
@@ -0,0 +1,31 @@
|
||||
services:
|
||||
laconicd:
|
||||
restart: unless-stopped
|
||||
image: cerc-io/laconicd:local-test
|
||||
environment:
|
||||
- TEST_AUCTION_ENABLED=true
|
||||
command: ["sh", "/docker-entrypoint-scripts.d/create-fixturenet.sh"]
|
||||
volumes:
|
||||
- ../../init.sh:/docker-entrypoint-scripts.d/create-fixturenet.sh
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-v", "http://127.0.0.1:6060"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
ports:
|
||||
- "6060"
|
||||
- "26657"
|
||||
- "26656"
|
||||
- "9473"
|
||||
- "8545"
|
||||
- "8546"
|
||||
- "9090"
|
||||
- "9091"
|
||||
- "1317"
|
||||
|
||||
sdk-test-runner:
|
||||
image: cerc-io/laconic-sdk-tester:local-test
|
||||
depends_on:
|
||||
laconicd:
|
||||
condition: service_healthy
|
||||
command: tail -F /dev/null
|
||||
@@ -0,0 +1,31 @@
|
||||
services:
|
||||
laconicd:
|
||||
restart: unless-stopped
|
||||
image: cerc-io/laconicd:local-test
|
||||
environment:
|
||||
- TEST_REGISTRY_EXPIRY=true
|
||||
command: ["sh", "/docker-entrypoint-scripts.d/create-fixturenet.sh"]
|
||||
volumes:
|
||||
- ../../init.sh:/docker-entrypoint-scripts.d/create-fixturenet.sh
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-v", "http://127.0.0.1:6060"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
ports:
|
||||
- "6060"
|
||||
- "26657"
|
||||
- "26656"
|
||||
- "9473"
|
||||
- "8545"
|
||||
- "8546"
|
||||
- "9090"
|
||||
- "9091"
|
||||
- "1317"
|
||||
|
||||
sdk-test-runner:
|
||||
image: cerc-io/laconic-sdk-tester:local-test
|
||||
depends_on:
|
||||
laconicd:
|
||||
condition: service_healthy
|
||||
command: tail -F /dev/null
|
||||
@@ -6,7 +6,7 @@ services:
|
||||
volumes:
|
||||
- ../../init.sh:/docker-entrypoint-scripts.d/create-fixturenet.sh
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-nv", "-t1", "--spider", "http://localhost:6060"]
|
||||
test: ["CMD", "curl", "-v", "http://127.0.0.1:6060"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
|
||||
set -x
|
||||
fi
|
||||
# Get the key from laconicd
|
||||
laconicd_key=$( docker compose exec laconicd echo y | docker compose exec laconicd laconicd keys export mykey --unarmored-hex --unsafe )
|
||||
# Set parameters for the test suite
|
||||
cosmos_chain_id=laconic_9000-1
|
||||
laconicd_rest_endpoint=http://laconicd:1317
|
||||
laconicd_gql_endpoint=http://laconicd:9473/api
|
||||
# Run tests
|
||||
docker network inspect sdk_tests_default
|
||||
sleep 30s
|
||||
docker logs sdk_tests-laconicd-1
|
||||
|
||||
docker compose exec sdk-test-runner sh -c "COSMOS_CHAIN_ID=${cosmos_chain_id} LACONICD_REST_ENDPOINT=${laconicd_rest_endpoint} LACONICD_GQL_ENDPOINT=${laconicd_gql_endpoint} PRIVATE_KEY=${laconicd_key} yarn test:auctions"
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
|
||||
set -x
|
||||
fi
|
||||
# Get the key from laconicd
|
||||
laconicd_key=$( docker compose exec laconicd echo y | docker compose exec laconicd laconicd keys export mykey --unarmored-hex --unsafe )
|
||||
# Set parameters for the test suite
|
||||
cosmos_chain_id=laconic_9000-1
|
||||
laconicd_rest_endpoint=http://laconicd:1317
|
||||
laconicd_gql_endpoint=http://laconicd:9473/api
|
||||
# Run tests
|
||||
docker network inspect sdk_tests_default
|
||||
sleep 30s
|
||||
docker logs sdk_tests-laconicd-1
|
||||
|
||||
docker compose exec sdk-test-runner sh -c "COSMOS_CHAIN_ID=${cosmos_chain_id} LACONICD_REST_ENDPOINT=${laconicd_rest_endpoint} LACONICD_GQL_ENDPOINT=${laconicd_gql_endpoint} PRIVATE_KEY=${laconicd_key} yarn test:nameservice-expiry"
|
||||
@@ -9,4 +9,10 @@ cosmos_chain_id=laconic_9000-1
|
||||
laconicd_rest_endpoint=http://laconicd:1317
|
||||
laconicd_gql_endpoint=http://laconicd:9473/api
|
||||
# Run tests
|
||||
docker network inspect sdk_tests_default
|
||||
sleep 30s
|
||||
docker logs laconicd
|
||||
docker compose exec laconicd sh -c "curl http://127.0.0.1:9473/api"
|
||||
docker compose exec laconicd sh -c "curl http://localhost:9473/api"
|
||||
|
||||
docker compose exec sdk-test-runner sh -c "COSMOS_CHAIN_ID=${cosmos_chain_id} LACONICD_REST_ENDPOINT=${laconicd_rest_endpoint} LACONICD_GQL_ENDPOINT=${laconicd_gql_endpoint} PRIVATE_KEY=${laconicd_key} yarn test"
|
||||
|
||||
+16
-15
@@ -8,17 +8,16 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/fluent"
|
||||
"github.com/ipld/go-ipld-prime/linking"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/multicodec"
|
||||
"github.com/ipld/go-ipld-prime/storage/memstore"
|
||||
|
||||
canonicalJson "github.com/gibson042/canonicaljson-go"
|
||||
"github.com/ipfs/go-cid"
|
||||
cbor "github.com/ipfs/go-ipld-cbor"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/ipld/go-ipld-prime/linking"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/multicodec"
|
||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||
"github.com/ipld/go-ipld-prime/storage/memstore"
|
||||
mh "github.com/multiformats/go-multihash"
|
||||
)
|
||||
|
||||
@@ -68,7 +67,17 @@ func GetAttributeAsString(obj map[string]interface{}, attr string) (string, erro
|
||||
}
|
||||
|
||||
// CIDFromJSONBytesUsingIpldPrime returns CID (dagcbor) for json (as bytes).
|
||||
// This is combination of samples for unmarshalling and linking
|
||||
// see: https://pkg.go.dev/github.com/ipld/go-ipld-prime
|
||||
func CIDFromJSONBytesUsingIpldPrime(content []byte) (string, error) {
|
||||
np := basicnode.Prototype.Any // Pick a stle for the in-memory data.
|
||||
nb := np.NewBuilder() // Create a builder.
|
||||
err := dagjson.Decode(nb, bytes.NewReader(content)) // Hand the builder to decoding -- decoding will fill it in!
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n := nb.Build() // Call 'Build' to get the resulting Node. (It's immutable!)
|
||||
|
||||
lsys := cidlink.DefaultLinkSystem()
|
||||
|
||||
// We want to store the serialized data somewhere.
|
||||
@@ -87,14 +96,6 @@ func CIDFromJSONBytesUsingIpldPrime(content []byte) (string, error) {
|
||||
MhLength: 32, // sha2-256 hash has a 32-byte sum.
|
||||
}}
|
||||
|
||||
// And we need some data to link to! Here's a quick piece of example data:
|
||||
n, err := fluent.Build(basicnode.Prototype.Any, func(na fluent.NodeAssembler) {
|
||||
na.AssignBytes(content)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Now: time to apply the LinkSystem, and do the actual store operation!
|
||||
lnk, err := lsys.Store(
|
||||
linking.LinkContext{}, // The zero value is fine. Configure it it you want cancellability or other features.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAndValidateCIDGeneration(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
content string
|
||||
expected string
|
||||
}{
|
||||
// empty string and empty json blows up
|
||||
// {
|
||||
// "empty string", "", "bafyreiengp2sbi6ez34a2jctv34bwyjl7yoliteleaswgcwtqzrhmpyt2m",
|
||||
// },
|
||||
// {
|
||||
// "empty json", "{}", "bafyreihpfkdvib5muloxlj5b3tgdwibjdcu3zdsuhyft33z7gtgnlzlkpm",
|
||||
// },
|
||||
|
||||
{
|
||||
"test record", "{\"build_artifact_cid\":\"QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9\",\"repo_registration_record_cid\":\"QmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D\",\"tls_cert_cid\":\"QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR\",\"type\":\"WebsiteRegistrationRecord\",\"url\":\"https://cerc.io\",\"version\":\"0.0.1\"}",
|
||||
"bafyreiek4hnoqmits66bjyxswapplweuoqe4en2ux6u772o4y3askpd3ny",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
deprecatedAndCorrect, _ := CIDFromJSONBytes([]byte(tc.content))
|
||||
newImpl, err := CIDFromJSONBytesUsingIpldPrime([]byte(tc.content))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, deprecatedAndCorrect, newImpl, tc.name)
|
||||
require.Equal(t, tc.expected, newImpl)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
```
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# testing package for ibc
|
||||
Customized version of cosmos-sdk x/ibc/testing
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
`
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user